Skip to content

index rebuild: abort cleanly on a corrupt object header, #10122 - #10168

Open
mr-raj12 wants to merge 5 commits into
borgbackup:masterfrom
mr-raj12:corrupt-pack-abort-10122
Open

index rebuild: abort cleanly on a corrupt object header, #10122#10168
mr-raj12 wants to merge 5 commits into
borgbackup:masterfrom
mr-raj12:corrupt-pack-abort-10122

Conversation

@mr-raj12

Copy link
Copy Markdown
Contributor

Fixes #10122. Stacked on #10094, and the first commit here belongs to that one, so review/merge #10094 first.

When the index/ fragments are gone or unreadable, borg rebuilds the chunks index by walking the object headers of every pack. Hit a corrupt header and iter_headers() raises IntegrityError, which nobody catches, so a plain borg repo-list dies with a traceback and rc 90.

build_chunkindex_from_repo() now catches it and raises CorruptPack, a new Error subclass with rc 93 and no traceback:

Data integrity error: pack c65e98ca7af6...: invalid object header at offset 30186
(pack corruption). Run "borg check --repair" to recover the objects that are still readable.

It aborts instead of skipping the damaged object, because everything after that offset would be missing from the index without the user ever knowing. borg check --repair is the one caller that passes a validate callable, and it still resyncs past the header as before.

CorruptPack is not an IntegrityError subclass. That would print a traceback, and it would get swallowed by the except IntegrityError handlers in check_cmd.py and compact_cmd.py that handle single corrupt objects. The , run "borg check" tail is gone from the iter_headers() message now that the wrapper says something more useful.

The other commit

Testing this turned up a gap in #10094. ArchiveChecker builds its own chunks index with a validator, but repository.get() resolves chunk locations through repository.chunks, a separate index built lazily on first access with no validator at all. So borg check --repair aborted at a header it had already resynced past. Once CorruptPack was in, that turned into the command telling the user to run the command they were running.

Repository.chunkindex_validate fixes it: the lazy rebuild hands it to iter_headers(), ArchiveChecker.check() sets it when repairing. Handing over the checker's index object instead does not work, it stores plaintext sizes and PackWriter.add() asserts size == 0.

Verification

Real repository, one header byte flipped at a known object offset, index/ deleted:

  • borg repo-list: the message above, rc 93, no traceback
  • same with BORG_EXIT_CODES=legacy: rc 2
  • borg compact: same message, rc 93
  • borg check --repair: resyncs, reports the one lost chunk, rc 0

test_build_chunkindex_repair_resyncs_after_corrupt_header (from #10094) now expects CorruptPack at the API level. test_repo_list_aborts_cleanly_on_corrupt_pack covers the exit code and the missing traceback through the real CLI, forked so the top level error handler actually runs.

One thing the test does not claim: borg check --repair does not rewrite the damaged pack yet, so a later index-less rebuild walks into the same header again. That is a TODO in the test rather than an assertion that the repository is fine afterwards.

Full suite 2528 passed, 333 skipped (-k "not remote and not binary"). ruff and black clean. Exit code 93 added to the table in frontends.rst, plus a note in the iter_headers() section of packs.rst.

…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.
…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.
…up#8476

A meta_size or data_size corrupted to a value that keeps the object inside the
pack leaves the header valid, so the walk only notices at the misaligned offset
it jumps to; scanning from there loses the intact objects in between. An object
is dropped when a recovered object starts inside it, its size field being wrong.
…rgbackup#8476

ArchiveChecker builds its own chunks index with a validate callable, but
repository.get() resolves chunk locations through repository.chunks, a separate
index that is built lazily on first access. That rebuild had no validator, so a
repair run aborted at a corrupt object header it had already resynced past when
building the checker index.

Add Repository.chunkindex_validate, passed on to the lazy rebuild, and set it in
ArchiveChecker.check() when repairing.
…0122

Rebuilding the chunks index from the packs walks every object header. Without a
validate callable, iter_headers raises IntegrityError at a corrupt header, which
reached the user as a traceback with rc 90.

Catch it in build_chunkindex_from_repo and raise CorruptPack (rc 93, no
traceback) instead, telling the user to run "borg check --repair". Continuing
past the header is not an option there: the resulting index would be incomplete.
Only check --repair passes a validator and resyncs.
@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.66667% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.60%. Comparing base (7bc4647) to head (e2a4fd1).
⚠️ Report is 1 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/borg/repository.py 95.08% 1 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #10168      +/-   ##
==========================================
+ Coverage   86.55%   86.60%   +0.05%     
==========================================
  Files         101      101              
  Lines       17996    18071      +75     
  Branches     2738     2753      +15     
==========================================
+ Hits        15577    15651      +74     
- Misses       1717     1718       +1     
  Partials      702      702              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

check --repair / repo-list: uncaught IntegrityError from iter_headers aborts the slow index rebuild with a traceback

1 participant