Skip to content

fix: a backup is not a baseline, and a re-run must not overwrite it - #21

Merged
jackulau merged 1 commit into
mainfrom
fix/backup-is-not-a-baseline
Jul 17, 2026
Merged

jackulau merged 1 commit into
mainfrom
fix/backup-is-not-a-baseline

Conversation

@jackulau

Copy link
Copy Markdown
Owner

write used ONE 32 MB read for two jobs that are opposites the moment a block is programmed.

  • A backup must hold the chip as it was ORIGINALLY.
  • A baseline must describe the chip as it is RIGHT NOW.

They agree only until the first write lands. After that, no single read can be both.

The dangerous half

stable_backup_path (from #19) hands every run the same filename. So on a failed write:

  1. Run 1 phase 1 reads the original BIOS to ratchet-backup-<id>-<size>.bin. Correct.
  2. Run 1 phase 2 dies partway. The chip is now half old BIOS, half new image.
  3. Run 2 phase 1 re-reads THAT and writes it over the same file.

The only copy of the old BIOS is destroyed. Silently, because the result is still exactly chip-sized and looks like a valid dump. That is precisely the failure this module exists to prevent, reintroduced by the fix for the resume cliff. The timestamped path it replaced could not clobber anything; it merely orphaned the sidecar.

A complete backup on disk now wins: never re-read, never overwritten. "Complete" is right size and no resume sidecar, since a sidecar exists only while a read is unfinished.

The other half

Those same bytes are then STALE as a baseline, so they are not used as one. Handing them to the skip check would fail to skip exactly the blocks an earlier run wrote, which IS the resume. With no fresh baseline the loop asks the chip per block instead.

A backup read THIS run is still the baseline, because it is fresh (nothing is programmed yet) and free (it cost a read that had to happen anyway). Pre-reading every block instead would pay for a read that the block's own verify read-back already covers, and would make a first-run rewrite slower, not faster.

Side effect worth having: --skip-backup is now resumable, which it never was. It used to forfeit the skip entirely and write every block blind.

Test isolation

write_chip_skips_blocks_that_already_hold_the_target_image read and wrote the REAL ~/.local/share/ratchet/backups, and was caught resuming a half-finished 16 MB dump another test had left there:

resuming read of ef4018: 6946816/16777216 bytes (41.4%) already captured

A test must never touch a user's only copy of their old BIOS. Sandboxed now.

Two mock queues get deeper because skip_backup reads each block before writing it. That is a real protocol change, not a brittle mock.

Tests

Both properties pinned by mutation:

  • Always re-reading (the shipped behaviour) destroys the backup: the assertion fires on the second run.
  • Feeding the stale backup in as a baseline re-erases blocks [0, 10000] that already held the image.

Results:

  • cargo test --workspace: exit 0, 518 + 16 + 10 + 5 passed, 0 failed
  • cargo clippy --workspace --all-targets -- -D warnings: exit 0

write used ONE 32 MB read for two jobs that are opposites the moment a
block is programmed. A BACKUP must hold the chip as it was ORIGINALLY. A
BASELINE must describe the chip as it is RIGHT NOW. They agree only until
the first write lands.

The dangerous half: stable_backup_path hands every run the same filename,
so run 2 re-read a half-written chip and wrote it over run 1's good backup.
The only copy of the old BIOS was destroyed silently, since the result is
still exactly chip-sized and looks like a valid dump. That is the failure
this module exists to prevent, reintroduced by the fix for the resume
cliff. The timestamped path it replaced could not clobber anything; it just
orphaned the sidecar.

A complete backup on disk now wins: it is never re-read and never
overwritten. Complete means right size and no resume sidecar, since a
sidecar exists only while a read is unfinished.

The other half: those bytes are then STALE as a baseline, so they are not
used as one. Handing them to the skip check would fail to skip exactly the
blocks an earlier run wrote, which is the resume itself. With no fresh
baseline the loop asks the chip per block instead. A backup read THIS run
is still used as the baseline, because it is fresh and it is free: it cost
a read that had to happen. Pre-reading every block instead would pay for a
read the block's own verify read-back already covers.

This also makes --skip-backup resumable, which it never was.

Fixes test isolation while here. write_chip_skips_blocks_that_already_hold
_the_target_image read and wrote the REAL ~/.local/share/ratchet/backups,
and was caught resuming a half-finished 16 MB dump another test left there
("resuming read of ef4018: 41.4% already captured"). A test must never
touch a user's only copy of their old BIOS. Two mock queues get deeper
because skip_backup now reads each block before writing it, which is a real
protocol change, not a brittle mock.

Both properties pinned by mutation. Always re-reading destroys the backup.
Feeding the stale backup in as a baseline re-erases blocks [0, 10000] that
already held the image.
@jackulau
jackulau merged commit 6c0f5df into main Jul 17, 2026
1 check passed
@jackulau
jackulau deleted the fix/backup-is-not-a-baseline branch July 17, 2026 08:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant