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