Skip to content

Commit 3dd9d15

Browse files
committed
gh-153636: Raise InvalidHeaderError for malformed GNU sparse pax numbers in tarfile
When a pax extended header carried a malformed GNU sparse number, tarfile parsed it with a bare int() and let the resulting ValueError escape to the caller from _proc_gnusparse_01, _proc_gnusparse_10, and the GNU.sparse.size and GNU.sparse.realsize branches of _apply_pax_info, unlike the GNU sparse 0.0 handler which already reports such corruption as InvalidHeaderError. Wrap those conversions so a bad number raises tarfile.InvalidHeaderError, which the reader treats as end of archive, giving the whole GNU sparse family consistent behavior on a corrupt header.
1 parent 832bc0b commit 3dd9d15

3 files changed

Lines changed: 94 additions & 5 deletions

File tree

Lib/tarfile.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,7 +1647,10 @@ def _proc_gnusparse_00(self, next, raw_headers):
16471647
def _proc_gnusparse_01(self, next, pax_headers):
16481648
"""Process a GNU tar extended sparse header, version 0.1.
16491649
"""
1650-
sparse = [int(x) for x in pax_headers["GNU.sparse.map"].split(",")]
1650+
try:
1651+
sparse = [int(x) for x in pax_headers["GNU.sparse.map"].split(",")]
1652+
except ValueError:
1653+
raise InvalidHeaderError("invalid header")
16511654
next.sparse = list(zip(sparse[::2], sparse[1::2]))
16521655

16531656
def _proc_gnusparse_10(self, next, pax_headers, tarfile):
@@ -1657,12 +1660,18 @@ def _proc_gnusparse_10(self, next, pax_headers, tarfile):
16571660
sparse = []
16581661
buf = tarfile.fileobj.read(BLOCKSIZE)
16591662
fields, buf = buf.split(b"\n", 1)
1660-
fields = int(fields)
1663+
try:
1664+
fields = int(fields)
1665+
except ValueError:
1666+
raise InvalidHeaderError("invalid header")
16611667
while len(sparse) < fields * 2:
16621668
if b"\n" not in buf:
16631669
buf += tarfile.fileobj.read(BLOCKSIZE)
16641670
number, buf = buf.split(b"\n", 1)
1665-
sparse.append(int(number))
1671+
try:
1672+
sparse.append(int(number))
1673+
except ValueError:
1674+
raise InvalidHeaderError("invalid header")
16661675
next.offset_data = tarfile.fileobj.tell()
16671676
next.sparse = list(zip(sparse[::2], sparse[1::2]))
16681677

@@ -1674,9 +1683,15 @@ def _apply_pax_info(self, pax_headers, encoding, errors):
16741683
if keyword == "GNU.sparse.name":
16751684
setattr(self, "path", value)
16761685
elif keyword == "GNU.sparse.size":
1677-
setattr(self, "size", int(value))
1686+
try:
1687+
setattr(self, "size", int(value))
1688+
except ValueError:
1689+
raise InvalidHeaderError("invalid header")
16781690
elif keyword == "GNU.sparse.realsize":
1679-
setattr(self, "size", int(value))
1691+
try:
1692+
setattr(self, "size", int(value))
1693+
except ValueError:
1694+
raise InvalidHeaderError("invalid header")
16801695
elif keyword in PAX_FIELDS:
16811696
if keyword in PAX_NUMBER_FIELDS:
16821697
try:

Lib/test/test_tarfile.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2435,6 +2435,77 @@ def test_pax_extended_header(self):
24352435
finally:
24362436
tar.close()
24372437

