Skip to content

import/export-tar: harden the PAX header handling - #10589

Open
ThomasWaldmann wants to merge 7 commits into
borgbackup:masterfrom
ThomasWaldmann:import-tar-hardening
Open

ThomasWaldmann wants to merge 7 commits into
borgbackup:masterfrom
ThomasWaldmann:import-tar-hardening

Conversation

@ThomasWaldmann

Copy link
Copy Markdown
Member

Follow-up to #10499 (and the 1.4 backport #10500), from a review of that code. One commit per issue:

  1. pax_time_to_ns: crash and slowness on huge exponents. Decimal("1e3000000").scaleb(9) raises decimal.Overflow, an ArithmeticError but not an OverflowError, so it was not caught and import-tar crashed. In-range huge exponents like 1e999999 made int() build a million-digit integer before safe_ns() clamped it (~15 s per header). Now the Decimal is clamped to [0, MAX_S] before the conversion (same result as safe_ns), NaN/Infinity are rejected and all decimal exceptions are caught.
  2. export-tar PAX crashed on an xattr name that is not valid UTF-8. A pax header keyword must be UTF-8 (tarfile encodes it strictly; only values get hdrcharset=BINARY), Linux xattr names are arbitrary bytes. Such an xattr is now skipped with a warning; silently with --tar-format=BORG, where BORG.item.meta has it anyway.
  3. Malformed ACL entries from a tar crashed borg extract on Linux. tar_acl_to_borg() passed e.g. user:foo through unchanged, and acl_use_local_uid_gid() then indexed fields[3] → IndexError, not an OSError, so no per-file warning but a traceback. tar_acl_to_borg() now raises ValueError for an unknown tag or a wrong field count and import-tar ignores such a header with a warning (TarfileObjectProcessors got a warning callback for that).
  4. Pre-existing: a pax mtime of 1e999 or nan crashed import-tar. tarfile parses it with float() unchecked, int(inf) / int(nan) raised in create_helper(). Falls back to 0 now (a valid pax mtime still overrides it).
  5. Unknown user/group names in ACLs. GNU tar writes named entries without a numeric id; if the name is unknown locally, the name is stored in place of the id (like borg create does) and every later extract of that file fails, also with --numeric-ids. import-tar now warns once per unknown name.
  6. Docs: the --tar-format table did not say what PAX/GNU still lose (BSD flags, NFSv4/extended ACLs, birthtime, content digests).
  7. Test: test_roundtrip_pax_timestamps never produced an atime header (no --atime); now checks all three.

Tests for 1–5 added (a crafted-tar test for each crash). Run on macOS and in a Linux container (where the ACL and non-UTF-8 xattr tests actually execute).

Not done here: acl_use_local_uid_gid() in platform/linux.pyx itself still indexes fields[3] unguarded, and the BORG-format import path trusts BORG.item.meta blindly (assert on the version, no make_path_safe, tracebacks on bad base64/msgpack) — separate PRs.

🤖 Generated with Claude Code

ThomasWaldmann and others added 7 commits November 3, 2026 22:42
A PAX timestamp header with a huge exponent made import-tar crash or
hang:

- "1e3000000": Decimal.scaleb() raises decimal.Overflow, which is an
  ArithmeticError, but not an OverflowError, so it was not caught.
- "1e999999": int() of the scaled Decimal builds an integer with a
  million digits before safe_ns() clamps it, taking ~15s per header.

Now the Decimal is clamped to [0, MAX_S] before the int conversion
(same result as safe_ns), NaN/Infinity are rejected and all decimal
exceptions are caught.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A pax header keyword must be valid UTF-8: the tarfile module encodes it
strictly, only the value may be arbitrary bytes (hdrcharset=BINARY).
Linux xattr names are arbitrary bytes, so export-tar --tar-format=PAX
crashed with a UnicodeEncodeError on such an xattr.

Now the xattr is skipped with a warning (rc 1). With the BORG format,
it is skipped silently, because BORG.item.meta has it anyway.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tar_acl_to_borg() passed entries it did not understand through
unchanged. An entry with too few fields (e.g. "user:foo") was stored
and made borg extract crash on Linux: acl_use_local_uid_gid() indexes
fields[3], raising an IndexError, which is not an OSError and thus not
turned into a per-file warning.

Now tar_acl_to_borg() raises ValueError for an entry with an unknown tag
or a wrong number of fields and import-tar ignores such an ACL header
with a warning. TarfileObjectProcessors got a warning callback for that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The tarfile module parses the pax mtime header with float() and does
not check the result, so a header like "1e999" or "nan" gave a float
inf or nan, and int() of that raised OverflowError or ValueError in
create_helper().

Now such a value falls back to 0 (a valid pax mtime still overrides it
via pax_time_to_ns, which clamps "1e999" to MAX_NS).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
When a named ACL entry from a tar PAX header has no numeric id (GNU tar
writes them that way) and the name is unknown locally, the name is
stored in place of the id, like borg create does. Unlike with borg
create, where the name always comes from the same host, this makes
every later borg extract of that file fail (also with --numeric-ids,
as the "stored id" is the name).

Now import-tar warns once per unknown name, so the user learns about it
when it can still be fixed (create the user/group, or use --noacls).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The --tar-format table only listed what PAX adds over GNU; the items'
BSD flags, NFSv4 ACLs (FreeBSD), extended ACLs (macOS), birthtime and
content digests are not exported with either format.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The test created the archive without --atime, so the atime header was
never produced and the only header checked was mtime.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@codecov

codecov Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.83673% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.36%. Comparing base (184cae0) to head (813e38d).
⚠️ Report is 8 commits behind head on master.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/borg/archive.py 88.23% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           master   #10589    +/-   ##
========================================
  Coverage   89.35%   89.36%            
========================================
  Files         104      105     +1     
  Lines       20091    20368   +277     
  Branches     3154     3198    +44     
========================================
+ Hits        17953    18202   +249     
- Misses       1481     1507    +26     
- Partials      657      659     +2     

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

This branch has not been deployed

No deployments
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.

1 participant