From 76f4576fe094776053d7f8bd786d9305fa282878 Mon Sep 17 00:00:00 2001 From: BlackMark Date: Tue, 21 Jul 2026 21:55:15 +0200 Subject: [PATCH] =?UTF-8?q?pureboot:=20a=20loader=20in=20the=20staging=20s?= =?UTF-8?q?lot=20is=20the=20staging=20copy=20=E2=80=94=20leave=20it=20ther?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The update flow's first step wrote the staging content over whatever the staging slot held; with a loader running right there (programmed by hand onto erased flash), that write met the copy's own running-slot guard on the composed through-word and the tool stopped at its verify — although the copy is exactly an installed staging copy, able to stream the new resident like any other. The install is now skipped when the slot holds a complete loader: its info block where every image carries it, matching the device's byte for byte, and the slot unchanged since the update began (the state file's snapshot) — so a resumed half-written install still differs from its snapshot and takes the install path, which completes it. pbrehome gains the staging-slot position (an older build at stage streaming a newer resident in); the README's wrong "cannot re-home from the staging slot" claim is corrected. Co-Authored-By: Claude Fable 5 --- pureboot/README.md | 21 ++++++----- pureboot/pureboot.py | 36 ++++++++++++++----- test/pbrehome.py | 86 ++++++++++++++++++++++++++++---------------- 3 files changed, 94 insertions(+), 49 deletions(-) diff --git a/pureboot/README.md b/pureboot/README.md index 49d85ae..0a485cd 100644 --- a/pureboot/README.md +++ b/pureboot/README.md @@ -120,14 +120,14 @@ On a boot-sectioned mega a copy at 0 is dead weight (SPM only executes from the boot section, so it cannot even heal itself — reflash the .hex); on the patched-vector chips it *runs* (the image is position-independent and reset enters word 0), reports its canonical geometry, and the ordinary -`--update-loader` flow re-homes a build into the top slot — the staging -install and the word-0 redirect both execute from copies outside page 0's -slot, so the running-slot guard never blocks the flow (`pureboot.rehome` -is the acceptance test). Flashing the application afterwards overwrites -the stale copy, vector surgery included. The one position that cannot -re-home itself is the staging slot: installing the staging copy would hit -the running copy's own guard and the update stops at its verify — flash -the .hex instead. +`--update-loader` flow re-homes a build into the top slot from any +position — the staging install and the word-0 redirect execute from +copies outside page 0's slot, and a copy sitting in the staging slot +itself is recognized as the installed staging copy and left in place (it +streams the new resident like any staged copy, so an older build installs +a newer one). `pureboot.rehome` is the acceptance test for both +positions. Flashing the application afterwards overwrites the stale copy, +vector surgery included. **Boot-sectioned megas**: program the loader at `flash − slot` with an external programmer. Every such mega has a BOOTSZ step whose boot section @@ -193,7 +193,10 @@ the loader resident) and peels it to the slot content by the embedded base. patched-vector chips the host composes the slot's last word — the same address as the resident's trampoline — as a jump to the resident base, so even an abandoned staging copy times out into a loader, never into - garbage. + garbage. A loader already sitting whole in the staging slot (its info + block in place, the slot unchanged since the update began) is left as + the staging copy instead — rewriting it would only meet its own + running-slot guard. 3. `J` enters the staging copy, which rewrites the resident slot. On the patched-vector chips whose staging slot sits away from page 0 the host first re-aims word 0 at the staging copy, so a power loss mid-rewrite diff --git a/pureboot/pureboot.py b/pureboot/pureboot.py index f97f8dd..2693cbe 100644 --- a/pureboot/pureboot.py +++ b/pureboot/pureboot.py @@ -725,15 +725,33 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes): state = UpdateState(state_path) state.load_or_save(loader) - # Install the staging copy. On a chip whose staging slot starts at - # address 0 (the 1 KB tiny13A), its first page carries the reset vector: - # written last, so any earlier interruption still resets into the old - # resident, and from then on resets enter the staging copy. - order = list(range(0, info.slot, page)) - if info.stage == 0: - order = order[1:] + [0] - if write_differing(loader, info.stage, staged, order): - print(f"staging copy installed at {info.stage:#06x}") + # Install the staging copy — unless a loader already sits whole in the + # staging slot (a build programmed there by hand): that copy IS the + # installed staging copy, and rewriting it would only trip its own + # running-slot guard on the composed through-word. Any pureboot with + # the device's own info block serves — the staged copy just streams + # pages, so an older build installs a newer resident all the same. Two + # checks make "already a loader" mean a *complete* one: the block must + # sit where every image carries it (within the slot's first 256 bytes + # — the build's position lint), matching the device's block byte for + # byte, and the slot must be unchanged since this update began (the + # state file's snapshot) — a resumed, half-written install differs + # from its snapshot and takes the install path below, which completes + # it page by page. + current = loader.read_flash(info.stage, info.slot) + staged_loader = image_info(current[:268]) + if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging: + print(f"staging slot already holds a loader — left in place") + else: + # On a chip whose staging slot starts at address 0 (the 1 KB + # tiny13s), its first page carries the reset vector: written last, + # so any earlier interruption still resets into the old resident, + # and from then on resets enter the staging copy. + order = list(range(0, info.slot, page)) + if info.stage == 0: + order = order[1:] + [0] + if write_differing(loader, info.stage, staged, order): + print(f"staging copy installed at {info.stage:#06x}") # Enter it and let it rewrite the resident slot. Where a patched reset # vector routes through the resident (a tiny with the staging slot away diff --git a/test/pbrehome.py b/test/pbrehome.py index 2ffb43f..56f1a32 100644 --- a/test/pbrehome.py +++ b/test/pbrehome.py @@ -1,13 +1,19 @@ #!/usr/bin/env python3 -"""Re-homing acceptance test: a pureboot image mistakenly programmed at -address 0 of a patched-vector chip (a raw .bin handed to a programmer, which -defaults to offset 0) must still be a working loader — position-independent, -guarding its accidental slot — and the ordinary --update-loader flow must -re-home a build into the canonical top slot: the staging install and the -word-0 redirect both run from copies whose slots are not page 0's, so the -running-slot guard never blocks the flow. Flashing an application through -the healed resident then overwrites the stale copy, vector surgery included, -and the banner proves the launch. +"""Re-homing acceptance test: a pureboot image programmed somewhere other +than its canonical top slot must still be a working loader — +position-independent, guarding its accidental slot — and the ordinary +--update-loader flow must put a build into the top slot from there. + +Two positions are exercised. Address 0 (a raw .bin handed to a programmer, +which defaults to offset 0): the staging install and the word-0 redirect +both run from copies whose slots are not page 0's, so the running-slot +guard never blocks the flow. The staging slot itself: a loader already +sitting there IS the installed staging copy — the tool recognizes it by +its embedded info block and leaves it in place instead of tripping the +copy's own guard on the composed through-word — and that (older) copy +streams the new resident like any staged copy. In both cases flashing an +application through the healed resident overwrites the stale copy, vector +surgery included, and the banner proves the launch. Usage: pbrehome.py @@ -22,23 +28,15 @@ def fail(message): sys.exit(1) -def main(): - (device_bin, elf, update_bin, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:] - base, baud = int(base_hex, 0), int(baud) - sys.path.insert(0, os.path.dirname(os.path.abspath(tool))) - sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) - import pbsim - import pureboot as pb - - os.makedirs(workdir, exist_ok=True) - dump = os.path.join(workdir, "dump.bin") - state = os.path.join(workdir, "rehome.pbstate") +def rehome_from(pbsim, pb, device_bin, elf, place_hex, guard_probe, update_bin, base, page, baud, app_bin, workdir, + mcu, hz): + """Place the loader at `place_hex`, heal through --update-loader, flash + the application, expect the banner.""" + dump = os.path.join(workdir, f"dump-{place_hex}.bin") + state = os.path.join(workdir, f"rehome-{place_hex}.pbstate") if os.path.exists(state): os.unlink(state) - - # The runner's base argument places the firmware: 0 is the misplaced - # flash, and the patched-vector reset at word 0 lands in it directly. - device = pbsim.Device(device_bin, elf, mcu, hz, "0x0", int(page), baud, dump, reset_hex="0") + device = pbsim.Device(device_bin, elf, mcu, hz, place_hex, page, baud, dump, reset_hex="0") try: port = pb.Port(device.pty, baud) loader = pb.Loader(port) @@ -46,14 +44,15 @@ def main(): if info.base != base: fail(f"the misplaced copy reports base {info.base:#06x} — the info block must stay canonical") - # The accidental slot still guards itself; the canonical base slot - # must be writable from it (that is what re-homing rides on). - before = loader.read_flash(0, info.page) - loader.write_page(0, bytes(info.page)) - if loader.read_flash(0, info.page) != before: + # The accidental slot still guards itself; re-homing rides on the + # canonical slots being writable from it. + probe = int(guard_probe, 0) + before = loader.read_flash(probe, info.page) + loader.write_page(probe, bytes(info.page)) + if loader.read_flash(probe, info.page) != before: fail("the misplaced copy's guard let its own slot change") - # The ordinary update flow re-homes the build into the top slot. + # The ordinary update flow puts the build into the top slot. pb.op_update_loader(loader, 25, update_bin, state, None) update = open(update_bin, "rb").read() if loader.read_flash(base, len(update)) != update: @@ -67,10 +66,35 @@ def main(): pb.verify_pages(loader, pages) loader.run_application() if port.read_exact(3, 5.0) != b"APP": - fail("application does not banner after the re-home") + fail(f"application does not banner after the re-home from {place_hex}") port.close() finally: device.stop() + + +def main(): + (device_bin, elf, update_bin, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:] + base, page, baud = int(base_hex, 0), int(page), int(baud) + sys.path.insert(0, os.path.dirname(os.path.abspath(tool))) + sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + import pbsim + import pureboot as pb + + os.makedirs(workdir, exist_ok=True) + + # Address 0: the raw-.bin-to-a-programmer accident. The guard probe is + # the copy's own page 0. + rehome_from(pbsim, pb, device_bin, elf, "0x0", "0x0", update_bin, base, page, baud, app_bin, workdir, mcu, hz) + print("re-home from address 0: converged") + + # The staging slot: erased flash with the loader sitting exactly where + # a staging copy would — the tool must leave it in place and let it + # stream the (different) update build into the resident slot. + stage = base - 512 + rehome_from(pbsim, pb, device_bin, elf, hex(stage), hex(stage), update_bin, base, page, baud, app_bin, workdir, + mcu, hz) + print("re-home from the staging slot: converged") + print("pbrehome: a misplaced loader re-homes through the ordinary update flow")