https://github.com/python/cpython/commit/671a953dd65292a5b69ba7393666ddcac93dbc44
commit: 671a953dd65292a5b69ba7393666ddcac93dbc44
branch: main
author: bkap123 <[email protected]>
committer: encukou <[email protected]>
date: 2026-03-03T13:46:02Z
summary:
gh-144475: Fix reference management in partial_repr (GH-145362)
files:
A Misc/NEWS.d/next/Library/2026-02-07-16-37-42.gh-issue-144475.8tFEXw.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 86652b7fa4d6df..dda42cb33072c3 100644
--- a/Lib/test/test_functools.py
+++ b/Lib/test/test_functools.py
@@ -514,6 +514,58 @@ def test_partial_genericalias(self):
self.assertEqual(alias.__args__, (int,))
self.assertEqual(alias.__parameters__, ())
+ # GH-144475: Tests that the partial object does not change until repr
finishes
+ def test_repr_safety_against_reentrant_mutation(self):
+ g_partial = None
+
+ class Function:
+ def __init__(self, name):
+ self.name = name
+
+ def __call__(self):
+ return None
+
+ def __repr__(self):
+ return f"Function({self.name})"
+
+ class EvilObject:
+ def __init__(self):
+ self.triggered = False
+
+ def __repr__(self):
+ if not self.triggered and g_partial is not None:
+ self.triggered = True
+ new_args_tuple = (None,)
+ new_keywords_dict = {"keyword": None}
+ new_tuple_state = (Function("new_function"),
new_args_tuple, new_keywords_dict, None)
+ g_partial.__setstate__(new_tuple_state)
+ gc.collect()
+ return f"EvilObject"
+
+ trigger = EvilObject()
+ func = Function("old_function")
+
+ g_partial = functools.partial(func, None, trigger=trigger)
+
self.assertEqual(repr(g_partial),"functools.partial(Function(old_function),
None, trigger=EvilObject)")
+
+ trigger.triggered = False
+ g_partial = functools.partial(func, trigger, arg=None)
+
self.assertEqual(repr(g_partial),"functools.partial(Function(old_function),
EvilObject, arg=None)")
+
+
+ trigger.triggered = False
+ g_partial = functools.partial(func, trigger, None)
+
self.assertEqual(repr(g_partial),"functools.partial(Function(old_function),
EvilObject, None)")
+
+ trigger.triggered = False
+ g_partial = functools.partial(func, trigger=trigger, arg=None)
+
self.assertEqual(repr(g_partial),"functools.partial(Function(old_function),
trigger=EvilObject, arg=None)")
+
+ trigger.triggered = False
+ g_partial = functools.partial(func, trigger, None, None, None, None,
arg=None)
+
self.assertEqual(repr(g_partial),"functools.partial(Function(old_function),
EvilObject, None, None, None, None, arg=None)")
+
+
@unittest.skipUnless(c_functools, 'requires the C _functools module')
class TestPartialC(TestPartial, unittest.TestCase):
diff --git
a/Misc/NEWS.d/next/Library/2026-02-07-16-37-42.gh-issue-144475.8tFEXw.rst
b/Misc/NEWS.d/next/Library/2026-02-07-16-37-42.gh-issue-144475.8tFEXw.rst
new file mode 100644
index 00000000000000..b458399bb40640
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-02-07-16-37-42.gh-issue-144475.8tFEXw.rst
@@ -0,0 +1,3 @@
+Calling :func:`repr` on :func:`functools.partial` is now safer
+when the partial object's internal attributes are replaced while
+the string representation is being generated.
diff --git a/Modules/_functoolsmodule.c b/Modules/_functoolsmodule.c
index 5773083ff68b46..5286be0b715fff 100644
--- a/Modules/_functoolsmodule.c
+++ b/Modules/_functoolsmodule.c
@@ -688,65 +688,72 @@ partial_repr(PyObject *self)
{
partialobject *pto = partialobject_CAST(self);
PyObject *result = NULL;
- PyObject *arglist;
- PyObject *mod;
- PyObject *name;
+ PyObject *arglist = NULL;
+ PyObject *mod = NULL;
+ PyObject *name = NULL;
Py_ssize_t i, n;
PyObject *key, *value;
int status;
status = Py_ReprEnter(self);
if (status != 0) {
- if (status < 0)
+ if (status < 0) {
return NULL;
+ }
return PyUnicode_FromString("...");
}
+ /* Reference arguments in case they change */
+ PyObject *fn = Py_NewRef(pto->fn);
+ PyObject *args = Py_NewRef(pto->args);
+ PyObject *kw = Py_NewRef(pto->kw);
+ assert(PyTuple_Check(args));
+ assert(PyDict_Check(kw));
arglist = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
- if (arglist == NULL)
+ if (arglist == NULL) {
goto done;
+ }
/* Pack positional arguments */
- assert(PyTuple_Check(pto->args));
- n = PyTuple_GET_SIZE(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) {
goto done;
+ }
}
/* Pack keyword arguments */
- assert (PyDict_Check(pto->kw));
- for (i = 0; PyDict_Next(pto->kw, &i, &key, &value);) {
+ 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) {
goto done;
+ }
}
mod = PyType_GetModuleName(Py_TYPE(pto));
if (mod == NULL) {
- goto error;
+ goto done;
}
+
name = PyType_GetQualName(Py_TYPE(pto));
if (name == NULL) {
- Py_DECREF(mod);
- goto error;
+ goto done;
}
- result = PyUnicode_FromFormat("%S.%S(%R%U)", mod, name, pto->fn, arglist);
- Py_DECREF(mod);
- Py_DECREF(name);
- Py_DECREF(arglist);
- done:
+ result = PyUnicode_FromFormat("%S.%S(%R%U)", mod, name, fn, arglist);
+done:
+ Py_XDECREF(name);
+ Py_XDECREF(mod);
+ Py_XDECREF(arglist);
+ Py_DECREF(fn);
+ Py_DECREF(args);
+ Py_DECREF(kw);
Py_ReprLeave(self);
return result;
- error:
- Py_DECREF(arglist);
- Py_ReprLeave(self);
- return NULL;
}
/* Pickle strategy:
_______________________________________________
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]