Index | Thread | Search

From:
Stuart Henderson <stu@spacehopper.org>
Subject:
python 3.13.12 update + CVE patches
To:
Kurt Mosiejczuk <kurt@cranky.work>, Theo Buehler <tb@openbsd.org>, ports <ports@openbsd.org>
Date:
Mon, 16 Mar 2026 23:23:40 +0000

Download raw body.

Thread
This updates to February's release, and cherry-picks two recent CVE
fixes (med/high). I expect another release will be on the way at some
point but seems worth bringing them in already.

Diff to follow for 7.8-stable.

Index: Makefile
===================================================================
RCS file: /cvs/ports/lang/python/3/Makefile,v
diff -u -p -r1.22 Makefile
--- Makefile	31 Jan 2026 18:24:22 -0000	1.22
+++ Makefile	16 Mar 2026 23:08:45 -0000
@@ -3,14 +3,10 @@
 # requirement of the PSF license, if it constitutes a change to
 # Python itself.
 
-FULL_VERSION =		3.13.11
+FULL_VERSION =		3.13.12
 SHARED_LIBS =		python3.13 0.0
 VERSION_SPEC =		>=3.13,<3.14
 PORTROACH =		limit:^3\.12
-
-# XXX keep REVISION-main above 7.8-stable and do not sync the
-# XXX @conflict or @pkgpath for glade/py-bsddb3
-REVISION-main =		2
 
 AUTOCONF_VERSION =	2.71
 
Index: distinfo
===================================================================
RCS file: /cvs/ports/lang/python/3/distinfo,v
diff -u -p -r1.9 distinfo
--- distinfo	12 Dec 2025 02:44:50 -0000	1.9
+++ distinfo	16 Mar 2026 23:08:45 -0000
@@ -1,2 +1,2 @@
-SHA256 (Python-3.13.11.tgz) = A8/tvgbOIbxEzgkkXgkad/L+6eyb5cUgaQSKGBMAsgI=
-SIZE (Python-3.13.11.tgz) = 29362906
+SHA256 (Python-3.13.12.tgz) = EufLFwrS0aaa7pahzH/I3lsel6K9rFFoOj2wFuyaKZY=
+SIZE (Python-3.13.12.tgz) = 29803459
Index: files/CHANGES.OpenBSD
===================================================================
RCS file: /cvs/ports/lang/python/3/files/CHANGES.OpenBSD,v
diff -u -p -r1.4 CHANGES.OpenBSD
--- files/CHANGES.OpenBSD	12 Dec 2025 02:44:50 -0000	1.4
+++ files/CHANGES.OpenBSD	16 Mar 2026 23:08:45 -0000
@@ -24,5 +24,7 @@ which results in loading an incorrect ve
 
 8.  Work around expat_config.h missing from base.
 
+9.  Cherry-pick fixes for CVE-2026-3644, CVE-2026-4224
+
 These changes are available in the OpenBSD CVS repository
 <http://www.openbsd.org/anoncvs.html> in ports/lang/python/3.
