https://github.com/python/cpython/commit/d4b8d6668a84b8f93b0d2de13f142d3721a5dafb commit: d4b8d6668a84b8f93b0d2de13f142d3721a5dafb branch: 3.15 author: Miss Islington (bot) <[email protected]> committer: vstinner <[email protected]> date: 2026-10-11T12:26:07+02:00 summary:
[3.15] gh-133998: Fix tarfile error on out-of-range mtime in streaming gzip mode (GH-151828) (#159158) gh-133998: Fix tarfile error on out-of-range mtime in streaming gzip mode (GH-151828) tarfile.open(..., "w|gz", mtime=...) packed mtime into the gzip header's 32-bit field with no range check, so a value < 0 or >= 2**32 raised struct.error. Mirror the merged gzip fix: substitute 0 for out-of-range values and coerce to int (RFC 1952), so floats and out-of-range system clocks behave the same as in gzip and the non-streaming "w:gz" path (which already delegates to GzipFile and was unaffected). (cherry picked from commit c6c060843b7635b5a1f82e0983d803eb0d185b5d) Co-authored-by: Harjoth Khara <[email protected]> Co-authored-by: Victor Stinner <[email protected]> files: A Misc/NEWS.d/next/Library/2026-06-20-20-24-03.gh-issue-133998.nkqhMz.rst M Lib/tarfile.py M Lib/test/test_tarfile.py diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 8a4f74de4f3e58..a3dbf1b1a7b94f 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -453,8 +453,12 @@ def _init_write_gz(self, compresslevel, mtime): self.zlib.DEF_MEM_LEVEL, 0) if mtime is None: - mtime = int(time.time()) - timestamp = struct.pack("<L", mtime) + mtime = time.time() + # gh-133998: substitute 0 for an out-of-range mtime and coerce to int, + # mirroring gzip (RFC 1952), so struct.pack cannot raise struct.error. + if not 0 <= mtime < 2**32: + mtime = 0 + timestamp = struct.pack("<L", int(mtime)) self.__write(b"\037\213\010\010" + timestamp + b"\002\377") if self.name.endswith(".gz"): self.name = self.name[:-3] diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 60666ff4cc100d..73e53d6cc3930f 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -1983,6 +1983,44 @@ def test_create_with_mtime(self): fobj.read() self.assertEqual(fobj.mtime, 0) + def test_create_with_mtime_out_of_range(self): + # gh-133998: an mtime outside the gzip header's 32-bit range is + # stored as 0 rather than raising struct.error. + self.addCleanup(os_helper.unlink, tmpname) + for mtime in (-1, 2**32): + with self.subTest(mtime=mtime): + tarfile.open(tmpname, self.mode, mtime=mtime).close() + with self.open(tmpname, 'r') as fobj: + fobj.read() + self.assertEqual(fobj.mtime, 0) + + def test_create_with_mtime_at_boundary(self): + # gh-133998: the largest in-range mtime is preserved, not clamped. + mtime = 2**32 - 1 + tarfile.open(tmpname, self.mode, mtime=mtime).close() + with self.open(tmpname, 'r') as fobj: + fobj.read() + self.assertEqual(fobj.mtime, mtime) + + def test_create_with_out_of_range_clock(self): + # gh-133998: an out-of-range system clock (mtime defaulting to + # time.time()) is stored as 0 rather than raising struct.error. + for clock in (-1, 2**32): + with self.subTest(clock=clock): + with unittest.mock.patch('time.time', return_value=float(clock)): + tarfile.open(tmpname, self.mode).close() + with self.open(tmpname, 'r') as fobj: + fobj.read() + self.assertEqual(fobj.mtime, 0) + os_helper.unlink(tmpname) + + def test_create_with_float_mtime(self): + # gh-133998: a float mtime is truncated like gzip, not rejected. + tarfile.open(tmpname, self.mode, mtime=123456789.9).close() + with self.open(tmpname, 'r') as fobj: + fobj.read() + self.assertEqual(fobj.mtime, 123456789) + def test_create_without_mtime(self): before = int(time.time()) tarfile.open(tmpname, self.mode).close() diff --git a/Misc/NEWS.d/next/Library/2026-06-20-20-24-03.gh-issue-133998.nkqhMz.rst b/Misc/NEWS.d/next/Library/2026-06-20-20-24-03.gh-issue-133998.nkqhMz.rst new file mode 100644 index 00000000000000..e7c03c662ec44f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-20-20-24-03.gh-issue-133998.nkqhMz.rst @@ -0,0 +1,4 @@ +Fix :func:`tarfile.open` in streaming write mode (``w|gz``) raising +:exc:`struct.error` when given an *mtime* outside the range of the gzip +header's 32-bit timestamp field. Out-of-range values are now stored as ``0``, +matching :mod:`gzip`. _______________________________________________ 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]
