Skip to content

Commit 5c75b50

Browse files
gh-159322: NUL-terminate owner names in PAX base headers (GH-159329)
Co-authored-by: Ethan Furman <ethan@stoneleaf.us>
1 parent 769d44c commit 5c75b50

3 files changed

Lines changed: 43 additions & 1 deletion

File tree

‎Lib/tarfile.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1139,7 +1139,7 @@ def create_pax_header(self, info, encoding):
11391139
# be represented in ASCII encoding.
11401140
for name, hname, length in (
11411141
("name", "path", LENGTH_NAME), ("linkname", "linkpath", LENGTH_LINK),
1142-
("uname", "uname", 32), ("gname", "gname", 32)):
1142+
("uname", "uname", 31), ("gname", "gname", 31)):
11431143

11441144
if hname in pax_headers:
11451145
# The pax header has priority.
@@ -1183,6 +1183,9 @@ def create_pax_header(self, info, encoding):
11831183
else:
11841184
buf = b""
11851185

1186+
# The owner names in the base header must be NUL-terminated.
1187+
info["uname"] = info["uname"][:31]
1188+
info["gname"] = info["gname"][:31]
11861189
return buf + self._create_header(info, USTAR_FORMAT, "ascii", "replace")
11871190

11881191
@classmethod

‎Lib/test/test_tarfile.py‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2560,6 +2560,43 @@ def test_dereference_hardlink(self):
25602560

25612561
class PaxWriteTest(GNUWriteTest):
25622562

2563+
def test_100_byte_names(self):
2564+
for field, offset in (("name", 0), ("linkname", 157)):
2565+
with self.subTest(field=field):
2566+
name = "a" * 100
2567+
tarinfo = tarfile.TarInfo("test")
2568+
if field == "linkname":
2569+
tarinfo.type = tarfile.SYMTYPE
2570+
setattr(tarinfo, field, name)
2571+
header = tarinfo.tobuf(format=tarfile.PAX_FORMAT)
2572+
self.assertEqual(len(header), tarfile.BLOCKSIZE)
2573+
self.assertEqual(header[offset:offset + 100],
2574+
name.encode("ascii"))
2575+
with tarfile.open(fileobj=io.BytesIO(header)) as tar:
2576+
member = tar.next()
2577+
self.assertEqual(getattr(member, field), name)
2578+
self.assertEqual(member.pax_headers, {})
2579+
2580+
def test_owner_names(self):
2581+
for field, offset in (("uname", 265), ("gname", 297)):
2582+
for length in (0, 31, 32, 33, 40):
2583+
with self.subTest(field=field, length=length):
2584+
name = "a" * length
2585+
tarinfo = tarfile.TarInfo("test")
2586+
setattr(tarinfo, field, name)
2587+
header = tarinfo.tobuf(format=tarfile.PAX_FORMAT)
2588+
# The base header must leave room for a NUL terminator.
2589+
base_header = header[-tarfile.BLOCKSIZE:]
2590+
self.assertEqual(base_header[offset:offset + 32],
2591+
name[:31].encode().ljust(32, b"\0"))
2592+
with tarfile.open(fileobj=io.BytesIO(header)) as tar:
2593+
member = tar.next()
2594+
self.assertEqual(getattr(member, field), name)
2595+
if length > 31:
2596+
self.assertEqual(member.pax_headers[field], name)
2597+
else:
2598+
self.assertNotIn(field, member.pax_headers)
2599+
25632600
def _test(self, name, link=None):
25642601
# See GNUWriteTest.
25652602
tarinfo = tarfile.TarInfo(name)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :mod:`tarfile` to NUL-terminate user and group names in PAX archive
2+
headers and use extended headers for names longer than 31 characters.

0 commit comments

Comments
 (0)