Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python310 for openSUSE:Factory checked in at 2026-08-01 18:34:12 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python310 (Old) and /work/SRC/openSUSE:Factory/.python310.new.16738 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python310" Sat Aug 1 18:34:12 2026 rev:79 rq:1368874 version:3.10.20 Changes: -------- --- /work/SRC/openSUSE:Factory/python310/python310.changes 2026-07-15 17:08:52.289952650 +0200 +++ /work/SRC/openSUSE:Factory/.python310.new.16738/python310.changes 2026-08-01 18:37:23.048466060 +0200 @@ -1,0 +2,48 @@ +Fri Jul 31 13:55:17 UTC 2026 - Matej Cepl <[email protected]> + +- CVE-2026-3276: Fix O(n^2) canonical ordering in + unicodedata.normalize() (bsc#1267581, gh#python/cpython#149079) +CVE-2026-3276-On2-unicodedata-normalize.patch + +------------------------------------------------------------------- +Fri Jul 31 13:42:27 UTC 2026 - Matej Cepl <[email protected]> + +- CVE-2026-7774: tarfile.data_filter: validate written link + target (bsc#1267821, gh#149486) + CVE-2026-7774-tarfile-data_filter-symlink.patch + +------------------------------------------------------------------- +Fri Jul 31 13:35:09 UTC 2026 - Matej Cepl <[email protected]> + +- CVE-2026-0864: Normalize all line endings (CR, CRLF, and LF) in + configparser (bsc#1269066, gh#python/cpython#143927) + CVE-2026-0864-normalize-LFTAB-configparser.patch + +------------------------------------------------------------------- +Fri Jul 31 13:22:45 UTC 2026 - Matej Cepl <[email protected]> + +- CVE-2026-11972: Make tarfile._Stream.seek break at EOF + (bsc#1269788, gh#python/cpython#151981) + CVE-2026-11972-tarfile-Stream-seek-EOF.patch + +------------------------------------------------------------------- +Fri Jul 31 13:07:50 UTC 2026 - Matej Cepl <[email protected]> + +- CVE-2026-4360: Pass filter_function to TarFile._extract_one() + during .extract() (bsc#1269959, gh#python/cpython#151987) + CVE-2026-4360-filter_function-TarFile-extractone.patch + +------------------------------------------------------------------- +Fri Jul 31 12:54:51 UTC 2026 - Matej Cepl <[email protected]> + +- CVE-2026-15308: Fix quadratic complexity in incremental parsing + in HTMLParser (bsc#1271192, gh#python/cpython#153030) + CVE-2026-15308-HTMLParser-CPU-exhaust.patch + +------------------------------------------------------------------- +Thu Jul 23 04:58:44 UTC 2026 - Steve Kowalik <[email protected]> + +- Add patch support-sphinx-9.patch: + * Support changes required for Sphinx 9. + +------------------------------------------------------------------- New: ---- CVE-2026-0864-normalize-LFTAB-configparser.patch CVE-2026-11972-tarfile-Stream-seek-EOF.patch CVE-2026-15308-HTMLParser-CPU-exhaust.patch CVE-2026-3276-On2-unicodedata-normalize.patch CVE-2026-4360-filter_function-TarFile-extractone.patch CVE-2026-7774-tarfile-data_filter-symlink.patch support-sphinx-9.patch ----------(New B)---------- New: configparser (bsc#1269066, gh#python/cpython#143927) CVE-2026-0864-normalize-LFTAB-configparser.patch New: (bsc#1269788, gh#python/cpython#151981) CVE-2026-11972-tarfile-Stream-seek-EOF.patch New: in HTMLParser (bsc#1271192, gh#python/cpython#153030) CVE-2026-15308-HTMLParser-CPU-exhaust.patch New: unicodedata.normalize() (bsc#1267581, gh#python/cpython#149079) CVE-2026-3276-On2-unicodedata-normalize.patch New: during .extract() (bsc#1269959, gh#python/cpython#151987) CVE-2026-4360-filter_function-TarFile-extractone.patch New: target (bsc#1267821, gh#149486) CVE-2026-7774-tarfile-data_filter-symlink.patch New: - Add patch support-sphinx-9.patch: * Support changes required for Sphinx 9. ----------(New E)---------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python310.spec ++++++ --- /var/tmp/diff_new_pack.fk7mtj/_old 2026-08-01 18:37:24.296509002 +0200 +++ /var/tmp/diff_new_pack.fk7mtj/_new 2026-08-01 18:37:24.300509139 +0200 @@ -256,6 +256,26 @@ # PATCH-FIX-OPENSUSE test_UDPLITE_support.patch bsc#1268375 [email protected] # improve testing of the presence of IPPROTO_UDPLITE support Patch55: test_UDPLITE_support.patch +# PATCH-FIX-UPSTREAM Based on gh#python/cpython#142057 Support fixes required for Sphinx 9 +Patch56: support-sphinx-9.patch +# PATCH-FIX-UPSTREAM CVE-2026-15308-HTMLParser-CPU-exhaust.patch bsc#1271192 [email protected] +# Fix quadratic complexity in incremental parsing in HTMLParser +Patch57: CVE-2026-15308-HTMLParser-CPU-exhaust.patch +# PATCH-FIX-UPSTREAM CVE-2026-4360-filter_function-TarFile-extractone.patch bsc#1269959 [email protected] +# Pass filter_function to TarFile._extract_one() during .extract() +Patch58: CVE-2026-4360-filter_function-TarFile-extractone.patch +# PATCH-FIX-UPSTREAM CVE-2026-11972-tarfile-Stream-seek-EOF.patch bsc#1269788 [email protected] +# Make tarfile._Stream.seek break at EOF +Patch59: CVE-2026-11972-tarfile-Stream-seek-EOF.patch +# PATCH-FIX-UPSTREAM CVE-2026-0864-normalize-LFTAB-configparser.patch bsc#1269066 [email protected] +# Normalize all line endings (CR, CRLF, and LF) in configparser +Patch60: CVE-2026-0864-normalize-LFTAB-configparser.patch +# PATCH-FIX-UPSTREAM CVE-2026-7774-tarfile-data_filter-symlink.patch bsc#1267821 [email protected] +# tarfile.data_filter: validate written link target +Patch61: CVE-2026-7774-tarfile-data_filter-symlink.patch +# PATCH-FIX-UPSTREAM CVE-2026-3276-On2-unicodedata-normalize.patch bsc#1267581 [email protected] +# gh-149079: Fix O(n^2) canonical ordering in unicodedata.normalize() +Patch62: CVE-2026-3276-On2-unicodedata-normalize.patch ### END OF PATCHES BuildRequires: autoconf-archive BuildRequires: automake ++++++ CVE-2026-0864-normalize-LFTAB-configparser.patch ++++++ >From 6141c1576ae9c5e6581dec01a9a729423ad69a9b Mon Sep 17 00:00:00 2001 From: Seth Larson <[email protected]> Date: Tue, 23 Jun 2026 08:33:51 -0500 Subject: [PATCH] gh-143927: Normalize all line endings (CR, CRLF, and LF) in configparser (GH-143929) (cherry picked from commit 5858e42c539dac8394636a6e9b30472b8994851f) Co-authored-by: Seth Larson <[email protected]> --- Lib/configparser.py | 4 ++- Lib/test/test_configparser.py | 11 ++++++++++ Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst | 2 + 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst Index: Python-3.10.20/Lib/configparser.py =================================================================== --- Python-3.10.20.orig/Lib/configparser.py 2026-07-31 17:25:09.972229477 +0200 +++ Python-3.10.20/Lib/configparser.py 2026-07-31 17:25:15.990119182 +0200 @@ -931,7 +931,9 @@ 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)) Index: Python-3.10.20/Lib/test/test_configparser.py =================================================================== --- Python-3.10.20.orig/Lib/test/test_configparser.py 2026-07-31 17:25:12.197596399 +0200 +++ Python-3.10.20/Lib/test/test_configparser.py 2026-07-31 17:25:15.990358974 +0200 @@ -528,6 +528,17 @@ 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, Index: Python-3.10.20/Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ Python-3.10.20/Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst 2026-07-31 17:25:15.990570894 +0200 @@ -0,0 +1,2 @@ +Normalize all line endings (CR, CRLF, and LF) to LF+TAB when writing +multi-line configparser values. ++++++ CVE-2026-11972-tarfile-Stream-seek-EOF.patch ++++++ >From f038444a8a996a746e1f4058f9a4cb6ad1ac28cc Mon Sep 17 00:00:00 2001 From: Petr Viktorin <[email protected]> Date: Tue, 23 Jun 2026 15:13:30 +0200 Subject: [PATCH] gh-151981: Make tarfile._Stream.seek break at EOF (GH-151982) (cherry picked from commit f50bf13566189c8d0ce5a814f33eff3d89951896) Co-authored-by: Petr Viktorin <[email protected]> Co-authored-by: Stan Ulbrych <[email protected]> --- Lib/tarfile.py | 4 +- Lib/test/test_tarfile.py | 16 ++++++++++ Misc/NEWS.d/next/Security/2026-06-23-13-28-16.gh-issue-151981.xBHEcU.rst | 2 + 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Security/2026-06-23-13-28-16.gh-issue-151981.xBHEcU.rst Index: Python-3.10.20/Lib/tarfile.py =================================================================== --- Python-3.10.20.orig/Lib/tarfile.py 2026-07-31 17:28:42.692969214 +0200 +++ Python-3.10.20/Lib/tarfile.py 2026-07-31 17:28:42.707639106 +0200 @@ -513,7 +513,9 @@ 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") Index: Python-3.10.20/Lib/test/test_tarfile.py =================================================================== --- Python-3.10.20.orig/Lib/test/test_tarfile.py 2026-07-31 17:28:42.693877226 +0200 +++ Python-3.10.20/Lib/test/test_tarfile.py 2026-07-31 17:28:42.708159177 +0200 @@ -4405,6 +4405,22 @@ 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 OffsetValidationTests(unittest.TestCase): tarname = tmpname Index: Python-3.10.20/Misc/NEWS.d/next/Security/2026-06-23-13-28-16.gh-issue-151981.xBHEcU.rst =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ Python-3.10.20/Misc/NEWS.d/next/Security/2026-06-23-13-28-16.gh-issue-151981.xBHEcU.rst 2026-07-31 17:28:42.708562327 +0200 @@ -0,0 +1,2 @@ +In :mod:`tarfile`, seeking a stream now stops when end of the stream is +reached. ++++++ CVE-2026-15308-HTMLParser-CPU-exhaust.patch ++++++ >From 0765ae4d3728db1e7d0870185d42e0025db6d2c5 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka <[email protected]> Date: Sat, 4 Jul 2026 20:40:22 +0300 Subject: [PATCH] gh-153030: Fix quadratic complexity in incremental parsing in HTMLParser (GH-153031) When an unterminated construct (e.g. a tag or comment) spanned many feed() calls, rescanning the growing buffer and concatenating new data onto it were both quadratic. New data is now accumulated in a list and only joined and parsed once enough has piled up. Co-Authored-By: Claude Opus 4.8 <[email protected]> (cherry picked from commit bcf98ddbc40ec9b3ee87da0124a5660b19b7e606) --- Lib/html/parser.py | 32 +++++++++- Lib/test/test_htmlparser.py | 20 ++++++ Misc/NEWS.d/next/Security/2026-07-04-17-00-00.gh-issue-153030.RovkP6.rst | 3 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2026-07-04-17-00-00.gh-issue-153030.RovkP6.rst Index: Python-3.10.20/Lib/html/parser.py =================================================================== --- Python-3.10.20.orig/Lib/html/parser.py 2026-07-31 17:02:24.015719181 +0200 +++ Python-3.10.20/Lib/html/parser.py 2026-07-31 17:02:32.281707169 +0200 @@ -137,6 +137,9 @@ self.cdata_elem = None self._support_cdata = True self._escapable = True + self._pending = [] + self._pending_len = 0 + self._parse_threshold = 1 _markupbase.ParserBase.reset(self) def feed(self, data): @@ -145,11 +148,36 @@ Call this as often as you want, with as little or as much text as you want (may include '\n'). """ - self.rawdata = self.rawdata + data - self.goahead(0) + # Accumulate new data in a list and only join and parse it once + # enough has piled up. Rescanning an unparsed buffer (e.g. an + # unterminated tag) and concatenating onto it on every call would + # both be quadratic in the input size. + self._pending_len += len(data) + if self._pending_len < self._parse_threshold: + self._pending.append(data) + else: + if not self._pending: + self.rawdata += data + else: + self._pending.append(data) + self.rawdata += ''.join(self._pending) + self._pending.clear() + self._pending_len = 0 + n = len(self.rawdata) + self.goahead(0) + if len(self.rawdata) < n: + # Some data was parsed; resume on the next call. + self._parse_threshold = 1 + else: + # Nothing was parsed; wait until the buffer doubles. + self._parse_threshold = len(self.rawdata) def close(self): """Handle any buffered data.""" + if self._pending: + self.rawdata += ''.join(self._pending) + self._pending.clear() + self._pending_len = 0 self.goahead(1) __starttag_text = None Index: Python-3.10.20/Lib/test/test_htmlparser.py =================================================================== --- Python-3.10.20.orig/Lib/test/test_htmlparser.py 2026-07-31 17:02:25.650388367 +0200 +++ Python-3.10.20/Lib/test/test_htmlparser.py 2026-07-31 17:02:32.281935820 +0200 @@ -929,6 +929,26 @@ check("<![CDATA[" * 9 * n) check("<!doctype" * 35 * n) + @support.requires_resource('cpu') + def test_incremental_no_quadratic_complexity(self): + # An unterminated construct fed in many small chunks used to take + # quadratic time, both to rescan and to concatenate the buffer. + # Now it takes a fraction of a second. + def check(prefix, chunk, suffix): + parser = html.parser.HTMLParser() + parser.feed(prefix) + for _ in range(200_000): + parser.feed(chunk) + parser.feed(suffix) + parser.close() + chunk = "a" * 64 + check("<!--", chunk, "-->") # comment + check("<?", chunk, ">") # processing instruction + check("<!doctype ", chunk, ">") # doctype + check("<![CDATA[", chunk, "]]>") # CDATA section + check("<a href='", chunk, "'>") # start tag + check("<script>", chunk, "</script>") # RAWTEXT element + class AttributesTestCase(TestCaseBase): Index: Python-3.10.20/Misc/NEWS.d/next/Security/2026-07-04-17-00-00.gh-issue-153030.RovkP6.rst =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ Python-3.10.20/Misc/NEWS.d/next/Security/2026-07-04-17-00-00.gh-issue-153030.RovkP6.rst 2026-07-31 17:02:32.282127121 +0200 @@ -0,0 +1,3 @@ +Fixed quadratic complexity in incremental parsing of long unterminated +constructs (such as tags or comments) in :class:`html.parser.HTMLParser`, +which could be exploited for a denial of service. ++++++ CVE-2026-3276-On2-unicodedata-normalize.patch ++++++ >From 01a50e4548b6c37aebb9544f0d8f30d19536fc26 Mon Sep 17 00:00:00 2001 From: Petr Viktorin <[email protected]> Date: Tue, 2 Jun 2026 18:12:42 +0200 Subject: [PATCH] [3.10] gh-149079: Fix O(n^2) canonical ordering in unicodedata.normalize() (GH-149080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the insertion sort used for canonical ordering of combining characters with a hybrid approach: insertion sort for short runs (< 20) and counting sort for longer runs, reducing worst-case complexity from O(n^2) to O(n). This prevents denial of service via crafted Unicode strings with many combining characters in alternating CCC order. (cherry picked from commit 991224b1e8311c85f198f6dd8208bf8cff7fc26f) Co-authored-by: Seth Larson <[email protected]> Co-authored-by: ch4n3-yoon <[email protected]> Co-authored-by: Seokchan Yoon <[email protected]> Co-authored-by: Stan Ulbrych <[email protected]> Co-authored-by: Bénédikt Tran <[email protected]> Co-authored-by: Serhiy Storchaka <[email protected]> Co-authored-by: Maurycy Pawłowski-Wieroński <[email protected]> --- Lib/test/test_unicodedata.py | 28 + Misc/NEWS.d/next/Security/2026-04-27-16-36-11.gh-issue-149079.vKl-LM.rst | 5 Modules/unicodedata.c | 143 ++++++++-- 3 files changed, 150 insertions(+), 26 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2026-04-27-16-36-11.gh-issue-149079.vKl-LM.rst Index: Python-3.10.20/Lib/test/test_unicodedata.py =================================================================== --- Python-3.10.20.orig/Lib/test/test_unicodedata.py 2026-07-31 17:31:01.889020342 +0200 +++ Python-3.10.20/Lib/test/test_unicodedata.py 2026-07-31 17:31:06.054061998 +0200 @@ -202,6 +202,34 @@ b = 'C\u0338' * 20 + '\xC7' self.assertEqual(self.db.normalize('NFC', a), b) + def test_long_combining_mark_run(self): + # gh-149079: avoid quadratic canonical ordering. + payload = "a" + ("\u0300\u0327" * 32) + nfd = "a" + ("\u0327" * 32) + ("\u0300" * 32) + nfc = "\u00e0" + ("\u0327" * 32) + ("\u0300" * 31) + + self.assertEqual(self.db.normalize("NFD", payload), nfd) + self.assertEqual(self.db.normalize("NFKD", payload), nfd) + self.assertEqual(self.db.normalize("NFC", payload), nfc) + self.assertEqual(self.db.normalize("NFKC", payload), nfc) + + def test_combining_mark_run_fast_paths(self): + # gh-149079: cover short runs and already-sorted long runs. + short_payload = "a" + ("\u0300\u0327" * 9) + "\u0300" + short_nfd = "a" + ("\u0327" * 9) + ("\u0300" * 10) + short_nfc = "\u00e0" + ("\u0327" * 9) + ("\u0300" * 9) + long_sorted = "a" + ("\u0327" * 30) + ("\u0300" * 30) + long_sorted_nfc = "\u00e0" + ("\u0327" * 30) + ("\u0300" * 29) + + self.assertEqual(self.db.normalize("NFD", short_payload), short_nfd) + self.assertEqual(self.db.normalize("NFKD", short_payload), short_nfd) + self.assertEqual(self.db.normalize("NFC", short_payload), short_nfc) + self.assertEqual(self.db.normalize("NFKC", short_payload), short_nfc) + self.assertEqual(self.db.normalize("NFD", long_sorted), long_sorted) + self.assertEqual(self.db.normalize("NFKD", long_sorted), long_sorted) + self.assertEqual(self.db.normalize("NFC", long_sorted), long_sorted_nfc) + self.assertEqual(self.db.normalize("NFKC", long_sorted), long_sorted_nfc) + def test_issue29456(self): # Fix #29456 u1176_str_a = '\u1100\u1176\u11a8' Index: Python-3.10.20/Misc/NEWS.d/next/Security/2026-04-27-16-36-11.gh-issue-149079.vKl-LM.rst =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ Python-3.10.20/Misc/NEWS.d/next/Security/2026-04-27-16-36-11.gh-issue-149079.vKl-LM.rst 2026-07-31 17:31:06.054277183 +0200 @@ -0,0 +1,5 @@ +Fix a potential denial of service in :func:`unicodedata.normalize`. The +canonical ordering step of Unicode normalization used a quadratic-time insertion +sort for reordering combining characters, which could be exploited with +crafted input containing many combining characters in non-canonical order. +Replaced with a linear-time counting sort for long runs. Index: Python-3.10.20/Modules/unicodedata.c =================================================================== --- Python-3.10.20.orig/Modules/unicodedata.c 2026-03-03 01:49:35.000000000 +0100 +++ Python-3.10.20/Modules/unicodedata.c 2026-07-31 17:31:06.054876273 +0200 @@ -485,6 +485,64 @@ (*index)++; } +/* Small combining runs are usually cheaper with insertion sort. */ +#define CANONICAL_ORDERING_COUNTING_SORT_THRESHOLD 20 + +static void +canonical_ordering_sort_insertion(int kind, void *data, + Py_ssize_t start, Py_ssize_t end) +{ + for (Py_ssize_t i = start + 1; i < end; i++) { + Py_UCS4 code = PyUnicode_READ(kind, data, i); + unsigned char combining = _getrecord_ex(code)->combining; + Py_ssize_t j = i; + + while (j > start) { + Py_UCS4 previous = PyUnicode_READ(kind, data, j - 1); + if (_getrecord_ex(previous)->combining <= combining) { + break; + } + PyUnicode_WRITE(kind, data, j, previous); + j--; + } + if (j != i) { + PyUnicode_WRITE(kind, data, j, code); + } + } +} + +static void +canonical_ordering_sort_counting(int kind, void *data, + Py_ssize_t start, Py_ssize_t end, + Py_UCS4 *sortbuf) +{ + Py_ssize_t counts[256] = {0}; + Py_ssize_t run_length = end - start; + Py_ssize_t total = 0; + + for (Py_ssize_t i = start; i < end; i++) { + Py_UCS4 code = PyUnicode_READ(kind, data, i); + unsigned char combining = _getrecord_ex(code)->combining; + counts[combining]++; + } + + for (size_t i = 0; i < Py_ARRAY_LENGTH(counts); i++) { + Py_ssize_t count = counts[i]; + counts[i] = total; + total += count; + } + + /* Reuse counts[] as the next output slot for each CCC. */ + for (Py_ssize_t i = start; i < end; i++) { + Py_UCS4 code = PyUnicode_READ(kind, data, i); + unsigned char combining = _getrecord_ex(code)->combining; + sortbuf[counts[combining]++] = code; + } + for (Py_ssize_t i = 0; i < run_length; i++) { + PyUnicode_WRITE(kind, data, start + i, sortbuf[i]); + } +} + #define SBase 0xAC00 #define LBase 0x1100 #define VBase 0x1161 @@ -501,13 +559,16 @@ PyObject *result; Py_UCS4 *output; Py_ssize_t i, o, osize; - int kind; - const void *data; + int input_kind, result_kind; + const void *input_data; + void *result_data; /* Longest decomposition in Unicode 3.2: U+FDFA */ Py_UCS4 stack[20]; Py_ssize_t space, isize; int index, prefix, count, stackptr; unsigned char prev, cur; + Py_UCS4 *sortbuf = NULL; + Py_ssize_t sortbuflen = 0; stackptr = 0; isize = PyUnicode_GET_LENGTH(input); @@ -527,11 +588,11 @@ return NULL; } i = o = 0; - kind = PyUnicode_KIND(input); - data = PyUnicode_DATA(input); + input_kind = PyUnicode_KIND(input); + input_data = PyUnicode_DATA(input); while (i < isize) { - stack[stackptr++] = PyUnicode_READ(kind, data, i++); + stack[stackptr++] = PyUnicode_READ(input_kind, input_data, i++); while(stackptr) { Py_UCS4 code = stack[--stackptr]; /* Hangul Decomposition adds three characters in @@ -597,34 +658,64 @@ if (!result) return NULL; /* result is guaranteed to be ready, as it is compact. */ - kind = PyUnicode_KIND(result); - data = PyUnicode_DATA(result); + result_kind = PyUnicode_KIND(result); + result_data = PyUnicode_DATA(result); - /* Sort canonically. */ + /* Sort each consecutive combining-character run canonically. */ i = 0; - prev = _getrecord_ex(PyUnicode_READ(kind, data, i))->combining; - for (i++; i < PyUnicode_GET_LENGTH(result); i++) { - cur = _getrecord_ex(PyUnicode_READ(kind, data, i))->combining; - if (prev == 0 || cur == 0 || prev <= cur) { - prev = cur; + while (i < o) { + Py_ssize_t run_length, run_start; + int needs_sort = 0; + + Py_UCS4 ch = PyUnicode_READ(result_kind, result_data, i); + prev = _getrecord_ex(ch)->combining; + if (prev == 0) { + i++; continue; } - /* Non-canonical order. Need to switch *i with previous. */ - o = i - 1; - while (1) { - Py_UCS4 tmp = PyUnicode_READ(kind, data, o+1); - PyUnicode_WRITE(kind, data, o+1, - PyUnicode_READ(kind, data, o)); - PyUnicode_WRITE(kind, data, o, tmp); - o--; - if (o < 0) - break; - prev = _getrecord_ex(PyUnicode_READ(kind, data, o))->combining; - if (prev == 0 || prev <= cur) + + run_start = i++; + while (i < o) { + Py_UCS4 ch = PyUnicode_READ(result_kind, result_data, i); + cur = _getrecord_ex(ch)->combining; + if (cur == 0) { break; + } + if (prev > cur) { + needs_sort = 1; + } + prev = cur; + i++; + } + if (!needs_sort) { + continue; + } + + run_length = i - run_start; + if (run_length < CANONICAL_ORDERING_COUNTING_SORT_THRESHOLD) { + canonical_ordering_sort_insertion(result_kind, result_data, + run_start, i); + continue; } - prev = _getrecord_ex(PyUnicode_READ(kind, data, i))->combining; + + if (run_length > sortbuflen) { + Py_UCS4 *new_sortbuf = PyMem_Resize(sortbuf, + Py_UCS4, + run_length); + if (new_sortbuf == NULL) { + PyErr_NoMemory(); + PyMem_Free(sortbuf); + Py_DECREF(result); + return NULL; + } + sortbuf = new_sortbuf; + sortbuflen = run_length; + } + + canonical_ordering_sort_counting(result_kind, result_data, + run_start, i, sortbuf); } + PyMem_Free(sortbuf); return result; } ++++++ CVE-2026-4360-filter_function-TarFile-extractone.patch ++++++ >From 1924e21bf78f27a34a21d3c9c83310dbaa78be45 Mon Sep 17 00:00:00 2001 From: Petr Viktorin <[email protected]> Date: Mon, 29 Jun 2026 17:41:26 +0200 Subject: [PATCH 1/2] gh-151987: Pass filter_function to TarFile._extract_one() during .extract() (GH-151988) (cherry picked from commit 7ccdbaba2c54250a70d7f25632152df7655a5e0a) Co-authored-by: Petr Viktorin <[email protected]> Co-authored-by: Seth Michael Larson <[email protected]> --- Lib/tarfile.py | 3 Lib/test/test_tarfile.py | 90 ++++++++++ Misc/NEWS.d/next/Security/2026-06-23-14-19-30.gh-issue-151987.8mNIMf.rst | 2 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Security/2026-06-23-14-19-30.gh-issue-151987.8mNIMf.rst Index: Python-3.10.20/Lib/tarfile.py =================================================================== --- Python-3.10.20.orig/Lib/tarfile.py 2026-07-31 17:22:30.939669742 +0200 +++ Python-3.10.20/Lib/tarfile.py 2026-07-31 17:22:31.027704182 +0200 @@ -2392,7 +2392,8 @@ 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* Index: Python-3.10.20/Lib/test/test_tarfile.py =================================================================== --- Python-3.10.20.orig/Lib/test/test_tarfile.py 2026-07-31 17:22:30.941669763 +0200 +++ Python-3.10.20/Lib/test/test_tarfile.py 2026-07-31 17:22:31.028402749 +0200 @@ -3984,6 +3984,96 @@ st_mode = cc.outerdir.stat().st_mode self.assertNotEqual(st_mode & 0o777, 0o777) + @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() + + @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 sys.platform != 'win32': + 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 Index: Python-3.10.20/Misc/NEWS.d/next/Security/2026-06-23-14-19-30.gh-issue-151987.8mNIMf.rst =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ Python-3.10.20/Misc/NEWS.d/next/Security/2026-06-23-14-19-30.gh-issue-151987.8mNIMf.rst 2026-07-31 17:22:31.028929563 +0200 @@ -0,0 +1,2 @@ +The :meth:`tarfile.TarFile.extract` method now applies the given filter when +it extracts a link target from the archive as a fallback. ++++++ CVE-2026-7774-tarfile-data_filter-symlink.patch ++++++ >From 6fd7650c9a5984e8914d79f4cc11e1e291aeeb74 Mon Sep 17 00:00:00 2001 From: Petr Viktorin <[email protected]> Date: Fri, 8 May 2026 14:16:06 +0200 Subject: [PATCH] [3.10] gh-149486: tarfile.data_filter: validate written link target (GH-149487) The data filter rewrote linknames with normpath() but ran the containment check against the un-normalised value, and computed a symlink's directory before stripping trailing slashes. Both let a crafted archive create links pointing outside the destination. Also reject link members that resolve to the destination directory itself, which could otherwise replace it with a symlink and redirect all subsequent members. (Patch by Greg; Petr's just reviewing & merging.) (cherry picked from commit 578411982c16f753f4893532510099ef665117da) Co-authored-by: Petr Viktorin <[email protected]> Co-authored-by: Gregory P. Smith <[email protected]> --- Lib/tarfile.py | 16 - Lib/test/test_tarfile.py | 147 +++++++--- Misc/NEWS.d/next/Security/2026-05-03-21-00-00.gh-issue-149486.tarflt.rst | 5 3 files changed, 129 insertions(+), 39 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2026-05-03-21-00-00.gh-issue-149486.tarflt.rst Index: Python-3.10.20/Lib/tarfile.py =================================================================== --- Python-3.10.20.orig/Lib/tarfile.py 2026-07-31 17:27:30.484847898 +0200 +++ Python-3.10.20/Lib/tarfile.py 2026-07-31 17:27:30.511720742 +0200 @@ -815,16 +815,22 @@ if member.islnk() or member.issym(): if os.path.isabs(member.linkname): raise AbsoluteLinkError(member) + # A link member that resolves to the destination directory itself + # would replace it with a (sym)link, redirecting the destination + # for all subsequent members. + if target_path == dest_path: + raise OutsideDestinationError(member, target_path) normalized = os.path.normpath(member.linkname) if normalized != member.linkname: new_attrs['linkname'] = normalized if member.issym(): - target_path = os.path.join(dest_path, - os.path.dirname(name), - member.linkname) + # The symlink is created at `name` with trailing separators + # stripped, so its target is relative to the directory + # containing that path. + link_dir = os.path.dirname(name.rstrip('/' + os.sep)) + target_path = os.path.join(dest_path, link_dir, normalized) else: - target_path = os.path.join(dest_path, - member.linkname) + target_path = os.path.join(dest_path, normalized) target_path = os.path.realpath(target_path, strict=os.path.ALLOW_MISSING) if os.path.commonpath([target_path, dest_path]) != dest_path: Index: Python-3.10.20/Lib/test/test_tarfile.py =================================================================== --- Python-3.10.20.orig/Lib/test/test_tarfile.py 2026-07-31 17:27:30.485346378 +0200 +++ Python-3.10.20/Lib/test/test_tarfile.py 2026-07-31 17:27:30.512820955 +0200 @@ -3357,6 +3357,39 @@ # The destination for the extraction, within `outerdir` destdir = outerdir / 'dest' + @classmethod + def setUpClass(cls): + # Posix and Windows have different pathname resolution: + # either symlink or a '..' component resolve first. + # Let's see which we are on. + if os_helper.can_symlink(): + testpath = os.path.join(TEMPDIR, 'resolution_test') + os.mkdir(testpath) + + # testpath/current links to `.` which is all of: + # - `testpath` + # - `testpath/current` + # - `testpath/current/current` + # - etc. + os.symlink('.', os.path.join(testpath, 'current')) + + # we'll test where `testpath/current/../file` ends up + with open(os.path.join(testpath, 'current', '..', 'file'), 'w'): + pass + + if os.path.exists(os.path.join(testpath, 'file')): + # Windows collapses 'current\..' to '.' first, leaving + # 'testpath\file' + cls.dotdot_resolves_early = True + elif os.path.exists(os.path.join(testpath, '..', 'file')): + # Posix resolves 'current' to '.' first, leaving + # 'testpath/../file' + cls.dotdot_resolves_early = False + else: + raise AssertionError('Could not determine link resolution') + else: + cls.dotdot_resolves_early = False + @contextmanager def check_context(self, tar, filter, *, check_flag=True, ignored_trees=()): """Extracts `tar` to `self.destdir` and allows checking the result @@ -3538,10 +3571,19 @@ + "which is outside the destination") with self.check_context(arc.open(), 'data'): - self.expect_exception( - tarfile.LinkOutsideDestinationError, - """'parent' would link to ['"].*outerdir['"], """ - + "which is outside the destination") + if self.dotdot_resolves_early: + # 'current/../..' normalises to '..', which is rejected. + self.expect_exception( + tarfile.LinkOutsideDestinationError, + """'parent' would link to ['"].*outerdir['"], """ + + "which is outside the destination") + else: + # 'current/..' normalises to '.'; the rewritten link is + # created and 'parent/evil' lands harmlessly inside the + # destination. + self.expect_file('current', symlink_to='.') + self.expect_file('parent', symlink_to='.') + self.expect_file('evil') else: # No symlink support. The symlinks are ignored. @@ -3628,35 +3670,6 @@ # Test interplaying symlinks # Inspired by 'dirsymlink2b' in jwilk/traversal-archives - # Posix and Windows have different pathname resolution: - # either symlink or a '..' component resolve first. - # Let's see which we are on. - if os_helper.can_symlink(): - testpath = os.path.join(TEMPDIR, 'resolution_test') - os.mkdir(testpath) - - # testpath/current links to `.` which is all of: - # - `testpath` - # - `testpath/current` - # - `testpath/current/current` - # - etc. - os.symlink('.', os.path.join(testpath, 'current')) - - # we'll test where `testpath/current/../file` ends up - with open(os.path.join(testpath, 'current', '..', 'file'), 'w'): - pass - - if os.path.exists(os.path.join(testpath, 'file')): - # Windows collapses 'current\..' to '.' first, leaving - # 'testpath\file' - dotdot_resolves_early = True - elif os.path.exists(os.path.join(testpath, '..', 'file')): - # Posix resolves 'current' to '.' first, leaving - # 'testpath/../file' - dotdot_resolves_early = False - else: - raise AssertionError('Could not determine link resolution') - with ArchiveMaker() as arc: # `current` links to `.` which is both the destination directory @@ -3692,7 +3705,7 @@ with self.check_context(arc.open(), 'data'): if os_helper.can_symlink(): - if dotdot_resolves_early: + if self.dotdot_resolves_early: # Fail when extracting a file outside destination self.expect_exception( tarfile.OutsideDestinationError, @@ -3809,6 +3822,72 @@ + """['"].*moo['"], which is outside the """ + "destination") + @os_helper.skip_unless_symlink + def test_normpath_realpath_mismatch(self): + # The link-target check must validate the value that will actually + # be written to disk (the normalised linkname), not the original. + # Here 'a' is a symlink to a deep nonexistent path, so realpath() + # of 'a/../../...' stays inside the destination while normpath() + # collapses 'a/..' lexically and escapes. + depth = len(self.destdir.parts) + 5 + deep = '/'.join(f'p{i}' for i in range(depth)) + sneaky = 'a/' + '../' * depth + 'flag' + for kind in 'symlink_to', 'hardlink_to': + with self.subTest(kind): + with ArchiveMaker() as arc: + arc.add('a', symlink_to=deep) + arc.add('escape', **{kind: sneaky}) + with self.check_context(arc.open(), 'data'): + self.expect_exception( + tarfile.LinkOutsideDestinationError) + + @os_helper.skip_unless_symlink + def test_symlink_trailing_slash(self): + # A trailing slash on a symlink member's name must not cause the + # link target to be resolved relative to the wrong directory. + with ArchiveMaker() as arc: + t = tarfile.TarInfo('x/') + t.type = tarfile.SYMTYPE + t.linkname = '..' + arc.tar_w.addfile(t) + arc.add('x/escaped', content='hi') + + with self.check_context(arc.open(), 'data'): + self.expect_exception(tarfile.LinkOutsideDestinationError) + + @os_helper.skip_unless_symlink + def test_link_at_destination(self): + # A link member whose name resolves to the destination directory + # itself must be rejected: otherwise the destination is replaced + # by a symlink and later members can be redirected through it. + for name in '', '.', './': + with ArchiveMaker() as arc: + t = tarfile.TarInfo(name) + t.type = tarfile.SYMTYPE + t.linkname = '.' + arc.tar_w.addfile(t) + + with self.check_context(arc.open(), 'data'): + self.expect_exception(tarfile.OutsideDestinationError) + + @os_helper.skip_unless_symlink + def test_empty_name_symlink_chain(self): + # Regression test for a chain of empty-named symlinks that + # incrementally redirects the destination outwards. + with ArchiveMaker() as arc: + for name, target in [('', ''), ('a/', '..'), + ('', 'dummy'), ('', 'a'), + ('b/', '..'), + ('', 'dummy'), ('', 'a/b')]: + t = tarfile.TarInfo(name) + t.type = tarfile.SYMTYPE + t.linkname = target + arc.tar_w.addfile(t) + arc.add('escaped', content='hi') + + with self.check_context(arc.open(), 'data'): + self.expect_exception(tarfile.FilterError) + def test_deep_symlink(self): # Test that symlinks and hardlinks inside a directory # point to the correct file (`target` of size 3). Index: Python-3.10.20/Misc/NEWS.d/next/Security/2026-05-03-21-00-00.gh-issue-149486.tarflt.rst =================================================================== --- /dev/null 1970-01-01 00:00:00.000000000 +0000 +++ Python-3.10.20/Misc/NEWS.d/next/Security/2026-05-03-21-00-00.gh-issue-149486.tarflt.rst 2026-07-31 17:27:30.513348800 +0200 @@ -0,0 +1,5 @@ +:func:`tarfile.data_filter` now validates link targets using the same +normalised value that is written to disk, strips trailing separators from +the member name when resolving a symlink's directory, and rejects link +members that would replace the destination directory itself. This closes +several path-traversal bypasses of the ``data`` extraction filter. ++++++ _scmsync.obsinfo ++++++ --- /var/tmp/diff_new_pack.fk7mtj/_old 2026-08-01 18:37:24.576518636 +0200 +++ /var/tmp/diff_new_pack.fk7mtj/_new 2026-08-01 18:37:24.576518636 +0200 @@ -1,6 +1,6 @@ -mtime: 1784043422 -commit: e7fbc1cb0a65ff2836b5d55d60cef3c526783db3d670d0f5cf7a8a8876a0ef3b +mtime: 1785511873 +commit: 44151aa6dc4ed3d66728b146b9ab9c31e458b54ff070faedce2fd7334b4ac33c url: https://src.opensuse.org/python-interpreters/python310 -revision: e7fbc1cb0a65ff2836b5d55d60cef3c526783db3d670d0f5cf7a8a8876a0ef3b +revision: 44151aa6dc4ed3d66728b146b9ab9c31e458b54ff070faedce2fd7334b4ac33c projectscmsync: https://src.opensuse.org/python-interpreters/_ObsPrj ++++++ build.specials.obscpio ++++++ ++++++ build.specials.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/.gitignore new/.gitignore --- old/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ new/.gitignore 2026-07-31 17:31:13.000000000 +0200 @@ -0,0 +1,7 @@ +.osc +*.obscpio +*.osc +_build.* +.pbuild +*.rej +python310*-build/ ++++++ support-sphinx-9.patch ++++++ >From e7eedd63aa09cd59f13c017084a91ad3591b0e16 Mon Sep 17 00:00:00 2001 From: Adam Turner <[email protected]> Date: Sat, 29 Nov 2025 00:49:46 +0000 Subject: [PATCH] Prepare for Sphinx 9 --- Doc/howto/urllib2.rst | 2 +- Doc/library/socket.rst | 7 ++----- Doc/tools/extensions/glossary_search.py | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) Index: Python-3.10.20/Doc/howto/urllib2.rst =================================================================== --- Python-3.10.20.orig/Doc/howto/urllib2.rst 2026-03-03 01:49:35.000000000 +0100 +++ Python-3.10.20/Doc/howto/urllib2.rst 2026-07-27 22:39:02.469360986 +0200 @@ -22,7 +22,7 @@ You may also find useful the following article on fetching web resources with Python: - * `Basic Authentication <https://web.archive.org/web/20201215133350/http://www.voidspace.org.uk/python/articles/authentication.shtml>`_ + * `Basic Authentication <https://web.archive.org/web/20201215133350/http://www.voidspace.org.uk/python/articles/authentication.shtml>`__ A tutorial on *Basic Authentication*, with examples in Python. Index: Python-3.10.20/Doc/library/socket.rst =================================================================== --- Python-3.10.20.orig/Doc/library/socket.rst 2026-07-27 22:38:58.293999697 +0200 +++ Python-3.10.20/Doc/library/socket.rst 2026-07-27 22:39:02.470643429 +0200 @@ -1729,11 +1729,8 @@ :attr:`socket.type`. -.. method:: socket.setsockopt(level, optname, value: int) -.. method:: socket.setsockopt(level, optname, value: buffer) - :noindex: -.. method:: socket.setsockopt(level, optname, None, optlen: int) - :noindex: +.. method:: socket.setsockopt(level, optname, value: int | Buffer) + socket.setsockopt(level, optname, None, optlen: int) .. index:: pair: module; struct Index: Python-3.10.20/Doc/tools/extensions/glossary_search.py =================================================================== --- Python-3.10.20.orig/Doc/tools/extensions/glossary_search.py 2026-03-03 01:49:35.000000000 +0100 +++ Python-3.10.20/Doc/tools/extensions/glossary_search.py 2026-07-27 22:39:48.150081477 +0200 @@ -33,7 +33,7 @@ rendered = app.builder.render_partial(definition) terms[term] = { 'title': glossary_item[0].astext(), - 'body': rendered['html_body'] + 'body': rendered['fragment'] } if hasattr(app.env, 'glossary_terms'):
