Download raw body.
python 3.13.12 update + CVE patches
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
python 3.13.12 update + CVE patches