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