pureboot: one position-independent binary — its own staging loader

The image now runs from any 512-byte slot with every command intact:
control flow stays PC-relative, the write guard keys on the running slot
(the return-address anchor, computed once), the info block is addressed
from that same anchor as a byte pair (no absolute 16-bit address in the
image), and the application jump is an indirect call through a noipa-
laundered pointer to the absolute entry. 'J' — jump to a wire word
address, the one transfer primitive — replaces 'G': the host knows the
application entry from the info block, and moving between loader copies
needs arbitrary targets. The activation window is a compile-time 8 s
(PUREBOOT_TIMEOUT overrides), counted as a single calibrated poll loop.

A refused page no longer poisons the write-once temporary buffer (a real
silicon trap: the next write would program the drained data): every page
write discards the buffer first — CTPB on the tinies, on the mega the same
RWWSRE store that re-enables RWW after programming. The tinies' post-op
busy-waits go with it: their CPU halts through page erase and write.

488 / 502 / 504 B on t13a / t85 / mega — under the tinies' 510-byte budget,
whose last slot word is the host-managed trampoline: the resident's holds
the application entry, a staging copy's the jump through which an abandoned
update still times out into a loader.

The host tool updates the loader with itself: --update-loader installs the
identical image one slot below the resident, jumps into it, lets it rewrite
the resident, and restores the staging region from a state file — each
phase idempotent off the flash state, resumable after any interruption
(t13a: the staging slot carries the reset vector, written last in and
first out; t85: word 0 redirected around the resident rewrite; mega:
fuse-matrix preflight with a hard BOOTSZ gate and --assume-fuses for
simulators). Application flashing recovers by reset from any interruption:
patched page 0 and trampoline first, erase descending, and a walk-region
refusal behind --force on BOOTRST-below-loader megas.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-20 19:37:43 +02:00
parent 6594c1b044
commit f964b875b9
4 changed files with 602 additions and 177 deletions

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""End-to-end pureboot protocol test: spawn the simavr device, then drive it
with the real host tool (pureboot.py, as a subprocess over the device's pty)
through flash + EEPROM + timeout + fuse + hand-over scenarios, and cross-check
through flash + EEPROM + fuse + hand-over scenarios, and cross-check
the tool's view against the simulator's ground-truth memory dumps.
Usage: pbtest.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
@@ -101,8 +101,8 @@ def main():
try:
# Session 1: knock from reset, identify, program everything, stay.
out = run_tool(tool, device.pty, baud, "--info", "--fuses", "--flash", app_bin,
"--eeprom", ee_path, "--timeout", "8", "--stay")
for needed in ("device: signature", "fuses:", "verify:", "activation timeout: 8 s", "stays"):
"--eeprom", ee_path, "--stay")
for needed in ("device: signature", "fuses:", "verify:", "stays"):
if needed not in out:
fail(f"session 1 output lacks {needed!r}")
@@ -116,8 +116,6 @@ def main():
eeprom_back = open(read_eeprom, "rb").read()
if eeprom_back[: len(ee_image)] != ee_image:
fail("EEPROM read-back mismatch")
if eeprom_back[-1] != 8:
fail(f"timeout cell reads {eeprom_back[-1]}, expected 8")
# The expected post-surgery flash, straight from the tool's planner.
pages = pb.plan_flash(open(app_bin, "rb").read(), info)
@@ -128,16 +126,14 @@ def main():
# An external reset re-enters through the patched word 0 (tinies; the
# runner resets them to address 0 like silicon) or BOOTRST (mega).
# The loader must answer a fresh knock, and 'G' must land in the
# application, which banners on the same link.
# The loader must answer a fresh knock, and the 'J' hand-over must
# land in the application, which banners on the same link.
device.proc.send_signal(signal.SIGUSR1)
port = pb.Port(device.pty, baud)
try:
loader = pb.Loader(port)
loader.connect(15)
port.write(b"G")
if port.read_exact(1, 5.0) != pb.PROMPT:
fail("no ack for G")
loader.run_application()
banner = port.read_exact(3, 5.0)
if banner != b"APP":
fail(f"application banner was {banner!r}")
@@ -168,7 +164,7 @@ def main():
ee_true_path = dump + ".eeprom"
if os.path.exists(ee_true_path):
ee_true = open(ee_true_path, "rb").read()
if ee_true[: len(ee_image)] != ee_image or ee_true[-1] != 8:
if ee_true[: len(ee_image)] != ee_image:
fail("ground-truth EEPROM does not match what was programmed")
print("pbtest: all scenarios pass")