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 <noreply@anthropic.com>
This commit is contained in:
BlackMarkandClaude Opus 5.5 committed 2026-10-23 21:41:55 +02:00
1 parent aafaf25a47
commit bc671618a7
2 files changed
+20 -31

No files matched your search

+17 -26
View File
@@ -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 <array>
#include <csignal>
#include <cstdint>
@@ -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: {} <tsb.elf> <boot_base_hex> [flash_dump.bin]", argv[0]);
if (argc != 3) {
std::println(stderr, "usage: {} <tsb.elf> <boot_base_hex>", argv[0]);
return 2;
}
auto boot_base = static_cast<std::uint32_t>(std::strtoul(argv[2], nullptr, 0));
dump_path = argc >= 4 ? argv[3] : nullptr;
const auto boot_base = static_cast<std::uint32_t>(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::uint8_t>(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;
}
+3 -5
View File
@@ -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")