Skip to content

fix: reads resume after a dropout, and verify contact after every chunk - #15

Merged
jackulau merged 1 commit into
mainfrom
fix/resumable-reads
Jul 17, 2026
Merged

jackulau merged 1 commit into
mainfrom
fix/resumable-reads

Conversation

@jackulau

Copy link
Copy Markdown
Owner

Both problems found by trying to take a real backup off real silicon.

1. A read that lost contact threw away everything it had read

On the bench the probe held ~20s; the read needed ~4 minutes. The attempt died at 7% and 2.4MB of correctly-read data went in the bin.

Those two numbers do not have to meet. Each attempt now keeps its completed 64KB chunks in the output file and records them in a sidecar, so re-running reads only what is missing and short attempts add up to a whole dump.

This matters more than it looks: without it, a marginal probe yields no backup at all - and no backup is what makes a reflash lose the board's MAC permanently, since the MAC lives in flash and is not in the vendor image.

The sidecar is deliberately paranoid about staleness. Chip id, total size and chunk stride must all match or it is ignored and the read starts fresh. A resume that spliced two different chips' bytes into one file would produce a corrupt backup that looks perfect - the exact failure this subsystem exists to prevent. A sidecar claiming progress with no partial file behind it is rejected too.

2. The contact check missed two real dropouts

read_chunk_checked only re-read the chip id when a chunk was entirely 0x00. That misses:

  • A probe that drops MID-chunk leaves real data followed by dropout bytes. The chunk is not uniform, so the all-zero test passes it and a corrupt chunk is marked done. The new resume test caught exactly this - contact dropped at offset 150000, inside chunk 2, and the resumed read skipped the corrupt chunk forever.
  • A probe whose MISO floats high leaves 0xFF, byte-for-byte identical to erased flash. A lifted probe on this part reads ffffff. This was never checked at all.

The chunk's bytes are not the evidence; the chip is. The id is now re-read after every chunk.

Cost: one RDID per 64KB - ~350us per chunk, ~0.2s over a 32MB dump. It deletes a whole class of silently-corrupt dumps for a fifth of a second.

Test fake

LoopbackFlash::losing_contact_at(offset) models the real signature: reads past the offset return 0x00 and the chip stops answering RDID.

Tests

528 pass, 0 fail. Clippy -D warnings and fmt clean.

Two existing tests updated, both still asserting their original property:

  • read_range_streams_single_opcode - expects the deliberate liveness RDID frame; READ is still issued exactly once per range, never re-addressed per KiB
  • verify_chip_exits_4byte_mode_after_completion - needed filler that decodes as its own chip id rather than a5a5a5

Two problems, found by trying to take a real backup off a real chip.

1. A read that lost contact threw away everything it had correctly read. On the
bench the probe held for ~20s and the read needed ~4 minutes, so the attempt
died at 7% and 2.4MB of good data went in the bin. Those two numbers do not have
to meet. Each attempt now keeps its completed 64KB chunks in the output file and
records them in a sidecar, so re-running reads only what is missing and short
attempts add up to a whole dump. Without this a marginal probe yields no backup
at all - and no backup is what makes a reflash lose the board's MAC permanently,
since the MAC lives in flash and is not in the vendor image.

The sidecar is paranoid about staleness: chip id, total size and chunk stride
must all match or it is ignored and the read starts fresh. A resume that spliced
two different chips' bytes into one file would produce a corrupt backup that
looks perfect, which is the exact failure this subsystem exists to prevent. A
sidecar claiming progress with no partial file behind it is likewise rejected.

2. read_chunk_checked only re-checked the chip id when a chunk was ENTIRELY
0x00, which misses two real dropouts:

  - a probe that drops MID-chunk leaves real data followed by dropout bytes. The
    chunk is not uniform, so the all-zero test passes it and a corrupt chunk gets
    marked done. The new resume test caught exactly this: contact dropped at
    150000, inside chunk 2, and the resumed read skipped the corrupt chunk
    forever.
  - a probe whose MISO floats high leaves 0xFF, byte-for-byte identical to
    erased flash. A lifted probe on this part reads ffffff. This was never
    checked at all.

The chunk's bytes are not the evidence; the chip is. The id is now re-read after
EVERY chunk. Cost is one RDID per 64KB: ~350us per chunk, ~0.2s over a 32MB
dump, and it deletes a whole class of silently-corrupt dumps.

Test fake gained losing_contact_at(offset), which models the real signature:
reads past the offset return 0x00 AND the chip stops answering RDID.

Two existing tests updated, both still asserting their original property:
read_range_streams_single_opcode now expects the deliberate liveness RDID frame
(READ is still issued exactly once per range, never re-addressed per KiB), and
verify_chip_exits_4byte_mode_after_completion needed filler that decodes as its
own chip id rather than a5a5a5.

528 tests pass; clippy -D warnings and fmt clean.

Goal: 020-soic-debug-identify-fix-ship
@jackulau
jackulau merged commit 6f9a2f7 into main Jul 17, 2026
1 check passed
@jackulau
jackulau deleted the fix/resumable-reads branch July 17, 2026 00:47
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