pureboot: a loader in the staging slot is the staging copy — leave it there

The update flow's first step wrote the staging content over whatever the
staging slot held; with a loader running right there (programmed by hand
onto erased flash), that write met the copy's own running-slot guard on
the composed through-word and the tool stopped at its verify — although
the copy is exactly an installed staging copy, able to stream the new
resident like any other. The install is now skipped when the slot holds a
complete loader: its info block where every image carries it, matching
the device's byte for byte, and the slot unchanged since the update began
(the state file's snapshot) — so a resumed half-written install still
differs from its snapshot and takes the install path, which completes it.
pbrehome gains the staging-slot position (an older build at stage
streaming a newer resident in); the README's wrong "cannot re-home from
the staging slot" claim is corrected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 21:55:15 +02:00
parent 5f9e736d66
commit 76f4576fe0
3 changed files with 94 additions and 49 deletions

View File

@@ -120,14 +120,14 @@ On a boot-sectioned mega a copy at 0 is dead weight (SPM only executes
from the boot section, so it cannot even heal itself — reflash the .hex);
on the patched-vector chips it *runs* (the image is position-independent
and reset enters word 0), reports its canonical geometry, and the ordinary
`--update-loader` flow re-homes a build into the top slot — the staging
install and the word-0 redirect both execute from copies outside page 0's
slot, so the running-slot guard never blocks the flow (`pureboot.rehome`
is the acceptance test). Flashing the application afterwards overwrites
the stale copy, vector surgery included. The one position that cannot
re-home itself is the staging slot: installing the staging copy would hit
the running copy's own guard and the update stops at its verify — flash
the .hex instead.
`--update-loader` flow re-homes a build into the top slot from any
position — the staging install and the word-0 redirect execute from
copies outside page 0's slot, and a copy sitting in the staging slot
itself is recognized as the installed staging copy and left in place (it
streams the new resident like any staged copy, so an older build installs
a newer one). `pureboot.rehome` is the acceptance test for both
positions. Flashing the application afterwards overwrites the stale copy,
vector surgery included.
**Boot-sectioned megas**: program the loader at `flash slot` with an
external programmer. Every such mega has a BOOTSZ step whose boot section
@@ -193,7 +193,10 @@ the loader resident) and peels it to the slot content by the embedded base.
patched-vector chips the host composes the slot's last word — the same
address as the resident's trampoline — as a jump to the resident base,
so even an abandoned staging copy times out into a loader, never into
garbage.
garbage. A loader already sitting whole in the staging slot (its info
block in place, the slot unchanged since the update began) is left as
the staging copy instead — rewriting it would only meet its own
running-slot guard.
3. `J` enters the staging copy, which rewrites the resident slot. On the
patched-vector chips whose staging slot sits away from page 0 the host
first re-aims word 0 at the staging copy, so a power loss mid-rewrite

View File

@@ -725,15 +725,33 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes):
state = UpdateState(state_path)
state.load_or_save(loader)
# Install the staging copy. On a chip whose staging slot starts at
# address 0 (the 1 KB tiny13A), its first page carries the reset vector:
# written last, so any earlier interruption still resets into the old
# resident, and from then on resets enter the staging copy.
order = list(range(0, info.slot, page))
if info.stage == 0:
order = order[1:] + [0]
if write_differing(loader, info.stage, staged, order):
print(f"staging copy installed at {info.stage:#06x}")
# Install the staging copy — unless a loader already sits whole in the
# staging slot (a build programmed there by hand): that copy IS the
# installed staging copy, and rewriting it would only trip its own
# running-slot guard on the composed through-word. Any pureboot with
# the device's own info block serves — the staged copy just streams
# pages, so an older build installs a newer resident all the same. Two
# checks make "already a loader" mean a *complete* one: the block must
# sit where every image carries it (within the slot's first 256 bytes
# — the build's position lint), matching the device's block byte for
# byte, and the slot must be unchanged since this update began (the
# state file's snapshot) — a resumed, half-written install differs
# from its snapshot and takes the install path below, which completes
# it page by page.
current = loader.read_flash(info.stage, info.slot)
staged_loader = image_info(current[:268])
if staged_loader is not None and staged_loader.raw == info.raw and current == state.staging:
print(f"staging slot already holds a loader — left in place")
else:
# On a chip whose staging slot starts at address 0 (the 1 KB
# tiny13s), its first page carries the reset vector: written last,
# so any earlier interruption still resets into the old resident,
# and from then on resets enter the staging copy.
order = list(range(0, info.slot, page))
if info.stage == 0:
order = order[1:] + [0]
if write_differing(loader, info.stage, staged, order):
print(f"staging copy installed at {info.stage:#06x}")
# Enter it and let it rewrite the resident slot. Where a patched reset
# vector routes through the resident (a tiny with the staging slot away