pureboot: a loader in the staging slot is the staging copy — leave it there

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 21:55:15 +02:00
parent 5f9e736d66
commit 76f4576fe0
3 changed files with 94 additions and 49 deletions

View File

@@ -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); 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 on the patched-vector chips it *runs* (the image is position-independent
and reset enters word 0), reports its canonical geometry, and the ordinary 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 `--update-loader` flow re-homes a build into the top slot from any
install and the word-0 redirect both execute from copies outside page 0's position — the staging install and the word-0 redirect execute from
slot, so the running-slot guard never blocks the flow (`pureboot.rehome` copies outside page 0's slot, and a copy sitting in the staging slot
is the acceptance test). Flashing the application afterwards overwrites itself is recognized as the installed staging copy and left in place (it
the stale copy, vector surgery included. The one position that cannot streams the new resident like any staged copy, so an older build installs
re-home itself is the staging slot: installing the staging copy would hit a newer one). `pureboot.rehome` is the acceptance test for both
the running copy's own guard and the update stops at its verify — flash positions. Flashing the application afterwards overwrites the stale copy,
the .hex instead. vector surgery included.
**Boot-sectioned megas**: program the loader at `flash slot` with an **Boot-sectioned megas**: program the loader at `flash slot` with an
external programmer. Every such mega has a BOOTSZ step whose boot section 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 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, 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 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 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 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 first re-aims word 0 at the staging copy, so a power loss mid-rewrite

View File

@@ -725,15 +725,33 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
state = UpdateState(state_path) state = UpdateState(state_path)
state.load_or_save(loader) state.load_or_save(loader)
# Install the staging copy. On a chip whose staging slot starts at # Install the staging copy — unless a loader already sits whole in the
# address 0 (the 1 KB tiny13A), its first page carries the reset vector: # staging slot (a build programmed there by hand): that copy IS the
# written last, so any earlier interruption still resets into the old # installed staging copy, and rewriting it would only trip its own
# resident, and from then on resets enter the staging copy. # running-slot guard on the composed through-word. Any pureboot with
order = list(range(0, info.slot, page)) # the device's own info block serves — the staged copy just streams
if info.stage == 0: # pages, so an older build installs a newer resident all the same. Two
order = order[1:] + [0] # checks make "already a loader" mean a *complete* one: the block must
if write_differing(loader, info.stage, staged, order): # sit where every image carries it (within the slot's first 256 bytes
print(f"staging copy installed at {info.stage:#06x}") # — 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 # 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 # vector routes through the resident (a tiny with the staging slot away

View File

@@ -1,13 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Re-homing acceptance test: a pureboot image mistakenly programmed at """Re-homing acceptance test: a pureboot image programmed somewhere other
address 0 of a patched-vector chip (a raw .bin handed to a programmer, which than its canonical top slot must still be a working loader —
defaults to offset 0) must still be a working loader — position-independent, position-independent, guarding its accidental slot — and the ordinary
guarding its accidental slot — and the ordinary --update-loader flow must --update-loader flow must put a build into the top slot from there.
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 Two positions are exercised. Address 0 (a raw .bin handed to a programmer,
running-slot guard never blocks the flow. Flashing an application through which defaults to offset 0): the staging install and the word-0 redirect
the healed resident then overwrites the stale copy, vector surgery included, both run from copies whose slots are not page 0's, so the running-slot
and the banner proves the launch. 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 <device_bin> <pureboot_elf> <update_bin> <mcu> <hz> Usage: pbrehome.py <device_bin> <pureboot_elf> <update_bin> <mcu> <hz>
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir> <base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
@@ -22,23 +28,15 @@ def fail(message):
sys.exit(1) sys.exit(1)
def main(): def rehome_from(pbsim, pb, device_bin, elf, place_hex, guard_probe, update_bin, base, page, baud, app_bin, workdir,
(device_bin, elf, update_bin, mcu, hz, base_hex, page, baud, app_bin, tool, workdir) = sys.argv[1:] mcu, hz):
base, baud = int(base_hex, 0), int(baud) """Place the loader at `place_hex`, heal through --update-loader, flash
sys.path.insert(0, os.path.dirname(os.path.abspath(tool))) the application, expect the banner."""
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) dump = os.path.join(workdir, f"dump-{place_hex}.bin")
import pbsim state = os.path.join(workdir, f"rehome-{place_hex}.pbstate")
import pureboot as pb
os.makedirs(workdir, exist_ok=True)
dump = os.path.join(workdir, "dump.bin")
state = os.path.join(workdir, "rehome.pbstate")
if os.path.exists(state): if os.path.exists(state):
os.unlink(state) os.unlink(state)
device = pbsim.Device(device_bin, elf, mcu, hz, place_hex, page, baud, dump, reset_hex="0")
# 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")
try: try:
port = pb.Port(device.pty, baud) port = pb.Port(device.pty, baud)
loader = pb.Loader(port) loader = pb.Loader(port)
@@ -46,14 +44,15 @@ def main():
if info.base != base: if info.base != base:
fail(f"the misplaced copy reports base {info.base:#06x} — the info block must stay canonical") 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 # The accidental slot still guards itself; re-homing rides on the
# must be writable from it (that is what re-homing rides on). # canonical slots being writable from it.
before = loader.read_flash(0, info.page) probe = int(guard_probe, 0)
loader.write_page(0, bytes(info.page)) before = loader.read_flash(probe, info.page)
if loader.read_flash(0, info.page) != before: 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") 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) pb.op_update_loader(loader, 25, update_bin, state, None)
update = open(update_bin, "rb").read() update = open(update_bin, "rb").read()
if loader.read_flash(base, len(update)) != update: if loader.read_flash(base, len(update)) != update:
@@ -67,10 +66,35 @@ def main():
pb.verify_pages(loader, pages) pb.verify_pages(loader, pages)
loader.run_application() loader.run_application()
if port.read_exact(3, 5.0) != b"APP": 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() port.close()
finally: finally:
device.stop() 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") print("pbrehome: a misplaced loader re-homes through the ordinary update flow")