Skip to content

Commit 08010fa

Browse files
committed
Fix Python 3.13+ thread state crash in GIL acquisition
Python 3.13+ crashes when PyGILState_Ensure() is called on a thread that has a detached (but still associated) thread state from PyEval_SaveThread(). Add safe_gil_acquire/release helpers that detect existing thread states and use PyEval_RestoreThread instead of creating new ones. Replace all PyGILState_Ensure/Release calls with the safe versions.
1 parent 6953372 commit 08010fa

7 files changed

Lines changed: 165 additions & 93 deletions

File tree

c_src/py_asgi.c

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,20 +1151,20 @@ static ERL_NIF_TERM nif_asgi_build_scope(ErlNifEnv *env, int argc, const ERL_NIF
11511151
return make_error(env, "asgi_not_initialized");
11521152
}
11531153

1154-
PyGILState_STATE gstate = PyGILState_Ensure();
1154+
safe_gil_guard_t guard = safe_gil_acquire();
11551155

11561156
PyObject *scope = asgi_scope_from_map(env, argv[0]);
11571157
if (scope == NULL) {
11581158
ERL_NIF_TERM error = make_py_error(env);
1159-
PyGILState_Release(gstate);
1159+
safe_gil_release(guard);
11601160
return error;
11611161
}
11621162

11631163
/* Wrap scope in a resource to return to Erlang */
11641164
py_object_t *wrapper = enif_alloc_resource(PYOBJ_RESOURCE_TYPE, sizeof(py_object_t));
11651165
if (wrapper == NULL) {
11661166
Py_DECREF(scope);
1167-
PyGILState_Release(gstate);
1167+
safe_gil_release(guard);
11681168
return make_error(env, "alloc_failed");
11691169
}
11701170

@@ -1173,7 +1173,7 @@ static ERL_NIF_TERM nif_asgi_build_scope(ErlNifEnv *env, int argc, const ERL_NIF
11731173
ERL_NIF_TERM result = enif_make_resource(env, wrapper);
11741174
enif_release_resource(wrapper);
11751175

1176-
PyGILState_Release(gstate);
1176+
safe_gil_release(guard);
11771177

11781178
return enif_make_tuple2(env, ATOM_OK, result);
11791179
}
@@ -1203,7 +1203,7 @@ static ERL_NIF_TERM nif_asgi_run(ErlNifEnv *env, int argc, const ERL_NIF_TERM ar
12031203
return make_error(env, "invalid_callable");
12041204
}
12051205

1206-
PyGILState_STATE gstate = PyGILState_Ensure();
1206+
safe_gil_guard_t guard = safe_gil_acquire();
12071207

12081208
ERL_NIF_TERM result;
12091209

@@ -1215,7 +1215,7 @@ static ERL_NIF_TERM nif_asgi_run(ErlNifEnv *env, int argc, const ERL_NIF_TERM ar
12151215
enif_free(runner_name);
12161216
enif_free(module_name);
12171217
enif_free(callable_name);
1218-
PyGILState_Release(gstate);
1218+
safe_gil_release(guard);
12191219
return make_error(env, "alloc_failed");
12201220
}
12211221

@@ -1303,7 +1303,7 @@ static ERL_NIF_TERM nif_asgi_run(ErlNifEnv *env, int argc, const ERL_NIF_TERM ar
13031303
enif_free(runner_name);
13041304
enif_free(module_name);
13051305
enif_free(callable_name);
1306-
PyGILState_Release(gstate);
1306+
safe_gil_release(guard);
13071307

13081308
return result;
13091309
}

