compact: authenticate a gap object before dropping its bytes, #10093 - #10167
compact: authenticate a gap object before dropping its bytes, #10093#10167mr-raj12 wants to merge 1 commit into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #10167 +/- ##
=======================================
Coverage 87.75% 87.76%
=======================================
Files 103 103
Lines 18821 18836 +15
Branches 2906 2907 +1
=======================================
+ Hits 16517 16532 +15
Misses 1600 1600
Partials 704 704 ☔ View full report in Codecov by Harness. |
|
@mr-raj12 please rebase onto current master. and then ask claude for feedback. |
|
@mr-raj12 ping |
…kup#10093 superseded_gap_ranges computed the byte ranges compact drops from object headers as they are: magic, chunk id, meta_size and data_size were all taken on trust. A wrong data_size in a header whose chunk id is indexed elsewhere extended the dropped range past the object into the gap bytes behind it, and those were dropped without ever being looked at. Gaps are where the only copy of a chunk can sit (a backup that crashed before writing its index, or a stale index), so that is not free. Parse the header with PackReader._parse_header, so a header that does not parse or overruns its gap ends the walk over that gap. Read the metadata slot in the same request and require: - validate accepts the header and metadata slot, the same check the repair walk applies (PackReader._validation_problem). - the object's total size equals the index entry's obj_size. That size sets how far the reported range reaches, so the entry is a second source for it, one that does not come from the bytes being examined. Anything else keeps its bytes and the walk continues past it, so a corrupt length field can desync the walk but can no longer drop anything. validate is threaded through delete(), compact_pack() and transform_pack() from the callers that have a key (compact, repo-compress, check --repair). Without it no gap bytes are dropped at all, so "borg debug delete-obj", which opens the repository without a key, stops reclaiming superseded gap bytes.
d1597ec to
c128990
Compare
|
review by claude fable 5.1 max Verdict. The authentication part is right and I found no correctness bug in it. I would still ask for one change before merging: drop the 1. Blocking: the size-equality check regresses reclaim. Equal chunk ids mean equal plaintext, not equal stored size. I ran
Sizes differ whenever two clients use different compression, when The check buys nothing that the validator does not already provide. The PR text itself notes that 2. Minor: the new read-ahead constant duplicates an existing one. 3. Minor: docs wording. "payloads are user content stored as it is" is only true with 4. Test nit. Verified fine:
Checks run at c128990: ruff, black and
|
|
@mr-raj12 ping?! |
Fixes #10093. Rebased on current master, now that #10094 is merged; what is left here is the #10093 change itself, in one commit.
superseded_gap_rangestook the object headers in a pack's gaps at face value. magic, chunk id, meta_size and data_size all came out of the bytes being examined, and(offset, obj_size)was then handed to compact as a range to delete. A wrongdata_sizein a header whose chunk id is indexed elsewhere stretched that range past the object and into the gap bytes behind it, which were then dropped without ever being looked at. Gaps are where the only copy of a chunk can sit (a backup that crashed before writing its index, or a stale index), so that is not free.A gap object's bytes now get dropped only if two independent things agree:
validateaccepts the header and metadata slot — the same check the repair walk applies, viaPackReader._validation_problem, so theMAX_VALIDATED_META_SIZEguard against an oversized slot read applies here too.obj_size. That size decides how far the dropped range reaches, so the index entry serves as a second source for it, one that does not come from the bytes being examined.The header goes through
PackReader._parse_headerfirst, so one that does not parse, or that overruns its gap, ends the walk over that gap. The metadata slot is read together with the header (GAP_META_READAHEAD), so this costs no extra round trip in the common case.Fail either check and the object keeps its bytes; the walk just continues past it. A corrupt length field can still push the walk to a wrong offset, but at a wrong offset nothing passes both checks, so nothing gets dropped.
validateis threaded in from the callers that hold a key: compact, repo-compress, check --repair. Without one, nothing in a gap is dropped at all. Worth calling out:borg debug delete-objopens the repository without a key, so it still deletes its target object but no longer reclaims superseded gap bytes from the pack it rewrites.Changed by the rebase
object_validator(the oldresync_validator) already moved torepoobj.pywith check --repair: resync past corrupt object headers when rebuilding the chunks index #10094, so that part is gone from this branch.object_validatoralso tiesdata_sizeto thecsizerecorded in the authenticated metadata, sodata_sizeis no longer outside whatvalidatecovers. Theobj_sizecross-check is kept: it is a second source that does not come from the object's own bytes, and it is what rules out a forged size in the modes whose tag is unkeyed. The docs, the docstrings andtest_superseded_gap_ranges_rejects_a_size_the_index_contradictssay so now; that test pins the index check on its own with anaccept_allvalidator, and asserts the real validator rejects it too.PackReader._validation_probleminstead of reading the metadata slot inline, so both walks share one implementation and the oversized-slot guard._parse_headerreturns(hdr, problem)on master, and itsMAX_DATA_SIZEcheck made the branch's own size check redundant.New docs section "Gap bytes" in
docs/internals/packs.rst.Full suite: 2992 passed, 1107 skipped. ruff and black clean.