Bug report
TarInfo._proc_sparse() (Lib/tarfile.py:1496-1507) reads the extended
header block for a GNU GNUTYPE_SPARSE member with a plain
tarfile.fileobj.read(BLOCKSIZE) instead of _safe_read(), so a truncated
archive can hand it a short or empty buffer. The parse loop only guards
nti() with except ValueError, but nti() raises IndexError on an
empty buffer (s[0] at Lib/tarfile.py:177), which escapes that guard and
every handler in next()/TarFile.__init__.
The documented contract for malformed/truncated archives is
tarfile.ReadError (a subclass of tarfile.TarError, itself an OSError
in some versions). An application that catches that contract around
archive parsing gets an unexpected IndexError instead when the truncation
happens to land inside a GNU sparse member's extended header.
Reproduction
import io, tarfile
# Minimal GNU sparse ('S' typeflag) header with isextended=1, followed by
# a truncated (missing) extended-header block.
def build_truncated_sparse_tar():
... # see attached repro_sparse.py
Traceback (most recent call last):
...
File ".../Lib/tarfile.py", line 1500, in _proc_sparse
offset = nti(buf[pos:pos + 12])
File ".../Lib/tarfile.py", line 177, in nti
if s[0] in (0o200, 0o377):
IndexError: index out of range
A second variant (300-byte truncated extended header instead of a fully
missing one) hits the same IndexError at a different nti() call site
(Lib/tarfile.py:1501).
Your environment
- CPython versions tested: main (commit
64d315ac11a341fa5622718a679d0f6ef1a8dd6b)
- Reproducible under 3.12 as well (the affected code is unchanged there);
this is a long-standing issue, not a new regression.
Suggested fix
Read the extended header with _safe_read() (which raises ReadError on
EOF, matching the documented contract) instead of a plain read(), and/or
widen the guard to except (ValueError, IndexError). Separately, nti()
raising IndexError for empty input rather than InvalidHeaderError is
itself worth fixing, since the same except ValueError pattern appears at
other nti()/nts() call sites in the sparse-header parsing path.
I'll follow up with a PR.
Found via an automated code-review pass using GLM-5.3-flash paired with
scopegrep, a semantic code-retrieval tool; independently re-verified by
hand against the source above.
Linked PRs
Bug report
TarInfo._proc_sparse()(Lib/tarfile.py:1496-1507) reads the extendedheader block for a GNU
GNUTYPE_SPARSEmember with a plaintarfile.fileobj.read(BLOCKSIZE)instead of_safe_read(), so a truncatedarchive can hand it a short or empty buffer. The parse loop only guards
nti()withexcept ValueError, butnti()raisesIndexErroron anempty buffer (
s[0]atLib/tarfile.py:177), which escapes that guard andevery handler in
next()/TarFile.__init__.The documented contract for malformed/truncated archives is
tarfile.ReadError(a subclass oftarfile.TarError, itself anOSErrorin some versions). An application that catches that contract around
archive parsing gets an unexpected
IndexErrorinstead when the truncationhappens to land inside a GNU sparse member's extended header.
Reproduction
A second variant (300-byte truncated extended header instead of a fully
missing one) hits the same
IndexErrorat a differentnti()call site(
Lib/tarfile.py:1501).Your environment
64d315ac11a341fa5622718a679d0f6ef1a8dd6b)this is a long-standing issue, not a new regression.
Suggested fix
Read the extended header with
_safe_read()(which raisesReadErroronEOF, matching the documented contract) instead of a plain
read(), and/orwiden the guard to
except (ValueError, IndexError). Separately,nti()raising
IndexErrorfor empty input rather thanInvalidHeaderErrorisitself worth fixing, since the same
except ValueErrorpattern appears atother
nti()/nts()call sites in the sparse-header parsing path.I'll follow up with a PR.
Found via an automated code-review pass using GLM-5.3-flash paired with
scopegrep, a semantic code-retrieval tool; independently re-verified by
hand against the source above.
Linked PRs