https://github.com/python/cpython/commit/3b2e6a1ac45c16ecee1f3f982fe707c440430154
commit: 3b2e6a1ac45c16ecee1f3f982fe707c440430154
branch: main
author: Rudra <[email protected]>
committer: ethanfurman <[email protected]>
date: 2026-11-05T09:56:11-08:00
summary:

gh-159435: Fix tarfile offsets after PAX headers and GNU long names (GH-159532)

files:
A Misc/NEWS.d/next/Library/2026-10-16-13-00-00.gh-issue-159435.OMNI.rst
M Lib/tarfile.py
M Lib/test/test_tarfile.py

diff --git a/Lib/tarfile.py b/Lib/tarfile.py
index a75adc520e5e90..227a28766b634d 100644
--- a/Lib/tarfile.py
+++ b/Lib/tarfile.py
@@ -1628,6 +1628,8 @@ def _proc_pax(self, tarfile):
         except HeaderError as e:
             raise SubsequentHeaderError(str(e)) from None
 
+        data_offset = next.offset_data
+
         # Process GNU sparse information.
         if "GNU.sparse.map" in pax_headers:
             # GNU extended sparse format version 0.1.
@@ -1649,7 +1651,7 @@ def _proc_pax(self, tarfile):
                 # If the extended header replaces the size field,
                 # we need to recalculate the offset where the next
                 # header starts.
-                offset = next.offset + BLOCKSIZE
+                offset = data_offset
                 if next.isreg() or next.type not in SUPPORTED_TYPES:
                     try:
                         size = PAX_NUMBER_FIELDS["size"](pax_headers["size"])
diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py
index a55eb8d7bc2580..2ad6d0452ce29a 100644
--- a/Lib/test/test_tarfile.py
+++ b/Lib/test/test_tarfile.py
@@ -1511,6 +1511,34 @@ def test_sparse_file_10_pax_size(self):
             self.assertEqual(tar.getnames(), ["sparse", "last"])
             self.assertEqual(tar.extractfile("last").read(), data)
 
+    def test_pax_size_with_gnu_longname(self):
+        # gh-159435: a GNU long-name header between a pax header and its
+        # member must not make the member header get read a second time.
+        long_name = "long-name-" * 12 + "file.txt"
+
+        def header(name, type, size):
+            info = tarfile.TarInfo(name)
+            info.type = type
+            info.size = size
+            return info.tobuf(tarfile.USTAR_FORMAT)
+
+        def padded(data):
+            return data + b"\0" * (-len(data) % tarfile.BLOCKSIZE)
+
+        pax_record = b"9 size=5\n"
+        buf = header("././@PaxHeader", tarfile.XHDTYPE, len(pax_record))
+        buf += padded(pax_record)
+        longname = long_name.encode() + b"\0"
+        buf += header("././@LongLink", tarfile.GNUTYPE_LONGNAME, len(longname))
+        buf += padded(longname)
+        buf += header("member-short", tarfile.REGTYPE, 5) + padded(b"hello")
+        buf += header("next.txt", tarfile.REGTYPE, 3) + padded(b"bye")
+        buf += b"\0" * (tarfile.BLOCKSIZE * 2)
+
+        with tarfile.open(fileobj=io.BytesIO(buf)) as tar:
+            self.assertEqual(tar.getnames(), [long_name, "next.txt"])
+            self.assertEqual(tar.extractfile("next.txt").read(), b"bye")
+
     @staticmethod
     def _fs_supports_holes():
         # Return True if the platform knows the st_blocks stat attribute and
diff --git 
a/Misc/NEWS.d/next/Library/2026-10-16-13-00-00.gh-issue-159435.OMNI.rst 
b/Misc/NEWS.d/next/Library/2026-10-16-13-00-00.gh-issue-159435.OMNI.rst
new file mode 100644
index 00000000000000..3f4c71c2c2e0a6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-16-13-00-00.gh-issue-159435.OMNI.rst
@@ -0,0 +1,3 @@
+Fix an offset calculation in :mod:`tarfile` when a PAX header with a ``size``
+record is followed by a GNU long-name header.  Such archives could duplicate or
+skip members while being read.

_______________________________________________
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