pureboot: artifact roles, wrong-chip refusal, and misplaced-loader re-homing

The README's deployment section now says what each build artifact is for:
the .hex is the programmer artifact (self-addressed into the top slot),
the .bin the self-update image — bare slot bytes a programmer would put
at address 0, where a boot-sectioned mega cannot even heal itself (SPM
only runs from the boot section) but a patched-vector chip runs the
position-independent copy and re-homes a build through the ordinary
--update-loader flow: the staging install and the word-0 redirect both
execute outside page 0's slot, so the running-slot guard never blocks it.
pbrehome.py is the acceptance test (misplaced at 0, guard intact,
re-home, app flash over the stale copy, banner); the staging slot is the
one position that cannot re-home itself, documented. The preflight's
wrong-chip refusal and loader_image's handling of padded images (peeled
to the slot content by the embedded base) are documented and the padded
case pinned in the planner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 21:18:10 +02:00
parent 9cbe2d682d
commit 5f9e736d66
4 changed files with 133 additions and 0 deletions

78
test/pbrehome.py Normal file
View File

@@ -0,0 +1,78 @@
#!/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.
Usage: pbrehome.py <device_bin> <pureboot_elf> <update_bin> <mcu> <hz>
<base_hex> <page> <baud> <app_bin> <tool_py> <workdir>
"""
import os
import sys
def fail(message):
print(f"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")
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")
try:
port = pb.Port(device.pty, baud)
loader = pb.Loader(port)
info = loader.connect(25)
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:
fail("the misplaced copy's guard let its own slot change")
# The ordinary update flow re-homes 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:
fail("the canonical slot does not hold the update image")
# An application flashed through the healed resident overwrites the
# stale copy (surgery included) and launches.
pages = pb.plan_flash(open(app_bin, "rb").read(), loader.info)
for address in pb.covered(pages, loader.info, skip_blank=False):
loader.write_page(address, pages[address])
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")
port.close()
finally:
device.stop()
print("pbrehome: a misplaced loader re-homes through the ordinary update flow")
if __name__ == "__main__":
main()

View File

@@ -155,6 +155,21 @@ def main():
if pb.image_info(bytes((0xAA,)) * 40) is not None:
fail("image_info invents a block")
# loader_image must peel a padded image down to the slot content: a raw
# .bin padded from address 0 (or a whole-flash read-back with the loader
# resident at base) yields the same bytes as the bare slot image.
import tempfile
slot_image = bytes((0xAA,)) * 10 + tiny.raw + bytes((0xCC,)) * 40
padded = bytes((0xFF,)) * tiny.base + slot_image
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as f:
f.write(padded)
padded_path = f.name
try:
if pb.loader_image(padded_path) != slot_image:
fail("loader_image does not peel a padded image to the slot content")
finally:
os.unlink(padded_path)
# Update preflight: the full fuse matrix, plus target mismatch.
other = info_of(pb, 0x1E00, 32, True, 0x2000)
expect_error("wrong-target image", lambda: pb.update_preflight(binary, other, None), "another target")