https://github.com/python/cpython/commit/1a742d403243ac9915ffcc54e29516e58c44dc42
commit: 1a742d403243ac9915ffcc54e29516e58c44dc42
branch: main
author: Brij Kapadia <[email protected]>
committer: encukou <[email protected]>
date: 2026-07-24T23:32:07+02:00
summary:
gh-154189: Fix use-after-free in `functools.partial_vectorcall` (GH-154508)
files:
A Misc/NEWS.d/next/Library/2026-07-22-15-56-11.gh-issue-154189.7zWWNZ.rst
M Lib/test/test_functools.py
M Modules/_functoolsmodule.c
diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py
index 941dd7249a48d9..b46a1ce6d634a5 100644
--- a/Lib/test/test_functools.py
+++ b/Lib/test/test_functools.py
@@ -579,6 +579,40 @@ def f(**kwargs):
with self.assertRaises(RuntimeError):
result = p(**{BadStr("poison"): "new_value"})
+ def test_call_safety_against_reentrant_mutation(self):
+ def old_function(*args, **kwargs):
+ return "old_function", args, kwargs
+
+ def new_function(*args, **kwargs):
+ return "new_function", args, kwargs
+
+ g_partial = None
+
+ class EvilKey(str):
+ armed = False
+ def __hash__(self):
+ if EvilKey.armed and g_partial is not None:
+ EvilKey.armed = False
+ new_args_tuple = ("new_arg",)
+ new_keywords_dict = {"new_keyword": None}
+ new_tuple_state = (new_function, new_args_tuple,
new_keywords_dict, None)
+ g_partial.__setstate__(new_tuple_state)
+ gc.collect()
+ return str.__hash__(self)
+
+ g_partial = functools.partial(old_function, "old_arg",
old_keyword=None)
+
+ kwargs = {EvilKey("evil_key"): None}
+ EvilKey.armed = True
+
+ result = g_partial(**kwargs)
+ expected = ("old_function", ("old_arg",), {"old_keyword": None,
"evil_key": None})
+ self.assertEqual(result, expected)
+
+ result = g_partial()
+ expected = ("new_function", ("new_arg",), {"new_keyword": None})
+ self.assertEqual(result, expected)
+
@unittest.skipUnless(c_functools, 'requires the C _functools module')
class TestPartialC(TestPartial, unittest.TestCase):
if c_functools:
diff --git
a/Misc/NEWS.d/next/Library/2026-07-22-15-56-11.gh-issue-154189.7zWWNZ.rst
b/Misc/NEWS.d/next/Library/2026-07-22-15-56-11.gh-issue-154189.7zWWNZ.rst
new file mode 100644
index 00000000000000..3a745a7c5ce67d
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-22-15-56-11.gh-issue-154189.7zWWNZ.rst
@@ -0,0 +1,4 @@
+Fixed a potential use-after-free when calling :func:`functools.partial`.
+Now, when invoking a :func:`~functools.partial` object, the stored function,
+positional arguments, and keyword arguments are preserved for the duration
+of the call in case of reentrancy.
diff --git a/Modules/_functoolsmodule.c b/Modules/_functoolsmodule.c
index b4595c55d519b9..1ab230218124a4 100644
--- a/Modules/_functoolsmodule.c
+++ b/Modules/_functoolsmodule.c
@@ -382,9 +382,14 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
return NULL;
}
- PyObject **pto_args = _PyTuple_ITEMS(pto->args);
- Py_ssize_t pto_nargs = PyTuple_GET_SIZE(pto->args);
- Py_ssize_t pto_nkwds = PyDict_GET_SIZE(pto->kw);
+ PyObject *result = NULL;
+ PyObject *partial_function = Py_NewRef(pto->fn);
+ PyObject *partial_args = Py_NewRef(pto->args);
+ PyObject *partial_keywords = Py_NewRef(pto->kw);
+
+ PyObject **pto_args = _PyTuple_ITEMS(partial_args);
+ Py_ssize_t pto_nargs = PyTuple_GET_SIZE(partial_args);
+ Py_ssize_t pto_nkwds = PyDict_GET_SIZE(partial_keywords);
Py_ssize_t nkwds = kwnames == NULL ? 0 : PyTuple_GET_SIZE(kwnames);
Py_ssize_t nargskw = nargs + nkwds;
@@ -392,8 +397,9 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
if (!pto_nkwds) {
/* Fast path if we're called without arguments */
if (nargskw == 0) {
- return _PyObject_VectorcallTstate(tstate, pto->fn, pto_args,
- pto_nargs, NULL);
+ result = _PyObject_VectorcallTstate(tstate, partial_function,
pto_args,
+ pto_nargs, NULL);
+ goto done;
}
/* Use PY_VECTORCALL_ARGUMENTS_OFFSET to prepend a single
@@ -402,10 +408,10 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
PyObject **newargs = (PyObject **)args - 1;
PyObject *tmp = newargs[0];
newargs[0] = pto_args[0];
- PyObject *ret = _PyObject_VectorcallTstate(tstate, pto->fn,
newargs,
- nargs + 1, kwnames);
+ result = _PyObject_VectorcallTstate(tstate, partial_function,
newargs,
+ nargs + 1, kwnames);
newargs[0] = tmp;
- return ret;
+ goto done;
}
}
@@ -435,7 +441,8 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
else {
stack = PyMem_Malloc(init_stack_size * sizeof(PyObject *));
if (stack == NULL) {
- return PyErr_NoMemory();
+ PyErr_NoMemory();
+ goto done;
}
}
@@ -457,20 +464,20 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
for (Py_ssize_t i = 0; i < nkwds; ++i) {
key = PyTuple_GET_ITEM(kwnames, i);
val = args[nargs + i];
- int contains = PyDict_Contains(pto->kw, key);
+ int contains = PyDict_Contains(partial_keywords, key);
if (contains < 0) {
- goto error;
+ goto clean_stack;
}
else if (contains == 1) {
if (pto_kw_merged == NULL) {
- pto_kw_merged = PyDict_Copy(pto->kw);
+ pto_kw_merged = PyDict_Copy(partial_keywords);
if (pto_kw_merged == NULL) {
- goto error;
+ goto clean_stack;
}
}
if (PyDict_SetItem(pto_kw_merged, key, val) < 0) {
Py_DECREF(pto_kw_merged);
- goto error;
+ goto clean_stack;
}
}
else {
@@ -486,7 +493,7 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
tot_kwnames = PyTuple_New(tot_nkwds - n_merges);
if (tot_kwnames == NULL) {
Py_XDECREF(pto_kw_merged);
- goto error;
+ goto clean_stack;
}
for (Py_ssize_t i = 0; i < n_tail; ++i) {
key = Py_NewRef(stack[tot_nargskw + i]);
@@ -496,7 +503,7 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
/* Copy pto_keywords with overlapping call keywords merged
* Note, tail is already coppied. */
Py_ssize_t pos = 0, i = 0;
- PyObject *keyword_dict = n_merges ? pto_kw_merged : pto->kw;
+ PyObject *keyword_dict = n_merges ? pto_kw_merged : partial_keywords;
Py_BEGIN_CRITICAL_SECTION(keyword_dict);
while (PyDict_Next(keyword_dict, &pos, &key, &val)) {
assert(i < pto_nkwds);
@@ -515,10 +522,8 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
tmp_stack = PyMem_Realloc(stack, (tot_nargskw - n_merges) *
sizeof(PyObject *));
if (tmp_stack == NULL) {
Py_DECREF(tot_kwnames);
- if (stack != small_stack) {
- PyMem_Free(stack);
- }
- return PyErr_NoMemory();
+ PyErr_NoMemory();
+ goto clean_stack;
}
stack = tmp_stack;
}
@@ -547,21 +552,22 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
memcpy(stack + pto_nargs, args, nargs * sizeof(PyObject*));
}
- PyObject *ret = _PyObject_VectorcallTstate(tstate, pto->fn, stack,
- tot_nargs, tot_kwnames);
- if (stack != small_stack) {
- PyMem_Free(stack);
- }
+ result = _PyObject_VectorcallTstate(tstate, partial_function, stack,
+ tot_nargs, tot_kwnames);
if (pto_nkwds) {
Py_DECREF(tot_kwnames);
}
- return ret;
- error:
+ clean_stack:
if (stack != small_stack) {
PyMem_Free(stack);
}
- return NULL;
+
+ done:
+ Py_DECREF(partial_function);
+ Py_DECREF(partial_args);
+ Py_DECREF(partial_keywords);
+ return result;
}
/* Set pto->vectorcall depending on the parameters of the partial object */
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]