pureboot: PI lint, tightened gates, and the self-update test suite
check_pi.py asserts the two link-time facts position independence rests on (no absolute jmp/call; the info block within the image's first 256 bytes); the size gates drop to 510 on the tinies for the trampoline word. New per-chip tests beside the reworked protocol test: the planner units (programming orders and their recovery properties, the surgery, staging composition, boot-fuse decode, and the update preflight's error/warning matrix over synthetic fuse bytes), the relocated-copy sweep (the identical image installed one slot lower serves the full command set — the PI acceptance test, and the one that caught the temporary-buffer trap), and the self-update end-to-end: --update-loader to a re-timed build (pureboot9, byte-different by PUREBOOT_TIMEOUT alone), then every power-fail phase killed mid-write, restarted from the runner's flash dump, and completed by a re-run with the application intact throughout. The mega rounds run the BOOTRST-unprogrammed profile: the fixture application's 'L' jump is the application-owned loader entry that profile relies on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
93
test/pbreloc.py
Normal file
93
test/pbreloc.py
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Position-independence acceptance test: the identical pureboot binary,
|
||||
flashed one slot below the resident loader, must serve the complete command
|
||||
set from there. The resident installs it (through-word composed by the host
|
||||
layer), 'J' transfers control, and every command is exercised against the
|
||||
staged copy — the info block must come back byte-identical, the write guard
|
||||
must protect the staged copy's own slot and permit the resident's, and the
|
||||
staged copy must be able to rewrite the resident slot verbatim.
|
||||
|
||||
Usage: pbreloc.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||
<baud> <tool_py> <workdir>
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def fail(message):
|
||||
print(f"FAIL: {message}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
device_bin, elf, mcu, hz, base_hex, page, baud, tool, workdir = sys.argv[1:]
|
||||
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
||||
stage = base - 512
|
||||
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)
|
||||
objcopy = os.environ.get("PB_OBJCOPY", "avr-objcopy")
|
||||
image_path = os.path.join(workdir, "pureboot.bin")
|
||||
subprocess.run([objcopy, "-O", "binary", elf, image_path], check=True)
|
||||
image = open(image_path, "rb").read()
|
||||
|
||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, os.path.join(workdir, "dump.bin"))
|
||||
try:
|
||||
port = pb.Port(device.pty, baud)
|
||||
loader = pb.Loader(port)
|
||||
info = loader.connect(25)
|
||||
if info.base != base:
|
||||
fail(f"info reports base {info.base:#06x}")
|
||||
resident_info = info.raw
|
||||
|
||||
# Install the staging copy exactly as the update flow would.
|
||||
staged = pb.staging_content(image, info)
|
||||
pb.write_differing(loader, stage, staged)
|
||||
|
||||
# Enter it; from here on, every command runs in the relocated copy.
|
||||
staged_info = loader.enter_copy(stage, 25)
|
||||
if staged_info.raw != resident_info:
|
||||
fail(f"staged info {staged_info.raw.hex()} != resident info {resident_info.hex()}")
|
||||
|
||||
# 'R' from the staged copy already proved itself in the install
|
||||
# verify; 'F' must answer 4 bytes (values are unmodeled in simavr).
|
||||
if len(loader.read_fuses()) != 4:
|
||||
fail("fuse read from the staged copy")
|
||||
|
||||
# EEPROM round-trip through the staged copy.
|
||||
pattern = bytes(range(0x50, 0x60))
|
||||
loader.write_eeprom(0, pattern)
|
||||
if loader.read_eeprom(0, len(pattern)) != pattern:
|
||||
fail("EEPROM round-trip through the staged copy")
|
||||
|
||||
# The guard, both ways: its own slot refused (drained, unchanged),
|
||||
# the resident slot writable.
|
||||
before = loader.read_flash(stage, page)
|
||||
loader.write_page(stage, bytes(page))
|
||||
if loader.read_flash(stage, page) != before:
|
||||
fail("the staged copy's guard let its own slot change")
|
||||
marker = bytes((i * 3) & 0xFF for i in range(page))
|
||||
loader.write_page(base, marker)
|
||||
if loader.read_flash(base, page) != marker:
|
||||
fail("the staged copy could not write the resident slot")
|
||||
|
||||
# Restore the resident image through the staged copy, then 'J' back
|
||||
# into it and prove it lives.
|
||||
resident = image + b"\xff" * (512 - len(image))
|
||||
pb.write_differing(loader, base, resident)
|
||||
back_info = loader.enter_copy(base, 25)
|
||||
if back_info.raw != resident_info:
|
||||
fail("the restored resident does not serve its info block")
|
||||
port.close()
|
||||
finally:
|
||||
device.stop()
|
||||
print("pbreloc: the relocated copy serves the full command set")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user