https://github.com/python/cpython/commit/769d44c87227c44d21ded11b06fb89cdebc64f2f commit: 769d44c87227c44d21ded11b06fb89cdebc64f2f branch: main author: Jason Mak <[email protected]> committer: ethanfurman <[email protected]> date: 2026-10-19T12:45:48-07:00 summary:
gh-156057: Wrap compression library exceptions in tarfile.ReadError (GH-156143) _FileInFile.read() (used by TarFile.extractfile() for seekable-mode archives) called the underlying compressed fileobj's read() directly. If a member's compressed payload was corrupted after the header had already been read successfully, the codec's own exception (zlib.error, OSError from bz2, lzma.LZMAError, or zstd.ZstdError) would leak straight through instead of being wrapped in tarfile.ReadError, unlike the existing streaming-mode (r|gz etc.) code path in _Stream, which already handles this correctly via a self.exception attribute set per codec. Give TarFile (and _FileInFile) the same per-codec exception attribute, set by gzopen/xzopen/zstopen to the specific error type (bz2 already matches the OSError class default), and wrap the read() call in _FileInFile with it. --------- Signed-off-by: Jason Mak <[email protected]> Co-authored-by: Ethan Furman <[email protected]> files: A Misc/NEWS.d/next/Library/2026-08-21-06-00-00.gh-issue-156057.tarfile-wrap-comp-err.rst M Lib/tarfile.py M Lib/test/test_tarfile.py diff --git a/Lib/tarfile.py b/Lib/tarfile.py index c0b9f3f429c10e..dc2d35fc27a3de 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -665,13 +665,15 @@ class _FileInFile(object): object. """ - def __init__(self, fileobj, offset, size, name, blockinfo=None): + def __init__(self, fileobj, offset, size, name, blockinfo=None, + exception=OSError): self.fileobj = fileobj self.offset = offset self.size = size self.position = 0 self.name = name self.closed = False + self.exception = exception if blockinfo is None: blockinfo = [(0, size)] @@ -748,7 +750,10 @@ def read(self, size=None): length = min(size, stop - self.position) if data: self.fileobj.seek(offset + (self.position - start)) - b = self.fileobj.read(length) + try: + b = self.fileobj.read(length) + except self.exception as e: + raise ReadError(f"invalid compressed data: {e}") from e if len(b) != length: raise ReadError("unexpected end of data") buf += b @@ -771,7 +776,8 @@ class ExFileObject(io.BufferedReader): def __init__(self, tarfile, tarinfo): fileobj = _FileInFile(tarfile.fileobj, tarinfo.offset_data, - tarinfo.size, tarinfo.name, tarinfo.sparse) + tarinfo.size, tarinfo.name, tarinfo.sparse, + exception=tarfile.exception) super().__init__(fileobj) #class ExFileObject @@ -1801,6 +1807,12 @@ class TarFile(object): fileobject = ExFileObject # The file-object for extractfile(). + exception = OSError # The exception raised by the underlying + # fileobj.read() on corrupt compressed + # data past the header (see gzopen() etc. + # for the more specific exception types + # used by each compression format). + extraction_filter = None # The default filter for extraction. def __init__(self, name=None, mode="r", fileobj=None, format=None, @@ -2046,6 +2058,7 @@ def gzopen(cls, name, mode="r", fileobj=None, compresslevel=6, **kwargs): try: from gzip import GzipFile + import zlib except ImportError: raise CompressionError("gzip module is not available") from None @@ -2068,6 +2081,7 @@ def gzopen(cls, name, mode="r", fileobj=None, compresslevel=6, **kwargs): fileobj.close() raise t._extfileobj = False + t.exception = zlib.error return t @classmethod @@ -2124,6 +2138,7 @@ def xzopen(cls, name, mode="r", fileobj=None, preset=None, **kwargs): fileobj.close() raise t._extfileobj = False + t.exception = LZMAError return t @classmethod @@ -2159,6 +2174,7 @@ def zstopen(cls, name, mode="r", fileobj=None, level=None, options=None, fileobj.close() raise t._extfileobj = False + t.exception = ZstdError return t # All *open() methods are registered here. diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 2b4cf748dffc1a..605e66b6c64c96 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -1204,6 +1204,72 @@ def runTest(self): tarfile.open(fileobj=f, mode='r|gz') +class FileInFileExceptionWrappingTest(unittest.TestCase): + """ + See: https://github.com/python/cpython/issues/156057 + + _FileInFile.read() must wrap whatever exception type its + underlying fileobj.read() raises for corrupt compressed data, + using the exception class attribute each TarFile.*open() + classmethod configures, in tarfile.ReadError -- and must not + swallow an exception of an unconfigured type. + """ + + class _FakeFileobj: + def __init__(self, exc): + self.exc = exc + + def seek(self, position): + pass + + def read(self, size): + raise self.exc + + def test_wraps_configured_exception_type(self): + class Boom(Exception): + pass + + fif = tarfile._FileInFile( + self._FakeFileobj(Boom("simulated corrupt compressed data")), + offset=0, size=10, name="member", exception=Boom) + with self.assertRaises(tarfile.ReadError): + fif.read() + + def test_does_not_wrap_unconfigured_exception_type(self): + class Boom(Exception): + pass + + class Unrelated(Exception): + pass + + fif = tarfile._FileInFile( + self._FakeFileobj(Unrelated("not the configured exception type")), + offset=0, size=10, name="member", exception=Boom) + with self.assertRaises(Unrelated): + fif.read() + + [email protected]_gzip() +class GzipExtractfileWrapsCompressionErrorTest(unittest.TestCase): + """ + See: https://github.com/python/cpython/issues/156057 + + Confirms the real gzopen() -> ExFileObject -> _FileInFile wiring: + a zlib.error raised while reading a member's compressed data past + the header surfaces as tarfile.ReadError, not the raw zlib.error. + """ + + def test_extractfile_read_wraps_zlib_error(self): + with tarfile.open(gzipname, mode='r:gz') as tar: + tarinfo = tar.getmember("ustar/regtype") + with tar.extractfile(tarinfo) as fobj: + with unittest.mock.patch.object( + tar.fileobj, "read", + side_effect=zlib.error("simulated corrupt data")): + with self.assertRaises(tarfile.ReadError): + fobj.read() + + class MemberReadTest(ReadTest, unittest.TestCase): def _test_member(self, tarinfo, chksum=None, **kwargs): diff --git a/Misc/NEWS.d/next/Library/2026-08-21-06-00-00.gh-issue-156057.tarfile-wrap-comp-err.rst b/Misc/NEWS.d/next/Library/2026-08-21-06-00-00.gh-issue-156057.tarfile-wrap-comp-err.rst new file mode 100644 index 00000000000000..1c8d8839dbf7bd --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-21-06-00-00.gh-issue-156057.tarfile-wrap-comp-err.rst @@ -0,0 +1,5 @@ +Wrap the underlying compression library's exception in :exc:`tarfile.ReadError` +when a member's compressed data is corrupt past its header, for all of +:mod:`tarfile`'s supported compression formats (gzip, bzip2, lzma, zstd). +Previously the raw exception (e.g. :exc:`zlib.error`) could leak through +:meth:`tarfile.TarFile.extractfile`. Patch by Jason Mak. _______________________________________________ Python-checkins mailing list -- [email protected] To unsubscribe send an email to [email protected] https://mail.python.org/mailman3//lists/python-checkins.python.org Member address: [email protected]
