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

@@ -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 <device_bin> <pureboot_elf> <update_bin> <mcu> <hz>
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
@@ -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")