pureboot v9: seal every command, and stop guarding what the seal covers
'W' handed the loader a whole page with no ack inside it and sp_spm handed any wire byte to SPMCSR, so a dropped byte re-aligned the stream and page data arrived where commands belong. That is how a page-address byte became BLBSET|SELFPRGEN on the tempmon board and programmed its lock bits. The first answer was to refuse that one command. It was the wrong shape twice over: it forbade a lock-bit write the owner may want, and it left every other command decided by bytes nobody checked. v9 checks them instead. One header for every command — opcode, selector, address, count, seal — folded and compared before the command is decoded, and *answered* before any payload moves: '+' accepts, 0xd4 (the ack inverted) refuses and nothing happened. An ack cannot do this job; it reports a command that has already run. It is smaller than v8 everywhere: 1284P 506→480, m8 498→480, 328P 484→468, t13A 474→460. The seal costs 14 bytes; bit opcodes in place of the letters pay for it twice over, since a letter costs a compare and a branch where a bit costs a skip. Both guards go — the lock-bit refusal because the seal covers it, the running-slot write guard because what it defended against was a wire fault naming an address and a wire fault can no longer name one. That one is a real trade: a host bug aimed at the running slot now lands. It buys a resident copy that can write its own slot, which is the only self-update route on a chip whose boot section *is* the slot. Two things the tests caught, both introduced here. Removing the invalid-opcode arm made every byte a command, so the knock stopped being harmless against a loader already in session and ate the five bytes behind it — identify moves to bit 5, which both 'p' and 'b' carry, so the knock is inert again and version discovery still works before the version is known. And the SPM value rides the count field because a data byte would arrive after the seal was checked. pbselfwrite and pbglitch are the new gates, both red-green: the same erase of the running page refused unsealed and performed sealed, and every header byte damaged after sealing refused where the identical damage before sealing is obeyed. Both judge by the simulator's flash, not the loader's opinion of it. pbreloc and pbrehome lose their write-guard probes, which is what those two gates replace. Defeating the seal in the loader turns seven tests red. 37 of 37 chips green with the exhaustive size matrix; README protocol section and every size row rewritten. pbhw gains an adversarial --seal-rounds sweep for the bench. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -131,19 +131,42 @@ def main():
|
||||
fail(f"loader reports pureboot {live.version}, the tool speaks "
|
||||
f"{pb.OLDEST_LOADER}..{pb.NEWEST_LOADER}")
|
||||
|
||||
# A W addressed inside a page rather than at its base must still
|
||||
# A fill addressed inside a page rather than at its base must still
|
||||
# consume exactly one page and prompt. The loader's own slot is the
|
||||
# target — the guard refuses to commit it — and the payload is
|
||||
# erased-state bytes, so the probe can disturb neither the image nor
|
||||
# the page buffer it leaves behind. Hand-built rather than through
|
||||
# write_page(), which would follow the fill with its erase and
|
||||
# write; the point here is that the fill alone consumes exactly one
|
||||
# page whatever the address's low bits say.
|
||||
# target and the payload is erased-state bytes, so the probe can
|
||||
# disturb neither the image nor the page buffer it leaves behind: a
|
||||
# fill only loads the buffer, and nothing commits it. Hand-built
|
||||
# rather than through write_page(), which would follow the fill
|
||||
# with its erase and write; the point here is that the fill alone
|
||||
# consumes exactly one page whatever the address's low bits say.
|
||||
# Hand-sealed too — a protocol probe that borrowed the tool's own
|
||||
# frame builder could not tell a wrong frame from a wrong loader.
|
||||
wire = base + 1
|
||||
port.write(bytes((ord("W"), pb.selector(pb.SP_FLASH, wire), wire & 0xFF, (wire >> 8) & 0xFF))
|
||||
+ b"\xff" * page)
|
||||
head = bytes((pb.OP_FILL, pb.selector(pb.SP_FLASH, wire), wire & 0xFF,
|
||||
(wire >> 8) & 0xFF, page & 0xFF))
|
||||
seal = pb.SEAL
|
||||
for byte in head:
|
||||
seal ^= byte
|
||||
port.write(head + bytes((seal,)))
|
||||
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||
fail("unaligned W did not return to the prompt")
|
||||
fail("the loader refused a correctly sealed fill")
|
||||
port.write(b"\xff" * page)
|
||||
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||
fail("unaligned fill did not return to the prompt")
|
||||
|
||||
# And the seal itself, red: one wrong bit in the address of that
|
||||
# same frame must be refused outright. The verdict has to arrive
|
||||
# *before* the page would have been sent — that ordering is what
|
||||
# keeps a refusal from turning into a desync — so the probe sends
|
||||
# no payload at all and expects the loader straight back at the
|
||||
# command level.
|
||||
broken = bytearray(head + bytes((seal,)))
|
||||
broken[2] ^= 0x01
|
||||
port.write(bytes(broken))
|
||||
if port.read_exact(1, 5.0) != pb.NAK:
|
||||
fail("a header with a broken seal was not refused")
|
||||
if port.read_exact(1, 5.0) != pb.PROMPT:
|
||||
fail("the loader did not re-prompt after refusing a broken seal")
|
||||
|
||||
loader.run_application()
|
||||
banner = port.read_exact(3, 5.0)
|
||||
|
||||
Reference in New Issue
Block a user