Research notes / Recovery evidence
What a recovered disk image can actually prove
HESELA RESEARCH NOTE | 18 OCTOBER 2026 | GNU DDRESCUE 1.30
A recovery image, a completed map and a zero exit status answer different questions. In a controlled test, resuming a copy brought the map to 4,096 finished bytes while only 3,584 bytes matched the source. The mismatch came from earlier sparse writes into a nonzero destination, a use explicitly excluded by the manual. Simply disabling sparse writes on resume did not revisit the already-finished sector.
This is a reproducible demonstration of documented software behavior and its combined consequences, not a newly discovered ddrescue defect or a study of failing drives. It connects recovery bookkeeping with the existing Hesela work on checksums and identity, sparse allocation and backup acceptance.
The experiment in one table
The source contains eight 512-byte sectors: A, zero, C, D, E, F, G and H. Each letter means 512 repetitions of that ASCII byte. A reused destination starts filled with Z. Errors are simulated by ddrescue’s own --test-mode; no storage device is opened. Every copy uses a one-sector copy buffer. Sector numbers start at zero.
| Case | Finished bytes | Matching bytes | Map complete? |
|---|---|---|---|
| Fresh, no error | 4,096 | 4,096 | Yes |
| Fresh, unread C sector | 3,584 | 3,584 | No |
| Reused, unread C sector | 3,584 | 3,584 | No |
| Fresh, unread zero sector | 3,584 | 4,096 | No |
| Sparse + fresh, no error | 4,096 | 4,096 | Yes |
| Sparse + reused, no error | 4,096 | 3,584 | Yes |
| Sparse + reused, unread C | 3,584 | 3,072 | No |
| Resume last case, no error | 4,096 | 3,584 | Yes |
“Finished” is the number of map bytes marked +; “Matches” is a separate byte comparison with the known fixture. Every rescue operation in this table returned 0. Full-domain ddrescuelog --done-status returned 1 for the incomplete maps and 0 for complete maps. Counts describe these deliberately constructed examples, not success rates.
Why a later successful resume can preserve wrong bytes
The sparse-reused case combines two independent mechanisms. At byte offset 512, ddrescue successfully reads a zero-filled copy buffer. Sparse mode skips the physical write, leaving the old Z bytes in the reused destination. At offset 1,024, the simulated read error prevents the C sector from being copied. The resulting map describes the first sector as finished and the second as bad.
Sector index 0 1 2 3 4 5 6 7
Known source A 0 C D E F G H
Before resume A Z Z D E F G H
Map before + + - + + + + +
After resume A Z C D E F G H
Map after + + + + + + + +
The resume disables both sparse mode and error simulation, and requests one retry pass. C is recovered. The old Z bytes at the earlier zero sector remain because that map range is already finished. The experiment therefore reaches a complete map with an incorrect image.
The upstream manual warns that sparse output requires an empty or zeroed destination. In the pinned source, Rescuebook::copy_block checks whether the entire copied buffer is zero before skipping its write. The one-sector buffer makes the zero region separable here; this does not imply that every zero byte in every mixed buffer is skipped. GNU manual, invoking ddrescue
Negative controls matter
A fresh destination with sparse writes matches all 4,096 source bytes. A reused destination with ordinary writes correctly replaces the readable zero sector. The incorrect sparse result is not evidence that sparse files inherently corrupt data. It demonstrates violation of a documented destination-state precondition, followed by a resume that relies on the original map.
Normal exit is not full recovery
Four incomplete rescue cases return 0 in this experiment. A second command checks the map: ddrescuelog --done-status. It returns 1 for each incomplete full-domain map. Restricting that same check to the first 512 bytes returns 0 in every case, including images with an unresolved sector elsewhere. Preserve the check’s range as part of the result. GNU manual, invoking ddrescuelog
No example command here should be adapted blindly to a failing device. In particular, truncating an existing recovery destination can destroy previously recovered data. The supplied harness accepts executable paths and an output receipt, not a production source or destination device.
The practical consequence
Keep the image, its original map, the acquisition commands and the source identity together. Check completeness over the intended range, then assess content integrity separately. A hash calculated only after recovery identifies that output; without a trusted comparison value it does not establish where the bytes came from.
A reconstructed map can invert the evidence
Generate mode derives an approximate map from the partial output. It uses nonzero sector contents as a proxy for completed copying. It does not recover sectors or perform a byte-for-byte source comparison. The source implementation Genbook::check_block reads the output descriptor and marks nonzero sectors finished. GNU manual, generate mode
| Original output | Original finished | Generated finished | Matching bytes |
|---|---|---|---|
| Fresh, unread C | 3,584 | 3,072 | 3,584 |
| Reused, unread C | 3,584 | 3,584 | 3,584 |
| Sparse + reused, unread C | 3,584 | 4,096 | 3,072 |
In the ordinary reused destination, the real zero sector becomes ?, while the unread sector containing old Z bytes becomes +. The original and generated maps both show 3,584 finished bytes, yet they identify different ranges. Comparing only aggregate completion counters would miss the inversion.
The sparse-reused output contains no all-zero sectors at all. Its generated map is entirely finished even though two sectors differ from the source. The generated map is not a replacement for the original acquisition record.
Identical image bytes can have different acquisition histories
The fresh-bad-zero case prevents reading the zero sector. Later writes extend the new output beyond that offset, and the gap reads as zeros. Its image is byte-identical to fresh-healthy, including the SHA-256 digest. One map is incomplete and the other is complete.
This is not a hash collision: the files really are identical. A trusted reference hash would provide evidence of byte equality under the hash’s usual assumptions. It would still not prove that every sector had been read successfully during this particular acquisition. The experiment has ground truth because the input is a constructed fixture; that ground truth may be unavailable in actual recovery.
The reverse distinction also appears. After a healthy copy, the harness changes one sector in the image while retaining the original map. --done-status still returns 0. It evaluates the map, not the current image’s checksum. A preserved map therefore needs explicit association with a particular image version.
Record three layers separately
- Acquisition: source identity, geometry, range and offsets, command, version, original map and unresolved ranges.
- Artifact integrity: hashes and sizes of the image and map after collection, preservation copies, and any known trusted reference digest.
- Recovered meaning: filesystem or format checks, required files and application-level acceptance. A complete block acquisition is not itself proof of application correctness.
Successful reads are not a content-integrity oracle
The FAST ’08 study by Bairavasundaram and colleagues distinguishes checksum mismatches, block-identity discrepancies and parity inconsistencies. In the studied storage stack, a block could pass its checksum yet fail the filesystem’s identity check; parity checks could detect inconsistencies even when individual block checksums passed. These are different observations, not interchangeable definitions of success. Full paper, Sections 2.2-2.3
Hesela’s implication for recovery is narrower: keep read coverage, byte integrity and application acceptance separate. Our test mode simulates reported read errors, not silently wrong source bytes. It therefore cannot estimate how often a readable recovered image is semantically correct. The FAST study used historical production telemetry, not ddrescue runs; its fleet frequencies must not be transferred to these twelve fixtures. See the checksum and block-identity analysis for that separate evidence.
Reproduce the result
The dataset and CSV include the harness, original receipt, tests and source ledger. The harness creates only disposable regular files, then removes them. Complete map text, sector hashes, commands and exit statuses remain in the JSON receipt. It refuses to replace an existing receipt.
Build upstream GNU ddrescue 1.30 from the GNU release archive. The recorded archive SHA-256 is:
2264622d309d6c87a1cfc19148292b8859a688e9bc02d4702f5cd4f288745542
The source was fetched over HTTPS and its bytes pinned; no detached-signature verification was performed. Build commands were ./configure, make -j2 and make check; the upstream test suite passed. Installation is unnecessary.
python3 observe.py \
--ddrescue /path/to/ddrescue-1.30/ddrescue \
--ddrescuelog /path/to/ddrescue-1.30/ddrescuelog \
--output fresh-results.json
DDRESCUE_BIN=/path/to/ddrescue-1.30/ddrescue \
DDRESCUELOG_BIN=/path/to/ddrescue-1.30/ddrescuelog \
python3 -m unittest -v test_observe
Recorded environment: Python 3.12.14, Linux 6.18.55, x86_64; g++ 13.3.0, -O2. All 12 cases reproduced in normal, -O and -OO Python runs after excluding map timestamps and comments from the comparison, while retaining byte hashes, per-sector outcomes, range states and exit statuses. Thirteen tests passed, including live replay and mutations of misleading receipts. The raw receipt retains the original map comments.
Scope and limits
This is a small, deterministic counterexample set using one upstream release and one build. It establishes possible outcomes under the tested configuration, not their frequency. There are no physical read errors, power interruptions, filesystem mounts, throughput measurements or hardware-recovery claims. Test mode bypasses selected source reads; it does not reproduce firmware retries, unstable data returned without an error, or a failing device’s mechanics.
The independent oracle is the fixture’s complete byte sequence. No sampling confidence interval applies. Additional runs on another pinned release would test version portability; they should not overwrite this receipt. Real-device validation would require a separate non-destructive protocol and hardware access.
Evidence trail
Primary evidence is the GNU 1.30 source distribution, especially doc/ddrescue.texi, rescuebook.cc, genbook.cc and ddrescuelog.cc. sources.json pins their hashes and exact sections/functions. claims.json connects each synthesis claim to source locations and observed case IDs. No third-party source code or executable is redistributed in this bundle.
A second environment rebuilt the same pinned GNU release, passed its upstream tests and reproduced all twelve cases. All thirteen harness tests passed, including live replay. The second receipt preserves its own environment, executable hashes and timestamps; the original receipt remains unchanged. This checks software repeatability, not hardware recovery performance.
The machine-readable dataset includes column definitions, source-to-claim mappings and a deterministic CSV generator. The corpus also defines the ddrescue mapfile separately from the recovered image.