@@ -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