From a445c16d0b6acd5408ca3ca1bbb5d4b92e54f160 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 2 Mar 2026 02:27:01 +0900 Subject: [PATCH 1/3] gh-144475: Fix use-after-free in functools.partial.__repr__() Hold strong references to pto->args, pto->kw, and pto->fn during partial_repr() to prevent them from being freed by a user-defined __repr__() that mutates the partial object via __setstate__(). Previously, partial_repr() iterated over pto->args using a size 'n' captured before the loop, and accessed tuple items via borrowed references. If a __repr__() called during formatting invoked pto.__setstate__() with a new (smaller) args tuple, the original tuple could be freed while the loop was still iterating, leading to a heap-buffer-overflow (out-of-bounds read). The fix takes a new reference (Py_NewRef) to the args tuple, kw dict, and fn callable before using them, ensuring they stay alive regardless of any mutations to the partial object during formatting. --- Modules/_functoolsmodule.c | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/Modules/_functoolsmodule.c b/Modules/_functoolsmodule.c index 5773083ff68b46..ab79453e3fa906 100644 --- a/Modules/_functoolsmodule.c +++ b/Modules/_functoolsmodule.c @@ -705,26 +705,39 @@ partial_repr(PyObject *self) arglist = Py_GetConstant(Py_CONSTANT_EMPTY_STR); if (arglist == NULL) goto done; - /* Pack positional arguments */ + /* Pack positional arguments. + * Hold a strong reference to pto->args across the loop, because + * a user-defined __repr__ called via %R could mutate 'pto' (e.g. + * via __setstate__), freeing the original args tuple while we're + * still iterating over it. See gh-144475. */ assert(PyTuple_Check(pto->args)); - n = PyTuple_GET_SIZE(pto->args); + PyObject *args = Py_NewRef(pto->args); + n = PyTuple_GET_SIZE(args); for (i = 0; i < n; i++) { Py_SETREF(arglist, PyUnicode_FromFormat("%U, %R", arglist, - PyTuple_GET_ITEM(pto->args, i))); - if (arglist == NULL) + PyTuple_GET_ITEM(args, i))); + if (arglist == NULL) { + Py_DECREF(args); goto done; + } } - /* Pack keyword arguments */ + Py_DECREF(args); + /* Pack keyword arguments. + * Similarly, hold a strong reference to pto->kw. See gh-144475. */ assert (PyDict_Check(pto->kw)); - for (i = 0; PyDict_Next(pto->kw, &i, &key, &value);) { + PyObject *kw = Py_NewRef(pto->kw); + for (i = 0; PyDict_Next(kw, &i, &key, &value);) { /* Prevent key.__str__ from deleting the value. */ Py_INCREF(value); Py_SETREF(arglist, PyUnicode_FromFormat("%U, %S=%R", arglist, key, value)); Py_DECREF(value); - if (arglist == NULL) + if (arglist == NULL) { + Py_DECREF(kw); goto done; + } } + Py_DECREF(kw); mod = PyType_GetModuleName(Py_TYPE(pto)); if (mod == NULL) { @@ -735,7 +748,10 @@ partial_repr(PyObject *self) Py_DECREF(mod); goto error; } - result = PyUnicode_FromFormat("%S.%S(%R%U)", mod, name, pto->fn, arglist); + /* Hold a strong reference to pto->fn for the same reason as args/kw. */ + PyObject *fn = Py_NewRef(pto->fn); + result = PyUnicode_FromFormat("%S.%S(%R%U)", mod, name, fn, arglist); + Py_DECREF(fn); Py_DECREF(mod); Py_DECREF(name); Py_DECREF(arglist); From 7e73c5d50fda33d56a87b20e49c646ad3f04180c Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 2 Mar 2026 03:31:16 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gh-144475: Add NEWS entry for functools.partial.__repr__ fix --- .../2026-03-02-02-30-00.gh-issue-144475.CfVOu8Hi.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-03-02-02-30-00.gh-issue-144475.CfVOu8Hi.rst diff --git a/Misc/NEWS.d/next/Library/2026-03-02-02-30-00.gh-issue-144475.CfVOu8Hi.rst b/Misc/NEWS.d/next/Library/2026-03-02-02-30-00.gh-issue-144475.CfVOu8Hi.rst new file mode 100644 index 00000000000000..13bb99a51a870b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-02-02-30-00.gh-issue-144475.CfVOu8Hi.rst @@ -0,0 +1,6 @@ +Fix a heap-buffer-overflow in :func:`functools.partial.__repr__` where a +user-defined :meth:`~object.__repr__` on an argument could mutate the +:class:`~functools.partial` object via :meth:`~functools.partial.__setstate__`, +freeing the args tuple while iteration was still in progress. The fix holds +strong references to the ``args`` tuple, ``kw`` dict, and ``fn`` callable +during formatting. From 6c18a9e335ec42f001c9c95dc8c5ff944cf28b85 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 2 Mar 2026 03:36:44 +0900 Subject: [PATCH 3/3] Fix NEWS entry Sphinx references Use inline code markup instead of :func: and :meth: roles for partial.__repr__ and __setstate__ to avoid Sphinx reference resolution failures in the docs CI. --- .../2026-03-02-02-30-00.gh-issue-144475.CfVOu8Hi.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-03-02-02-30-00.gh-issue-144475.CfVOu8Hi.rst b/Misc/NEWS.d/next/Library/2026-03-02-02-30-00.gh-issue-144475.CfVOu8Hi.rst index 13bb99a51a870b..169212a7ba2a81 100644 --- a/Misc/NEWS.d/next/Library/2026-03-02-02-30-00.gh-issue-144475.CfVOu8Hi.rst +++ b/Misc/NEWS.d/next/Library/2026-03-02-02-30-00.gh-issue-144475.CfVOu8Hi.rst @@ -1,6 +1,5 @@ -Fix a heap-buffer-overflow in :func:`functools.partial.__repr__` where a +Fix a heap-buffer-overflow in :func:`functools.partial` ``__repr__`` where a user-defined :meth:`~object.__repr__` on an argument could mutate the -:class:`~functools.partial` object via :meth:`~functools.partial.__setstate__`, -freeing the args tuple while iteration was still in progress. The fix holds -strong references to the ``args`` tuple, ``kw`` dict, and ``fn`` callable -during formatting. +:class:`~functools.partial` object via ``__setstate__()``, freeing the args +tuple while iteration was still in progress. The fix holds strong references +to the ``args`` tuple, ``kw`` dict, and ``fn`` callable during formatting.