From a2e4a277984573ad499b917a820af557c2d81049 Mon Sep 17 00:00:00 2001 From: KHAN <277948067+Khan3K@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:10:13 +0800 Subject: [PATCH] gh-155629: do not treat GNU header atime as a ustar path prefix When parsing an old-GNU header, the byte range at offset 345 is used for the member's atime rather than a ustar path prefix. The header magic is now checked before reconstructing a long name from the prefix field, so the atime is no longer prepended to the member name. --- Lib/tarfile.py | 9 +++++++-- Lib/test/test_tarfile.py | 14 ++++++++++++++ .../2026-08-13-12-00-00.gh-issue-155629.b8X6gH.rst | 4 ++++ 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-13-12-00-00.gh-issue-155629.b8X6gH.rst diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 592c4638c52c9b1..7e96dba8ca917f0 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -1353,6 +1353,7 @@ def _frombuf(cls, buf, encoding, errors, *, dircheck=True): obj.gname = nts(buf[297:329], encoding, errors) obj.devmajor = nti(buf[329:337]) obj.devminor = nti(buf[337:345]) + magic = buf[257:265] prefix = nts(buf[345:500], encoding, errors) # Old V7 tar format represents a directory as a regular @@ -1382,8 +1383,12 @@ def _frombuf(cls, buf, encoding, errors, *, dircheck=True): if obj.isdir(): obj.name = obj.name.rstrip("/") - # Reconstruct a ustar longname. - if prefix and obj.type not in GNU_TYPES: + # Reconstruct a ustar longname. The prefix field is only defined + # by the ustar format; in the GNU and star formats this byte range + # is used for other data (e.g. atime and ctime), so it must not be + # interpreted as a path prefix unless the header magic identifies + # a ustar header. + if prefix and magic == POSIX_MAGIC and obj.type not in GNU_TYPES: obj.name = prefix + "/" + obj.name return obj diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index bd544dfea51da32..f70dbe0c511d0f3 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -1443,6 +1443,20 @@ def test_gnulong_dirname_strips_all_trailing_slashes(self): self.assertEqual(names, ["b" * 20, "a" * 120]) + def test_gnu_atime_not_used_as_ustar_prefix(self): + # gh-155629: in an old-GNU header, bytes 345:357 hold the member's + # atime rather than a ustar path prefix. The atime must not be + # prepended to the member name. + member = tarfile.TarInfo("victim") + header = bytearray(member.tobuf(format=tarfile.GNU_FORMAT)) + header[345:357] = b"00000000001\0" + header[148:156] = b" " * 8 + header[148:156] = f"{sum(header):06o}\0 ".encode("ascii") + + buf = io.BytesIO(bytes(header) + b"\0" * 1024) + with tarfile.open(fileobj=buf, mode="r:") as tar: + self.assertEqual(tar.getnames(), ["victim"]) + class PaxReadTest(LongnameTest, ReadTest, unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2026-08-13-12-00-00.gh-issue-155629.b8X6gH.rst b/Misc/NEWS.d/next/Library/2026-08-13-12-00-00.gh-issue-155629.b8X6gH.rst new file mode 100644 index 000000000000000..55b61ce576b7544 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-13-12-00-00.gh-issue-155629.b8X6gH.rst @@ -0,0 +1,4 @@ +When reading a GNU-format archive, the member's atime, which is stored +in the byte range that the ustar format uses for the path prefix, is no +longer mistaken for a ustar path prefix and prepended to the member +name.