From 6dd14d9245884b1795e25770c1bc1e7dd0eac160 Mon Sep 17 00:00:00 2001 From: AlmaLinux RelEng Bot Date: Tue, 19 May 2026 19:16:50 -0400 Subject: [PATCH] import UBI python3.12-3.12.13-2.el10_2 --- .gitignore | 2 +- ..._seterror-handling-ssl_error_syscall.patch | 2 +- 00471-cve-2025-12084.patch | 139 --------------- 00472-cve-2025-13836.patch | 159 ------------------ 00473-cve-2026-0865.patch | 90 ---------- 00476-cve-2026-1299.patch | 110 ------------ 00479-cve-2026-1502.patch | 107 ++++++++++++ 00483-cve-2026-2297.patch | 33 ++++ 00484-cve-2026-3644.patch | 146 ++++++++++++++++ 00485-cve-2026-4224.patch | 98 +++++++++++ Python-3.12.12.tar.xz.asc | 18 -- Python-3.12.13.tar.xz.asc | 18 ++ expat-requires.py | 50 ++++++ python3.12.spec | 103 ++++++------ sources | 2 +- 15 files changed, 511 insertions(+), 566 deletions(-) delete mode 100644 00471-cve-2025-12084.patch delete mode 100644 00472-cve-2025-13836.patch delete mode 100644 00473-cve-2026-0865.patch delete mode 100644 00476-cve-2026-1299.patch create mode 100644 00479-cve-2026-1502.patch create mode 100644 00483-cve-2026-2297.patch create mode 100644 00484-cve-2026-3644.patch create mode 100644 00485-cve-2026-4224.patch delete mode 100644 Python-3.12.12.tar.xz.asc create mode 100644 Python-3.12.13.tar.xz.asc create mode 100755 expat-requires.py diff --git a/.gitignore b/.gitignore index eb2a680..ce292f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -Python-3.12.12.tar.xz +Python-3.12.13.tar.xz diff --git a/00462-fix-pyssl_seterror-handling-ssl_error_syscall.patch b/00462-fix-pyssl_seterror-handling-ssl_error_syscall.patch index bfa2f8e..93984f2 100644 --- a/00462-fix-pyssl_seterror-handling-ssl_error_syscall.patch +++ b/00462-fix-pyssl_seterror-handling-ssl_error_syscall.patch @@ -84,7 +84,7 @@ index 0000000000..75d926ab59 +Fix the :mod:`ssl` module error handling of connection terminate by peer. +It now throws an OSError with the appropriate error code instead of an EOFError. diff --git a/Modules/_ssl.c b/Modules/_ssl.c -index 0b8cf0b6df..42a4c95890 100644 +index aae4dc323d..27dd7bbe11 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -573,7 +573,7 @@ PySSL_ChainExceptions(PySSLSocket *sslsock) { diff --git a/00471-cve-2025-12084.patch b/00471-cve-2025-12084.patch deleted file mode 100644 index bb0903c..0000000 --- a/00471-cve-2025-12084.patch +++ /dev/null @@ -1,139 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: "Miss Islington (bot)" - <31488909+miss-islington@users.noreply.github.com> -Date: Mon, 22 Dec 2025 14:48:49 +0100 -Subject: 00471: CVE-2025-12084 - -* gh-142145: Remove quadratic behavior in node ID cache clearing (GH-142146) -* gh-142754: Ensure that Element & Attr instances have the ownerDocument attribute (GH-142794) -(cherry picked from commit 1cc7551b3f9f71efbc88d96dce90f82de98b2454) -(cherry picked from commit 08d8e18ad81cd45bc4a27d6da478b51ea49486e4) -(cherry picked from commit 8d2d7bb2e754f8649a68ce4116271a4932f76907) - -Co-authored-by: Jacob Walls <38668450+jacobtylerwalls@users.noreply.github.com> -Co-authored-by: Seth Michael Larson -Co-authored-by: Petr Viktorin -Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> -Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> -Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> -Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com> -Co-authored-by: Gregory P. Smith ---- - Lib/test/test_minidom.py | 33 ++++++++++++++++++- - Lib/xml/dom/minidom.py | 11 ++----- - ...-12-01-09-36-45.gh-issue-142145.tcAUhg.rst | 6 ++++ - 3 files changed, 41 insertions(+), 9 deletions(-) - create mode 100644 Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst - -diff --git a/Lib/test/test_minidom.py b/Lib/test/test_minidom.py -index 699265ccad..ab4823c831 100644 ---- a/Lib/test/test_minidom.py -+++ b/Lib/test/test_minidom.py -@@ -2,13 +2,14 @@ - - import copy - import pickle -+import time - import io - from test import support - import unittest - - import xml.dom.minidom - --from xml.dom.minidom import parse, Attr, Node, Document, parseString -+from xml.dom.minidom import parse, Attr, Node, Document, Element, parseString - from xml.dom.minidom import getDOMImplementation - from xml.parsers.expat import ExpatError - -@@ -176,6 +177,36 @@ def testAppendChild(self): - self.confirm(dom.documentElement.childNodes[-1].data == "Hello") - dom.unlink() - -+ @support.requires_resource('cpu') -+ def testAppendChildNoQuadraticComplexity(self): -+ impl = getDOMImplementation() -+ -+ newdoc = impl.createDocument(None, "some_tag", None) -+ top_element = newdoc.documentElement -+ children = [newdoc.createElement(f"child-{i}") for i in range(1, 2 ** 15 + 1)] -+ element = top_element -+ -+ start = time.monotonic() -+ for child in children: -+ element.appendChild(child) -+ element = child -+ end = time.monotonic() -+ -+ # This example used to take at least 30 seconds. -+ # Conservative assertion due to the wide variety of systems and -+ # build configs timing based tests wind up run under. -+ # A --with-address-sanitizer --with-pydebug build on a rpi5 still -+ # completes this loop in <0.5 seconds. -+ self.assertLess(end - start, 4) -+ -+ def testSetAttributeNodeWithoutOwnerDocument(self): -+ # regression test for gh-142754 -+ elem = Element("test") -+ attr = Attr("id") -+ attr.value = "test-id" -+ elem.setAttributeNode(attr) -+ self.assertEqual(elem.getAttribute("id"), "test-id") -+ - def testAppendChildFragment(self): - dom, orig, c1, c2, c3, frag = self._create_fragment_test_nodes() - dom.documentElement.appendChild(frag) -diff --git a/Lib/xml/dom/minidom.py b/Lib/xml/dom/minidom.py -index ef8a159833..cada981f39 100644 ---- a/Lib/xml/dom/minidom.py -+++ b/Lib/xml/dom/minidom.py -@@ -292,13 +292,6 @@ def _append_child(self, node): - childNodes.append(node) - node.parentNode = self - --def _in_document(node): -- # return True iff node is part of a document tree -- while node is not None: -- if node.nodeType == Node.DOCUMENT_NODE: -- return True -- node = node.parentNode -- return False - - def _write_data(writer, data): - "Writes datachars to writer." -@@ -355,6 +348,7 @@ class Attr(Node): - def __init__(self, qName, namespaceURI=EMPTY_NAMESPACE, localName=None, - prefix=None): - self.ownerElement = None -+ self.ownerDocument = None - self._name = qName - self.namespaceURI = namespaceURI - self._prefix = prefix -@@ -680,6 +674,7 @@ class Element(Node): - - def __init__(self, tagName, namespaceURI=EMPTY_NAMESPACE, prefix=None, - localName=None): -+ self.ownerDocument = None - self.parentNode = None - self.tagName = self.nodeName = tagName - self.prefix = prefix -@@ -1539,7 +1534,7 @@ def _clear_id_cache(node): - if node.nodeType == Node.DOCUMENT_NODE: - node._id_cache.clear() - node._id_search_stack = None -- elif _in_document(node): -+ elif node.ownerDocument: - node.ownerDocument._id_cache.clear() - node.ownerDocument._id_search_stack= None - -diff --git a/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst b/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst -new file mode 100644 -index 0000000000..05c7df35d1 ---- /dev/null -+++ b/Misc/NEWS.d/next/Security/2025-12-01-09-36-45.gh-issue-142145.tcAUhg.rst -@@ -0,0 +1,6 @@ -+Remove quadratic behavior in ``xml.minidom`` node ID cache clearing. In order -+to do this without breaking existing users, we also add the *ownerDocument* -+attribute to :mod:`xml.dom.minidom` elements and attributes created by directly -+instantiating the ``Element`` or ``Attr`` class. Note that this way of creating -+nodes is not supported; creator functions like -+:py:meth:`xml.dom.Document.documentElement` should be used instead. diff --git a/00472-cve-2025-13836.patch b/00472-cve-2025-13836.patch deleted file mode 100644 index 9b2947d..0000000 --- a/00472-cve-2025-13836.patch +++ /dev/null @@ -1,159 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: "Miss Islington (bot)" - <31488909+miss-islington@users.noreply.github.com> -Date: Mon, 22 Dec 2025 14:50:18 +0100 -Subject: 00472: CVE-2025-13836 - -[3.12] gh-119451: Fix a potential denial of service in http.client (GH-119454) (#142140) - -gh-119451: Fix a potential denial of service in http.client (GH-119454) - -Reading the whole body of the HTTP response could cause OOM if -the Content-Length value is too large even if the server does not send -a large amount of data. Now the HTTP client reads large data by chunks, -therefore the amount of consumed memory is proportional to the amount -of sent data. -(cherry picked from commit 5a4c4a033a4a54481be6870aa1896fad732555b5) - -Co-authored-by: Serhiy Storchaka ---- - Lib/http/client.py | 28 ++++++-- - Lib/test/test_httplib.py | 66 +++++++++++++++++++ - ...-05-23-11-47-48.gh-issue-119451.qkJe9-.rst | 5 ++ - 3 files changed, 95 insertions(+), 4 deletions(-) - create mode 100644 Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst - -diff --git a/Lib/http/client.py b/Lib/http/client.py -index fb29923d94..70451d67d4 100644 ---- a/Lib/http/client.py -+++ b/Lib/http/client.py -@@ -111,6 +111,11 @@ - _MAXLINE = 65536 - _MAXHEADERS = 100 - -+# Data larger than this will be read in chunks, to prevent extreme -+# overallocation. -+_MIN_READ_BUF_SIZE = 1 << 20 -+ -+ - # Header name/value ABNF (http://tools.ietf.org/html/rfc7230#section-3.2) - # - # VCHAR = %x21-7E -@@ -639,10 +644,25 @@ def _safe_read(self, amt): - reading. If the bytes are truly not available (due to EOF), then the - IncompleteRead exception can be used to detect the problem. - """ -- data = self.fp.read(amt) -- if len(data) < amt: -- raise IncompleteRead(data, amt-len(data)) -- return data -+ cursize = min(amt, _MIN_READ_BUF_SIZE) -+ data = self.fp.read(cursize) -+ if len(data) >= amt: -+ return data -+ if len(data) < cursize: -+ raise IncompleteRead(data, amt - len(data)) -+ -+ data = io.BytesIO(data) -+ data.seek(0, 2) -+ while True: -+ # This is a geometric increase in read size (never more than -+ # doubling out the current length of data per loop iteration). -+ delta = min(cursize, amt - cursize) -+ data.write(self.fp.read(delta)) -+ if data.tell() >= amt: -+ return data.getvalue() -+ cursize += delta -+ if data.tell() < cursize: -+ raise IncompleteRead(data.getvalue(), amt - data.tell()) - - def _safe_readinto(self, b): - """Same as _safe_read, but for reading into a buffer.""" -diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py -index 01f5a10190..e46dac0077 100644 ---- a/Lib/test/test_httplib.py -+++ b/Lib/test/test_httplib.py -@@ -1452,6 +1452,72 @@ def run_server(): - thread.join() - self.assertEqual(result, b"proxied data\n") - -+ def test_large_content_length(self): -+ serv = socket.create_server((HOST, 0)) -+ self.addCleanup(serv.close) -+ -+ def run_server(): -+ [conn, address] = serv.accept() -+ with conn: -+ while conn.recv(1024): -+ conn.sendall( -+ b"HTTP/1.1 200 Ok\r\n" -+ b"Content-Length: %d\r\n" -+ b"\r\n" % size) -+ conn.sendall(b'A' * (size//3)) -+ conn.sendall(b'B' * (size - size//3)) -+ -+ thread = threading.Thread(target=run_server) -+ thread.start() -+ self.addCleanup(thread.join, 1.0) -+ -+ conn = client.HTTPConnection(*serv.getsockname()) -+ try: -+ for w in range(15, 27): -+ size = 1 << w -+ conn.request("GET", "/") -+ with conn.getresponse() as response: -+ self.assertEqual(len(response.read()), size) -+ finally: -+ conn.close() -+ thread.join(1.0) -+ -+ def test_large_content_length_truncated(self): -+ serv = socket.create_server((HOST, 0)) -+ self.addCleanup(serv.close) -+ -+ def run_server(): -+ while True: -+ [conn, address] = serv.accept() -+ with conn: -+ conn.recv(1024) -+ if not size: -+ break -+ conn.sendall( -+ b"HTTP/1.1 200 Ok\r\n" -+ b"Content-Length: %d\r\n" -+ b"\r\n" -+ b"Text" % size) -+ -+ thread = threading.Thread(target=run_server) -+ thread.start() -+ self.addCleanup(thread.join, 1.0) -+ -+ conn = client.HTTPConnection(*serv.getsockname()) -+ try: -+ for w in range(18, 65): -+ size = 1 << w -+ conn.request("GET", "/") -+ with conn.getresponse() as response: -+ self.assertRaises(client.IncompleteRead, response.read) -+ conn.close() -+ finally: -+ conn.close() -+ size = 0 -+ conn.request("GET", "/") -+ conn.close() -+ thread.join(1.0) -+ - def test_putrequest_override_domain_validation(self): - """ - It should be possible to override the default validation -diff --git a/Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst b/Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst -new file mode 100644 -index 0000000000..6d6f25cd2f ---- /dev/null -+++ b/Misc/NEWS.d/next/Security/2024-05-23-11-47-48.gh-issue-119451.qkJe9-.rst -@@ -0,0 +1,5 @@ -+Fix a potential memory denial of service in the :mod:`http.client` module. -+When connecting to a malicious server, it could cause -+an arbitrary amount of memory to be allocated. -+This could have led to symptoms including a :exc:`MemoryError`, swapping, out -+of memory (OOM) killed processes or containers, or even system crashes. diff --git a/00473-cve-2026-0865.patch b/00473-cve-2026-0865.patch deleted file mode 100644 index 3a93b65..0000000 --- a/00473-cve-2026-0865.patch +++ /dev/null @@ -1,90 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Seth Michael Larson -Date: Sat, 17 Jan 2026 11:46:21 -0600 -Subject: 00473: CVE-2026-0865 - - gh-143916: Reject control characters in wsgiref.headers.Headers (GH-143917) - -* Add 'test.support' fixture for C0 control characters -* gh-143916: Reject control characters in wsgiref.headers.Headers ---- - Lib/test/support/__init__.py | 7 +++++++ - Lib/test/test_wsgiref.py | 12 +++++++++++- - Lib/wsgiref/headers.py | 3 +++ - .../2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst | 2 ++ - 4 files changed, 23 insertions(+), 1 deletion(-) - create mode 100644 Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst - -diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py -index 4c42234ccc..26c0af4b13 100644 ---- a/Lib/test/support/__init__.py -+++ b/Lib/test/support/__init__.py -@@ -2599,3 +2599,10 @@ def __iter__(self): - if self.iter_raises: - 1/0 - return self -+ -+ -+def control_characters_c0() -> list[str]: -+ """Returns a list of C0 control characters as strings. -+ C0 control characters defined as the byte range 0x00-0x1F, and 0x7F. -+ """ -+ return [chr(c) for c in range(0x00, 0x20)] + ["\x7F"] -diff --git a/Lib/test/test_wsgiref.py b/Lib/test/test_wsgiref.py -index 9316d0ecbc..28e3656632 100644 ---- a/Lib/test/test_wsgiref.py -+++ b/Lib/test/test_wsgiref.py -@@ -1,6 +1,6 @@ - from unittest import mock - from test import support --from test.support import socket_helper -+from test.support import socket_helper, control_characters_c0 - from test.test_httpservers import NoLogRequestHandler - from unittest import TestCase - from wsgiref.util import setup_testing_defaults -@@ -503,6 +503,16 @@ def testExtras(self): - '\r\n' - ) - -+ def testRaisesControlCharacters(self): -+ headers = Headers() -+ for c0 in control_characters_c0(): -+ self.assertRaises(ValueError, headers.__setitem__, f"key{c0}", "val") -+ self.assertRaises(ValueError, headers.__setitem__, "key", f"val{c0}") -+ self.assertRaises(ValueError, headers.add_header, f"key{c0}", "val", param="param") -+ self.assertRaises(ValueError, headers.add_header, "key", f"val{c0}", param="param") -+ self.assertRaises(ValueError, headers.add_header, "key", "val", param=f"param{c0}") -+ -+ - class ErrorHandler(BaseCGIHandler): - """Simple handler subclass for testing BaseHandler""" - -diff --git a/Lib/wsgiref/headers.py b/Lib/wsgiref/headers.py -index fab851c5a4..fd98e85d75 100644 ---- a/Lib/wsgiref/headers.py -+++ b/Lib/wsgiref/headers.py -@@ -9,6 +9,7 @@ - # existence of which force quoting of the parameter value. - import re - tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]') -+_control_chars_re = re.compile(r'[\x00-\x1F\x7F]') - - def _formatparam(param, value=None, quote=1): - """Convenience function to format and return a key=value pair. -@@ -41,6 +42,8 @@ def __init__(self, headers=None): - def _convert_string_type(self, value): - """Convert/check value type.""" - if type(value) is str: -+ if _control_chars_re.search(value): -+ raise ValueError("Control characters not allowed in headers") - return value - raise AssertionError("Header names/values must be" - " of type str (got {0})".format(repr(value))) -diff --git a/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst b/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst -new file mode 100644 -index 0000000000..44bd0b2705 ---- /dev/null -+++ b/Misc/NEWS.d/next/Security/2026-01-16-11-07-36.gh-issue-143916.dpWeOD.rst -@@ -0,0 +1,2 @@ -+Reject C0 control characters within wsgiref.headers.Headers fields, values, -+and parameters. diff --git a/00476-cve-2026-1299.patch b/00476-cve-2026-1299.patch deleted file mode 100644 index ae6a4b2..0000000 --- a/00476-cve-2026-1299.patch +++ /dev/null @@ -1,110 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: "Miss Islington (bot)" - <31488909+miss-islington@users.noreply.github.com> -Date: Fri, 13 Feb 2026 17:04:54 +0100 -Subject: [PATCH] [3.12] gh-144125: email: verify headers are sound in - BytesGenerator - -gh-144125: email: verify headers are sound in BytesGenerator -(cherry picked from commit 052e55e7d44718fe46cbba0ca995cb8fcc359413) - -Co-authored-by: Seth Michael Larson -Co-authored-by: Denis Ledoux -Co-authored-by: Denis Ledoux <5822488+beledouxdenis@users.noreply.github.com> -Co-authored-by: Petr Viktorin <302922+encukou@users.noreply.github.com> -Co-authored-by: Bas Bloemsaat <1586868+basbloemsaat@users.noreply.github.com> -Co-authored-by: Petr Viktorin ---- - Lib/email/generator.py | 12 +++++++++++- - Lib/test/test_email/test_generator.py | 4 +++- - Lib/test/test_email/test_policy.py | 6 +++++- - .../2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst | 4 ++++ - 4 files changed, 23 insertions(+), 3 deletions(-) - create mode 100644 Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst - -diff --git a/Lib/email/generator.py b/Lib/email/generator.py -index 47b9df8f4e6090..8cbc43ef5bc647 100644 ---- a/Lib/email/generator.py -+++ b/Lib/email/generator.py -@@ -22,6 +22,7 @@ - NLCRE = re.compile(r'\r\n|\r|\n') - fcre = re.compile(r'^From ', re.MULTILINE) - NEWLINE_WITHOUT_FWSP = re.compile(r'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]') -+NEWLINE_WITHOUT_FWSP_BYTES = re.compile(br'\r\n[^ \t]|\r[^ \n\t]|\n[^ \t]') - - - class Generator: -@@ -429,7 +430,16 @@ def _write_headers(self, msg): - # This is almost the same as the string version, except for handling - # strings with 8bit bytes. - for h, v in msg.raw_items(): -- self._fp.write(self.policy.fold_binary(h, v)) -+ folded = self.policy.fold_binary(h, v) -+ if self.policy.verify_generated_headers: -+ linesep = self.policy.linesep.encode() -+ if not folded.endswith(linesep): -+ raise HeaderWriteError( -+ f'folded header does not end with {linesep!r}: {folded!r}') -+ if NEWLINE_WITHOUT_FWSP_BYTES.search(folded.removesuffix(linesep)): -+ raise HeaderWriteError( -+ f'folded header contains newline: {folded!r}') -+ self._fp.write(folded) - # A blank line always separates headers from body - self.write(self._NL) - -diff --git a/Lib/test/test_email/test_generator.py b/Lib/test/test_email/test_generator.py -index c75a842c33578e..3ca79edf6a65d9 100644 ---- a/Lib/test/test_email/test_generator.py -+++ b/Lib/test/test_email/test_generator.py -@@ -313,7 +313,7 @@ def test_flatten_unicode_linesep(self): - self.assertEqual(s.getvalue(), self.typ(expected)) - - def test_verify_generated_headers(self): -- """gh-121650: by default the generator prevents header injection""" -+ # gh-121650: by default the generator prevents header injection - class LiteralHeader(str): - name = 'Header' - def fold(self, **kwargs): -@@ -334,6 +334,8 @@ def fold(self, **kwargs): - - with self.assertRaises(email.errors.HeaderWriteError): - message.as_string() -+ with self.assertRaises(email.errors.HeaderWriteError): -+ message.as_bytes() - - - class TestBytesGenerator(TestGeneratorBase, TestEmailBase): -diff --git a/Lib/test/test_email/test_policy.py b/Lib/test/test_email/test_policy.py -index baa35fd68e49c5..71ec0febb0fd86 100644 ---- a/Lib/test/test_email/test_policy.py -+++ b/Lib/test/test_email/test_policy.py -@@ -296,7 +296,7 @@ def test_short_maxlen_error(self): - policy.fold("Subject", subject) - - def test_verify_generated_headers(self): -- """Turning protection off allows header injection""" -+ # Turning protection off allows header injection - policy = email.policy.default.clone(verify_generated_headers=False) - for text in ( - 'Header: Value\r\nBad: Injection\r\n', -@@ -319,6 +319,10 @@ def fold(self, **kwargs): - message.as_string(), - f"{text}\nBody", - ) -+ self.assertEqual( -+ message.as_bytes(), -+ f"{text}\nBody".encode(), -+ ) - - # XXX: Need subclassing tests. - # For adding subclassed objects, make sure the usual rules apply (subclass -diff --git a/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst b/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst -new file mode 100644 -index 00000000000000..e6333e724972c5 ---- /dev/null -+++ b/Misc/NEWS.d/next/Security/2026-01-21-12-34-05.gh-issue-144125.TAz5uo.rst -@@ -0,0 +1,4 @@ -+:mod:`~email.generator.BytesGenerator` will now refuse to serialize (write) headers -+that are unsafely folded or delimited; see -+:attr:`~email.policy.Policy.verify_generated_headers`. (Contributed by Bas -+Bloemsaat and Petr Viktorin in :gh:`121650`). diff --git a/00479-cve-2026-1502.patch b/00479-cve-2026-1502.patch new file mode 100644 index 0000000..16dc99b --- /dev/null +++ b/00479-cve-2026-1502.patch @@ -0,0 +1,107 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Seth Larson +Date: Fri, 10 Apr 2026 10:21:42 -0500 +Subject: 00479: CVE-2026-1502 + +Reject CR/LF in HTTP tunnel request headers + +Co-authored-by: Illia Volochii +--- + Lib/http/client.py | 11 ++++- + Lib/test/test_httplib.py | 45 +++++++++++++++++++ + ...-03-20-09-29-42.gh-issue-146211.PQVbs7.rst | 2 + + 3 files changed, 57 insertions(+), 1 deletion(-) + create mode 100644 Misc/NEWS.d/next/Security/2026-03-20-09-29-42.gh-issue-146211.PQVbs7.rst + +diff --git a/Lib/http/client.py b/Lib/http/client.py +index 70451d67d4..7db4807b30 100644 +--- a/Lib/http/client.py ++++ b/Lib/http/client.py +@@ -972,13 +972,22 @@ def _wrap_ipv6(self, ip): + return ip + + def _tunnel(self): ++ if _contains_disallowed_url_pchar_re.search(self._tunnel_host): ++ raise ValueError('Tunnel host can\'t contain control characters %r' ++ % (self._tunnel_host,)) + connect = b"CONNECT %s:%d %s\r\n" % ( + self._wrap_ipv6(self._tunnel_host.encode("idna")), + self._tunnel_port, + self._http_vsn_str.encode("ascii")) + headers = [connect] + for header, value in self._tunnel_headers.items(): +- headers.append(f"{header}: {value}\r\n".encode("latin-1")) ++ header_bytes = header.encode("latin-1") ++ value_bytes = value.encode("latin-1") ++ if not _is_legal_header_name(header_bytes): ++ raise ValueError('Invalid header name %r' % (header_bytes,)) ++ if _is_illegal_header_value(value_bytes): ++ raise ValueError('Invalid header value %r' % (value_bytes,)) ++ headers.append(b"%s: %s\r\n" % (header_bytes, value_bytes)) + headers.append(b"\r\n") + # Making a single send() call instead of one per line encourages + # the host OS to use a more optimal packet size instead of +diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py +index e46dac0077..e027d930d9 100644 +--- a/Lib/test/test_httplib.py ++++ b/Lib/test/test_httplib.py +@@ -369,6 +369,51 @@ def test_invalid_headers(self): + with self.assertRaisesRegex(ValueError, 'Invalid header'): + conn.putheader(name, value) + ++ def test_invalid_tunnel_headers(self): ++ cases = ( ++ ('Invalid\r\nName', 'ValidValue'), ++ ('Invalid\rName', 'ValidValue'), ++ ('Invalid\nName', 'ValidValue'), ++ ('\r\nInvalidName', 'ValidValue'), ++ ('\rInvalidName', 'ValidValue'), ++ ('\nInvalidName', 'ValidValue'), ++ (' InvalidName', 'ValidValue'), ++ ('\tInvalidName', 'ValidValue'), ++ ('Invalid:Name', 'ValidValue'), ++ (':InvalidName', 'ValidValue'), ++ ('ValidName', 'Invalid\r\nValue'), ++ ('ValidName', 'Invalid\rValue'), ++ ('ValidName', 'Invalid\nValue'), ++ ('ValidName', 'InvalidValue\r\n'), ++ ('ValidName', 'InvalidValue\r'), ++ ('ValidName', 'InvalidValue\n'), ++ ) ++ for name, value in cases: ++ with self.subTest((name, value)): ++ conn = client.HTTPConnection('example.com') ++ conn.set_tunnel('tunnel', headers={ ++ name: value ++ }) ++ conn.sock = FakeSocket('') ++ with self.assertRaisesRegex(ValueError, 'Invalid header'): ++ conn._tunnel() # Called in .connect() ++ ++ def test_invalid_tunnel_host(self): ++ cases = ( ++ 'invalid\r.host', ++ '\ninvalid.host', ++ 'invalid.host\r\n', ++ 'invalid.host\x00', ++ 'invalid host', ++ ) ++ for tunnel_host in cases: ++ with self.subTest(tunnel_host): ++ conn = client.HTTPConnection('example.com') ++ conn.set_tunnel(tunnel_host) ++ conn.sock = FakeSocket('') ++ with self.assertRaisesRegex(ValueError, 'Tunnel host can\'t contain control characters'): ++ conn._tunnel() # Called in .connect() ++ + def test_headers_debuglevel(self): + body = ( + b'HTTP/1.1 200 OK\r\n' +diff --git a/Misc/NEWS.d/next/Security/2026-03-20-09-29-42.gh-issue-146211.PQVbs7.rst b/Misc/NEWS.d/next/Security/2026-03-20-09-29-42.gh-issue-146211.PQVbs7.rst +new file mode 100644 +index 0000000000..4993633b8e +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2026-03-20-09-29-42.gh-issue-146211.PQVbs7.rst +@@ -0,0 +1,2 @@ ++Reject CR/LF characters in tunnel request headers for the ++HTTPConnection.set_tunnel() method. diff --git a/00483-cve-2026-2297.patch b/00483-cve-2026-2297.patch new file mode 100644 index 0000000..8b504c9 --- /dev/null +++ b/00483-cve-2026-2297.patch @@ -0,0 +1,33 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Steve Dower +Date: Wed, 4 Mar 2026 19:55:52 +0000 +Subject: 00483: CVE-2026-2297 + +Logging Bypass in Legacy .pyc File Handling +--- + Lib/importlib/_bootstrap_external.py | 2 +- + .../Security/2026-03-04-18-59-17.gh-issue-145506.6hwvEh.rst | 2 ++ + 2 files changed, 3 insertions(+), 1 deletion(-) + create mode 100644 Misc/NEWS.d/next/Security/2026-03-04-18-59-17.gh-issue-145506.6hwvEh.rst + +diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py +index 9b8a8dfc5a..6e4a087a10 100644 +--- a/Lib/importlib/_bootstrap_external.py ++++ b/Lib/importlib/_bootstrap_external.py +@@ -1186,7 +1186,7 @@ def get_filename(self, fullname): + + def get_data(self, path): + """Return the data from path as raw bytes.""" +- if isinstance(self, (SourceLoader, ExtensionFileLoader)): ++ if isinstance(self, (SourceLoader, SourcelessFileLoader, ExtensionFileLoader)): + with _io.open_code(str(path)) as file: + return file.read() + else: +diff --git a/Misc/NEWS.d/next/Security/2026-03-04-18-59-17.gh-issue-145506.6hwvEh.rst b/Misc/NEWS.d/next/Security/2026-03-04-18-59-17.gh-issue-145506.6hwvEh.rst +new file mode 100644 +index 0000000000..dcdb44d4fa +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2026-03-04-18-59-17.gh-issue-145506.6hwvEh.rst +@@ -0,0 +1,2 @@ ++Fixes :cve:`2026-2297` by ensuring that ``SourcelessFileLoader`` uses ++:func:`io.open_code` when opening ``.pyc`` files. diff --git a/00484-cve-2026-3644.patch b/00484-cve-2026-3644.patch new file mode 100644 index 0000000..a1c12bd --- /dev/null +++ b/00484-cve-2026-3644.patch @@ -0,0 +1,146 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> +Date: Mon, 16 Mar 2026 13:43:43 +0000 +Subject: 00484: CVE-2026-3644 + +Incomplete control character validation in http.cookies + +Co-authored-by: Victor Stinner +--- + Lib/http/cookies.py | 24 ++++++++++-- + Lib/test/test_http_cookies.py | 38 +++++++++++++++++++ + ...-03-06-17-03-38.gh-issue-145599.kchwZV.rst | 4 ++ + 3 files changed, 62 insertions(+), 4 deletions(-) + create mode 100644 Misc/NEWS.d/next/Security/2026-03-06-17-03-38.gh-issue-145599.kchwZV.rst + +diff --git a/Lib/http/cookies.py b/Lib/http/cookies.py +index d0a69cbe19..63d119ad46 100644 +--- a/Lib/http/cookies.py ++++ b/Lib/http/cookies.py +@@ -335,9 +335,16 @@ def update(self, values): + key = key.lower() + if key not in self._reserved: + raise CookieError("Invalid attribute %r" % (key,)) ++ if _has_control_character(key, val): ++ raise CookieError("Control characters are not allowed in " ++ f"cookies {key!r} {val!r}") + data[key] = val + dict.update(self, data) + ++ def __ior__(self, values): ++ self.update(values) ++ return self ++ + def isReservedKey(self, K): + return K.lower() in self._reserved + +@@ -363,9 +370,15 @@ def __getstate__(self): + } + + def __setstate__(self, state): +- self._key = state['key'] +- self._value = state['value'] +- self._coded_value = state['coded_value'] ++ key = state['key'] ++ value = state['value'] ++ coded_value = state['coded_value'] ++ if _has_control_character(key, value, coded_value): ++ raise CookieError("Control characters are not allowed in cookies " ++ f"{key!r} {value!r} {coded_value!r}") ++ self._key = key ++ self._value = value ++ self._coded_value = coded_value + + def output(self, attrs=None, header="Set-Cookie:"): + return "%s %s" % (header, self.OutputString(attrs)) +@@ -377,13 +390,16 @@ def __repr__(self): + + def js_output(self, attrs=None): + # Print javascript ++ output_string = self.OutputString(attrs) ++ if _has_control_character(output_string): ++ raise CookieError("Control characters are not allowed in cookies") + return """ + +- """ % (self.OutputString(attrs).replace('"', r'\"')) ++ """ % (output_string.replace('"', r'\"')) + + def OutputString(self, attrs=None): + # Build up our result +diff --git a/Lib/test/test_http_cookies.py b/Lib/test/test_http_cookies.py +index f196bcc48e..2478a6c630 100644 +--- a/Lib/test/test_http_cookies.py ++++ b/Lib/test/test_http_cookies.py +@@ -573,6 +573,14 @@ def test_control_characters(self): + with self.assertRaises(cookies.CookieError): + morsel["path"] = c0 + ++ # .__setstate__() ++ with self.assertRaises(cookies.CookieError): ++ morsel.__setstate__({'key': c0, 'value': 'val', 'coded_value': 'coded'}) ++ with self.assertRaises(cookies.CookieError): ++ morsel.__setstate__({'key': 'key', 'value': c0, 'coded_value': 'coded'}) ++ with self.assertRaises(cookies.CookieError): ++ morsel.__setstate__({'key': 'key', 'value': 'val', 'coded_value': c0}) ++ + # .setdefault() + with self.assertRaises(cookies.CookieError): + morsel.setdefault("path", c0) +@@ -587,6 +595,18 @@ def test_control_characters(self): + with self.assertRaises(cookies.CookieError): + morsel.set("path", "val", c0) + ++ # .update() ++ with self.assertRaises(cookies.CookieError): ++ morsel.update({"path": c0}) ++ with self.assertRaises(cookies.CookieError): ++ morsel.update({c0: "val"}) ++ ++ # .__ior__() ++ with self.assertRaises(cookies.CookieError): ++ morsel |= {"path": c0} ++ with self.assertRaises(cookies.CookieError): ++ morsel |= {c0: "val"} ++ + def test_control_characters_output(self): + # Tests that even if the internals of Morsel are modified + # that a call to .output() has control character safeguards. +@@ -607,6 +627,24 @@ def test_control_characters_output(self): + with self.assertRaises(cookies.CookieError): + cookie.output() + ++ # Tests that .js_output() also has control character safeguards. ++ for c0 in support.control_characters_c0(): ++ morsel = cookies.Morsel() ++ morsel.set("key", "value", "coded-value") ++ morsel._key = c0 # Override private variable. ++ cookie = cookies.SimpleCookie() ++ cookie["cookie"] = morsel ++ with self.assertRaises(cookies.CookieError): ++ cookie.js_output() ++ ++ morsel = cookies.Morsel() ++ morsel.set("key", "value", "coded-value") ++ morsel._coded_value = c0 # Override private variable. ++ cookie = cookies.SimpleCookie() ++ cookie["cookie"] = morsel ++ with self.assertRaises(cookies.CookieError): ++ cookie.js_output() ++ + + def load_tests(loader, tests, pattern): + tests.addTest(doctest.DocTestSuite(cookies)) +diff --git a/Misc/NEWS.d/next/Security/2026-03-06-17-03-38.gh-issue-145599.kchwZV.rst b/Misc/NEWS.d/next/Security/2026-03-06-17-03-38.gh-issue-145599.kchwZV.rst +new file mode 100644 +index 0000000000..e53a932d12 +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2026-03-06-17-03-38.gh-issue-145599.kchwZV.rst +@@ -0,0 +1,4 @@ ++Reject control characters in :class:`http.cookies.Morsel` ++:meth:`~http.cookies.Morsel.update` and ++:meth:`~http.cookies.BaseCookie.js_output`. ++This addresses :cve:`2026-3644`. diff --git a/00485-cve-2026-4224.patch b/00485-cve-2026-4224.patch new file mode 100644 index 0000000..14f8734 --- /dev/null +++ b/00485-cve-2026-4224.patch @@ -0,0 +1,98 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com> +Date: Sun, 15 Mar 2026 21:46:06 +0000 +Subject: 00485: CVE-2026-4224 +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Stack overflow parsing XML with deeply nested DTD content models + +Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> +--- + Lib/test/test_pyexpat.py | 18 ++++++++++++++++++ + ...6-03-14-17-31-39.gh-issue-145986.ifSSr8.rst | 4 ++++ + Modules/pyexpat.c | 9 ++++++++- + 3 files changed, 30 insertions(+), 1 deletion(-) + create mode 100644 Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst + +diff --git a/Lib/test/test_pyexpat.py b/Lib/test/test_pyexpat.py +index 38f951573f..37d9086f40 100644 +--- a/Lib/test/test_pyexpat.py ++++ b/Lib/test/test_pyexpat.py +@@ -675,6 +675,24 @@ def test_change_size_2(self): + parser.Parse(xml2, True) + self.assertEqual(self.n, 4) + ++class ElementDeclHandlerTest(unittest.TestCase): ++ def test_deeply_nested_content_model(self): ++ # This should raise a RecursionError and not crash. ++ # See https://github.com/python/cpython/issues/145986. ++ N = 500_000 ++ data = ( ++ b'\n]>\n\n' ++ ) ++ ++ parser = expat.ParserCreate() ++ parser.ElementDeclHandler = lambda _1, _2: None ++ with support.infinite_recursion(): ++ with self.assertRaises(RecursionError): ++ parser.Parse(data) ++ ++ + class MalformedInputTest(unittest.TestCase): + def test1(self): + xml = b"\0\r\n" +diff --git a/Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst b/Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst +new file mode 100644 +index 0000000000..79536d1fef +--- /dev/null ++++ b/Misc/NEWS.d/next/Security/2026-03-14-17-31-39.gh-issue-145986.ifSSr8.rst +@@ -0,0 +1,4 @@ ++:mod:`xml.parsers.expat`: Fixed a crash caused by unbounded C recursion when ++converting deeply nested XML content models with ++:meth:`~xml.parsers.expat.xmlparser.ElementDeclHandler`. ++This addresses :cve:`2026-4224`. +diff --git a/Modules/pyexpat.c b/Modules/pyexpat.c +index 79492ca5c4..8673540f35 100644 +--- a/Modules/pyexpat.c ++++ b/Modules/pyexpat.c +@@ -3,6 +3,7 @@ + #endif + + #include "Python.h" ++#include "pycore_ceval.h" // _Py_EnterRecursiveCall() + #include "pycore_runtime.h" // _Py_ID() + #include + +@@ -578,6 +579,10 @@ static PyObject * + conv_content_model(XML_Content * const model, + PyObject *(*conv_string)(const XML_Char *)) + { ++ if (_Py_EnterRecursiveCall(" in conv_content_model")) { ++ return NULL; ++ } ++ + PyObject *result = NULL; + PyObject *children = PyTuple_New(model->numchildren); + int i; +@@ -589,7 +594,7 @@ conv_content_model(XML_Content * const model, + conv_string); + if (child == NULL) { + Py_XDECREF(children); +- return NULL; ++ goto done; + } + PyTuple_SET_ITEM(children, i, child); + } +@@ -597,6 +602,8 @@ conv_content_model(XML_Content * const model, + model->type, model->quant, + conv_string,model->name, children); + } ++done: ++ _Py_LeaveRecursiveCall(); + return result; + } + diff --git a/Python-3.12.12.tar.xz.asc b/Python-3.12.12.tar.xz.asc deleted file mode 100644 index 462b73e..0000000 --- a/Python-3.12.12.tar.xz.asc +++ /dev/null @@ -1,18 +0,0 @@ ------BEGIN PGP SIGNATURE----- - -iQKTBAABCgB9FiEEcWlgX2LHUTVtBUomqCHmgOX6YwUFAmjnnr1fFIAAAAAALgAo -aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDcx -Njk2MDVGNjJDNzUxMzU2RDA1NEEyNkE4MjFFNjgwRTVGQTYzMDUACgkQqCHmgOX6 -YwXF3Q//VrreGa+P8lvp9UMjoj/YquKPwLqjzzAWf5vzHipkebdiESsB1HfGu04k -Jw+ctTnXHf/12u0W7ijv+56JtcJFqEzh8yGokWqOzc99rpCeCY9qtuwaVYtZrTNx -wepRaDAHdhP4Z2kLPDiE6pCXu2NIR5wHqHjQ8JGmprhASc07uxEhNN/gucVR2Sbr -cCfC9rHfHkdhoPpZRRbcraAaxPGL3VyBXf7HuYbHhf4GuF9EVDlFg5I0BzHCKJDd -ebPXYHvsoDgrMMqPXiX/YkGNByf3Ze6KZTNSGICy8SDzIzZgpmtOe5rzvlOXJBZZ -SVfX8SqP4Ufml+MfJrGEx30S9reYYvnyTSmttpbDznonROKPEZOuDt08+CG3yR+T -o5RdIneWmGXRf1mBrFKH9Br5tfOd+YeldfxdoQgla2fFHFVRnab1lsZFOC/HZ5z2 -Q3rPfVMDYKO8yoIKqv0BUzlkn9wYphCWoPHq0Y+SGjcP+Zh5qRTMqZYIaGekhWmx -86egHHVqedMI0Q9hvgIEirupVJ1q34FZn2+3sEka9hdOie9aNHXWTmgWCGDm46qj -qC9tT/jkMzWIY2Y4RdVDMdSCb7HkBEl1eAANq511gJ+eSWAXbP1sVrQoiAQY+EkC -Yu2ceZYsl9i6zm7i/QaU/mOGB7xMZhMQLZBnZTHSzAZo/pBN7y8= -=RuLK ------END PGP SIGNATURE----- diff --git a/Python-3.12.13.tar.xz.asc b/Python-3.12.13.tar.xz.asc new file mode 100644 index 0000000..31701e1 --- /dev/null +++ b/Python-3.12.13.tar.xz.asc @@ -0,0 +1,18 @@ +-----BEGIN PGP SIGNATURE----- + +iQKTBAABCgB9FiEEcWlgX2LHUTVtBUomqCHmgOX6YwUFAmmm3hlfFIAAAAAALgAo +aXNzdWVyLWZwckBub3RhdGlvbnMub3BlbnBncC5maWZ0aGhvcnNlbWFuLm5ldDcx +Njk2MDVGNjJDNzUxMzU2RDA1NEEyNkE4MjFFNjgwRTVGQTYzMDUACgkQqCHmgOX6 +YwW/NA/+MsD99kuy8nBgrK65H3nVj104QcYaau9qhQ62x5+d7P04sVs8bf0mo/AM +zec7eCTHnwoLVborEiqyd5NPRUFNJRfsIn0g4z0PIWRBaXsPztMT4suQdNw+VOTd +3+up8Um0cA2tnesiXZvT5mXY/2kjffcosP/D4OjdADEGYcUawgNeY4LQi9sTOzs6 +ubI+mrtSqlAzgKM5NeVj2LdXPeLhpFz4TPk5GTlp0N9p5I1/7ZjK6eLw9p1gPZNN +xzN84JL191a9DybGJXIEJ6AcN5TxuBwXHV/KlnEK57AGj1WS5VOhKbHGoSwpRU5m +BAtCfJj0zBVqME9W/rROR5mxhta/kG71594STPhXKppyQaH+f41lseNf32So0O+w +PdCVebFJgNnCM8Sl+NMrfqiVKW17bAwk4POtbSVqALq3Y2mseDUr0ggU/ZNgeKHy +bDxPBSWAAI3kE75yZlXrAJYYiEWgc5GMZJQgFqOJfw1nDmfZL626ckyDdsJotiJO +6pZKmFqLlTPkXzw1t45z8/ZI8p/oWrMnTS/IGMcTobXcWHEBvbn9Ljt6nBLrZgO6 +67vTGss5KhjvfyKwNyQGkG+KIoUJLoewFJ6l5okZB5JIALYn1f/P3bl/kJPUDNp4 +eP2ao9kSYBBOwutUe1lH8tQYrNGA0C80hPykvbWDHcvF13J5k2E= +=KNz3 +-----END PGP SIGNATURE----- diff --git a/expat-requires.py b/expat-requires.py new file mode 100755 index 0000000..fe23fa9 --- /dev/null +++ b/expat-requires.py @@ -0,0 +1,50 @@ +import pathlib +import pyexpat +import sys + + +# This will determine the version of currently installed expat +EXPAT_VERSION = pyexpat.EXPAT_VERSION.removeprefix('expat_') +MAJOR, MINOR, PATCH = (int(i) for i in EXPAT_VERSION.split('.')) +EXPAT_COMBINED_VERSION = 10000*MAJOR + 100*MINOR + PATCH + +# For the listed files, we find all XML_COMBINED_VERSION-based #ifs +SRC = pathlib.Path.cwd() +SOURCES = [ + SRC / 'Modules/pyexpat.c', + SRC / 'Modules/clinic/pyexpat.c.h', +] +versions = set() +for source in SOURCES: + for line in source.read_text().splitlines(): + if 'XML_COMBINED_VERSION' not in line: + continue + words = line.split() + if words[0] == '#define': + continue + if len(words) != 4: + continue + if words[0] not in ('#if', '#elif'): + continue + if words[1].startswith('(') and words[-1].endswith(')'): + words[1] = words[1][1:] + words[-1] = words[-1][:-1] + if words[1] == 'XML_COMBINED_VERSION': + version = int(words[3]) + if words[2] == '>': + versions.add(version+1) + continue + if words[2] == '>=': + versions.add(version) + continue + raise ValueError( + 'Unknown line with XML_COMBINED_VERSION, adjust this script:\n\n' + f'{line}' + ) + +# We need the highest satisfiable version used in the #ifs, in x.y.z notation +v = max({v for v in versions if v <= EXPAT_COMBINED_VERSION}) +major, minor_patch = divmod(v, 10000) +minor, patch = divmod(minor_patch, 100) +print(f"{major}.{minor}.{patch}") + diff --git a/python3.12.spec b/python3.12.spec index 332c6ab..7f3fda2 100644 --- a/python3.12.spec +++ b/python3.12.spec @@ -13,11 +13,11 @@ URL: https://www.python.org/ # WARNING When rebasing to a new Python version, # remember to update the python3-docs package as well -%global general_version %{pybasever}.12 +%global general_version %{pybasever}.13 #global prerel ... %global upstream_version %{general_version}%{?prerel} Version: %{general_version}%{?prerel:~%{prerel}} -Release: 3%{?dist}.3 +Release: 2%{?dist} License: Python-2.0.1 @@ -347,6 +347,9 @@ Source2: https://github.com/Yhg1s.gpg # Originally written by bkabrda Source8: check-pyc-timestamps.py +# A script that determines the required expat version +Source9: expat-requires.py + # Desktop menu entry for idle3 Source10: idle3.desktop @@ -434,36 +437,6 @@ Patch462: 00462-fix-pyssl_seterror-handling-ssl_error_syscall.patch # will not show the Python functions, irrespective of this patch. Patch464: 00464-enable-pac-and-bti-protections-for-aarch64.patch -# 00471 # 37c05f26d11e8e24f2a760167015a267996b1d69 -# CVE-2025-12084 -# -# * gh-142145: Remove quadratic behavior in node ID cache clearing (GH-142146) -# * gh-142754: Ensure that Element & Attr instances have the ownerDocument attribute (GH-142794) -Patch471: 00471-cve-2025-12084.patch - -# 00472 # 2ba215eaba508b2cdd7c3acfdf3b9a6e32872274 -# CVE-2025-13836 -# -# [3.12] gh-119451: Fix a potential denial of service in http.client (GH-119454) (#142140) -# -# gh-119451: Fix a potential denial of service in http.client (GH-119454) -# -# Reading the whole body of the HTTP response could cause OOM if -# the Content-Length value is too large even if the server does not send -# a large amount of data. Now the HTTP client reads large data by chunks, -# therefore the amount of consumed memory is proportional to the amount -# of sent data. -Patch472: 00472-cve-2025-13836.patch - -# 00473 # dd705786aa0c1ccfde913858598e34e1f196be2e -# CVE-2026-0865 -# -# gh-143916: Reject control characters in wsgiref.headers.Headers (GH-143917) -# -# * Add 'test.support' fixture for C0 control characters -# * gh-143916: Reject control characters in wsgiref.headers.Headers -Patch473: 00473-cve-2026-0865.patch - # 00474 # 837ddca0372fa87ff9cee47142200caa21e77def # CVE-2025-15366 # @@ -480,18 +453,18 @@ Patch474: 00474-cve-2025-15366.patch # (cherry-picked from commit b234a2b67539f787e191d2ef19a7cbdce32874e7) Patch475: 00475-cve-2025-15367.patch -# 00476 -# CVE-2026-1299 -# -# gh-144125: email: verify headers are sound in BytesGenerator -Patch476: 00476-cve-2026-1299.patch - # 00478 # eb93352dc8e31f4d52546b84daad875e6ff7f29e # CVE-2026-4519 # # Reject leading dashes in webbrowser URLs (GH-146360) Patch478: 00478-cve-2026-4519.patch +# 00479 # 97404b2cf62e545c2d41be7ccfed4e74da9ee665 +# CVE-2026-1502 +# +# Reject CR/LF in HTTP tunnel request headers +Patch479: 00479-cve-2026-1502.patch + # 00480 # 6f4eef3ba4d9818a53698e994550ee8db17a1e2e # CVE-2026-4786 # @@ -504,6 +477,24 @@ Patch480: 00480-cve-2026-4786.patch # Fix a possible UAF in {LZMA,BZ2,_Zlib}Decompressor Patch482: 00482-cve-2026-6100.patch +# 00483 # 577c595137ce6ff92158ddaf2d7b7ea86437825d +# CVE-2026-2297 +# +# Logging Bypass in Legacy .pyc File Handling +Patch483: 00483-cve-2026-2297.patch + +# 00484 # 8b5133c1ab17a060cd134bea2a4b6e1831c47fed +# CVE-2026-3644 +# +# Incomplete control character validation in http.cookies +Patch484: 00484-cve-2026-3644.patch + +# 00485 # 12a5b206676927bcee131ab4f2bd6783d2f5914a +# CVE-2026-4224 +# +# Stack overflow parsing XML with deeply nested DTD content models +Patch485: 00485-cve-2026-4224.patch + # (New patches go here ^^^) # # When adding new patches to "python" and "python3" in Fedora, EL, etc., @@ -662,12 +653,13 @@ Recommends: (%{pkgname}-tkinter%{?_isa} = %{version}-%{release} if tk%{?_isa}) Requires: tzdata # The requirement on libexpat is generated, but we need to version it. -# When built with expat >= 2.6, but installed with older expat, we get: +# When built with newer expat, but installed with older expat, we get: # ImportError: /usr/lib64/python3.X/lib-dynload/pyexpat.cpython-....so: # undefined symbol: XML_SetReparseDeferralEnabled # This breaks many things, including python -m venv. # Other subpackages (like -debug) also need this, but they all depend on -libs. -Requires: expat >= 2.6 +%global expat_min_version 2.7.2 +Requires: expat%{_isa} >= %{expat_min_version} %description -n %{pkgname}-libs This package contains runtime libraries for use by Python: @@ -1312,6 +1304,11 @@ for Module in %{buildroot}/%{dynload_dir}/*.so ; do esac done +# Check the expat compatibility +expat_found=$(LD_LIBRARY_PATH="%{buildroot}%{_libdir}" PYTHONPATH="%{buildroot}%{pylibdir}" %{buildroot}%{_bindir}/python%{pybasever} %{SOURCE9}) +if [ "${expat_found}" != "%{expat_min_version}" ]; then + echo "Found expat version is different than the declared one, found: ${expat_found}" ; exit 1 +fi # ====================================================== # Running the upstream test suite @@ -1869,17 +1866,29 @@ CheckPython optimized # ====================================================== %changelog -* Thu Apr 16 2026 Charalampos Stratakis - 3.12.12-3.3 -- Security fixes for CVE-2026-4786, CVE-2026-6100 -Resolves: RHEL-167885, RHEL-168119 +* Thu Apr 16 2026 Charalampos Stratakis - 3.12.13-2 +- Security fixes for CVE-2026-1502, CVE-2026-4786, CVE-2026-6100, CVE-2026-2297, CVE-2026-3644, CVE-2026-4224 +Resolves: RHEL-167886, RHEL-168120 -* Fri Mar 27 2026 Tomáš Hrnčiar - 3.12.12-3.2 +* Thu Apr 16 2026 Tomáš Hrnčiar - 3.12.13-1 +- Update to 3.12.13 +- Security fixes for CVE-2025-6075, CVE-2025-13837, CVE-2025-15282, CVE-2025-59375, CVE-2026-0672 +- Require expat >= 2.7.2 to prevent symbol lookup errors at runtime with older expat +Related: RHEL-167886, RHEL-168120 + +* Fri Mar 27 2026 Tomáš Hrnčiar - 3.12.12-6 - Security fix for CVE-2026-4519 -Resolves: RHEL-158127 +Resolves: RHEL-158079 -* Fri Feb 27 2026 Tomáš Hrnčiar - 3.12.12-3.1 +* Mon Mar 09 2026 Tomáš Hrnčiar - 3.12.12-5 +- Rebuilding previous fixes for different build target +Related: RHEL-143057, RHEL-143109, RHEL-144854 + +* Fri Feb 27 2026 Tomáš Hrnčiar - 3.12.12-4 - Security fixes for CVE-2026-0865, CVE-2025-15366, CVE-2025-15367 and CVE-2026-1299 -Resolves: RHEL-143054 RHEL-143105 RHEL-144852 +Resolves: RHEL-143057 +Resolves: RHEL-143109 +Resolves: RHEL-144854 * Fri Jan 16 2026 Lumír Balhar - 3.12.12-3 - Security fix for CVE-2025-13836 diff --git a/sources b/sources index 1ceb20e..5e92403 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -SHA512 (Python-3.12.12.tar.xz) = 4b99d240dd96a6e154909dcffe87f8bb38193d634cd80a1c3d9e819b7a63af2afa46d5e6423e81f00dd388840dc29a4a71580f6aa1ce9a12e559c1d63f65a205 +SHA512 (Python-3.12.13.tar.xz) = e1eb66f0b34581f0155e3ce25ba72cf0b4b1107672ed0ad3e86bcfe616945c9204c41ffc492f32b1066b9154913ff88343038967ad8711dd05e6f2332fdb735b