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]

Reply via email to