Preserve torn commit slot bytes during recovery#1290
Merged
Conversation
4e6cf41 to
3eb7a70
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1290 +/- ##
==========================================
+ Coverage 90.16% 90.18% +0.01%
==========================================
Files 36 36
Lines 16161 16227 +66
==========================================
+ Hits 14572 14634 +62
- Misses 1589 1593 +4
🚀 New features to boost your workflow:
|
cberner
commented
Jul 5, 2026
cberner
left a comment
Owner
Author
There was a problem hiding this comment.
The comments are too specific to the fix. Like the write secondary should not mention the issue. The change log should focus only on the user visible issue and its conditions to trigger, not the details
3eb7a70 to
9e0ca80
Compare
Owner
Author
|
Addressed both points (force-pushed):
Generated by Claude Code |
9e0ca80 to
ab1fd84
Compare
A crash mid-fsync of a 1-phase commit can leave the secondary slot torn (invalid checksum). select_primary_slot() correctly keeps the valid primary, but recovery then rewrote the whole header via to_bytes(), which re-serialized the parsed secondary with a freshly computed, now-valid checksum -- "laundering" the torn slot. The laundered slot sat on disk for the entire repair (three full tree walks). A second crash in that window left two valid slots, and the next open promoted the laundered slot (higher transaction id), silently rolling back or mixing durably committed transactions. This defeats the checksum design, whose whole point is that a torn slot stays invalid forever. Preserve the original bytes of any slot that fails checksum verification and write them back verbatim, so a torn slot keeps its invalid checksum until a genuine commit overwrites it. Replace secondary_slot_mut() with write_secondary_slot(), which clears the preserved bytes as part of installing a new commit. Assisted-by: Claude Code Claude-Session: https://claude.ai/code/session_018uK6KdWZRdM91oRQyiEGF8
ab1fd84 to
1021153
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A crash mid-
fsyncof a 1-phase commit legitimately leaves the secondary slot torn (invalid checksum).select_primary_slot()correctly keeps the valid primary — but on reopen withneeds_recovery, the header is immediately rewritten viato_bytes(true), andTransactionHeader::to_bytes()always recomputes the slot checksum. This re-serializes the parsed torn secondary with a freshly computed, now-valid checksum, "laundering" it.The laundered slot then sits on disk for the entire repair (three full tree walks — minutes on a large database). If the process crashes again in that window, the next open sees two valid slots and promotes the laundered one (its transaction id
T+1 > T). Tree-checksum verification is the only remaining defense, and torn byte-mixes exist that pass it:DATA_FREED_TABLElists pages the old user root still references, so a later commit frees and reuses live pages -> corruption.Preconditions are narrow (byte-granular tear + double crash), but this is exactly the "poorly behaved media" model
docs/design.mdcommits to defending, and the laundering defeats the checksum design: without it, a torn slot stays invalid forever and none of this is reachable.This affects the two recovery rewrite paths (the open path and
clear_cache_and_reload), and is present in released v4.1.0 (verified against the tagged source — sameto_bytesre-checksum +pick_primary_for_repairpromotion), so it is not a regression onmaster.Fix
Preserve the original on-disk bytes of any slot that fails checksum verification, and write them back verbatim during a header rewrite, so a torn slot keeps its invalid checksum until a genuine commit overwrites it.
TransactionHeadergains acorrupt_bytes: Option<[u8; TRANSACTION_SIZE]>field, set byfrom_bytes()whenever the parsed slot's checksum did not verify.to_bytes()returns those bytes verbatim when present, instead of recomputing a fresh (valid) checksum. This fixes both rewrite sites at the source, and means the newer-secondary promotion inselect_primary_slot()can never see a laundered slot.secondary_slot_mut()is replaced bywrite_secondary_slot(), which clearscorrupt_bytesas an atomic part of installing a new commit — so a torn slot's bytes are preserved only until the slot is genuinely overwritten, with no way to take a mutable reference and forget to clear it.Testing
recovery_does_not_launder_torn_slot: fabricates a torn, newer secondary slot, opens with the repair aborted (freezing the exact post-rewrite on-disk state), and asserts the torn slot is byte-for-byte unchanged. Confirmed it fails on the pre-fix behavior ("recovery laundered the torn secondary slot") and passes with the fix.cargo fmt --checkandcargo clippy --all-targets --all-featuresclean.cargo test --all-featuressuite passes, includingcrash_consistency.cargo fuzz run fuzz_redbsmoke run on the crash-recovery target — no failures.CHANGELOG.mdentry.🤖 Generated with Claude Code
https://claude.ai/code/session_018uK6KdWZRdM91oRQyiEGF8
Generated by Claude Code