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:
53
test/check_pi.py
Normal file
53
test/check_pi.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Position-independence lint for the pureboot image.
|
||||
|
||||
The self-staging design lets the identical binary run from any 512-byte
|
||||
slot, which holds only if nothing in the image addresses itself absolutely.
|
||||
Two link-time facts guarantee it, both asserted here from the built ELF:
|
||||
|
||||
1. No absolute jmp/call opcodes — all control flow is PC-relative
|
||||
(rjmp/rcall/ijmp/icall). -mrelax normally guarantees this; a code
|
||||
change that grows a branch out of relaxation range would break it
|
||||
silently.
|
||||
2. The info block sits within the image's first 256 bytes: the 'b'
|
||||
command rebuilds its address as (running slot high byte : low byte of
|
||||
the link address), which needs the offset to fit that low byte.
|
||||
|
||||
Usage: check_pi.py <objdump> <nm> <elf> <text_start_hex>
|
||||
"""
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
objdump, nm, elf, text_start = sys.argv[1:]
|
||||
text_start = int(text_start, 0)
|
||||
|
||||
listing = subprocess.run([objdump, "-d", elf], capture_output=True, text=True, check=True).stdout
|
||||
absolute = [
|
||||
line
|
||||
for line in listing.splitlines()
|
||||
if re.search(r"\t(jmp|call)\t", line)
|
||||
]
|
||||
if absolute:
|
||||
print("FAIL: absolute control flow in the image:")
|
||||
print("\n".join(absolute))
|
||||
sys.exit(1)
|
||||
|
||||
symbols = subprocess.run([nm, "-C", elf], capture_output=True, text=True, check=True).stdout
|
||||
info = [line for line in symbols.splitlines() if "flash_table" in line and "::storage" in line]
|
||||
if len(info) != 1:
|
||||
print(f"FAIL: expected one info-block storage symbol, found {len(info)}")
|
||||
sys.exit(1)
|
||||
offset = int(info[0].split()[0], 16) - text_start
|
||||
if not 0 <= offset < 256:
|
||||
print(f"FAIL: info block at image offset {offset:#x}, must sit in the first 256 bytes")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"PI lint: control flow PC-relative, info block at offset {offset:#x}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user