Skip to content

Commit ede7ba3

Browse files
committed
gh-133998: Clamp out-of-range mtime in tarfile streaming gzip writes
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).
1 parent 1b9fe5c commit ede7ba3

3 files changed

Lines changed: 48 additions & 2 deletions

File tree

Lib/tarfile.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -427,8 +427,12 @@ def _init_write_gz(self, compresslevel, mtime):
427427
self.zlib.DEF_MEM_LEVEL,
428428
0)
429429
if mtime is None:
430-
mtime = int(time.time())
431-
timestamp = struct.pack("<L", mtime)
430+
mtime = time.time()
431+
# gh-133998: substitute 0 for an out-of-range mtime and coerce to int,
432+
# mirroring gzip (RFC 1952), so struct.pack cannot raise struct.error.
433+
if not 0 <= mtime < 2**32:
434+
mtime = 0
435+
timestamp = struct.pack("<L", int(mtime))
432436
self.__write(b"\037\213\010\010" + timestamp + b"\002\377")
433437
if self.name.endswith(".gz"):
434438
self.name = self.name[:-3]

Lib/test/test_tarfile.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1885,6 +1885,44 @@ def test_create_with_mtime(self):
18851885
fobj.read()
18861886
self.assertEqual(fobj.mtime, 0)
18871887

1888+
def test_create_with_mtime_out_of_range(self):
1889+
# gh-133998: an mtime outside the gzip header's 32-bit range is
1890+
# stored as 0 rather than raising struct.error.
1891+
for mtime in (-1, 2**32):
1892+
with self.subTest(mtime=mtime):
1893+
tarfile.open(tmpname, self.mode, mtime=mtime).close()
1894+
with self.open(tmpname, 'r') as fobj:
1895+
fobj.read()
1896+
self.assertEqual(fobj.mtime, 0)
1897+
os_helper.unlink(tmpname)
1898+
1899+
def test_create_with_mtime_at_boundary(self):
1900+
# gh-133998: the largest in-range mtime is preserved, not clamped.
1901+
mtime = 2**32 - 1
1902+
tarfile.open(tmpname, self.mode, mtime=mtime).close()
1903+
with self.open(tmpname, 'r') as fobj:
1904+
fobj.read()
1905+
self.assertEqual(fobj.mtime, mtime)
1906+
1907+
def test_create_with_out_of_range_clock(self):
1908+
# gh-133998: an out-of-range system clock (mtime defaulting to
1909+
# time.time()) is stored as 0 rather than raising struct.error.
1910+
for clock in (-1, 2**32):
1911+
with self.subTest(clock=clock):
1912+
with unittest.mock.patch('time.time', return_value=float(clock)):
1913+
tarfile.open(tmpname, self.mode).close()
1914+
with self.open(tmpname, 'r') as fobj:
1915+
fobj.read()
1916+
self.assertEqual(fobj.mtime, 0)
1917+
os_helper.unlink(tmpname)
1918+
1919+
def test_create_with_float_mtime(self):
1920+
# gh-133998: a float mtime is truncated like gzip, not rejected.
1921+
tarfile.open(tmpname, self.mode, mtime=123456789.9).close()
1922+
with self.open(tmpname, 'r') as fobj:
1923+
fobj.read()
1924+
self.assertEqual(fobj.mtime, 123456789)
1925+
18881926
def test_create_without_mtime(self):
18891927
before = int(time.time())
18901928
tarfile.open(tmpname, self.mode).close()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :func:`tarfile.open` in streaming write mode (``w|gz``) raising
2+
:exc:`struct.error` when given an *mtime* outside the range of the gzip
3+
header's 32-bit timestamp field. Out-of-range values are now stored as ``0``,
4+
matching :mod:`gzip`.

0 commit comments

Comments
 (0)