Skip to content

Commit 082e340

Browse files
committed
gh-154189: Fix use-after-free in functools.partial_vectorcall
1 parent a2a8466 commit 082e340

3 files changed

Lines changed: 67 additions & 20 deletions

File tree

Lib/test/test_functools.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,40 @@ def f(**kwargs):
579579
with self.assertRaises(RuntimeError):
580580
result = p(**{BadStr("poison"): "new_value"})
581581

582+
def test_call_safety_against_reentrant_mutation(self):
583+
def old_function(*args, **kwargs):
584+
return "old_function", args, kwargs
585+
586+
def new_function(*args, **kwargs):
587+
return "new_function", args, kwargs
588+
589+
g_partial = None
590+
591+
class EvilKey(str):
592+
armed = False
593+
def __hash__(self):
594+
if EvilKey.armed and g_partial is not None:
595+
EvilKey.armed = False
596+
new_args_tuple = ("new_arg",)
597+
new_keywords_dict = {"new_keyword": None}
598+
new_tuple_state = (new_function, new_args_tuple, new_keywords_dict, None)
599+
g_partial.__setstate__(new_tuple_state)
600+
gc.collect()
601+
return str.__hash__(self)
602+
603+
g_partial = functools.partial(old_function, "old_arg", old_keyword=None)
604+
605+
kwargs = {EvilKey("evil_key"): None}
606+
EvilKey.armed = True
607+
608+
result = g_partial(**kwargs)
609+
expected = ("old_function", ("old_arg",), {"old_keyword": None, "evil_key": None})
610+
self.assertEqual(result, expected)
611+
612+
result = g_partial()
613+
expected = ("new_function", ("new_arg",), {"new_keyword": None})
614+
self.assertEqual(result, expected)
615+
582616
@unittest.skipUnless(c_functools, 'requires the C _functools module')
583617
class TestPartialC(TestPartial, unittest.TestCase):
584618
if c_functools:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fixed a potential use-after-free when calling :func:`functools.partial`.
2+
Now, when invoking a :func:`~functools.partial` object, the stored function,
3+
positional arguments, and keyword arguments are preserved for the duration
4+
of the call in case of reentrancy.

Modules/_functoolsmodule.c

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -382,18 +382,24 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
382382
return NULL;
383383
}
384384

385-
PyObject **pto_args = _PyTuple_ITEMS(pto->args);
386-
Py_ssize_t pto_nargs = PyTuple_GET_SIZE(pto->args);
387-
Py_ssize_t pto_nkwds = PyDict_GET_SIZE(pto->kw);
385+
PyObject *result = NULL;
386+
PyObject *partial_function = Py_NewRef(pto->fn);
387+
PyObject *partial_args = Py_NewRef(pto->args);
388+
PyObject *partial_keywords = Py_NewRef(pto->kw);
389+
390+
PyObject **pto_args = _PyTuple_ITEMS(partial_args);
391+
Py_ssize_t pto_nargs = PyTuple_GET_SIZE(partial_args);
392+
Py_ssize_t pto_nkwds = PyDict_GET_SIZE(partial_keywords);
388393
Py_ssize_t nkwds = kwnames == NULL ? 0 : PyTuple_GET_SIZE(kwnames);
389394
Py_ssize_t nargskw = nargs + nkwds;
390395

391396
/* Special cases */
392397
if (!pto_nkwds) {
393398
/* Fast path if we're called without arguments */
394399
if (nargskw == 0) {
395-
return _PyObject_VectorcallTstate(tstate, pto->fn, pto_args,
396-
pto_nargs, NULL);
400+
result = _PyObject_VectorcallTstate(tstate, partial_function, pto_args,
401+
pto_nargs, NULL);
402+
goto done;
397403
}
398404

399405
/* Use PY_VECTORCALL_ARGUMENTS_OFFSET to prepend a single
@@ -402,10 +408,10 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
402408
PyObject **newargs = (PyObject **)args - 1;
403409
PyObject *tmp = newargs[0];
404410
newargs[0] = pto_args[0];
405-
PyObject *ret = _PyObject_VectorcallTstate(tstate, pto->fn, newargs,
406-
nargs + 1, kwnames);
411+
result = _PyObject_VectorcallTstate(tstate, partial_function, newargs,
412+
nargs + 1, kwnames);
407413
newargs[0] = tmp;
408-
return ret;
414+
goto done;
409415
}
410416
}
411417

@@ -435,7 +441,8 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
435441
else {
436442
stack = PyMem_Malloc(init_stack_size * sizeof(PyObject *));
437443
if (stack == NULL) {
438-
return PyErr_NoMemory();
444+
PyErr_NoMemory();
445+
goto done;
439446
}
440447
}
441448

@@ -457,13 +464,13 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
457464
for (Py_ssize_t i = 0; i < nkwds; ++i) {
458465
key = PyTuple_GET_ITEM(kwnames, i);
459466
val = args[nargs + i];
460-
int contains = PyDict_Contains(pto->kw, key);
467+
int contains = PyDict_Contains(partial_keywords, key);
461468
if (contains < 0) {
462469
goto error;
463470
}
464471
else if (contains == 1) {
465472
if (pto_kw_merged == NULL) {
466-
pto_kw_merged = PyDict_Copy(pto->kw);
473+
pto_kw_merged = PyDict_Copy(partial_keywords);
467474
if (pto_kw_merged == NULL) {
468475
goto error;
469476
}
@@ -496,7 +503,7 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
496503
/* Copy pto_keywords with overlapping call keywords merged
497504
* Note, tail is already coppied. */
498505
Py_ssize_t pos = 0, i = 0;
499-
PyObject *keyword_dict = n_merges ? pto_kw_merged : pto->kw;
506+
PyObject *keyword_dict = n_merges ? pto_kw_merged : partial_keywords;
500507
Py_BEGIN_CRITICAL_SECTION(keyword_dict);
501508
while (PyDict_Next(keyword_dict, &pos, &key, &val)) {
502509
assert(i < pto_nkwds);
@@ -518,7 +525,8 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
518525
if (stack != small_stack) {
519526
PyMem_Free(stack);
520527
}
521-
return PyErr_NoMemory();
528+
PyErr_NoMemory();
529+
goto done;
522530
}
523531
stack = tmp_stack;
524532
}
@@ -547,21 +555,22 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
547555
memcpy(stack + pto_nargs, args, nargs * sizeof(PyObject*));
548556
}
549557

550-
PyObject *ret = _PyObject_VectorcallTstate(tstate, pto->fn, stack,
551-
tot_nargs, tot_kwnames);
552-
if (stack != small_stack) {
553-
PyMem_Free(stack);
554-
}
558+
result = _PyObject_VectorcallTstate(tstate, partial_function, stack,
559+
tot_nargs, tot_kwnames);
555560
if (pto_nkwds) {
556561
Py_DECREF(tot_kwnames);
557562
}
558-
return ret;
559563

560564
error:
561565
if (stack != small_stack) {
562566
PyMem_Free(stack);
563567
}
564-
return NULL;
568+
569+
done:
570+
Py_DECREF(partial_function);
571+
Py_DECREF(partial_args);
572+
Py_DECREF(partial_keywords);
573+
return result;
565574
}
566575

567576
/* Set pto->vectorcall depending on the parameters of the partial object */

0 commit comments

Comments
 (0)