Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python312 for openSUSE:Factory 
checked in at 2026-08-01 18:28:21
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python312 (Old)
 and      /work/SRC/openSUSE:Factory/.python312.new.16738 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python312"

Sat Aug  1 18:28:21 2026 rev:51 rq:1368858 version:3.12.13

Changes:
--------
--- /work/SRC/openSUSE:Factory/python312/python312.changes      2026-07-02 
20:07:48.213949367 +0200
+++ /work/SRC/openSUSE:Factory/.python312.new.16738/python312.changes   
2026-08-01 18:28:54.534910693 +0200
@@ -1,0 +2,70 @@
+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.
+
+-------------------------------------------------------------------
+Sat Jul  4 10:25:43 UTC 2026 - Matej Cepl <[email protected]>
+
+- CVE-2026-7210: Use XML_SetHashSalt16Bytes in
+  pyexpat/_elementtree when possible (bsc#1264962,
+  gh#python/cpython#149018).
+  CVE-2026-7210-pyexpat-entropy-hash-flooding.patch
+
+-------------------------------------------------------------------
+Thu Jul  2 22:49:04 UTC 2026 - Matej Cepl <[email protected]>
+
+- CVE-2026-8328: Make ftplib not trust the PASV response
+  (bsc#1265268, gh#python/cpython#87451)
+  CVE-2026-8328-ftplib-no-trust-PASV-resp.patch
+
+-------------------------------------------------------------------
+Wed Jul  1 23:10:26 UTC 2026 - Matej Cepl <[email protected]>
+
+- CVE-2026-11940: fix the symlink escape via tarfile
+  hardlink-extraction fallback (bsc#1268977)
+  CVE-2026-11940-tarfile-escape.patch
+
+-------------------------------------------------------------------

New:
----
  CVE-2026-0864-normalize-LFTAB-configparser.patch
  CVE-2026-11940-tarfile-escape.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-7210-pyexpat-entropy-hash-flooding.patch
  CVE-2026-7774-tarfile-data_filter-symlink.patch
  CVE-2026-8328-ftplib-no-trust-PASV-resp.patch
  support-sphinx-9.patch

----------(New B)----------
  New:  configparser (bsc#1269066, gh#python/cpython#143927)
  CVE-2026-0864-normalize-LFTAB-configparser.patch
  New:  hardlink-extraction fallback (bsc#1268977)
  CVE-2026-11940-tarfile-escape.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:  gh#python/cpython#149018).
  CVE-2026-7210-pyexpat-entropy-hash-flooding.patch
  New:  target (bsc#1267821, gh#149486)
  CVE-2026-7774-tarfile-data_filter-symlink.patch
  New:  (bsc#1265268, gh#python/cpython#87451)
  CVE-2026-8328-ftplib-no-trust-PASV-resp.patch
  New:
- Add patch support-sphinx-9.patch:
  * Support changes required for Sphinx 9.
----------(New E)----------

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python312.spec ++++++
--- /var/tmp/diff_new_pack.53gIG0/_old  2026-08-01 18:28:59.095066053 +0200
+++ /var/tmp/diff_new_pack.53gIG0/_new  2026-08-01 18:28:59.095066053 +0200
@@ -236,6 +236,35 @@
 # PATCH-FIX-UPSTREAM CVE-2026-6019-Morsel-js_output.patch bsc#1262654 
[email protected]
 # Base64-encode cookie values embedded in JS
 Patch69:        CVE-2026-6019-Morsel-js_output.patch
+# PATCH-FIX-UPSTREAM CVE-2026-11940-tarfile-escape.patch bsc#1268977 
[email protected]
+# Fix symlink escape via tarfile hardlink-extraction fallback
+Patch70:        CVE-2026-11940-tarfile-escape.patch
+# PATCH-FIX-UPSTREAM CVE-2026-8328-ftplib-no-trust-PASV-resp.patch bsc#1265268 
[email protected]
+# Make ftplib not trust the PASV response
+Patch71:        CVE-2026-8328-ftplib-no-trust-PASV-resp.patch
+# PATCH-FIX-UPSTREAM CVE-2026-7210-pyexpat-entropy-hash-flooding.patch 
bsc#1264962 [email protected]
+# Use XML_SetHashSalt16Bytes in pyexpat/_elementtree when possible
+Patch72:        CVE-2026-7210-pyexpat-entropy-hash-flooding.patch
+# PATCH-FIX-UPSTREAM Based on gh#python/cpython#142057 Support fixes required 
for Sphinx 9
+Patch73:        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
+Patch74:        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()
+Patch75:        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
+Patch76:        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
+Patch77:        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
+Patch78:        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()
+Patch79:        CVE-2026-3276-On2-unicodedata-normalize.patch
 ### END OF PATCHES
 BuildRequires:  autoconf-archive
 BuildRequires:  automake

++++++ CVE-2026-0864-normalize-LFTAB-configparser.patch ++++++
>From 2c2aeeac9b2e4838f24456993142a222de45aa5a 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.12.13/Lib/configparser.py
===================================================================
--- Python-3.12.13.orig/Lib/configparser.py     2026-07-31 15:34:50.123179939 
+0200
+++ Python-3.12.13/Lib/configparser.py  2026-07-31 15:35:00.535569373 +0200
@@ -907,7 +907,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.12.13/Lib/test/test_configparser.py
===================================================================
--- Python-3.12.13.orig/Lib/test/test_configparser.py   2026-07-31 
15:34:51.948621214 +0200
+++ Python-3.12.13/Lib/test/test_configparser.py        2026-07-31 
15:35:00.535885028 +0200
@@ -527,6 +527,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.12.13/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.12.13/Misc/NEWS.d/next/Security/2026-01-16-11-58-19.gh-issue-143927.aviFeG.rst
     2026-07-31 15:35:00.536155047 +0200
@@ -0,0 +1,2 @@
+Normalize all line endings (CR, CRLF, and LF) to LF+TAB when writing
+multi-line configparser values.

++++++ CVE-2026-11940-tarfile-escape.patch ++++++
>From d0ba16dce6df2f68e8968699d5c247eddeef9960 Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <[email protected]>
Date: Tue, 23 Jun 2026 14:31:38 +0100
Subject: [PATCH] gh-151558: Fix symlink escape via `tarfile`
 hardlink-extraction fallback (GH-151559) (cherry picked from commit
 27dd970bf6b17ebca7c8ed486a40ab043ed7af8f)

Co-authored-by: Stan Ulbrych <[email protected]>
---
 Lib/tarfile.py                                                           |    
3 +
 Lib/test/test_tarfile.py                                                 |   
24 ++++++++++
 Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst |    
3 +
 3 files changed, 30 insertions(+)
 create mode 100644 
Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst

Index: Python-3.12.13/Lib/tarfile.py
===================================================================
--- Python-3.12.13.orig/Lib/tarfile.py  2026-07-02 12:07:53.963703857 +0200
+++ Python-3.12.13/Lib/tarfile.py       2026-07-02 12:07:54.089227749 +0200
@@ -2670,6 +2670,9 @@
                     "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
Index: Python-3.12.13/Lib/test/test_tarfile.py
===================================================================
--- Python-3.12.13.orig/Lib/test/test_tarfile.py        2026-07-02 
12:07:53.964225393 +0200
+++ Python-3.12.13/Lib/test/test_tarfile.py     2026-07-02 12:07:54.090227768 
+0200
@@ -4100,6 +4100,30 @@
                     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
Index: 
Python-3.12.13/Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst
===================================================================
--- /dev/null   1970-01-01 00:00:00.000000000 +0000
+++ 
Python-3.12.13/Misc/NEWS.d/next/Security/2026-06-10-13-08-19.gh-issue-151558.mL74i2.rst
     2026-07-02 12:07:54.090551855 +0200
@@ -0,0 +1,3 @@
+Fixed an vulnerability in the :mod:`tarfile` ``data`` and ``tar`` extraction
+filters where crafted archives could create a symlink pointing outside the
+destination directory. This was a bypass of :cve:`2025-4330`.

++++++ CVE-2026-11972-tarfile-Stream-seek-EOF.patch ++++++
>From 8f833e43ff025bf492b714de4b0138a51be68973 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.12.13/Lib/tarfile.py
===================================================================
--- Python-3.12.13.orig/Lib/tarfile.py  2026-07-31 15:24:32.499837215 +0200
+++ Python-3.12.13/Lib/tarfile.py       2026-07-31 15:24:32.514837359 +0200
@@ -515,7 +515,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.12.13/Lib/test/test_tarfile.py
===================================================================
--- Python-3.12.13.orig/Lib/test/test_tarfile.py        2026-07-31 
15:24:32.501112240 +0200
+++ Python-3.12.13/Lib/test/test_tarfile.py     2026-07-31 15:24:32.516123600 
+0200
@@ -4591,6 +4591,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 OverwriteTests(archiver_tests.OverwriteTests, unittest.TestCase):
     testdir = os.path.join(TEMPDIR, "testoverwrite")
Index: 
Python-3.12.13/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.12.13/Misc/NEWS.d/next/Security/2026-06-23-13-28-16.gh-issue-151981.xBHEcU.rst
     2026-07-31 15:24:32.516651254 +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 effae3f6f868409068cc0b7ab16fe8e4251352be 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.
(cherry picked from commit bcf98ddbc40ec9b3ee87da0124a5660b19b7e606)

Co-authored-by: Serhiy Storchaka <[email protected]>
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---
 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.12.13/Lib/html/parser.py
===================================================================
--- Python-3.12.13.orig/Lib/html/parser.py      2026-07-31 14:56:12.461798461 
+0200
+++ Python-3.12.13/Lib/html/parser.py   2026-07-31 14:56:18.773048016 +0200
@@ -138,6 +138,9 @@
         self.cdata_elem = None
         self._support_cdata = True
         self._escapable = True
+        self._pending = []
+        self._pending_len = 0
+        self._parse_threshold = 1
         super().reset()
 
     def feed(self, data):
@@ -146,11 +149,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.12.13/Lib/test/test_htmlparser.py
===================================================================
--- Python-3.12.13.orig/Lib/test/test_htmlparser.py     2026-07-31 
14:56:14.511914931 +0200
+++ Python-3.12.13/Lib/test/test_htmlparser.py  2026-07-31 14:56:18.773765745 
+0200
@@ -930,6 +930,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.12.13/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.12.13/Misc/NEWS.d/next/Security/2026-07-04-17-00-00.gh-issue-153030.RovkP6.rst
     2026-07-31 14:56:18.774005106 +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 d8deb45e25f8eae007449f4018a4f1e117a772d4 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <[email protected]>
Date: Tue, 2 Jun 2026 18:12:42 +0200
Subject: [PATCH] [3.12] 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                                                    |  
144 ++++++++--
 3 files changed, 151 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.12.13/Lib/test/test_unicodedata.py
===================================================================
--- Python-3.12.13.orig/Lib/test/test_unicodedata.py    2026-07-31 
15:54:54.381130595 +0200
+++ Python-3.12.13/Lib/test/test_unicodedata.py 2026-07-31 15:55:01.273145567 
+0200
@@ -203,6 +203,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.12.13/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.12.13/Misc/NEWS.d/next/Security/2026-04-27-16-36-11.gh-issue-149079.vKl-LM.rst
     2026-07-31 15:55:01.273321169 +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.12.13/Modules/unicodedata.c
===================================================================
--- Python-3.12.13.orig/Modules/unicodedata.c   2026-03-03 13:39:30.000000000 
+0100
+++ Python-3.12.13/Modules/unicodedata.c        2026-07-31 15:55:01.273759706 
+0200
@@ -490,19 +490,80 @@
 #define NCount  (VCount*TCount)
 #define SCount  (LCount*NCount)
 
+/* 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]);
+    }
+}
+
 static PyObject*
 nfd_nfkd(PyObject *self, PyObject *input, int k)
 {
     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);
@@ -522,11 +583,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
@@ -591,35 +652,66 @@
     PyMem_Free(output);
     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++;
         }
-        prev = _getrecord_ex(PyUnicode_READ(kind, data, i))->combining;
+        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;
+        }
+
+        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 61d17f4516bc3284b59c6ec01ee2e81da619f4ff Mon Sep 17 00:00:00 2001
From: Petr Viktorin <[email protected]>
Date: Mon, 29 Jun 2026 17:41:26 +0200
Subject: [PATCH] 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                                                 |   
92 ++++++++++
 Misc/NEWS.d/next/Security/2026-06-23-14-19-30.gh-issue-151987.8mNIMf.rst |    
2 
 3 files changed, 96 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.12.13/Lib/tarfile.py
===================================================================
--- Python-3.12.13.orig/Lib/tarfile.py  2026-07-31 15:09:56.986590323 +0200
+++ Python-3.12.13/Lib/tarfile.py       2026-07-31 15:09:57.066061566 +0200
@@ -2401,7 +2401,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.12.13/Lib/test/test_tarfile.py
===================================================================
--- Python-3.12.13.orig/Lib/test/test_tarfile.py        2026-07-31 
15:09:56.987688633 +0200
+++ Python-3.12.13/Lib/test/test_tarfile.py     2026-07-31 15:09:57.067008080 
+0200
@@ -4178,6 +4178,98 @@
                     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
Index: 
Python-3.12.13/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.12.13/Misc/NEWS.d/next/Security/2026-06-23-14-19-30.gh-issue-151987.8mNIMf.rst
     2026-07-31 15:09:57.067631004 +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-7210-pyexpat-entropy-hash-flooding.patch ++++++
>From 47c9d32583821997bb6bf11e6293a4e2d0d590c2 Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <[email protected]>
Date: Tue, 9 Jun 2026 15:07:36 +0100
Subject: [PATCH] [3.13] gh-149018: Use `XML_SetHashSalt16Bytes` in
 `pyexpat`/`_elementtree` when possible (GH-149023) (cherry picked from commit
 eeea765cb9d8f1fc3d8918b272ac3c477983f27a)

Co-authored-by: Stan Ulbrych <[email protected]>
(cherry picked from commit 24b8f12544468e4cedf5bfbe25442fcd495391e4)
---
 Include/pyexpat.h                                                        |    
3 +++
 Include/pyhash.h                                                         |    
7 ++++---
 Misc/NEWS.d/next/Security/2026-04-26-19-30-45.gh-issue-149018.a9SqWb.rst |    
3 +++
 Modules/_elementtree.c                                                   |    
8 ++++++--
 Modules/pyexpat.c                                                        |   
10 +++++++++-
 5 files changed, 25 insertions(+), 6 deletions(-)
 create mode 100644 
Misc/NEWS.d/next/Security/2026-04-26-19-30-45.gh-issue-149018.a9SqWb.rst

Index: Python-3.12.13/Include/pyexpat.h
===================================================================
--- Python-3.12.13.orig/Include/pyexpat.h       2026-03-03 13:39:30.000000000 
+0100
+++ Python-3.12.13/Include/pyexpat.h    2026-07-05 01:12:50.817822904 +0200
@@ -57,6 +57,9 @@
         XML_Parser parser, unsigned long long activationThresholdBytes);
     XML_Bool (*SetAllocTrackerMaximumAmplification)(
         XML_Parser parser, float maxAmplificationFactor);
+    /* might be NULL for expat < 2.8.0 */
+    XML_Bool (*SetHashSalt16Bytes)(
+        XML_Parser parser, const uint8_t entropy[16]);
     /* always add new stuff to the end! */
 };
 
Index: Python-3.12.13/Include/pyhash.h
===================================================================
--- Python-3.12.13.orig/Include/pyhash.h        2026-03-03 13:39:30.000000000 
+0100
+++ Python-3.12.13/Include/pyhash.h     2026-07-05 01:46:22.091756242 +0200
@@ -35,7 +35,7 @@
 /* hash secret
  *
  * memory layout on 64 bit systems
- *   cccccccc cccccccc cccccccc  uc -- unsigned char[24]
+ *   cccccccc cccccccc cccccccc cccccccc cccccccc  uc -- unsigned char[40]
  *   pppppppp ssssssss ........  fnv -- two Py_hash_t
  *   k0k0k0k0 k1k1k1k1 ........  siphash -- two uint64_t
  *   ........ ........ ssssssss  djbx33a -- 16 bytes padding + one Py_hash_t
@@ -53,8 +53,8 @@
  */
 #ifndef Py_LIMITED_API
 typedef union {
-    /* ensure 24 bytes */
-    unsigned char uc[24];
+    /* ensure 40 bytes */
+    unsigned char uc[40];
     /* two Py_hash_t for FNV */
     struct {
         Py_hash_t prefix;
@@ -73,6 +73,7 @@
     struct {
         unsigned char padding[16];
         Py_hash_t hashsalt;
+        uint8_t hashsalt16[16];
     } expat;
 } _Py_HashSecret_t;
 PyAPI_DATA(_Py_HashSecret_t) _Py_HashSecret;
Index: 
Python-3.12.13/Misc/NEWS.d/next/Security/2026-04-26-19-30-45.gh-issue-149018.a9SqWb.rst
===================================================================
--- /dev/null   1970-01-01 00:00:00.000000000 +0000
+++ 
Python-3.12.13/Misc/NEWS.d/next/Security/2026-04-26-19-30-45.gh-issue-149018.a9SqWb.rst
     2026-07-05 01:12:50.819638091 +0200
@@ -0,0 +1,3 @@
+Improved protection against XML hash-flooding attacks in
+:mod:`xml.parsers.expat` and :mod:`xml.etree.ElementTree` when Python is
+compiled with libExpat 2.8.0 or later.
Index: Python-3.12.13/Modules/_elementtree.c
===================================================================
--- Python-3.12.13.orig/Modules/_elementtree.c  2026-03-03 13:39:30.000000000 
+0100
+++ Python-3.12.13/Modules/_elementtree.c       2026-07-05 01:12:50.820729257 
+0200
@@ -3657,8 +3657,12 @@
         PyErr_NoMemory();
         return -1;
     }
-    /* expat < 2.1.0 has no XML_SetHashSalt() */
-    if (EXPAT(st, SetHashSalt) != NULL) {
+    // Prefer 16-byte entropy, only expat >= 2.8.0. See gh-149018
+    if (EXPAT(st, SetHashSalt16Bytes) != NULL) {
+        EXPAT(st, SetHashSalt16Bytes)(self->parser,
+                                      _Py_HashSecret.expat.hashsalt16);
+    }
+    else if (EXPAT(st, SetHashSalt) != NULL) {
         EXPAT(st, SetHashSalt)(self->parser,
                            (unsigned long)_Py_HashSecret.expat.hashsalt);
     }
Index: Python-3.12.13/Modules/pyexpat.c
===================================================================
--- Python-3.12.13.orig/Modules/pyexpat.c       2026-07-05 01:12:50.593816872 
+0200
+++ Python-3.12.13/Modules/pyexpat.c    2026-07-05 01:12:50.821624893 +0200
@@ -1395,7 +1395,10 @@
         Py_DECREF(self);
         return NULL;
     }
-#if XML_COMBINED_VERSION >= 20100
+#if XML_COMBINED_VERSION >= 20800
+    /* This feature was added upstream in libexpat 2.8.0. */
+    XML_SetHashSalt16Bytes(self->itself, _Py_HashSecret.expat.hashsalt16);
+#elif XML_COMBINED_VERSION >= 20100
     /* This feature was added upstream in libexpat 2.1.0. */
     XML_SetHashSalt(self->itself,
                     (unsigned long)_Py_HashSecret.expat.hashsalt);
@@ -2264,6 +2267,11 @@
 #else
     capi->SetHashSalt = NULL;
 #endif
+#if XML_COMBINED_VERSION >= 20800
+    capi->SetHashSalt16Bytes = XML_SetHashSalt16Bytes;
+#else
+    capi->SetHashSalt16Bytes = NULL;
+#endif
 #if XML_COMBINED_VERSION >= 20600
     capi->SetReparseDeferralEnabled = XML_SetReparseDeferralEnabled;
 #else

++++++ CVE-2026-7774-tarfile-data_filter-symlink.patch ++++++
>From 6df9892a802c124c4223e17c4a05ea7054166ac7 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <[email protected]>
Date: Fri, 8 May 2026 14:16:06 +0200
Subject: [PATCH 1/2] 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                                                 |  
151 +++++++---
 Misc/NEWS.d/next/Security/2026-05-03-21-00-00.gh-issue-149486.tarflt.rst |    
5 
 3 files changed, 133 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.12.13/Lib/tarfile.py
===================================================================
--- Python-3.12.13.orig/Lib/tarfile.py  2026-07-31 15:47:33.973188784 +0200
+++ Python-3.12.13/Lib/tarfile.py       2026-07-31 15:47:34.000601885 +0200
@@ -817,16 +817,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.12.13/Lib/test/test_tarfile.py
===================================================================
--- Python-3.12.13.orig/Lib/test/test_tarfile.py        2026-07-31 
15:47:33.974021896 +0200
+++ Python-3.12.13/Lib/test/test_tarfile.py     2026-07-31 15:47:34.003155983 
+0200
@@ -3549,6 +3549,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):
         """Extracts `tar` to `self.destdir` and allows checking the result
@@ -3720,10 +3753,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.
@@ -3813,35 +3855,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
@@ -3877,7 +3890,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,
@@ -3998,6 +4011,76 @@
                     + "destination")
 
     @symlink_test
+    @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)
+
+    @symlink_test
+    @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)
+
+    @symlink_test
+    @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)
+
+    @symlink_test
+    @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)
+
+    @symlink_test
     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.12.13/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.12.13/Misc/NEWS.d/next/Security/2026-05-03-21-00-00.gh-issue-149486.tarflt.rst
     2026-07-31 15:47:34.002049787 +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.

++++++ CVE-2026-8328-ftplib-no-trust-PASV-resp.patch ++++++
>From b026be60f7e023719a4d8de89ae3c3248b7c5d40 Mon Sep 17 00:00:00 2001
From: "Gregory P. Smith" <[email protected]>
Date: Wed, 13 May 2026 10:33:43 -0700
Subject: [PATCH] gh-87451: Apply CVE-2021-4189 PASV fix to ftplib.ftpcp()
 (GH-149648)

ftpcp() called parse227() directly and passed the source server's
self-reported PASV IPv4 address to the target server's PORT command,
bypassing the CVE-2021-4189 fix that was applied only to FTP.makepasv().
A malicious source FTP server could use this to redirect the target
server's data connection to an arbitrary host:port (SSRF).

ftpcp() now uses the source server's actual peer address, honoring the
existing trust_server_pasv_ipv4_address opt-out, the same as makepasv().

Thanks to Qi Ding at Aurascape AI for the report. (GHSA-w8c5-q2xf-gf7c)
(cherry picked from commit eac4fe3b2c77693790a5ef7dfab127c1fee81bf9)

Co-authored-by: Gregory P. Smith <[email protected]>
---
 Lib/ftplib.py                                                           |   11 
++-
 Lib/test/test_ftplib.py                                                 |   36 
+++++++++-
 Misc/NEWS.d/next/Security/2026-05-10-18-05-32.gh-issue-87451.XkKB6M.rst |    6 
+
 3 files changed, 51 insertions(+), 2 deletions(-)
 create mode 100644 
Misc/NEWS.d/next/Security/2026-05-10-18-05-32.gh-issue-87451.XkKB6M.rst

Index: Python-3.12.13/Lib/ftplib.py
===================================================================
--- Python-3.12.13.orig/Lib/ftplib.py   2026-07-03 00:50:14.031086570 +0200
+++ Python-3.12.13/Lib/ftplib.py        2026-07-03 00:50:20.046470436 +0200
@@ -883,7 +883,16 @@
     type = 'TYPE ' + type
     source.voidcmd(type)
     target.voidcmd(type)
-    sourcehost, sourceport = parse227(source.sendcmd('PASV'))
+    # Don't trust the IPv4 address the source server advertises in its PASV
+    # reply: a malicious source could otherwise point the target's data
+    # connection at an arbitrary host (SSRF).  A caller that needs the old
+    # behavior can set trust_server_pasv_ipv4_address on the source FTP
+    # object.  See FTP.makepasv(), which applies the same rule.
+    untrusted_host, sourceport = parse227(source.sendcmd('PASV'))
+    if source.trust_server_pasv_ipv4_address:
+        sourcehost = untrusted_host
+    else:
+        sourcehost = source.sock.getpeername()[0]
     target.sendport(sourcehost, sourceport)
     # RFC 959: the user must "listen" [...] BEFORE sending the
     # transfer request.
Index: Python-3.12.13/Lib/test/test_ftplib.py
===================================================================
--- Python-3.12.13.orig/Lib/test/test_ftplib.py 2026-07-03 00:50:15.720789920 
+0200
+++ Python-3.12.13/Lib/test/test_ftplib.py      2026-07-03 00:50:20.046728874 
+0200
@@ -16,7 +16,7 @@
 except ImportError:
     ssl = None
 
-from unittest import TestCase, skipUnless
+from unittest import mock, TestCase, skipUnless
 from test import support
 from test.support import threading_helper
 from test.support import socket_helper
@@ -1142,6 +1142,40 @@
         ftp.close()
 
 
+class TestFtpcpSecurity(TestCase):
+    """ftpcp() must not trust the host a source server advertises in PASV.
+
+    A malicious source server can otherwise redirect the target server's
+    data connection to an arbitrary host:port (SSRF), so ftpcp() uses the
+    source server's actual peer address instead, the same as FTP.makepasv().
+    """
+
+    def _make_pair(self, *, advertised_host, real_host, trust=False):
+        source = mock.Mock(spec=ftplib.FTP)
+        source.trust_server_pasv_ipv4_address = trust
+        source.sock.getpeername.return_value = (real_host, 21)
+        # PASV replies give the host as comma-separated octets, not dotted.
+        advertised = advertised_host.replace('.', ',')
+        source.sendcmd.side_effect = lambda cmd: (
+            f'227 Entering Passive Mode ({advertised},1,2).'
+            if cmd == 'PASV' else '150 ok')
+        target = mock.Mock(spec=ftplib.FTP)
+        target.sendcmd.return_value = '150 ok'
+        return source, target
+
+    def test_ftpcp_ignores_untrusted_pasv_host(self):
+        source, target = self._make_pair(advertised_host='10.0.0.5',
+                                         real_host='198.51.100.7')
+        ftplib.ftpcp(source, 'a', target, 'b')
+        target.sendport.assert_called_once_with('198.51.100.7', 258)
+
+    def test_ftpcp_trust_server_pasv_ipv4_address(self):
+        source, target = self._make_pair(advertised_host='10.0.0.5',
+                                         real_host='198.51.100.7', trust=True)
+        ftplib.ftpcp(source, 'a', target, 'b')
+        target.sendport.assert_called_once_with('10.0.0.5', 258)
+
+
 class MiscTestCase(TestCase):
     def test__all__(self):
         not_exported = {
Index: 
Python-3.12.13/Misc/NEWS.d/next/Security/2026-05-10-18-05-32.gh-issue-87451.XkKB6M.rst
===================================================================
--- /dev/null   1970-01-01 00:00:00.000000000 +0000
+++ 
Python-3.12.13/Misc/NEWS.d/next/Security/2026-05-10-18-05-32.gh-issue-87451.XkKB6M.rst
      2026-07-03 00:50:20.046898824 +0200
@@ -0,0 +1,6 @@
+The :mod:`ftplib` module's undocumented ``ftpcp`` function no longer trusts
+the IPv4 address value returned from the source server in response to the
+``PASV`` command by default, completing the fix for CVE-2021-4189.  As with
+:class:`ftplib.FTP`, the former behavior can be re-enabled by setting the
+``trust_server_pasv_ipv4_address`` attribute on the source :class:`ftplib.FTP`
+instance to ``True``.  Thanks to Qi Deng at Aurascape AI for the report.


++++++ _scmsync.obsinfo ++++++
--- /var/tmp/diff_new_pack.53gIG0/_old  2026-08-01 18:28:59.559081862 +0200
+++ /var/tmp/diff_new_pack.53gIG0/_new  2026-08-01 18:28:59.563081998 +0200
@@ -1,6 +1,6 @@
-mtime: 1782731285
-commit: 73df3475a766625be6b1895e2c6390867a4cc128f3291020fa0fa0a87be4fe99
+mtime: 1785506276
+commit: dc5782910c671851c0c6988f459e9ac9e73a1c74881059612ce050729d67cda3
 url: https://src.opensuse.org/python-interpreters/python312
-revision: 73df3475a766625be6b1895e2c6390867a4cc128f3291020fa0fa0a87be4fe99
+revision: dc5782910c671851c0c6988f459e9ac9e73a1c74881059612ce050729d67cda3
 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 15:57:56.000000000 +0200
@@ -0,0 +1,6 @@
+_build.*
+*.obscpio
+*.osc
+.osc
+.pbuild
+python312-3.12.*-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.12.13/Doc/howto/urllib2.rst
===================================================================
--- Python-3.12.13.orig/Doc/howto/urllib2.rst
+++ Python-3.12.13/Doc/howto/urllib2.rst
@@ -15,7 +15,7 @@ Introduction
     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.12.13/Doc/library/socket.rst
===================================================================
--- Python-3.12.13.orig/Doc/library/socket.rst
+++ Python-3.12.13/Doc/library/socket.rst
@@ -1961,11 +1961,8 @@ to sockets.
       :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.12.13/Doc/tools/extensions/glossary_search.py
===================================================================
--- Python-3.12.13.orig/Doc/tools/extensions/glossary_search.py
+++ Python-3.12.13/Doc/tools/extensions/glossary_search.py
@@ -42,7 +42,7 @@ def process_glossary_nodes(
             rendered = app.builder.render_partial(definition)
             terms[term.lower()] = {
                 'title': term,
-                'body': rendered['html_body'],
+                'body': rendered['fragment'],
             }
 
 

Reply via email to