From: Stuart Henderson Subject: python 3.13.12 update + CVE patches To: Kurt Mosiejczuk , Theo Buehler , ports Date: Mon, 16 Mar 2026 23:23:40 +0000 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 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 """ + +- """ % (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'\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" 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 #include // 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