Skip to content

Commit 3c99c16

Browse files
[3.14] gh-144475: Fix reference management in partial_repr (GH-145362) (GH-145470)
(cherry picked from commit 671a953) Co-authored-by: bkap123 <97006829+bkap123@users.noreply.github.com>
1 parent 85f8073 commit 3c99c16

File tree

3 files changed

+86
-24
lines changed

3 files changed

+86
-24
lines changed

Lib/test/test_functools.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,58 @@ def test_partial_genericalias(self):
514514
self.assertEqual(alias.__args__, (int,))
515515
self.assertEqual(alias.__parameters__, ())
516516

517+
# GH-144475: Tests that the partial object does not change until repr finishes
518+
def test_repr_safety_against_reentrant_mutation(self):
519+
g_partial = None
520+
521+
class Function:
522+
def __init__(self, name):
523+
self.name = name
524+
525+
def __call__(self):
526+
return None
527+
528+
def __repr__(self):
529+
return f"Function({self.name})"
530+
531+
class EvilObject:
532+
def __init__(self):
533+
self.triggered = False
534+
535+
def __repr__(self):
536+
if not self.triggered and g_partial is not None:
537+
self.triggered = True
538+
new_args_tuple = (None,)
539+
new_keywords_dict = {"keyword": None}
540+
new_tuple_state = (Function("new_function"), new_args_tuple, new_keywords_dict, None)
541+
g_partial.__setstate__(new_tuple_state)
542+
gc.collect()
543+
return f"EvilObject"
544+
545+
trigger = EvilObject()
546+
func = Function("old_function")
547+
548+
g_partial = functools.partial(func, None, trigger=trigger)
549+
self.assertEqual(repr(g_partial),"functools.partial(Function(old_function), None, trigger=EvilObject)")
550+
551+
trigger.triggered = False
552+
g_partial = functools.partial(func, trigger, arg=None)
553+
self.assertEqual(repr(g_partial),"functools.partial(Function(old_function), EvilObject, arg=None)")
554+
555+
556+
trigger.triggered = False
557+
g_partial = functools.partial(func, trigger, None)
558+
self.assertEqual(repr(g_partial),"functools.partial(Function(old_function), EvilObject, None)")
559+
560+
trigger.triggered = False
561+
g_partial = functools.partial(func, trigger=trigger, arg=None)
562+
self.assertEqual(repr(g_partial),"functools.partial(Function(old_function), trigger=EvilObject, arg=None)")
563+
564+
trigger.triggered = False
565+
g_partial = functools.partial(func, trigger, None, None, None, None, arg=None)
566+
self.assertEqual(repr(g_partial),"functools.partial(Function(old_function), EvilObject, None, None, None, None, arg=None)")
567+
568+
517569

518570
@unittest.skipUnless(c_functools, 'requires the C _functools module')
519571
class TestPartialC(TestPartial, unittest.TestCase):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Calling :func:`repr` on :func:`functools.partial` is now safer
2+
when the partial object's internal attributes are replaced while
3+
the string representation is being generated.

Modules/_functoolsmodule.c

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -611,65 +611,72 @@ partial_repr(PyObject *self)
611611
{
612612
partialobject *pto = partialobject_CAST(self);
613613
PyObject *result = NULL;
614-
PyObject *arglist;
615-
PyObject *mod;
616-
PyObject *name;
614+
PyObject *arglist = NULL;
615+
PyObject *mod = NULL;
616+
PyObject *name = NULL;
617617
Py_ssize_t i, n;
618618
PyObject *key, *value;
619619
int status;
620620

621621
status = Py_ReprEnter(self);
622622
if (status != 0) {
623-
if (status < 0)
623+
if (status < 0) {
624624
return NULL;
625+
}
625626
return PyUnicode_FromString("...");
626627
}
628+
/* Reference arguments in case they change */
629+
PyObject *fn = Py_NewRef(pto->fn);
630+
PyObject *args = Py_NewRef(pto->args);
631+
PyObject *kw = Py_NewRef(pto->kw);
632+
assert(PyTuple_Check(args));
633+
assert(PyDict_Check(kw));
627634

628635
arglist = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
629-
if (arglist == NULL)
636+
if (arglist == NULL) {
630637
goto done;
638+
}
631639
/* Pack positional arguments */
632-
assert(PyTuple_Check(pto->args));
633-
n = PyTuple_GET_SIZE(pto->args);
640+
n = PyTuple_GET_SIZE(args);
634641
for (i = 0; i < n; i++) {
635642
Py_SETREF(arglist, PyUnicode_FromFormat("%U, %R", arglist,
636-
PyTuple_GET_ITEM(pto->args, i)));
637-
if (arglist == NULL)
643+
PyTuple_GET_ITEM(args, i)));
644+
if (arglist == NULL) {
638645
goto done;
646+
}
639647
}
640648
/* Pack keyword arguments */
641-
assert (PyDict_Check(pto->kw));
642-
for (i = 0; PyDict_Next(pto->kw, &i, &key, &value);) {
649+
for (i = 0; PyDict_Next(kw, &i, &key, &value);) {
643650
/* Prevent key.__str__ from deleting the value. */
644651
Py_INCREF(value);
645652
Py_SETREF(arglist, PyUnicode_FromFormat("%U, %S=%R", arglist,
646653
key, value));
647654
Py_DECREF(value);
648-
if (arglist == NULL)
655+
if (arglist == NULL) {
649656
goto done;
657+
}
650658
}
651659

652660
mod = PyType_GetModuleName(Py_TYPE(pto));
653661
if (mod == NULL) {
654-
goto error;
662+
goto done;
655663
}
664+
656665
name = PyType_GetQualName(Py_TYPE(pto));
657666
if (name == NULL) {
658-
Py_DECREF(mod);
659-
goto error;
667+
goto done;
660668
}
661-
result = PyUnicode_FromFormat("%S.%S(%R%U)", mod, name, pto->fn, arglist);
662-
Py_DECREF(mod);
663-
Py_DECREF(name);
664-
Py_DECREF(arglist);
665669

666-
done:
670+
result = PyUnicode_FromFormat("%S.%S(%R%U)", mod, name, fn, arglist);
671+
done:
672+
Py_XDECREF(name);
673+
Py_XDECREF(mod);
674+
Py_XDECREF(arglist);
675+
Py_DECREF(fn);
676+
Py_DECREF(args);
677+
Py_DECREF(kw);
667678
Py_ReprLeave(self);
668679
return result;
669-
error:
670-
Py_DECREF(arglist);
671-
Py_ReprLeave(self);
672-
return NULL;
673680
}
674681

675682
/* Pickle strategy:

0 commit comments

Comments
 (0)