Recover databases with torn region-count header fields#1293
Merged
Conversation
9200acc to
edf36bc
Compare
full_regions and trailing_partial_region_pages record how the file is divided into regions. They are rewritten on every commit that resizes the file and are covered by no checksum, so a crash midway through a resize can tear them -- partially updating one -- even while both commit slots stay valid. The layout they describe is fully recoverable from the file length, which is why the design documents them as valid only when the database does not need recovery, and otherwise recalculated from the file length. Two checks rejected such a database as Corrupted before recovery could run: from_bytes() treated a zeroed pair (num_regions == 0) as an invalid region count, and finalize() treated a trailing count torn above the true file length as a truncation below the stored layout. In both cases recovery was required and the committed data was intact, yet every subsequent open returned a terminal error. When recovery is required, distrust these two fields and rebuild the layout from the file length using only the immutable region geometry (page size and region sizes), which is fixed at creation and so cannot tear. The mutable counts are validated only for a cleanly shut down database, where they are trusted; such a file whose length no longer matches its stored layout (external truncation or extension) is still rejected as before. The torn-header case has existed since at least 4.1.0, where it panicked on open rather than returning a corruption error. Assisted-by: Claude Code https://claude.ai/code/session_01EkvNuVjLL1MJGyEqZAv6t6
edf36bc to
8f0f4ce
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #1293 +/- ##
==========================================
+ Coverage 90.20% 90.21% +0.01%
==========================================
Files 36 36
Lines 16226 16289 +63
==========================================
+ Hits 14636 14695 +59
- Misses 1590 1594 +4
🚀 New features to boost your workflow:
|
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
full_regionsandtrailing_partial_region_pagesin the database header record how the file is divided into regions. They are rewritten on every commit that resizes the file and are covered by no checksum, so a crash midway through a resize can tear them (partially update one field) even while both commit slots stay valid. The layout they describe is fully recoverable from the file length — which is whydesign.mddocuments these fields as valid only when the database does not need recovery, and otherwise recalculated from the file length.Two checks rejected such a database as
Corruptedbefore recovery could run, making it permanently unopenable despite intact, recoverable data:from_bytes()treated a zeroed pair (num_regions == 0) as an invalid region count.finalize()treated a trailing count torn above the true file length as a truncation below the stored layout ("File truncated below stored layout"— truncation happens after the header flush, so the stored count can exceed the actual file page count).Concretely: a shrink
{full=1, trailing=0} -> {0, N}tearing to{0, 0}hit the first check; a trailing-page shrink like0x3E8 -> 0x1F4tearing to0x3F4hit the second. In both casesrecovery_requiredwas set and both commit slots verified.Fix
When recovery is required, distrust these two fields and rebuild the layout from the file length using only the immutable region geometry (page size and region sizes), which is fixed at creation and so cannot tear:
from_bytes()validates the immutable geometry unconditionally, but validates the region counts only for a cleanly shut down database (!recovery_required).finalize()recomputes the layout from the file length when recovery is required, never derivingstored_lenfrom the possibly-torn counts. Thecleanreturn value is derived by comparing the recomputed counts to the stored ones, preserving the exact behaviorcheck_integrity()relies on. The clean-shutdown path (including external truncation/extension rejection) is unchanged.layout_from_file_lenhelper with min/max/boundary guards, so an adversarial or externally-mangled length returnsCorruptedrather than panicking.Regression status
This is a pre-existing bug, not a regression introduced this cycle. Against the released v4.1.0, the same torn header instead panicked on open (an
assert!(raw_file_len >= header.layout().len())failure / anum_regions() - 1underflow); the header-hardening work in the 4.2.0 cycle converted that panic into a terminalCorruptederror but still failed to recover.Testing
torn_layout_fields_recover_from_file_lengthreproduces both tear patterns ({0,0}and an inflated trailing count) with the commit slots left intact, and asserts the database opens and preserves the committed data. Verified it fails against the old code and passes with the fix.cargo fmt --check,cargo clippy --all-targets --all-features(--deny warnings), and the fullcargo test --all-featuressuite pass.Generated by Claude Code