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:
@@ -305,6 +305,20 @@ if(PROJECT_IS_TOP_LEVEL)
|
|||||||
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
|
set_tests_properties(pureboot.reloc PROPERTIES TIMEOUT 180
|
||||||
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
ENVIRONMENT "PB_OBJCOPY=${CMAKE_OBJCOPY}")
|
||||||
|
|
||||||
|
# Re-homing: a loader mistakenly programmed at address 0 (a raw .bin
|
||||||
|
# handed to a programmer) must heal into the canonical slot through
|
||||||
|
# the ordinary --update-loader flow. Patched-vector behavior, so one
|
||||||
|
# representative chip carries it.
|
||||||
|
if(LIBAVR_MCU STREQUAL "attiny85")
|
||||||
|
add_test(NAME pureboot.rehome
|
||||||
|
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbrehome.py
|
||||||
|
${PB_DEVICE} $<TARGET_FILE:pureboot> $<TARGET_FILE:pureboot9>.bin ${_pb_sim_mcu}
|
||||||
|
${_pb_hz} ${_pb_base_hex} ${_pb_page} ${_pb_baud} $<TARGET_FILE:pbapp>.bin
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||||
|
${CMAKE_BINARY_DIR}/pbrehome-work)
|
||||||
|
set_tests_properties(pureboot.rehome PROPERTIES TIMEOUT 180)
|
||||||
|
endif()
|
||||||
|
|
||||||
# The self-update end-to-end: the re-timed build (same source, only
|
# The self-update end-to-end: the re-timed build (same source, only
|
||||||
# PUREBOOT_TIMEOUT differs — a byte-different image) replaces the
|
# PUREBOOT_TIMEOUT differs — a byte-different image) replaces the
|
||||||
# resident through --update-loader, with every power-fail phase
|
# resident through --update-loader, with every power-fail phase
|
||||||
|
|||||||
@@ -111,6 +111,24 @@ write `0xff` (per page for flash, per byte for EEPROM).
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
|
The build leaves three artifacts per chip. The ELF is a container for the
|
||||||
|
tests and objcopy — never flashed. The **.hex is the programmer artifact**:
|
||||||
|
it carries its own addresses and lands the loader in its top slot,
|
||||||
|
touching nothing else. The **.bin is the self-update image** — the slot's
|
||||||
|
bare bytes with no addressing, which a programmer would put at address 0.
|
||||||
|
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.
|
||||||
|
|
||||||
**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
|
||||||
is exactly the loader slot — 512 B, the second-smallest step on the 8 KiB
|
is exactly the loader slot — 512 B, the second-smallest step on the 8 KiB
|
||||||
@@ -161,6 +179,14 @@ loader itself as its own staging loader. The image is the loader's own 512
|
|||||||
bytes as a raw binary, or the Intel HEX the build emits beside it, which
|
bytes as a raw binary, or the Intel HEX the build emits beside it, which
|
||||||
links the loader at its base inside an otherwise blank flash image:
|
links the loader at its base inside an otherwise blank flash image:
|
||||||
|
|
||||||
|
The preflight refuses an image built for another chip: the info block
|
||||||
|
embedded in every pureboot binary (signature, page size, loader base,
|
||||||
|
EEPROM size, flags) must match the device's own, and the error names both.
|
||||||
|
Die revisions share their base signature and geometry, so their images are
|
||||||
|
interchangeable — as the silicon is. `loader_image()` also accepts a
|
||||||
|
padded image (a raw .bin padded from 0, or a whole-flash read-back with
|
||||||
|
the loader resident) and peels it to the slot content by the embedded base.
|
||||||
|
|
||||||
1. The staging slot `[base−slot, base)` is saved to a host-side state file
|
1. The staging slot `[base−slot, base)` is saved to a host-side state file
|
||||||
(on the 1 KB tiny13s that is the whole application, vectors included).
|
(on the 1 KB tiny13s that is the whole application, vectors included).
|
||||||
2. The resident installs the identical update image there. On the
|
2. The resident installs the identical update image there. On the
|
||||||
|
|||||||
78
test/pbrehome.py
Normal file
78
test/pbrehome.py
Normal 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()
|
||||||
@@ -155,6 +155,21 @@ def main():
|
|||||||
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
if pb.image_info(bytes((0xAA,)) * 40) is not None:
|
||||||
fail("image_info invents a block")
|
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.
|
# Update preflight: the full fuse matrix, plus target mismatch.
|
||||||
other = info_of(pb, 0x1E00, 32, True, 0x2000)
|
other = info_of(pb, 0x1E00, 32, True, 0x2000)
|
||||||
expect_error("wrong-target image", lambda: pb.update_preflight(binary, other, None), "another target")
|
expect_error("wrong-target image", lambda: pb.update_preflight(binary, other, None), "another target")
|
||||||
|
|||||||
Reference in New Issue
Block a user