Bug#1144419: trixie-pu: package python3.13/3.13.5-2+deb13u5

Moritz Muehlenhoff <[email protected]>
Newsgroups gmane.linux.debian.devel.release
Message-ID <178673709995.98822.8310215598590040583.reportbug__15689.7118360334$1786737221$gmane$org@soju.westfalen.local>
Package: release.debian.org
Severity: normal
Tags: trixie
X-Debbugs-Cc: [email protected], [email protected], [email protected], [email protected]
Control: affects -1 + src:python3.13
User: [email protected]
Usertags: pu

The debdiff below fixes five security issues in Python 3.13, all are cherrypicks
from the upstream 3.13 branch. All tests in debusine are looking good.

Cheers,
        Moritz

diff -Nru python3.13-3.13.5/debian/changelog python3.13-3.13.5/debian/changelog
--- python3.13-3.13.5/debian/changelog	2026-07-15 22:25:40.000000000 +0200
+++ python3.13-3.13.5/debian/changelog	2026-08-10 14:06:59.000000000 +0200
@@ -1,3 +1,13 @@
+python3.13 (3.13.5-2+deb13u5) trixie; urgency=medium
+
+  * CVE-2026-6879
+  * CVE-2026-0864 (Closes: #1141524)
+  * CVE-2026-4360 (Closes: #1141531)
+  * CVE-2026-11940 (Closes: #1141533)
+  * CVE-2026-11972 (Closes: #1141534)
+
+ -- Moritz Mühlenhoff <[email protected]>  Mon, 10 Aug 2026 14:06:59 +0200
+
 python3.13 (3.13.5-2+deb13u4) trixie; urgency=medium
 
   * Patch: Fix use-after-free in dict.clear() with embedded values.
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-0864.patch python3.13-3.13.5/debian/patches/CVE-2026-0864.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-0864.patch	1970-01-01 01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-0864.patch	2026-08-10 14:00:50.000000000 +0200
@@ -0,0 +1,40 @@
+From aaf850fd333cd89e9aada03d92aaa788a6cb1bb8 Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Wed, 24 Jun 2026 11:46:43 +0200
+Subject: [PATCH] [3.13] gh-143927: Normalize all line endings (CR, CRLF, and
+ LF) in configparser (GH-143929) (GH-152004)
+
+--- python3.13-3.13.5.orig/Lib/configparser.py
++++ python3.13-3.13.5/Lib/configparser.py
+@@ -966,7 +966,9 @@ class RawConfigParser(MutableMapping):
+             value = self._interpolation.before_write(self, section_name, key,
+                                                      value)
+             if value is not None or not self._allow_no_value:
+-                value = delimiter + str(value).replace('\n', '\n\t')
++                # Convert all possible line-endings into '\n\t'
++                value = (delimiter + str(value).replace('\r\n', '\n')
++                         .replace('\r', '\n').replace('\n', '\n\t'))
+             else:
+                 value = ""
+             fp.write("{}{}\n".format(key, value))
+--- python3.13-3.13.5.orig/Lib/test/test_configparser.py
++++ python3.13-3.13.5/Lib/test/test_configparser.py
+@@ -526,6 +526,17 @@ boolean {0[0]} NO
+             cf.get(self.default_section, "Foo"), "Bar",
+             "could not locate option, expecting case-insensitive defaults")
+ 
++    def test_crlf_normalization(self):
++        cf = self.newconfig({"key1": "a\nb","key2": "a\rb", "key3": "a\r\nb", "key4": "a\r\nb"})
++        buf = io.StringIO()
++        cf.write(buf)
++        cf_str = buf.getvalue()
++        self.assertNotIn("\r", cf_str)
++        self.assertNotIn("\r\n", cf_str)
++        self.assertEqual(cf_str.count("\n"), 10)
++        self.assertEqual(cf_str.count("\n\t"), 4)
++        self.assertTrue(cf_str.endswith("\n\n"))
++
+     def test_parse_errors(self):
+         cf = self.newconfig()
+         self.parse_error(cf, configparser.ParsingError,
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-11940.patch python3.13-3.13.5/debian/patches/CVE-2026-11940.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-11940.patch	1970-01-01 01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-11940.patch	2026-08-10 14:04:19.000000000 +0200
@@ -0,0 +1,52 @@
+From 771d12dda5140313db0ac550292987975651bbde Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Tue, 23 Jun 2026 15:56:51 +0200
+Subject: [PATCH] [3.13] gh-151558: Fix symlink escape via `tarfile`
+ hardlink-extraction fallback (GH-151559)
+
+--- python3.13-3.13.5.orig/Lib/tarfile.py
++++ python3.13-3.13.5/Lib/tarfile.py
+@@ -2710,6 +2710,9 @@ class TarFile(object):
+                     "makelink_with_filter: if filter_function is not None, "
+                     + "extraction_root must also not be None")
+             try:
++                filter_function(
++                    unfiltered.replace(name=tarinfo.name, deep=False),
++                    extraction_root)
+                 filtered = filter_function(unfiltered, extraction_root)
+             except _FILTER_ERRORS as cause:
+                 raise LinkFallbackError(tarinfo, unfiltered.name) from cause
+--- python3.13-3.13.5.orig/Lib/test/test_tarfile.py
++++ python3.13-3.13.5/Lib/test/test_tarfile.py
+@@ -4222,6 +4222,30 @@ class TestExtractionFilters(unittest.Tes
+                     self.expect_file("c", symlink_to='b')
+ 
+     @symlink_test
++    def test_sneaky_hardlink_fallback_deep(self):
++        # (CVE-2026-11940)
++        with ArchiveMaker() as arc:
++            arc.add("a/b/s", symlink_to=os.path.join("..", "escape"))
++            arc.add("s", hardlink_to=os.path.join("a", "b", "s"))
++
++        with self.check_context(arc.open(), 'data'):
++            e = self.expect_exception(
++                tarfile.LinkFallbackError,
++                "link 's' would be extracted as a copy of "
++                + "'a/b/s', which was rejected")
++            self.assertIsInstance(e.__cause__,
++                                  tarfile.LinkOutsideDestinationError)
++
++        for filter in 'tar', 'fully_trusted':
++            with self.subTest(filter), self.check_context(arc.open(), filter):
++                if not os_helper.can_symlink():
++                    self.expect_file("a/")
++                    self.expect_file("a/b/")
++                else:
++                    self.expect_file("a/b/s", symlink_to=os.path.join('..', 'escape'))
++                    self.expect_file("s", symlink_to=os.path.join('..', 'escape'))
++
++    @symlink_test
+     def test_exfiltration_via_symlink(self):
+         # (CVE-2025-4138)
+         # Test changing symlinks that result in a symlink pointing outside
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-11972.patch python3.13-3.13.5/debian/patches/CVE-2026-11972.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-11972.patch	1970-01-01 01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-11972.patch	2026-08-10 14:04:58.000000000 +0200
@@ -0,0 +1,45 @@
+From 3f031d431f80668e14f3bc066bbf4369cd9281b9 Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Tue, 23 Jun 2026 15:46:38 +0200
+Subject: [PATCH] [3.13] gh-151981: Make tarfile._Stream.seek break at EOF
+ (GH-151982) (#151993)
+
+--- python3.13-3.13.5.orig/Lib/tarfile.py
++++ python3.13-3.13.5/Lib/tarfile.py
+@@ -515,7 +515,9 @@ class _Stream:
+         if pos - self.pos >= 0:
+             blocks, remainder = divmod(pos - self.pos, self.bufsize)
+             for i in range(blocks):
+-                self.read(self.bufsize)
++                data = self.read(self.bufsize)
++                if not data:
++                    break
+             self.read(remainder)
+         else:
+             raise StreamError("seeking backwards is not allowed")
+--- python3.13-3.13.5.orig/Lib/test/test_tarfile.py
++++ python3.13-3.13.5/Lib/test/test_tarfile.py
+@@ -4764,6 +4764,22 @@ class TestExtractionFilters(unittest.Tes
+         with self.check_context(arc.open(errorlevel='boo!'), filtererror_filter):
+             self.expect_exception(TypeError)  # errorlevel is not int
+ 
++    @support.subTests('format', [tarfile.GNU_FORMAT, tarfile.PAX_FORMAT])
++    def test_getmembers_big_size(self, format):
++        # gh-151981: A loop in seek() for streaming files tried to read the
++        # declared number of blocks even at EOF
++        tinfo = tarfile.TarInfo("huge-file")
++        tinfo.size = 1 << 64
++        bio = io.BytesIO()
++        # Write header without data
++        bio.write(tinfo.tobuf(format))
++
++        # Reset & try to get contents
++        bio.seek(0)
++        with tarfile.open(fileobj=bio, mode="r|") as tar:
++            with self.assertRaises(tarfile.ReadError):
++                tar.getmembers()
++
+ 
+ class OverwriteTests(archiver_tests.OverwriteTests, unittest.TestCase):
+     testdir = os.path.join(TEMPDIR, "testoverwrite")
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-4360.patch python3.13-3.13.5/debian/patches/CVE-2026-4360.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-4360.patch	1970-01-01 01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-4360.patch	2026-08-10 14:01:29.000000000 +0200
@@ -0,0 +1,120 @@
+From eee3ddf0ca10283cc7fea724aae9cd8665f8d15e Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Mon, 29 Jun 2026 21:11:44 +0200
+Subject: [PATCH] [3.13] gh-151987: Pass filter_function to
+ `TarFile._extract_one()` during `.extract()` (GH-151988) (#152610)
+
+--- python3.13-3.13.5.orig/Lib/tarfile.py
++++ python3.13-3.13.5/Lib/tarfile.py
+@@ -2440,7 +2440,8 @@ class TarFile(object):
+         tarinfo, unfiltered = self._get_extract_tarinfo(
+             member, filter_function, path)
+         if tarinfo is not None:
+-            self._extract_one(tarinfo, path, set_attrs, numeric_owner)
++            self._extract_one(tarinfo, path, set_attrs, numeric_owner,
++                              filter_function=filter_function)
+ 
+     def _get_extract_tarinfo(self, member, filter_function, path):
+         """Get (filtered, unfiltered) TarInfos from *member*
+--- python3.13-3.13.5.orig/Lib/test/test_tarfile.py
++++ python3.13-3.13.5/Lib/test/test_tarfile.py
+@@ -4276,6 +4276,98 @@ class TestExtractionFilters(unittest.Tes
+                     st_mode = cc.outerdir.stat().st_mode
+                     self.assertNotEqual(st_mode & 0o777, 0o777)
+ 
++    @symlink_test
++    @unittest.skipUnless(hasattr(os, 'chown'), "missing os.chown")
++    @unittest.skipUnless(hasattr(os, 'lchown'), "missing os.lchown")
++    @unittest.skipUnless(hasattr(os, 'geteuid'), "missing os.geteuid")
++    @support.subTests('link_type', (tarfile.SYMTYPE, tarfile.LNKTYPE))
++    def test_chown_links_on_extract(self, link_type):
++        with ArchiveMaker() as arc:
++            arc.add("test.txt",
++                    uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
++            arc.add("link",
++                    type=link_type,
++                    linkname='test.txt',
++                    uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
++
++        with (
++            os_helper.temp_dir() as tmpdir,
++            arc.open() as tar,
++            unittest.mock.patch("os.chown") as mock_chown,
++            unittest.mock.patch("os.lchown") as mock_lchown,
++            unittest.mock.patch("os.geteuid") as mock_geteuid,
++        ):
++            # Set UID to 0 so chown() is attempted.
++            mock_geteuid.return_value = 0
++            tar.extract("link", path=tmpdir, filter='data')
++            extract_path = os.path.join(tmpdir, "link")
++
++            if link_type == tarfile.SYMTYPE:
++                mock_chown.assert_not_called()
++                mock_lchown.assert_called_once_with(extract_path, -1, -1)
++            else:
++                mock_chown.assert_has_calls([
++                    unittest.mock.call(extract_path, -1, -1),
++                    unittest.mock.call(extract_path, -1, -1)
++                ])
++                mock_lchown.assert_not_called()
++
++    @symlink_test
++    @unittest.skipUnless(hasattr(os, 'chown'), "missing os.chown")
++    @unittest.skipUnless(hasattr(os, 'lchown'), "missing os.lchown")
++    @unittest.skipUnless(hasattr(os, 'geteuid'), "missing os.geteuid")
++    @support.subTests('link_type', (tarfile.SYMTYPE, tarfile.LNKTYPE))
++    def test_chown_links_on_extractall(self, link_type):
++        with ArchiveMaker() as arc:
++            arc.add("test.txt",
++                    uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
++            arc.add("link",
++                    type=link_type,
++                    linkname='test.txt',
++                    uid=1337, gid=1337, uname="", gname="", mode='-rwxr-xr-x')
++
++        with (
++            os_helper.temp_dir() as tmpdir,
++            arc.open() as tar,
++            unittest.mock.patch("os.chown") as mock_chown,
++            unittest.mock.patch("os.lchown") as mock_lchown,
++            unittest.mock.patch("os.geteuid") as mock_geteuid,
++        ):
++            # Set UID to 0 so chown() is attempted.
++            mock_geteuid.return_value = 0
++            tar.extractall(path=tmpdir, filter='data')
++            extract_link_path = os.path.join(tmpdir, "link")
++            extract_file_path = os.path.join(tmpdir, "test.txt")
++
++            if link_type == tarfile.SYMTYPE:
++                mock_chown.assert_called_once_with(extract_file_path, -1, -1)
++                mock_lchown.assert_called_once_with(extract_link_path, -1, -1)
++            else:
++                mock_chown.assert_has_calls([
++                    unittest.mock.call(extract_file_path, -1, -1),
++                    unittest.mock.call(extract_link_path, -1, -1)
++                ])
++                mock_lchown.assert_not_called()
++
++    def test_extract_filters_target(self):
++        # Test that when extract() falls back to extracting (rather than
++        # linking) a hardlink target, it filters the target.
++        with ArchiveMaker() as arc:
++            arc.add("target")
++            arc.add("link", hardlink_to="target")
++        def testing_filter(member, path):
++            if member.name == 'target':
++                # target: set read-only
++                return member.replace(mode=stat.S_IRUSR)
++            # link: don't overwrite the mode
++            return member.replace(mode=None)
++        tempdir = pathlib.Path(TEMPDIR) / 'extract'
++        with os_helper.temp_dir(tempdir), arc.open() as tar:
++            tar.extract("link", path=tempdir, filter=testing_filter)
++            path = tempdir / 'link'
++            if os_helper.can_chmod():
++                self.assertFalse(path.stat().st_mode & stat.S_IWUSR)
++
+     def test_link_fallback_normalizes(self):
+         # Make sure hardlink fallbacks work for non-normalized paths for all
+         # filters
diff -Nru python3.13-3.13.5/debian/patches/CVE-2026-6879.patch python3.13-3.13.5/debian/patches/CVE-2026-6879.patch
--- python3.13-3.13.5/debian/patches/CVE-2026-6879.patch	1970-01-01 01:00:00.000000000 +0100
+++ python3.13-3.13.5/debian/patches/CVE-2026-6879.patch	2026-08-10 13:57:09.000000000 +0200
@@ -0,0 +1,77 @@
+From 390337b8ba1658833fdef379e1739c9f9533a8db Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <[email protected]>
+Date: Wed, 29 Jul 2026 13:14:03 +0200
+Subject: [PATCH] [3.13] gh-152674: Avoid quadratic behavior in
+ xml.etree.ElementPath index predicates (GH-152676) (GH-154811)
+
+--- python3.13-3.13.5.orig/Lib/test/test_xml_etree.py
++++ python3.13-3.13.5/Lib/test/test_xml_etree.py
+@@ -3212,6 +3212,37 @@ class ElementFindTest(unittest.TestCase)
+         self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[last()-0]')
+         self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[last()+1]')
+ 
++    def test_find_xpath_index_no_quadratic_complexity(self):
++        class CountingElement(ET.Element):
++            findall_calls = 0
++            def findall(self, *args, **kwargs):
++                type(self).findall_calls += 1
++                return super().findall(*args, **kwargs)
++
++        def work(n, pattern):
++            root = CountingElement("root")
++            for _ in range(n):
++                ET.SubElement(root, "a")
++            CountingElement.findall_calls = 0
++            root.findall(pattern)
++            return CountingElement.findall_calls
++
++        for pattern in [".//a[1]", ".//a[last()]"]:
++            w1 = work(1024, pattern)
++            w2 = work(2048, pattern)
++            w3 = work(4096, pattern)
++
++            self.assertGreater(w1, 0)
++            r1 = w2 / w1
++            r2 = w3 / w2
++            # Doubling N must not ~double the parent.findall calls.
++            # Linear-in-N call counts indicate the cache is missing.
++            self.assertLess(
++                max(r1, r2), 1.5,
++                msg=f"Possible quadratic behavior on {pattern!r}: "
++                    f"calls={w1, w2, w3} ratios={r1, r2}",
++            )
++
+     def test_findall(self):
+         e = ET.XML(SAMPLE_XML)
+         e[2] = ET.XML(SAMPLE_SECTION)
+--- python3.13-3.13.5.orig/Lib/xml/etree/ElementPath.py
++++ python3.13-3.13.5/Lib/xml/etree/ElementPath.py
+@@ -324,15 +324,22 @@ def prepare_predicate(next, token):
+                 index = -1
+         def select(context, result):
+             parent_map = get_parent_map(context)
++            cache = {}
+             for elem in result:
+                 try:
+                     parent = parent_map[elem]
++                except KeyError:
++                    continue
++                key = (parent, elem.tag)
++                if key not in cache:
+                     # FIXME: what if the selector is "*" ?
+-                    elems = list(parent.findall(elem.tag))
+-                    if elems[index] is elem:
+-                        yield elem
+-                except (IndexError, KeyError):
+-                    pass
++                    elems = parent.findall(elem.tag)
++                    try:
++                        cache[key] = elems[index]
++                    except IndexError:
++                        cache[key] = None
++                if cache[key] is elem:
++                    yield elem
+         return select
+     raise SyntaxError("invalid predicate")
+ 
diff -Nru python3.13-3.13.5/debian/patches/series python3.13-3.13.5/debian/patches/series
--- python3.13-3.13.5/debian/patches/series	2026-07-15 22:25:40.000000000 +0200
+++ python3.13-3.13.5/debian/patches/series	2026-08-10 14:04:45.000000000 +0200
@@ -58,3 +58,8 @@
 CVE-2026-7774.patch
 CVE-2026-8328.patch
 CVE-2026-9669.patch
+CVE-2026-6879.patch
+CVE-2026-0864.patch
+CVE-2026-4360.patch
+CVE-2026-11940.patch
+CVE-2026-11972.patch
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.