c_src/py_callback.c

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,13 +1643,13 @@ static void *async_event_loop_thread(void *arg) {
16431643
py_async_worker_t *worker = (py_async_worker_t *)arg;
16441644

16451645
/* Acquire GIL for this thread */
1646-
PyGILState_STATE gstate = PyGILState_Ensure();
1646+
safe_gil_guard_t guard = safe_gil_acquire();
16471647

16481648
/* Import asyncio */
16491649
PyObject *asyncio = PyImport_ImportModule("asyncio");
16501650
if (asyncio == NULL) {
16511651
PyErr_Print();
1652-
PyGILState_Release(gstate);
1652+
safe_gil_release(guard);
16531653
worker->loop_running = false;
16541654
return NULL;
16551655
}
@@ -1659,7 +1659,7 @@ static void *async_event_loop_thread(void *arg) {
16591659
if (loop == NULL) {
16601660
PyErr_Print();
16611661
Py_DECREF(asyncio);
1662-
PyGILState_Release(gstate);
1662+
safe_gil_release(guard);
16631663
worker->loop_running = false;
16641664
return NULL;
16651665
}
@@ -1778,7 +1778,7 @@ static void *async_event_loop_thread(void *arg) {
17781778
Py_DECREF(loop);
17791779

17801780
worker->loop_running = false;
1781-
PyGILState_Release(gstate);
1781+
safe_gil_release(guard);
17821782

17831783
return NULL;
17841784
}
@@ -1883,7 +1883,7 @@ static ERL_NIF_TERM nif_resume_callback_dirty(ErlNifEnv *env, int argc, const ER
18831883
memcpy(func_name, state->orig_func.data, state->orig_func.size);
18841884
func_name[state->orig_func.size] = '\0';
18851885

1886-
PyGILState_STATE gstate = PyGILState_Ensure();
1886+
safe_gil_guard_t guard = safe_gil_acquire();
18871887

18881888
PyObject *func = NULL;
18891889

@@ -1983,7 +1983,7 @@ static ERL_NIF_TERM nif_resume_callback_dirty(ErlNifEnv *env, int argc, const ER
19831983
}
19841984

19851985
call_cleanup:
1986-
PyGILState_Release(gstate);
1986+
safe_gil_release(guard);
19871987
enif_free(module_name);
19881988
enif_free(func_name);
19891989

@@ -1997,7 +1997,7 @@ static ERL_NIF_TERM nif_resume_callback_dirty(ErlNifEnv *env, int argc, const ER
19971997
memcpy(code, state->orig_code.data, state->orig_code.size);
19981998
code[state->orig_code.size] = '\0';
19991999

2000-
PyGILState_STATE gstate = PyGILState_Ensure();
2000+
safe_gil_guard_t guard = safe_gil_acquire();
20012001

20022002
/* Update locals if provided */
20032003
if (enif_is_map(state->orig_env, state->orig_locals)) {
@@ -2051,7 +2051,7 @@ static ERL_NIF_TERM nif_resume_callback_dirty(ErlNifEnv *env, int argc, const ER
20512051
}
20522052
}
20532053

2054-
PyGILState_Release(gstate);
2054+
safe_gil_release(guard);
20552055
enif_free(code);
20562056

20572057
} else {

c_src/py_event_loop.c

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -90,37 +90,15 @@ static const char *EVENT_LOOP_ATTR_NAME = "_loop";
9090
* GIL Acquisition Helpers
9191
* ============================================================================
9292
*
93-
* Python 3.13+ is stricter about PyGILState_Ensure() - it will abort if
94-
* called when the current thread already has a thread state attached.
95-
* These helpers safely acquire/release the GIL only when needed.
93+
* Use safe_gil_acquire() / safe_gil_release() from py_nif.h for
94+
* Python 3.13+ compatible GIL management.
95+
*
96+
* Local aliases for backward compatibility within this file.
9697
*/
9798

98-
typedef struct {
99-
PyGILState_STATE gstate;
100-
int already_held;
101-
} gil_guard_t;
102-
103-
/**
104-
* Safely acquire the GIL if we don't already hold it.
105-
* Returns a gil_guard_t that must be passed to gil_release().
106-
*/
107-
static gil_guard_t gil_acquire(void) {
108-
gil_guard_t guard;
109-
guard.already_held = PyGILState_Check();
110-
if (!guard.already_held) {
111-
guard.gstate = PyGILState_Ensure();
112-
}
113-
return guard;
114-
}
115-
116-
/**
117-
* Release the GIL if we acquired it.
118-
*/
119-
static void gil_release(gil_guard_t guard) {
120-
if (!guard.already_held) {
121-
PyGILState_Release(guard.gstate);
122-
}
123-
}
99+
typedef safe_gil_guard_t gil_guard_t;
100+
#define gil_acquire() safe_gil_acquire()
101+
#define gil_release(guard) safe_gil_release(guard)
124102

125103
/**
126104
* Get the py_event_loop module for the current interpreter.

c_src/py_exec.c

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,7 @@ static void *executor_thread_main(void *arg) {
653653
(void)arg;
654654

655655
/* Acquire GIL for this thread */
656-
PyGILState_STATE gstate = PyGILState_Ensure();
656+
safe_gil_guard_t guard = safe_gil_acquire();
657657

658658
g_executor_running = true;
659659

@@ -712,7 +712,7 @@ static void *executor_thread_main(void *arg) {
712712
}
713713

714714
g_executor_running = false;
715-
PyGILState_Release(gstate);
715+
safe_gil_release(guard);
716716

717717
return NULL;
718718
}
@@ -727,9 +727,9 @@ static void executor_enqueue(py_request_t *req) {
727727
case PY_MODE_FREE_THREADED:
728728
/* Execute directly in free-threaded mode - no executor needed */
729729
{
730-
PyGILState_STATE gstate = PyGILState_Ensure();
730+
safe_gil_guard_t guard = safe_gil_acquire();
731731
process_request(req);
732-
PyGILState_Release(gstate);
732+
safe_gil_release(guard);
733733
/* Signal completion immediately */
734734
pthread_mutex_lock(&req->mutex);
735735
req->completed = true;
@@ -841,7 +841,7 @@ static void *multi_executor_thread_main(void *arg) {
841841
executor_t *exec = (executor_t *)arg;
842842

843843
/* Acquire GIL for this thread */
844-
PyGILState_STATE gstate = PyGILState_Ensure();
844+
safe_gil_guard_t guard = safe_gil_acquire();
845845

846846
exec->running = true;
847847

@@ -890,7 +890,7 @@ static void *multi_executor_thread_main(void *arg) {
890890
}
891891

892892
exec->running = false;
893-
PyGILState_Release(gstate);
893+
safe_gil_release(guard);
894894

895895
return NULL;
896896
}

0 commit comments

Comments
 (0)