]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-144475: Fix reference management in partial_repr (GH-145362) (#145882)
authorBrij Kapadia <97006829+bkap123@users.noreply.github.com>
Tue, 24 Mar 2026 01:30:45 +0000 (21:30 -0400)
committerGitHub <noreply@github.com>
Tue, 24 Mar 2026 01:30:45 +0000 (02:30 +0100)
(cherry picked from commit 671a953dd65292a5b69ba7393666ddcac93dbc44)

Lib/test/test_functools.py
Misc/NEWS.d/next/Library/2026-02-07-16-37-42.gh-issue-144475.8tFEXw.rst [new file with mode: 0644]
Modules/_functoolsmodule.c

index e042a7909ef05b14347011c89c9ab6c522503796..be8ef5d0bcd8b907b6738088fc0567c19fdd8545 100644 (file)
@@ -420,6 +420,58 @@ class TestPartial:
         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 (file)
index 0000000..b458399
--- /dev/null
@@ -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.
index 40c573b3fdf7b642c09581749327587d1ee73fdc..a1682ec4c0ea2204a6ebfd5fac2e99a79bec0e95 100644 (file)
@@ -388,65 +388,72 @@ static PyObject *
 partial_repr(partialobject *pto)
 {
     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((PyObject *)pto);
     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 = PyUnicode_FromString("");
-    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((PyObject *)pto);
     return result;
- error:
-    Py_DECREF(arglist);
-    Py_ReprLeave((PyObject *)pto);
-    return NULL;
 }
 
 /* Pickle strategy: