@@ -3007,6 +3007,108 @@ _pyerr_setkeyerror(PyObject *self, PyObject *arg)
30073007 return NULL ;
30083008}
30093009
3010+ static PyObject *
3011+ test_thread_state_ensure_from_view_interp_switch (PyObject * self , PyObject * unused )
3012+ {
3013+ /* The main tstate is already attached and was NOT created by
3014+ * PyThreadState_Ensure, so delete_on_release == 0. */
3015+ PyInterpreterState * interp = _PyInterpreterState_GET ();
3016+ assert (interp != NULL );
3017+ PyInterpreterView * view = PyInterpreterView_FromCurrent ();
3018+ assert (view != NULL );
3019+
3020+ /* First Ensure/Release pair on this pre-existing tstate. */
3021+ assert (_PyThreadState_GET () != NULL );
3022+ PyThreadStateToken * t1 = PyThreadState_EnsureFromView (view );
3023+ assert (t1 != NULL );
3024+ assert (_PyInterpreterState_GuardCountdown (interp ) == 1 );
3025+ PyThreadState_Release (t1 );
3026+ assert (_PyInterpreterState_GuardCountdown (interp ) == 0 );
3027+ assert (_PyThreadState_GET () != NULL );
3028+
3029+ /* tstate->ensure.owned_guard now points at the freed guard. */
3030+
3031+ /* Re-attach: Bug B detaches us as a side effect (separate repro). */
3032+ PyThreadState * save = PyThreadState_Swap (NULL );
3033+
3034+ PyThreadStateToken * t2 = PyThreadState_EnsureFromView (view );
3035+ assert (_PyInterpreterState_GuardCountdown (interp ) == 1 );
3036+ assert (t2 != NULL );
3037+ PyThreadState_Release (t2 );
3038+ assert (_PyInterpreterState_GuardCountdown (interp ) == 0 );
3039+ assert (_PyThreadState_GET () == NULL );
3040+
3041+ PyThreadState_Swap (save );
3042+
3043+ /* In a release build (no assertion) the second Ensure silently
3044+ * skipped storing its guard and Release decremented the global
3045+ * counter from 0, wrapping it to GUARDS_NOT_ALLOWED. All future
3046+ * guard acquisitions then fail: */
3047+ PyInterpreterGuard * g = PyInterpreterGuard_FromCurrent ();
3048+ assert (g != NULL );
3049+ assert (_PyInterpreterState_GuardCountdown (interp ) == 1 );
3050+ PyInterpreterGuard_Close (g );
3051+ assert (_PyInterpreterState_GuardCountdown (interp ) == 0 );
3052+
3053+ PyInterpreterView_Close (view );
3054+ Py_RETURN_NONE ;
3055+ }
3056+
3057+ /* A capsule destructor that calls Ensure/Release while the tstate is being
3058+ * cleared by PyThreadState_Release. */
3059+ static void
3060+ capsule_destructor (PyObject * capsule )
3061+ {
3062+ assert (capsule != NULL );
3063+ PyInterpreterGuard * guard = PyCapsule_GetPointer (capsule , "x" );
3064+ PyThreadStateToken * token = PyThreadState_Ensure (guard );
3065+ assert (token != NULL );
3066+ PyThreadState_Release (token );
3067+ }
3068+
3069+ static void
3070+ thread_with_tstate_destructor (void * arg )
3071+ {
3072+ PyInterpreterGuard * guard = (PyInterpreterGuard * )arg ;
3073+
3074+ /* Triggers fresh tstate path */
3075+ PyThreadStateToken * token = PyThreadState_Ensure (guard );
3076+ assert (token != NULL );
3077+
3078+ /* Stash a capsule whose destructor will run during PyThreadState_Clear. */
3079+ PyObject * capsule = PyCapsule_New (guard , "x" , capsule_destructor );
3080+ assert (capsule != NULL );
3081+
3082+ /* We need to put it somewhere it gets cleaned up at PyThreadState_Clear.
3083+ * tstate->dict is cleared during PyThreadState_Clear. */
3084+ PyObject * dict = PyThreadState_GetDict ();
3085+ assert (dict != NULL );
3086+ int res = PyDict_SetItemString (dict , "key" , capsule );
3087+ assert (res == 0 );
3088+ Py_DECREF (capsule );
3089+
3090+ PyThreadState_Release (token );
3091+ PyInterpreterGuard_Close (guard );
3092+ }
3093+
3094+ static PyObject *
3095+ test_thread_state_release_with_destructor (PyObject * self , PyObject * unused )
3096+ {
3097+ PyInterpreterGuard * guard = PyInterpreterGuard_FromCurrent ();
3098+ assert (guard != NULL );
3099+
3100+ PyThread_handle_t handle ;
3101+ PyThread_ident_t ident ;
3102+ if (PyThread_start_joinable_thread (thread_with_tstate_destructor ,
3103+ guard , & ident , & handle ) < 0 ) {
3104+ PyInterpreterGuard_Close (guard );
3105+ return PyErr_NoMemory ();
3106+ }
3107+
3108+ PyThread_join_thread (handle );
3109+ Py_RETURN_NONE ;
3110+ }
3111+
30103112
30113113static PyMethodDef module_functions [] = {
30123114 {"get_configs" , get_configs , METH_NOARGS },
@@ -3134,6 +3236,7 @@ static PyMethodDef module_functions[] = {
31343236 {"_pyerr_setkeyerror ", _pyerr_setkeyerror , METH_O },
31353237 {"test_interp_guard_countdown ", test_interp_guard_countdown , METH_NOARGS },
31363238 {"test_interp_view_countdown ", test_interp_view_countdown , METH_NOARGS },
3239+ {"test_thread_state_ensure_from_view_interp_switch ", test_thread_state_ensure_from_view_interp_switch , METH_NOARGS },
31373240 {NULL, NULL } /* sentinel */
31383241};
31393242
0 commit comments