diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index 3801a82a610891..6d0c0f4ef0e869 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -514,6 +514,70 @@ def test_partial_genericalias(self): self.assertEqual(alias.__args__, (int,)) self.assertEqual(alias.__parameters__, ()) + # Issue 144475 + def test_repr_for_segfault(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, name, is_trigger=False): + self.name = name + self.is_trigger = is_trigger + self.triggered = False + + def __repr__(self): + if self.is_trigger and 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({self.name})" + + trigger = EvilObject("trigger", is_trigger=True) + victim = EvilObject("victim") + + p = functools.partial(Function("old_function"), victim, victim=trigger) + g_partial = p + self.assertEqual(repr(g_partial),"functools.partial(Function(old_function), EvilObject(victim), victim=EvilObject(trigger))") + + trigger.triggered = False + g_partial = None + p = functools.partial(Function("old_function"), trigger, victim=victim) + g_partial = p + self.assertEqual(repr(g_partial),"functools.partial(Function(old_function), EvilObject(trigger), victim=EvilObject(victim))") + + + trigger.triggered = False + p = functools.partial(Function("old_function"), trigger, victim) + g_partial = p + + trigger.triggered = False + p = functools.partial(Function("old_function"), trigger=trigger, victim=victim) + g_partial = p + self.assertEqual(repr(g_partial),"functools.partial(Function(old_function), trigger=EvilObject(trigger), victim=EvilObject(victim))") + + trigger.triggered = False + victim1 = EvilObject("victim") + victim2 = EvilObject("victim") + victim3 = EvilObject("victim") + victim4 = EvilObject("victim") + victim5 = EvilObject("victim") + p = functools.partial(Function("old_function"), trigger, victim1, victim2, victim3, victim4, victim=victim5) + g_partial = p + self.assertEqual(repr(g_partial),"functools.partial(Function(old_function), EvilObject(trigger), EvilObject(victim), EvilObject(victim), EvilObject(victim), EvilObject(victim), victim=EvilObject(victim))") + + @unittest.skipUnless(c_functools, 'requires the C _functools module') class TestPartialC(TestPartial, unittest.TestCase): diff --git a/Misc/NEWS.d/next/C_API/2026-02-07-16-37-42.gh-issue-144475.8tFEXw.rst b/Misc/NEWS.d/next/C_API/2026-02-07-16-37-42.gh-issue-144475.8tFEXw.rst new file mode 100644 index 00000000000000..82587fb2288d67 --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-02-07-16-37-42.gh-issue-144475.8tFEXw.rst @@ -0,0 +1,4 @@ +Fixed a bug in ``partial.__repr__()`` that could occur when ``fn``, ``args``, +or ``kw`` are modified during a ``repr`` call. Now, ``partial.__repr__()`` +will use the original arguments when generating the return string. +Subsequent calls will use the updated arguments instead. diff --git a/Modules/_functoolsmodule.c b/Modules/_functoolsmodule.c index 5773083ff68b46..d8940269d495d1 100644 --- a/Modules/_functoolsmodule.c +++ b/Modules/_functoolsmodule.c @@ -689,6 +689,9 @@ partial_repr(PyObject *self) partialobject *pto = partialobject_CAST(self); PyObject *result = NULL; PyObject *arglist; + PyObject *fn; + PyObject *args; + PyObject *kw; PyObject *mod; PyObject *name; Py_ssize_t i, n; @@ -701,52 +704,56 @@ partial_repr(PyObject *self) return NULL; return PyUnicode_FromString("..."); } + /* Reference arguments in case they change */ + fn = Py_NewRef(pto->fn); + args = Py_NewRef(pto->args); + kw = Py_NewRef(pto->kw); + assert(PyTuple_Check(args)); + assert(PyDict_Check(kw)); arglist = Py_GetConstant(Py_CONSTANT_EMPTY_STR); if (arglist == NULL) - goto done; + goto free_arguments; /* 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))); + PyTuple_GET_ITEM(args, i))); if (arglist == NULL) - goto done; + goto free_arguments; + } /* 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) - goto done; + goto free_arguments; } mod = PyType_GetModuleName(Py_TYPE(pto)); if (mod == NULL) { - goto error; + goto free_arglist; } name = PyType_GetQualName(Py_TYPE(pto)); if (name == NULL) { - Py_DECREF(mod); - goto error; + goto free_mod; } - result = PyUnicode_FromFormat("%S.%S(%R%U)", mod, name, pto->fn, arglist); - Py_DECREF(mod); + result = PyUnicode_FromFormat("%S.%S(%R%U)", mod, name, fn, arglist); Py_DECREF(name); + free_mod: + Py_DECREF(mod); + free_arglist: Py_DECREF(arglist); - - done: + free_arguments: + 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: