@@ -192,6 +192,19 @@ void PythonState::operator<<(const PyThreadState *const tstate) noexcept
192192 // ``greenlet.tests.test_greenlet_trash`` tries, but under 3.14,
193193 // at least, fails to do so.
194194 this ->delete_later = Py_XNewRef (tstate->delete_later );
195+ #ifdef Py_GIL_DISABLED
196+ // Switching greenlets swaps C stacks, which to the free-threaded runtime is
197+ // the same predicament as detaching the thread: the PyCriticalSection nodes
198+ // chained off tstate->critical_section live on the stack we're leaving, and
199+ // their PyMutexes would stay locked behind our back. The greenlet we switch
200+ // to could then block forever taking one of those same locks -- e.g. an
201+ // asyncio event dispatched onto another fiber re-enters a Task/Future that
202+ // the suspended fiber is mid-step on. So drop the locks here the way
203+ // _PyThreadState_Detach() does and let operator>> re-take them on resume.
204+ if (tstate->critical_section != 0 ) {
205+ _PyCriticalSection_SuspendAll (const_cast <PyThreadState*>(tstate));
206+ }
207+ #endif
195208 this ->critical_section = tstate->critical_section ;
196209 #elif GREENLET_PY312
197210 this ->trash_delete_nesting = tstate->trash .delete_nesting ;
@@ -301,6 +314,16 @@ void PythonState::operator>>(PyThreadState *const tstate) noexcept
301314 Py_CLEAR (this ->delete_later );
302315 }
303316 tstate->critical_section = this ->critical_section ;
317+ #ifdef Py_GIL_DISABLED
318+ // Re-acquire whatever operator<< suspended when this greenlet last yielded.
319+ // A no-op for a greenlet that held no locks, and for a brand-new one whose
320+ // chain starts empty. Mirrors the resume in _PyThreadState_Attach(); note
321+ // _PyCriticalSection_Resume() dereferences the head, so the != 0 guard is
322+ // load-bearing, not just a fast path.
323+ if (tstate->critical_section != 0 ) {
324+ _PyCriticalSection_Resume (tstate);
325+ }
326+ #endif
304327
305328 #elif GREENLET_PY312
306329 tstate->trash .delete_nesting = this ->trash_delete_nesting ;
0 commit comments