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

Reply via email to