Skip to content

Commit 4e37207

Browse files
authored
[3.13] gh-144475: Fix reference management in partial_repr (GH-145362) (#145882)
(cherry picked from commit 671a953)
1 parent 684e7af commit 4e37207

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
@@ -420,6 +420,58 @@ def test_partial_genericalias(self):
420420
self.assertEqual(alias.__args__, (int,))
421421
self.assertEqual(alias.__parameters__, ())
422422

423+
# GH-144475: Tests that the partial object does not change until repr finishes
424+
def test_repr_safety_against_reentrant_mutation(self):
425+
g_partial = None
426+
427+
class Function:
428+
def __init__(self, name):
429+
self.name = name
430+
431+
def __call__(self):
432+
return None
433+
434+
def __repr__(self):
435+
return f"Function({self.name})"
436+
437+
class EvilObject:
438+
def __init__(self):
439+
self.triggered = False
440+
441+
def __repr__(self):
442+
if not self.triggered and g_partial is not None:
443+
self.triggered = True
444+
new_args_tuple = (None,)
445+
new_keywords_dict = {"keyword": None}
446+
new_tuple_state = (Function("new_function"), new_args_tuple, new_keywords_dict, None)
447+
g_partial.__setstate__(new_tuple_state)
448+
gc.collect()
449+
return f"EvilObject"
450+
451+
trigger = EvilObject()
452+
func = Function("old_function")
453+
454+
g_partial = functools.partial(func, None, trigger=trigger)
455+
self.assertEqual(repr(g_partial),"functools.partial(Function(old_function), None, trigger=EvilObject)")
456+
457+
trigger.triggered = False
458+
g_partial = functools.partial(func, trigger, arg=None)
459+
self.assertEqual(repr(g_partial),"functools.partial(Function(old_function), EvilObject, arg=None)")
460+
461+
462+
trigger.triggered = False
463+
g_partial = functools.partial(func, trigger, None)
464+
self.assertEqual(repr(g_partial),"functools.partial(Function(old_function), EvilObject, None)")
465+
466+
trigger.triggered = False
467+
g_partial = functools.partial(func, trigger=trigger, arg=None)
468+
self.assertEqual(repr(g_partial),"functools.partial(Function(old_function), trigger=EvilObject, arg=None)")
469+
470+
trigger.triggered = False
471+
g_partial = functools.partial(func, trigger, None, None, None, None, arg=None)
472+
self.assertEqual(repr(g_partial),"functools.partial(Function(old_function), EvilObject, None, None, None, None, arg=None)")
473+
474+
423475

424476
@unittest.skipUnless(c_functools, 'requires the C _functools module')
425477
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
@@ -388,65 +388,72 @@ static PyObject *
388388
partial_repr(partialobject *pto)
389389
{
390390
PyObject *result = NULL;
391-
PyObject *arglist;
392-
PyObject *mod;
393-
PyObject *name;
391+
PyObject *arglist = NULL;
392+
PyObject *mod = NULL;
393+
PyObject *name = NULL;
394394
Py_ssize_t i, n;
395395
PyObject *key, *value;
396396
int status;
397397

398398
status = Py_ReprEnter((PyObject *)pto);
399399
if (status != 0) {
400-
if (status < 0)
400+
if (status < 0) {
401401
return NULL;
402+
}
402403
return PyUnicode_FromString("...");
403404
}
405+
/* Reference arguments in case they change */
406+
PyObject *fn = Py_NewRef(pto->fn);
407+
PyObject *args = Py_NewRef(pto->args);
408+
PyObject *kw = Py_NewRef(pto->kw);
409+
assert(PyTuple_Check(args));
410+
assert(PyDict_Check(kw));
404411

405412
arglist = PyUnicode_FromString("");
406-
if (arglist == NULL)
413+
if (arglist == NULL) {
407414
goto done;
415+
}
408416
/* Pack positional arguments */
409-
assert (PyTuple_Check(pto->args));
410-
n = PyTuple_GET_SIZE(pto->args);
417+
n = PyTuple_GET_SIZE(args);
411418
for (i = 0; i < n; i++) {
412419
Py_SETREF(arglist, PyUnicode_FromFormat("%U, %R", arglist,
413-
PyTuple_GET_ITEM(pto->args, i)));
414-
if (arglist == NULL)
420+
PyTuple_GET_ITEM(args, i)));
421+
if (arglist == NULL) {
415422
goto done;
423+
}
416424
}
417425
/* Pack keyword arguments */
418-
assert (PyDict_Check(pto->kw));
419-
for (i = 0; PyDict_Next(pto->kw, &i, &key, &value);) {
426+
for (i = 0; PyDict_Next(kw, &i, &key, &value);) {
420427
/* Prevent key.__str__ from deleting the value. */
421428
Py_INCREF(value);
422429
Py_SETREF(arglist, PyUnicode_FromFormat("%U, %S=%R", arglist,
423430
key, value));
424431
Py_DECREF(value);
425-
if (arglist == NULL)
432+
if (arglist == NULL) {
426433
goto done;
434+
}
427435
}
428436

429437
mod = PyType_GetModuleName(Py_TYPE(pto));
430438
if (mod == NULL) {
431-
goto error;
439+
goto done;
432440
}
441+
433442
name = PyType_GetQualName(Py_TYPE(pto));
434443
if (name == NULL) {
435-
Py_DECREF(mod);
436-
goto error;
444+
goto done;
437445
}
438-
result = PyUnicode_FromFormat("%S.%S(%R%U)", mod, name, pto->fn, arglist);
439-
Py_DECREF(mod);
440-
Py_DECREF(name);
441-
Py_DECREF(arglist);
442446

443-
done:
447+
result = PyUnicode_FromFormat("%S.%S(%R%U)", mod, name, fn, arglist);
448+
done:
449+
Py_XDECREF(name);
450+
Py_XDECREF(mod);
451+
Py_XDECREF(arglist);
452+
Py_DECREF(fn);
453+
Py_DECREF(args);
454+
Py_DECREF(kw);
444455
Py_ReprLeave((PyObject *)pto);
445456
return result;
446-
error:
447-
Py_DECREF(arglist);
448-
Py_ReprLeave((PyObject *)pto);
449-
return NULL;
450457
}
451458

452459
/* Pickle strategy:

0 commit comments

Comments
 (0)