validate object headers when walking a pack (PackReader.iter_headers) - #10083
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #10083 +/- ##
==========================================
+ Coverage 86.77% 86.82% +0.04%
==========================================
Files 98 99 +1
Lines 17277 17342 +65
Branches 2622 2631 +9
==========================================
+ Hits 14992 15057 +65
- Misses 1587 1589 +2
+ Partials 698 696 -2 ☔ View full report in Codecov by Harness. |
|
raising is ok for normal usage, so users can notice corruption. for repair usage, if length is corrupted, guess we need to re-sync using a scan for the magic. |
cc13ab8 to
c421eb1
Compare
|
The magic scan should be combined with authentication / id check:
|
|
Agreed on the auth/id check. For none/authenticated mode it has to be the id check on the decompressed plaintext: Before implementing it I got stuck on an ordering problem. The validation needs the key, but Two ways out that I see. One is Also, if no usable key can be obtained at all: should the resync be refused, or done unvalidated with a warning? |
|
I'll check make_key ... Update: It can be used with Guess for now, this is an acceptable limitation to unblock progress here. Later, we can also try to decrypt all sorts of other stuff (index, cache, ...) without needing a chunks index. Thinking about it: making the key should be one of the very first steps. we'll try to have most stuff encrypted in the repo and almost everything is running client-side now (in contrast to borg 1.x, where Repository code ran server-side and there is no key on the server). |
|
ping? |
PackReader.iter_headers took the next offset from the sizes in each object header without ever checking that header. On corruption the walk either continues on payload bytes and yields garbage (chunk_id, offset, size) tuples, or skips past the end of the pack, where the short read looks like a clean EOF. The index build_chunkindex_from_repo rebuilds from that is wrong either way, without saying so. Check OBJ_MAGIC and that the object fits into the pack, raise IntegrityError naming the pack otherwise, like check_pack_objects does. The bounds check needs the pack size, which PackReader did not have, so add PackReader.size(): len(pack_contents) in memory, one store.info() per pack otherwise. No per-object roundtrip is added.
c421eb1 to
39ce693
Compare
|
Pushed, rebased on master. No new BORG_ASSERT_ID place was needed: The pack itself stays damaged though, and |
|
Hmm, guess we need another change:
|
39ce693 to
0ccf95b
Compare
|
Agreed on both points, so I took the resync commits back out. This PR is now only the header check, which stands on its own; the description is updated. On authenticating the metadata instead of the whole object: that is clearly right,
Which is the same thing you said about the format: with the current one a candidate is either cheap to check or safe to check, not both. And the cost is not only in the corrupt case, the healthy walk is one store range read per object, so an index rebuild over ssh/rest is that many roundtrips per pack. Two ways out I can see:
I would rather build the follow-up on whichever of those you want than tune the scan on the current format. Do you have a preference? Same question decides whether repairing the pack itself (#10026) belongs in that follow-up. |
|
The resync work is now #10094, stacked on this one. |
|
Noticed this while looking at #8476, which wants the chunks index rebuilt by iterating only over the object headers of the packs. That walk is
PackReader.iter_headers(used bybuild_chunkindex_from_repo) and it did not check the headers at all.It unpacks each fixed header and takes the next offset from the sizes in it:
So on a corrupt header
obj_sizeis wrong and the walk either keeps going on payload bytes and yields garbage(chunk_id, offset, size)tuples, or skips past the end of the pack, where the short read hits thelen(hdr_data) < hdr_sizebranch and looks like a clean EOF. The index rebuilt from that is wrong either way, and nothing says so.This PR is now just the check. Each header must have OBJ_MAGIC, a supported version and sizes that keep the object inside the pack, otherwise IntegrityError naming the pack, like
check_pack_objectsdoes.superseded_gap_rangesalready does the same check when it walks headers in the gaps. The bounds check needs the pack size, which PackReader did not have, so there is nowPackReader.size():len(pack_contents)in memory,store.info(key).sizeotherwise, one metadata lookup per iter_headers call. No per-object roundtrip.Tests
Corrupt magic, unsupported version, and an object declared past the end of the pack all raise, in memory and through the store.
Split off: repair-time resync
The two resync commits that were here (scan forward to the next object header, accept a candidate only if it authenticates) have been dropped from this PR and kept for a follow-up, because they depend on questions about the pack format itself rather than on this check:
data_sizeis authenticated by nothing, andparse_meta()does no id check, sonone/authenticatedlose the one check that rejects a decoy object stored verbatim inside a file's content.iter_headerscosts one store range read per object. Rebuilding an index over ssh/rest is that many roundtrips per pack.Both go away if a pack carries an authenticated directory of its objects, or if the header itself is authenticated. Happy to work on either; the follow-up is easier to judge once that is decided.
What this does not fix
The pack stays damaged.
check --repairdrops the index infinish(), so the next command rebuilds from the packs and raises on the same header again. Before this PR that rebuild would have silently produced a wrong index instead, so this is not a regression, but a repo is only really usable again once the pack itself is repaired (#10026).