check --repair: resync past corrupt object headers when rebuilding the chunks index - #10094
check --repair: resync past corrupt object headers when rebuilding the chunks index#10094mr-raj12 wants to merge 2 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #10094 +/- ##
==========================================
- Coverage 87.05% 87.05% -0.01%
==========================================
Files 101 101
Lines 17742 17846 +104
Branches 2680 2702 +22
==========================================
+ Hits 15445 15535 +90
- Misses 1597 1609 +12
- Partials 700 702 +2 ☔ View full report in Codecov by Harness. |
|
Does #10092 (comment) apply here too?
The consequence is in Either is fine by me, just say which. |
a6533ae to
1877f65
Compare
1877f65 to
cc0d2c0
Compare
…e chunks index, borgbackup#8476 When check --repair rebuilds the chunks index from the packs, a corrupt object header now makes iter_headers resync rather than raise: it takes a validate function and scans forward for the next object, in 1 MiB windows that overlap by one header so a header on a window boundary is still found. Repository-only checks pass no validate and keep raising IntegrityError on a corrupt header. OBJ_MAGIC also occurs inside payloads, so a candidate is accepted only when it authenticates. For AEAD keys, decrypting the metadata authenticates it against the header's magic, version and chunk_id, so the walk confirms a chunk id from a few hundred bytes. Keys that authenticate by chunk_id == id_hash(content) (id_check_is_authentication) read the whole object and parse() at the "repair" id place; validate.needs_data selects between the two. Authentication needs the key, so check --repair makes it before the rebuild with manifest_only=True. A repair that cannot read the manifest has no key and walks without resyncing.
cc0d2c0 to
6d67228
Compare
…one, borgbackup#8476 Every key mode covers the object header by the metadata slot's AAD, so parse_meta confirms a candidate and validate.needs_data is gone.
6d67228 to
ff0f7a0
Compare
|
Answered by #10095: every mode now covers the object header by the metadata slot's AAD, so The Rebased on master, the |
|
Reviewed this with a focus on the resync logic — nice work overall. One gap: A corrupted size field that stays within the pack still loses intact objects. The tests cover a broken magic and a Repro (3-object pack, obj1's obj1 = bytearray(fchunk(b"A" * 100, meta=b"m1", chunk_id=H(1)))
obj2 = fchunk(b"B" * 100, meta=b"m2", chunk_id=H(2))
obj3 = fchunk(b"C" * 100, meta=b"m3", chunk_id=H(3))
obj1[45:49] = struct.pack("<I", 100 + len(obj2) + 60)
pack = bytes(obj1) + obj2 + obj3
list(PackReader(pack_contents=pack).iter_headers(validate=accept_all))
# pack <no id>: invalid object header at offset 362 and none after it, skipping the remaining 91 bytes.
# [(H(1), 0, 362)] <- obj2 and obj3 both lost, although they are intactScanning from just past the last accepted header instead finds obj2 at its correct offset (151). Suggestion: on resync, scan from just after the last accepted object's header rather than from the misaligned offset (or equivalently, only trust a jump once the header at its landing point checks out — lookahead by one). Trade-offs to weigh: it re-reads up to one object's payload per resync, and the index can end up with the bogus wrong-size entry overlapping the recovered ones — harmless, since reading it fails either way (the chunk is unrecoverable without its size field), but worth a code comment. Either way, a test for the within-pack shape would be good: it behaves qualitatively differently from the past-pack-end shape the tests already cover. |
Follow-up to #10083 (header validation, #8476).
When
borg check --repairrebuilds the chunks index from the packs, a corrupt object header no longer aborts the whole pack.iter_headers(validate=...)scans forward for the next object that validates and resumes there, so the objects after the damaged region are still indexed. The skipped byte range is logged.Without a validate function (repository-only check, routine rebuild) the behavior is unchanged: a corrupt header raises IntegrityError.
Details:
_find_headersearches for OBJ_MAGIC a window at a time, the windows overlapping by one header so a header on a boundary is still found. OBJ_MAGIC also occurs inside payloads, so a candidate is accepted only when its header parses, describes an object of at most MAX_DATA_SIZE bytes that fits into the pack, and validate confirms it.resync_validatorbuilds validate from the key: it parses the candidate's metadata slot, whose tag is computed over the header's magic, version and chunk id as well. That confirms a chunk id from the header plus a few hundred bytes.--repairdoes before the rebuild, withmanifest_only=True(self.chunksis only built afterwards). A repair that cannot read the manifest has no key, warns and walks without resyncing.Rebased on master. The
none-*/authenticated-*modes from #10095 changed what this needs: every mode now covers the object header by the metadata slot's AAD, soparse_metadecides for all of them and thevalidate.needs_databranch I asked about above is gone (second commit). In thenone-*modes that tag is an unkeyed checksum, so the scan accepts any well-formed object, including one a backed up file contains - those modes have no secret, so reading the payload and checkingchunk_id == id_hash(content)would not separate the two either.Tested in repository_test, cache_test and check_cmd_test.