Index: patches/patch-Lib_http_cookies_py
===================================================================
RCS file: patches/patch-Lib_http_cookies_py
diff -N patches/patch-Lib_http_cookies_py
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ patches/patch-Lib_http_cookies_py	16 Mar 2026 23:08:45 -0000
@@ -0,0 +1,71 @@
+https://mail.python.org/archives/list/security-announce@python.org/thread/H6CADMBCDRFGWCMOXWUIHFJNV43GABJ7/
+
+From d16ecc6c3626f0e2cc8f08c309c83934e8a979dd Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <31488909+miss-islington@users.noreply.github.com>
+Date: Mon, 16 Mar 2026 15:05:13 +0100
+Subject: [PATCH] [3.13] gh-145599, CVE 2026-3644: Reject control characters in
+ `http.cookies.Morsel.update()` (GH-145600) (#146024)
+
+gh-145599, CVE 2026-3644: Reject control characters in `http.cookies.Morsel.update()` (GH-145600)
+
+Reject control characters in `http.cookies.Morsel.update()` and `http.cookies.BaseCookie.js_output`.
+(cherry picked from commit 57e88c1cf95e1481b94ae57abe1010469d47a6b4)
+
+Index: Lib/http/cookies.py
+--- Lib/http/cookies.py.orig
++++ Lib/http/cookies.py
+@@ -335,9 +335,16 @@ class Morsel(dict):
+             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 @@ class Morsel(dict):
+         }
+ 
+     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 @@ class Morsel(dict):
+ 
+     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 """
+         <script type="text/javascript">
+         <!-- begin hiding
+         document.cookie = \"%s\";
+         // end hiding -->
+         </script>
+-        """ % (self.OutputString(attrs).replace('"', r'\"'))
++        """ % (output_string.replace('"', r'\"'))
+ 
+     def OutputString(self, attrs=None):
+         # Build up our result
Index: patches/patch-Lib_test_test_http_cookies_py
===================================================================
RCS file: patches/patch-Lib_test_test_http_cookies_py
diff -N patches/patch-Lib_test_test_http_cookies_py
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ patches/patch-Lib_test_test_http_cookies_py	16 Mar 2026 23:08:45 -0000
@@ -0,0 +1,79 @@
+https://mail.python.org/archives/list/security-announce@python.org/thread/H6CADMBCDRFGWCMOXWUIHFJNV43GABJ7/
+
+From d16ecc6c3626f0e2cc8f08c309c83934e8a979dd Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <31488909+miss-islington@users.noreply.github.com>
+Date: Mon, 16 Mar 2026 15:05:13 +0100
+Subject: [PATCH] [3.13] gh-145599, CVE 2026-3644: Reject control
+characters in
+ `http.cookies.Morsel.update()` (GH-145600) (#146024)
+
+gh-145599, CVE 2026-3644: Reject control characters in
+`http.cookies.Morsel.update()` (GH-145600)
+
+Reject control characters in `http.cookies.Morsel.update()` and
+`http.cookies.BaseCookie.js_output`.
+(cherry picked from commit 57e88c1cf95e1481b94ae57abe1010469d47a6b4)
+
+Index: Lib/test/test_http_cookies.py
+--- Lib/test/test_http_cookies.py.orig
++++ Lib/test/test_http_cookies.py
+@@ -574,6 +574,14 @@ class MorselTests(unittest.TestCase):
+             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)
+@@ -588,6 +596,18 @@ class MorselTests(unittest.TestCase):
+             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 @@ class MorselTests(unittest.TestCase):
+             cookie["cookie"] = morsel
+             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):
Index: patches/patch-Lib_test_test_pyexpat_py
===================================================================
RCS file: patches/patch-Lib_test_test_pyexpat_py
diff -N patches/patch-Lib_test_test_pyexpat_py
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ patches/patch-Lib_test_test_pyexpat_py	16 Mar 2026 23:08:45 -0000
@@ -0,0 +1,47 @@
+https://mail.python.org/archives/list/security-announce@python.org/thread/5M7CGUW3XBRY7II4DK43KF7NQQ3TPZ6R/
+
+From 196edfb06a7458377d4d0f4b3cd41724c1f3bd4a Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <31488909+miss-islington@users.noreply.github.com>
+Date: Mon, 16 Mar 2026 10:09:27 +0100
+Subject: [PATCH] [3.13] gh-145986: Avoid unbound C recursion in
+ `conv_content_model` in `pyexpat.c` (CVE 2026-4224) (GH-145987) (#145996)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+* gh-145986: Avoid unbound C recursion in `conv_content_model` in `pyexpat.c` (CVE 2026-4224) (GH-145987)
+
+Fix C stack overflow (CVE-2026-4224) when an Expat parser
+with a registered `ElementDeclHandler` parses inline DTD
+containing deeply nested content model.
+
+---------
+(cherry picked from commit eb0e8be3a7e11b87d198a2c3af1ed0eccf532768)
+
+Index: Lib/test/test_pyexpat.py
+--- Lib/test/test_pyexpat.py.orig
++++ Lib/test/test_pyexpat.py
+@@ -688,6 +688,22 @@ class ElementDeclHandlerTest(unittest.TestCase):
+         parser.ElementDeclHandler = lambda _1, _2: None
+         self.assertRaises(TypeError, parser.Parse, data, True)
+ 
++    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'<!DOCTYPE root [\n<!ELEMENT root '
++            + b'(a, ' * N + b'a' + b')' * N
++            + b'>\n]>\n<root/>\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"
Index: patches/patch-Modules_pyexpat_c
===================================================================
RCS file: /cvs/ports/lang/python/3/patches/patch-Modules_pyexpat_c,v
diff -u -p -r1.1 patch-Modules_pyexpat_c
--- patches/patch-Modules_pyexpat_c	12 Dec 2025 02:44:50 -0000	1.1
+++ patches/patch-Modules_pyexpat_c	16 Mar 2026 23:08:45 -0000
@@ -1,11 +1,75 @@
+- one hunk removes #include "expat_config.h"; this (or an alternative,
+see textproc/xmlwf for another approach) needs to be kept unless expat
+in base starts providing this config header
+
+- other hunks are:
+
+https://mail.python.org/archives/list/security-announce@python.org/thread/5M7CGUW3XBRY7II4DK43KF7NQQ3TPZ6R/
+
+From 196edfb06a7458377d4d0f4b3cd41724c1f3bd4a Mon Sep 17 00:00:00 2001
+From: "Miss Islington (bot)"
+ <31488909+miss-islington@users.noreply.github.com>
+Date: Mon, 16 Mar 2026 10:09:27 +0100
+Subject: [PATCH] [3.13] gh-145986: Avoid unbound C recursion in
+ `conv_content_model` in `pyexpat.c` (CVE 2026-4224) (GH-145987) (#145996)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+* gh-145986: Avoid unbound C recursion in `conv_content_model` in `pyexpat.c` (CVE 2026-4224) (GH-145987)
+
+Fix C stack overflow (CVE-2026-4224) when an Expat parser
+with a registered `ElementDeclHandler` parses inline DTD
+containing deeply nested content model.
+
+---------
+(cherry picked from commit eb0e8be3a7e11b87d198a2c3af1ed0eccf532768)
+
 Index: Modules/pyexpat.c
 --- Modules/pyexpat.c.orig
 +++ Modules/pyexpat.c
-@@ -10,7 +10,6 @@
+@@ -3,6 +3,7 @@
+ #endif
+ 
+ #include "Python.h"
++#include "pycore_ceval.h"         // _Py_EnterRecursiveCall()
+ #include "pycore_import.h"        // _PyImport_SetModule()
+ #include "pycore_pyhash.h"        // _Py_HashSecret
+ #include "pycore_traceback.h"     // _PyTraceback_Add()
+@@ -10,7 +11,6 @@
  #include <stdbool.h>
  #include <stddef.h>               // offsetof()
  
 -#include "expat_config.h"
  #include "expat.h"
  #include "pyexpat.h"
+ 
+@@ -572,6 +572,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;
+@@ -583,7 +587,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);
+         }
+@@ -591,6 +595,8 @@ conv_content_model(XML_Content * const model,
+                                model->type, model->quant,
+                                conv_string,model->name, children);
+     }
++done:
++    _Py_LeaveRecursiveCall();
+     return result;
+ }
  
Index: pkg/PLIST-main
===================================================================
RCS file: /cvs/ports/lang/python/3/pkg/PLIST-main,v
diff -u -p -r1.15 PLIST-main
--- pkg/PLIST-main	30 Jan 2026 13:30:34 -0000	1.15
+++ pkg/PLIST-main	16 Mar 2026 23:08:45 -0000
@@ -2233,10 +2233,14 @@ lib/python3.13/pydoc_data/__pycache__/
 lib/python3.13/pydoc_data/__pycache__/__init__.cpython-313.opt-1.pyc
 lib/python3.13/pydoc_data/__pycache__/__init__.cpython-313.opt-2.pyc
 lib/python3.13/pydoc_data/__pycache__/__init__.cpython-313.pyc
+lib/python3.13/pydoc_data/__pycache__/module_docs.cpython-313.opt-1.pyc
+lib/python3.13/pydoc_data/__pycache__/module_docs.cpython-313.opt-2.pyc
+lib/python3.13/pydoc_data/__pycache__/module_docs.cpython-313.pyc
 lib/python3.13/pydoc_data/__pycache__/topics.cpython-313.opt-1.pyc
 lib/python3.13/pydoc_data/__pycache__/topics.cpython-313.opt-2.pyc
 lib/python3.13/pydoc_data/__pycache__/topics.cpython-313.pyc
 lib/python3.13/pydoc_data/_pydoc.css
+lib/python3.13/pydoc_data/module_docs.py
 lib/python3.13/pydoc_data/topics.py
 lib/python3.13/queue.py
 lib/python3.13/quopri.py
Index: pkg/PLIST-tests
===================================================================
RCS file: /cvs/ports/lang/python/3/pkg/PLIST-tests,v
diff -u -p -r1.10 PLIST-tests
--- pkg/PLIST-tests	14 Nov 2025 17:46:17 -0000	1.10
+++ pkg/PLIST-tests	16 Mar 2026 23:08:45 -0000
@@ -6,6 +6,7 @@
 @pkgpath lang/python/3.10,-tests
 @pkgpath lang/python/3.11,-tests
 lib/python3.13/test/
+lib/python3.13/test/NormalizationTest-3.2.0.txt
 lib/python3.13/test/__init__.py
 lib/python3.13/test/__main__.py
 lib/python3.13/test/__pycache__/
@@ -93,9 +94,15 @@ lib/python3.13/test/__pycache__/mp_prelo
 lib/python3.13/test/__pycache__/mp_preload_main.cpython-313.opt-1.pyc
 lib/python3.13/test/__pycache__/mp_preload_main.cpython-313.opt-2.pyc
 lib/python3.13/test/__pycache__/mp_preload_main.cpython-313.pyc
+lib/python3.13/test/__pycache__/mp_preload_sysargv.cpython-313.opt-1.pyc
+lib/python3.13/test/__pycache__/mp_preload_sysargv.cpython-313.opt-2.pyc
+lib/python3.13/test/__pycache__/mp_preload_sysargv.cpython-313.pyc
 lib/python3.13/test/__pycache__/multibytecodec_support.cpython-313.opt-1.pyc
 lib/python3.13/test/__pycache__/multibytecodec_support.cpython-313.opt-2.pyc
 lib/python3.13/test/__pycache__/multibytecodec_support.cpython-313.pyc
+lib/python3.13/test/__pycache__/picklecommon.cpython-313.opt-1.pyc
+lib/python3.13/test/__pycache__/picklecommon.cpython-313.opt-2.pyc
+lib/python3.13/test/__pycache__/picklecommon.cpython-313.pyc
 lib/python3.13/test/__pycache__/pickletester.cpython-313.opt-1.pyc
 lib/python3.13/test/__pycache__/pickletester.cpython-313.opt-2.pyc
 lib/python3.13/test/__pycache__/pickletester.cpython-313.pyc
@@ -1284,6 +1291,9 @@ lib/python3.13/test/__pycache__/test_xml
 lib/python3.13/test/__pycache__/test_xmlrpc.cpython-313.opt-1.pyc
 lib/python3.13/test/__pycache__/test_xmlrpc.cpython-313.opt-2.pyc
 lib/python3.13/test/__pycache__/test_xmlrpc.cpython-313.pyc
+lib/python3.13/test/__pycache__/test_xpickle.cpython-313.opt-1.pyc
+lib/python3.13/test/__pycache__/test_xpickle.cpython-313.opt-2.pyc
+lib/python3.13/test/__pycache__/test_xpickle.cpython-313.pyc
 lib/python3.13/test/__pycache__/test_xxlimited.cpython-313.opt-1.pyc
 lib/python3.13/test/__pycache__/test_xxlimited.cpython-313.opt-2.pyc
 lib/python3.13/test/__pycache__/test_xxlimited.cpython-313.pyc
@@ -1320,6 +1330,9 @@ lib/python3.13/test/__pycache__/win_cons
 lib/python3.13/test/__pycache__/xmltests.cpython-313.opt-1.pyc
 lib/python3.13/test/__pycache__/xmltests.cpython-313.opt-2.pyc
 lib/python3.13/test/__pycache__/xmltests.cpython-313.pyc
+lib/python3.13/test/__pycache__/xpickle_worker.cpython-313.opt-1.pyc
+lib/python3.13/test/__pycache__/xpickle_worker.cpython-313.opt-2.pyc
+lib/python3.13/test/__pycache__/xpickle_worker.cpython-313.pyc
 lib/python3.13/test/_test_atexit.py
 lib/python3.13/test/_test_eintr.py
 lib/python3.13/test/_test_embed_set_config.py
@@ -1773,7 +1786,9 @@ lib/python3.13/test/mp_fork_bomb.py
 lib/python3.13/test/mp_preload.py
 lib/python3.13/test/mp_preload_flush.py
 lib/python3.13/test/mp_preload_main.py
+lib/python3.13/test/mp_preload_sysargv.py
 lib/python3.13/test/multibytecodec_support.py
+lib/python3.13/test/picklecommon.py
 lib/python3.13/test/pickletester.py
 lib/python3.13/test/profilee.py
 lib/python3.13/test/pstats.pck
@@ -4694,6 +4709,7 @@ lib/python3.13/test/test_xml_dom_xmlbuil
 lib/python3.13/test/test_xml_etree.py
 lib/python3.13/test/test_xml_etree_c.py
 lib/python3.13/test/test_xmlrpc.py
+lib/python3.13/test/test_xpickle.py
 lib/python3.13/test/test_xxlimited.py
 lib/python3.13/test/test_xxtestfuzz.py
 lib/python3.13/test/test_yield_from.py
@@ -4944,6 +4960,7 @@ lib/python3.13/test/xmltestdata/simple.x
 lib/python3.13/test/xmltestdata/test.xml
 lib/python3.13/test/xmltestdata/test.xml.out
 lib/python3.13/test/xmltests.py
+lib/python3.13/test/xpickle_worker.py
 lib/python3.13/test/zipimport_data/
 lib/python3.13/test/zipimport_data/sparse-zip64-c0-0x000000000.part
 lib/python3.13/test/zipimport_data/sparse-zip64-c0-0x100000000.part