build: the libavr pin advances 29 commits, and selfwrite stops being flaky
dc1e87d -> aec9955. Every generated workflow green on all 37 chips, and no image moves: this loader uses uart, spm, eeprom and startup, and the library's advance is in i2c, the uart ring's field order, percent_t's constructor and a spare-vector stub, none of which pureboot links. The gate came back red on atmega16 and atmega32a, both pureboot.selfwrite, and the advance is not why. Measured at both pins over twenty runs each: 2/20 red at dc1e87d and 1/20 at aec9955, so the flake predates the pin and the eight clean runs that first suggested otherwise were luck. The cause is in the test. It writes the sealed erase and waits with read_exact(2, 2.0) - but the loader issues its verdict *before* the SPM, as the comment above that line already said, so the reply arrives while the erase has not happened and device.stop() then races it. That is why every failure was fast (0.39 s, 0.64 s) and every pass slow (2.44 s): the runs that passed were the ones whose read timed out. The second mode is the same race seen from the host - the loader erases its own command loop mid-reply, the pty closes, and errno 5 escapes an except that names only pb.Error. So the wait is a settle nothing may shorten, and a closing pty ends it rather than escaping it. A fixed settle is still wall clock against the simulator's progress through it, which is load-dependent - it measured 1/10 red with the machine saturated - so the scenario is attempted with a doubling budget and the claim stays exact: a loader that does not erase fails every attempt. 0/30 quiet and 0/20 with all four cores saturated, against 2/20 before. Red-checked by settling for zero, which still reports the erase never landed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2
libavr
2
libavr
Submodule libavr updated: dc1e87d86e...aec9955ad3
@@ -19,6 +19,15 @@ Usage: pbselfwrite.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
# How long the device is left running after the sealed command, for the frame
|
||||||
|
# to arrive at the wire's rate and the erase to reach flash. It is spent in
|
||||||
|
# full on every attempt, because the only cheaper signal - the loader answering
|
||||||
|
# - is the one that precedes the SPM.
|
||||||
|
settle_seconds = 2.0
|
||||||
|
settle_attempts = 3
|
||||||
|
|
||||||
|
|
||||||
def fail(message):
|
def fail(message):
|
||||||
@@ -38,16 +47,9 @@ def sealed_frame(pb, op, space, address, count):
|
|||||||
return head + bytes((seal,))
|
return head + bytes((seal,))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def scenario(pb, pbsim, device_bin, elf, mcu, hz, base_hex, page, baud, dump, base, settle):
|
||||||
device_bin, elf, mcu, hz, base_hex, page, baud, tool, workdir = sys.argv[1:]
|
"""The whole scenario once, answering with the running page as the
|
||||||
base, page, baud = int(base_hex, 0), int(page), int(baud)
|
simulator's own flash holds it afterwards."""
|
||||||
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")
|
|
||||||
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump)
|
device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump)
|
||||||
try:
|
try:
|
||||||
port = pb.Port(device.pty, baud)
|
port = pb.Port(device.pty, baud)
|
||||||
@@ -74,29 +76,57 @@ def main():
|
|||||||
if alive == b"\xff" * 8:
|
if alive == b"\xff" * 8:
|
||||||
fail("the refused erase happened anyway - the running page reads erased")
|
fail("the refused erase happened anyway - the running page reads erased")
|
||||||
|
|
||||||
# Green: the identical command, correctly sealed. Nothing is required
|
# Green: the identical command, correctly sealed. The claim is that the
|
||||||
# of the link from here on. The verdict is *issued* before the SPM, but
|
# erase reached flash, and the dump is the only witness for it - it
|
||||||
# the erase takes the code that would have finished saying it, and how
|
# tells "accepted and performed" from "merely answered".
|
||||||
# much of it survives is the chip's business - an erase removes one page
|
#
|
||||||
# and nothing else, so a loader whose command loop lives past the page
|
# Nothing the link says may shorten the settle, because the verdict is
|
||||||
# erased will prompt as usual where one with 128-byte pages goes with
|
# issued *before* the SPM: a prompt reply means the erase has not
|
||||||
# the stub. None of that is the claim. The claim is that the erase
|
# happened yet, and stopping the device on it races the write. The link
|
||||||
# reached flash, and the dump is both the only witness for it and a
|
# dying here is an expected outcome rather than a failure - the loader
|
||||||
# better one: it tells "accepted and performed" from "merely answered".
|
# is erasing its own command loop - so a closing pty ends the settle
|
||||||
|
# instead of escaping it.
|
||||||
port.write(frame)
|
port.write(frame)
|
||||||
|
deadline = time.monotonic() + settle
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
try:
|
||||||
|
port.read_available(0.1)
|
||||||
|
except (pb.Error, OSError):
|
||||||
|
break
|
||||||
try:
|
try:
|
||||||
port.read_exact(2, 2.0)
|
|
||||||
except pb.Error:
|
|
||||||
pass
|
|
||||||
port.close()
|
port.close()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
finally:
|
finally:
|
||||||
device.stop()
|
device.stop()
|
||||||
|
|
||||||
# Ground truth: the simulator's flash, not the loader's opinion of it.
|
# Ground truth: the simulator's flash, not the loader's opinion of it.
|
||||||
flash = open(dump, "rb").read()
|
return open(dump, "rb").read()[base : base + page]
|
||||||
if flash[base : base + page] != b"\xff" * page:
|
|
||||||
fail("the sealed erase did not reach flash - the running page is intact")
|
|
||||||
|
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)
|
||||||
|
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")
|
||||||
|
|
||||||
|
# A settle is wall clock and the erase is the simulator's progress through
|
||||||
|
# it, so a loaded machine needs more of the first for the same amount of
|
||||||
|
# the second - which is what made a single fixed wait flaky under the gate's
|
||||||
|
# own parallelism. Doubling until the claim holds keeps the claim exact: a
|
||||||
|
# loader that does not erase fails every attempt, and only the budget moves.
|
||||||
|
settle = settle_seconds
|
||||||
|
for _ in range(settle_attempts):
|
||||||
|
if scenario(pb, pbsim, device_bin, elf, mcu, hz, base_hex, page, baud, dump, base, settle) == b"\xff" * page:
|
||||||
print("pbselfwrite: the running slot is refused unsealed and erased sealed")
|
print("pbselfwrite: the running slot is refused unsealed and erased sealed")
|
||||||
|
return
|
||||||
|
settle *= 2
|
||||||
|
fail("the sealed erase did not reach flash - the running page is intact")
|
||||||
|
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user