diff --git a/libavr b/libavr index dc1e87d..aec9955 160000 --- a/libavr +++ b/libavr @@ -1 +1 @@ -Subproject commit dc1e87d86e8718c8d6d3b926dadfd5633995fdbf +Subproject commit aec9955ad390bb0888eea4f0c21ea7936c7e879f diff --git a/test/pbselfwrite.py b/test/pbselfwrite.py index 3262c5a..a023c0f 100644 --- a/test/pbselfwrite.py +++ b/test/pbselfwrite.py @@ -19,6 +19,15 @@ Usage: pbselfwrite.py import os 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): @@ -38,16 +47,9 @@ def sealed_frame(pb, op, space, address, count): return head + bytes((seal,)) -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") +def scenario(pb, pbsim, device_bin, elf, mcu, hz, base_hex, page, baud, dump, base, settle): + """The whole scenario once, answering with the running page as the + simulator's own flash holds it afterwards.""" device = pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump) try: port = pb.Port(device.pty, baud) @@ -74,29 +76,57 @@ def main(): if alive == b"\xff" * 8: fail("the refused erase happened anyway - the running page reads erased") - # Green: the identical command, correctly sealed. Nothing is required - # of the link from here on. The verdict is *issued* before the SPM, but - # the erase takes the code that would have finished saying it, and how - # 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 - # erased will prompt as usual where one with 128-byte pages goes with - # the stub. None of that is the claim. The claim is that the erase - # reached flash, and the dump is both the only witness for it and a - # better one: it tells "accepted and performed" from "merely answered". + # Green: the identical command, correctly sealed. The claim is that the + # erase reached flash, and the dump is the only witness for it - it + # tells "accepted and performed" from "merely answered". + # + # Nothing the link says may shorten the settle, because the verdict is + # issued *before* the SPM: a prompt reply means the erase has not + # happened yet, and stopping the device on it races the write. The link + # dying here is an expected outcome rather than a failure - the loader + # is erasing its own command loop - so a closing pty ends the settle + # instead of escaping it. port.write(frame) + deadline = time.monotonic() + settle + while time.monotonic() < deadline: + try: + port.read_available(0.1) + except (pb.Error, OSError): + break try: - port.read_exact(2, 2.0) - except pb.Error: + port.close() + except OSError: pass - port.close() finally: device.stop() # Ground truth: the simulator's flash, not the loader's opinion of it. - flash = open(dump, "rb").read() - if flash[base : base + page] != b"\xff" * page: - fail("the sealed erase did not reach flash - the running page is intact") - print("pbselfwrite: the running slot is refused unsealed and erased sealed") + return open(dump, "rb").read()[base : base + page] + + +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") + return + settle *= 2 + fail("the sealed erase did not reach flash - the running page is intact") main()