2438+
def _make_malformed_gnusparse_tar(self, pax_entries, following=b""):
2439+
# Build an in-memory tar whose pax extended ('x') header carries the
2440+
# given GNU.sparse.* records, followed by a regular file member (and
2441+
# optionally a leading data block for the sparse-1.0 map).
2442+
def pax_record(key, value):
2443+
kv = key.encode() + b"=" + value.encode() + b"\n"
2444+
n = len(kv)
2445+
p = len(str(n)) + 1 + n
2446+
while len(str(p)) + 1 + n != p:
2447+
p = len(str(p)) + 1 + n
2448+
return str(p).encode() + b" " + kv
2449+
2450+
def header(name, size, typeflag):
2451+
buf = bytearray(512)
2452+
buf[0:len(name)] = name
2453+
buf[100:108] = b"0000644\x00"
2454+
buf[108:116] = b"0000000\x00"
2455+
buf[116:124] = b"0000000\x00"
2456+
buf[124:136] = ("%011o\x00" % size).encode()
2457+
buf[136:148] = b"00000000000\x00"
2458+
buf[148:156] = b" "
2459+
buf[156] = ord(typeflag)
2460+
buf[257:265] = b"ustar\x0000"
2461+
chksum = sum(buf) & 0o777777
2462+
buf[148:156] = ("%06o\x00 " % chksum).encode()
2463+
return bytes(buf)
2464+
2465+
records = b"".join(pax_record(k, v) for k, v in pax_entries)
2466+
blocks = header(b"././@PaxHeader", len(records), "x")
2467+
blocks += records + b"\x00" * (-len(records) % 512)
2468+
blocks += header(b"testfile", 0, "0")
2469+
if following:
2470+
blocks += following
2471+
blocks += b"\x00" * 1024
2472+
return io.BytesIO(blocks)
2473+
2474+
def test_pax_gnusparse_bad_number(self):
2475+
# A malformed GNU sparse number in a pax extended header is reported
2476+
# through InvalidHeaderError instead of a bare ValueError, so the
2477+
# reader stops cleanly like it does for the GNU sparse 0.0 format.
2478+
cases = [
2479+
[("GNU.sparse.map", "1,notanumber")], # sparse 0.1
2480+
[("GNU.sparse.size", "notanumber")],
2481+
[("GNU.sparse.realsize", "notanumber")],
2482+
]
2483+
for entries in cases:
2484+
with self.subTest(entries=entries):
2485+
fobj = self._make_malformed_gnusparse_tar(entries)
2486+
with tarfile.open(fileobj=fobj) as tar:
2487+
self.assertEqual(tar.getmembers(), [])
2488+
2489+
for data in (b"notanumber\n", b"1\nnotanumber\n"): # sparse 1.0
2490+
with self.subTest(data=data):
2491+
following = data + b"\x00" * (-len(data) % 512)
2492+
fobj = self._make_malformed_gnusparse_tar(
2493+
[("GNU.sparse.major", "1"), ("GNU.sparse.minor", "0")],
2494+
following=following)
2495+
with tarfile.open(fileobj=fobj) as tar:
2496+
self.assertEqual(tar.getmembers(), [])
2497+
2498+
def test_pax_gnusparse_bad_number_direct(self):
2499+
# The parsing helpers themselves raise InvalidHeaderError, not
2500+
# ValueError, mirroring _proc_gnusparse_00.
2501+
ti = tarfile.TarInfo("x")
2502+
with self.assertRaises(tarfile.InvalidHeaderError):
2503+
ti._apply_pax_info({"GNU.sparse.size": "notanumber"},
2504+
"utf-8", "strict")
2505+
with self.assertRaises(tarfile.InvalidHeaderError):
2506+
ti._apply_pax_info({"GNU.sparse.realsize": "notanumber"},
2507+
"utf-8", "strict")
2508+
24382509
def test_create_pax_header(self):
24392510
# The ustar header should contain values that can be
24402511
# represented reasonably, even if a better (e.g. higher
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:mod:`tarfile` now raises :exc:`tarfile.InvalidHeaderError` instead of a bare
2+
:exc:`ValueError` when a pax extended header contains a malformed GNU sparse
3+
number, matching the existing handling of the GNU sparse 0.0 format.

0 commit comments

Comments
 (0)