From b4d4475e134b19ab8015e87332c9b7807cf3dd94 Mon Sep 17 00:00:00 2001 From: esadomer <54475303+esadomer@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:48:51 +0300 Subject: [PATCH 1/2] gh-159322: NUL-terminate owner names in PAX base headers --- Lib/tarfile.py | 5 ++++- Lib/test/test_tarfile.py | 20 +++++++++++++++++++ ...-10-13-19-10-00.gh-issue-159322.hP6k3A.rst | 2 ++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-10-13-19-10-00.gh-issue-159322.hP6k3A.rst diff --git a/Lib/tarfile.py b/Lib/tarfile.py index c0b9f3f429c10e2..397c5d1da3d86e8 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -1133,7 +1133,7 @@ def create_pax_header(self, info, encoding): # be represented in ASCII encoding. for name, hname, length in ( ("name", "path", LENGTH_NAME), ("linkname", "linkpath", LENGTH_LINK), - ("uname", "uname", 32), ("gname", "gname", 32)): + ("uname", "uname", 31), ("gname", "gname", 31)): if hname in pax_headers: # The pax header has priority. @@ -1177,6 +1177,9 @@ def create_pax_header(self, info, encoding): else: buf = b"" + # The owner names in the base header must be NUL-terminated. + info["uname"] = info["uname"][:31] + info["gname"] = info["gname"][:31] return buf + self._create_header(info, USTAR_FORMAT, "ascii", "replace") @classmethod diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 2b4cf748dffc1a9..1ee14c7393a6694 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -2494,6 +2494,26 @@ def test_dereference_hardlink(self): class PaxWriteTest(GNUWriteTest): + def test_owner_names(self): + for field, offset in (("uname", 265), ("gname", 297)): + for length in (0, 31, 32, 33, 40): + with self.subTest(field=field, length=length): + name = "a" * length + tarinfo = tarfile.TarInfo("test") + setattr(tarinfo, field, name) + header = tarinfo.tobuf(format=tarfile.PAX_FORMAT) + # The base header must leave room for a NUL terminator. + base_header = header[-tarfile.BLOCKSIZE:] + self.assertEqual(base_header[offset:offset + 32], + name[:31].encode().ljust(32, b"\0")) + with tarfile.open(fileobj=io.BytesIO(header)) as tar: + member = tar.next() + self.assertEqual(getattr(member, field), name) + if length > 31: + self.assertEqual(member.pax_headers[field], name) + else: + self.assertNotIn(field, member.pax_headers) + def _test(self, name, link=None): # See GNUWriteTest. tarinfo = tarfile.TarInfo(name) diff --git a/Misc/NEWS.d/next/Library/2026-10-13-19-10-00.gh-issue-159322.hP6k3A.rst b/Misc/NEWS.d/next/Library/2026-10-13-19-10-00.gh-issue-159322.hP6k3A.rst new file mode 100644 index 000000000000000..f9c3d1a98acc357 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-13-19-10-00.gh-issue-159322.hP6k3A.rst @@ -0,0 +1,2 @@ +Fix :mod:`tarfile` to NUL-terminate user and group names in PAX archive +headers and use extended headers for names longer than 31 characters. From 55da16e4ed2c2e6d652b0e43f84151aed3cb30c6 Mon Sep 17 00:00:00 2001 From: esadomer <54475303+esadomer@users.noreply.github.com> Date: Wed, 14 Oct 2026 11:48:09 +0300 Subject: [PATCH 2/2] gh-159322: Test full-width PAX name and linkname fields --- Lib/test/test_tarfile.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index 1ee14c7393a6694..feff2ce2c220f21 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -2494,6 +2494,23 @@ def test_dereference_hardlink(self): class PaxWriteTest(GNUWriteTest): + def test_100_byte_names(self): + for field, offset in (("name", 0), ("linkname", 157)): + with self.subTest(field=field): + name = "a" * 100 + tarinfo = tarfile.TarInfo("test") + if field == "linkname": + tarinfo.type = tarfile.SYMTYPE + setattr(tarinfo, field, name) + header = tarinfo.tobuf(format=tarfile.PAX_FORMAT) + self.assertEqual(len(header), tarfile.BLOCKSIZE) + self.assertEqual(header[offset:offset + 100], + name.encode("ascii")) + with tarfile.open(fileobj=io.BytesIO(header)) as tar: + member = tar.next() + self.assertEqual(getattr(member, field), name) + self.assertEqual(member.pax_headers, {}) + def test_owner_names(self): for field, offset in (("uname", 265), ("gname", 297)): for length in (0, 31, 32, 33, 40):