Skip to content

Commit bb78b23

Browse files
author
Alessio Attilio
committed
gh-154189: Fix use-after-free in partial_vectorcall from reentrant __setstate__
1 parent e81f190 commit bb78b23

3 files changed

Lines changed: 66 additions & 7 deletions

File tree

Lib/test/test_functools.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,42 @@ 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+
# gh-154189: partial_vectorcall cached a raw pointer to pto->args
584+
# before the keyword merge loop. If a key's __hash__ reentrantly
585+
# called __setstate__, pto->args was freed and the pointer dangled.
586+
import gc
587+
588+
def original(*args, **kwargs):
589+
return "original"
590+
591+
def replacement(*args, **kwargs):
592+
return "replacement"
593+
594+
g = None
595+
596+
class EvilKey(str):
597+
armed = False
598+
def __hash__(self):
599+
if EvilKey.armed and g is not None:
600+
EvilKey.armed = False
601+
g.__setstate__((replacement, (), {"k": 1}, None))
602+
gc.collect()
603+
return str.__hash__(self)
604+
def __eq__(self, other):
605+
return str.__eq__(self, other)
606+
607+
stored = tuple(object() for _ in range(5))
608+
g = self.partial(original, *stored, k=0)
609+
del stored
610+
611+
ek = EvilKey("zzz")
612+
kwargs = {ek: 2}
613+
EvilKey.armed = True
614+
# Must not crash (use-after-free in the C partial before the fix).
615+
g(**kwargs)
616+
self.assertFalse(EvilKey.armed) # the re-entrant __setstate__ ran
617+
582618
@unittest.skipUnless(c_functools, 'requires the C _functools module')
583619
class TestPartialC(TestPartial, unittest.TestCase):
584620
if c_functools:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix use-after-free in :func:`functools.partial` vectorcall path. A reentrant
2+
``__hash__`` on a keyword argument key could trigger ``__setstate__`` on the
3+
partial mid-call, freeing the cached args tuple and leaving a dangling pointer.

Modules/_functoolsmodule.c

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -382,18 +382,28 @@ 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+
/* Hold strong references to pto->args and pto->kw across the function.
386+
* A keyword hash callback (line ~460) can reentrantly mutate the partial
387+
* via __setstate__, replacing pto->args/pto->kw and freeing the originals.
388+
* Without these references, pto_args becomes a dangling pointer (UAF). */
389+
PyObject *args_ref = Py_NewRef(pto->args);
390+
PyObject *kw_ref = Py_NewRef(pto->kw);
391+
392+
PyObject **pto_args = _PyTuple_ITEMS(args_ref);
393+
Py_ssize_t pto_nargs = PyTuple_GET_SIZE(args_ref);
394+
Py_ssize_t pto_nkwds = PyDict_GET_SIZE(kw_ref);
388395
Py_ssize_t nkwds = kwnames == NULL ? 0 : PyTuple_GET_SIZE(kwnames);
389396
Py_ssize_t nargskw = nargs + nkwds;
390397

391398
/* Special cases */
392399
if (!pto_nkwds) {
393400
/* Fast path if we're called without arguments */
394401
if (nargskw == 0) {
395-
return _PyObject_VectorcallTstate(tstate, pto->fn, pto_args,
402+
PyObject *ret = _PyObject_VectorcallTstate(tstate, pto->fn, pto_args,
396403
pto_nargs, NULL);
404+
Py_DECREF(args_ref);
405+
Py_DECREF(kw_ref);
406+
return ret;
397407
}
398408

399409
/* Use PY_VECTORCALL_ARGUMENTS_OFFSET to prepend a single
@@ -405,6 +415,8 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
405415
PyObject *ret = _PyObject_VectorcallTstate(tstate, pto->fn, newargs,
406416
nargs + 1, kwnames);
407417
newargs[0] = tmp;
418+
Py_DECREF(args_ref);
419+
Py_DECREF(kw_ref);
408420
return ret;
409421
}
410422
}
@@ -435,6 +447,8 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
435447
else {
436448
stack = PyMem_Malloc(init_stack_size * sizeof(PyObject *));
437449
if (stack == NULL) {
450+
Py_DECREF(args_ref);
451+
Py_DECREF(kw_ref);
438452
return PyErr_NoMemory();
439453
}
440454
}
@@ -457,13 +471,13 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
457471
for (Py_ssize_t i = 0; i < nkwds; ++i) {
458472
key = PyTuple_GET_ITEM(kwnames, i);
459473
val = args[nargs + i];
460-
int contains = PyDict_Contains(pto->kw, key);
474+
int contains = PyDict_Contains(kw_ref, key);
461475
if (contains < 0) {
462476
goto error;
463477
}
464478
else if (contains == 1) {
465479
if (pto_kw_merged == NULL) {
466-
pto_kw_merged = PyDict_Copy(pto->kw);
480+
pto_kw_merged = PyDict_Copy(kw_ref);
467481
if (pto_kw_merged == NULL) {
468482
goto error;
469483
}
@@ -496,7 +510,7 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
496510
/* Copy pto_keywords with overlapping call keywords merged
497511
* Note, tail is already coppied. */
498512
Py_ssize_t pos = 0, i = 0;
499-
PyObject *keyword_dict = n_merges ? pto_kw_merged : pto->kw;
513+
PyObject *keyword_dict = n_merges ? pto_kw_merged : kw_ref;
500514
Py_BEGIN_CRITICAL_SECTION(keyword_dict);
501515
while (PyDict_Next(keyword_dict, &pos, &key, &val)) {
502516
assert(i < pto_nkwds);
@@ -518,6 +532,8 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
518532
if (stack != small_stack) {
519533
PyMem_Free(stack);
520534
}
535+
Py_DECREF(args_ref);
536+
Py_DECREF(kw_ref);
521537
return PyErr_NoMemory();
522538
}
523539
stack = tmp_stack;
@@ -555,12 +571,16 @@ partial_vectorcall(PyObject *self, PyObject *const *args,
555571
if (pto_nkwds) {
556572
Py_DECREF(tot_kwnames);
557573
}
574+
Py_DECREF(args_ref);
575+
Py_DECREF(kw_ref);
558576
return ret;
559577

560578
error:
561579
if (stack != small_stack) {
562580
PyMem_Free(stack);
563581
}
582+
Py_DECREF(args_ref);
583+
Py_DECREF(kw_ref);
564584
return NULL;
565585
}
566586

0 commit comments

Comments
 (0)