Skip to content

Commit 1a742d4

Browse files
authored
gh-154189: Fix use-after-free in functools.partial_vectorcall (GH-154508)
1 parent f717b25 commit 1a742d4

3 files changed

Lines changed: 72 additions & 28 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: 34 additions & 28 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,20 +464,20 @@ 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) {
462-
goto error;
469+
goto clean_stack;
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) {
468-
goto error;
475+
goto clean_stack;
469476
}
470477
}
471478
if (PyDict_SetItem(pto_kw_merged, key, val) < 0) {
472479
Py_DECREF(pto_kw_merged);
473-
goto error;
480+
goto clean_stack;
474481
}
475482
}
476483
else {
@@ -486,7 +493,7 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
486493
tot_kwnames = PyTuple_New(tot_nkwds - n_merges);
487494
if (tot_kwnames == NULL) {
488495
Py_XDECREF(pto_kw_merged);
489-
goto error;
496+
goto clean_stack;
490497
}
491498
for (Py_ssize_t i = 0; i < n_tail; ++i) {
492499
key = Py_NewRef(stack[tot_nargskw + i]);
@@ -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);
@@ -515,10 +522,8 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
515522
tmp_stack = PyMem_Realloc(stack, (tot_nargskw - n_merges) * sizeof(PyObject *));
516523
if (tmp_stack == NULL) {
517524
Py_DECREF(tot_kwnames);
518-
if (stack != small_stack) {
519-
PyMem_Free(stack);
520-
}
521-
return PyErr_NoMemory();
525+
PyErr_NoMemory();
526+
goto clean_stack;
522527
}
523528
stack = tmp_stack;
524529
}
@@ -547,21 +552,22 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
547552
memcpy(stack + pto_nargs, args, nargs * sizeof(PyObject*));
548553
}
549554

550-
PyObject *ret = _PyObject_VectorcallTstate(tstate, pto->fn, stack,
551-
tot_nargs, tot_kwnames);
552-
if (stack != small_stack) {
553-
PyMem_Free(stack);
554-
}
555+
result = _PyObject_VectorcallTstate(tstate, partial_function, stack,
556+
tot_nargs, tot_kwnames);
555557
if (pto_nkwds) {
556558
Py_DECREF(tot_kwnames);
557559
}
558-
return ret;
559560

560-
error:
561+
clean_stack:
561562
if (stack != small_stack) {
562563
PyMem_Free(stack);
563564
}
564-
return NULL;
565+
566+
done:
567+
Py_DECREF(partial_function);
568+
Py_DECREF(partial_args);
569+
Py_DECREF(partial_keywords);
570+
return result;
565571
}
566572

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

0 commit comments

Comments
 (0)