From bc671618a7f16e198e6f93412e49217f71ebbb1a Mon Sep 17 00:00:00 2001 From: BlackMark Date: Fri, 23 Oct 2026 21:41:55 +0200 Subject: [PATCH] test: the simavr device stops dumping a flash image nobody reads Every protocol run wrote the simulated flash to /tmp/tsb_dump.bin on exit - outside the repository, from four tests that run in parallel into one file - for a cross-check no test made. The dump goes, and the device's clock and page size are named. Co-Authored-By: Claude Opus 5.5 --- test/device.cpp | 43 +++++++++++++++++-------------------------- test/tsbtest.py | 8 +++----- 2 files changed, 20 insertions(+), 31 deletions(-) diff --git a/test/device.cpp b/test/device.cpp index 91568fd..b86689d 100644 --- a/test/device.cpp +++ b/test/device.cpp @@ -1,12 +1,8 @@ // simavr "device" for the TSB bootloader: load the boot-linked ELF into the -// ATmega328P boot section, enter it (BOOTRST is not modelled, so we set PC to +// ATmega328P boot section, enter it (BOOTRST is not modelled, so PC is set to // the boot base, exactly as simavr's own board_simduino does), and expose // UART0 as a pty. A host client (Python pyserial, or the real tsbloader) then // speaks the TSB protocol over that pty and actually flashes the device. -// -// SPM genuinely writes avr->flash on the mega cores, so on exit (or SIGTERM) -// we dump the flash image to a file for a ground-truth cross-check against -// what the client read back through the bootloader. #include #include #include @@ -28,19 +24,16 @@ extern "C" { namespace { +// What the loaders are built for: tsb/*.cpp's device clock and the chip's SPM +// page, which is also the config page sitting directly below the boot section. +constexpr std::uint32_t clock_hz = 16'000'000; +constexpr std::uint32_t page_bytes = 128; + avr_t *avr; uart_pty_t uart_pty; -const char *dump_path; [[noreturn]] void finish(int) { - if (dump_path) { - std::FILE *f = std::fopen(dump_path, "wb"); - if (f) { - std::fwrite(avr->flash, 1, avr->flashend + 1, f); - std::fclose(f); - } - } uart_pty_stop(&uart_pty); _exit(0); } @@ -49,12 +42,11 @@ const char *dump_path; int main(int argc, char *argv[]) { - if (argc < 3) { - std::println(stderr, "usage: {} [flash_dump.bin]", argv[0]); + if (argc != 3) { + std::println(stderr, "usage: {} ", argv[0]); return 2; } - auto boot_base = static_cast(std::strtoul(argv[2], nullptr, 0)); - dump_path = argc >= 4 ? argv[3] : nullptr; + const auto boot_base = static_cast(std::strtoul(argv[2], nullptr, 0)); avr = avr_make_mcu_by_name("atmega328p"); if (!avr) { @@ -62,14 +54,14 @@ int main(int argc, char *argv[]) return 1; } avr_init(avr); - avr->frequency = 16000000; + avr->frequency = clock_hz; // Real flash powers up erased (0xff); the app region must look erased // before the bootloader programs it. std::memset(avr->flash, 0xff, avr->flashend + 1); // simavr's ELF loader flattens the flash base to 0 (it expects an app at - // 0x0), but it hands back the boot code in fw.flash; place it at the boot - // section base ourselves and enter there (BOOTRST is not modelled). + // 0x0), but it hands back the boot code in fw.flash, so it is placed at the + // boot section base by hand. elf_firmware_t fw{}; if (elf_read_firmware(argv[1], &fw) != 0) { std::println(stderr, "device: cannot read {}", argv[1]); @@ -88,12 +80,11 @@ int main(int argc, char *argv[]) avr->pc = boot_base; avr->codeend = avr->flashend; - // Optional: seed the config page (one page below the boot section) with a - // hex byte string, so the password gate and emergency erase can be tested. + // Optional: seed the config page with a hex byte string, so the password + // gate and emergency erase can be tested. // Layout: [appjump lo][appjump hi][timeout][password...][0xff]. - const char *cfg = std::getenv("TSB_CONFIG"); - if (cfg) { - std::uint32_t app_end = boot_base - 128; // config page sits directly below the boot code + if (const char *cfg = std::getenv("TSB_CONFIG")) { + const auto app_end = boot_base - page_bytes; for (int i = 0; cfg[i] && cfg[i + 1]; i += 2) { const std::array pair{cfg[i], cfg[i + 1], '\0'}; avr->flash[app_end + i / 2] = static_cast(std::strtoul(pair.data(), nullptr, 16)); @@ -119,7 +110,7 @@ int main(int argc, char *argv[]) std::signal(SIGINT, finish); for (;;) { - int state = avr_run(avr); + const auto state = avr_run(avr); if (state == cpu_Done || state == cpu_Crashed) { break; } diff --git a/test/tsbtest.py b/test/tsbtest.py index 46e15d2..6a5afbd 100644 --- a/test/tsbtest.py +++ b/test/tsbtest.py @@ -23,14 +23,13 @@ class Device: page (via the device's TSB_CONFIG hook) so the password gate and emergency erase are exercisable.""" - def __init__(self, binary, elf, boot_base, dump="/tmp/tsb_dump.bin", config=None): + def __init__(self, binary, elf, boot_base, config=None): env = dict(os.environ) if config is not None: env["TSB_CONFIG"] = config self.proc = subprocess.Popen( - [binary, elf, boot_base, dump], + [binary, elf, boot_base], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=env) - self.dump = dump self.pty = None deadline = time.time() + 5 while time.time() < deadline: @@ -166,7 +165,6 @@ class Host: self.s.write(bytes([CONFIRM])) self._expect(CONFIRM, "emergency mainloop ready") - # A wrong password byte hangs the loader, still draining the line. Two # things must not happen: it must not activate, and it must not fall # through to the emergency erase - a byte the gate has already refused @@ -236,7 +234,7 @@ def scenario_emergency(host): def scenario_wrong_password(host): """A wrong password byte neither activates the loader nor opens the emergency erase behind it - the oracle carries a dedicated fix for the - second, and nothing here exercised either half.""" + second.""" check(host.refuse_password(PW_BYTES[0] ^ 1) == b"", "a wrong password byte draws no reply") check(host.say(0x00) == b"", "a 0 byte after it does not request the erase") check(host.say(CONFIRM) == b"", "and neither does a confirm")