@@ -147,6 +147,33 @@ static PyObject *get_current_process_error(void) {
147147 return exc_class ;
148148}
149149
150+ /**
151+ * Get the SuspensionRequired exception class from the current interpreter's
152+ * erlang module. Under OWN_GIL subinterpreters each interpreter has its own
153+ * erlang module/class, so raising the process-global object (which belongs to
154+ * whichever interpreter initialized last) is cross-interpreter UB. Mirrors
155+ * get_current_process_error().
156+ */
157+ static PyObject * get_current_suspension_required (void ) {
158+ PyObject * erlang_module = PyImport_ImportModule ("erlang" );
159+ if (erlang_module == NULL ) {
160+ PyErr_Clear ();
161+ return SuspensionRequiredException ; /* Fallback to global */
162+ }
163+
164+ PyObject * exc_class = PyObject_GetAttrString (erlang_module , "SuspensionRequired" );
165+ Py_DECREF (erlang_module );
166+
167+ if (exc_class == NULL ) {
168+ PyErr_Clear ();
169+ return SuspensionRequiredException ; /* Fallback to global */
170+ }
171+
172+ /* See get_current_process_error: decref and rely on the module keeping it alive. */
173+ Py_DECREF (exc_class );
174+ return exc_class ;
175+ }
176+
150177/* ============================================================================
151178 * Callback Name Registry
152179 *
@@ -399,6 +426,14 @@ static suspended_state_t *create_suspended_state_ex(
399426 } else {
400427 state -> worker = source -> data .existing -> worker ;
401428 }
429+ /* Keep the worker resource alive for as long as the suspended state exists.
430+ * Without this the worker can be GC'd while a callback is suspended, and
431+ * nif_resume_callback_dirty would dereference a freed worker (use-after-free
432+ * with the GIL held). Mirrors the enif_keep_resource(ctx) on the context path;
433+ * suspended_state_destructor releases it. */
434+ if (state -> worker != NULL ) {
435+ enif_keep_resource (state -> worker );
436+ }
402437
403438 state -> callback_id = PyLong_AsUnsignedLongLong (callback_id_obj );
404439
@@ -977,7 +1012,7 @@ static PyObject *decode_etf_string(const char *str, Py_ssize_t len) {
9771012
9781013 /* Decode the ETF binary to an Erlang term */
9791014 ERL_NIF_TERM term ;
980- if (enif_binary_to_term (tmp_env , (unsigned char * )bin_data , bin_len , & term , 0 ) == 0 ) {
1015+ if (enif_binary_to_term (tmp_env , (unsigned char * )bin_data , bin_len , & term , ERL_NIF_BIN2TERM_SAFE ) == 0 ) {
9811016 /* Decoding failed */
9821017 enif_free_env (tmp_env );
9831018 Py_DECREF (decoded );
@@ -2109,7 +2144,7 @@ static PyObject *erlang_call_impl(PyObject *self, PyObject *args) {
21092144 Py_XSETREF (tl_pending_args , call_args );
21102145
21112146 /* Raise exception to abort Python execution */
2112- PyErr_SetString (SuspensionRequiredException , "callback pending" );
2147+ PyErr_SetString (get_current_suspension_required () , "callback pending" );
21132148 return NULL ;
21142149}
21152150
@@ -2859,7 +2894,7 @@ static PyObject *erlang_channel_try_receive_impl(PyObject *self, PyObject *args)
28592894 }
28602895
28612896 ERL_NIF_TERM term ;
2862- if (enif_binary_to_term (tmp_env , data , size , & term , 0 ) == 0 ) {
2897+ if (enif_binary_to_term (tmp_env , data , size , & term , ERL_NIF_BIN2TERM_SAFE ) == 0 ) {
28632898 enif_free (data );
28642899 enif_free_env (tmp_env );
28652900 PyErr_SetString (PyExc_RuntimeError , "failed to decode term" );
@@ -2939,7 +2974,7 @@ static PyObject *erlang_channel_receive_impl(PyObject *self, PyObject *args) {
29392974 }
29402975
29412976 ERL_NIF_TERM term ;
2942- if (enif_binary_to_term (tmp_env , data , size , & term , 0 ) == 0 ) {
2977+ if (enif_binary_to_term (tmp_env , data , size , & term , ERL_NIF_BIN2TERM_SAFE ) == 0 ) {
29432978 enif_free (data );
29442979 enif_free_env (tmp_env );
29452980 PyErr_SetString (PyExc_RuntimeError , "failed to decode term" );
@@ -3251,7 +3286,7 @@ static PyObject *erlang_channel_wait_impl(PyObject *self, PyObject *args) {
32513286 }
32523287
32533288 ERL_NIF_TERM term ;
3254- if (enif_binary_to_term (tmp_env , data , msg_size , & term , 0 ) == 0 ) {
3289+ if (enif_binary_to_term (tmp_env , data , msg_size , & term , ERL_NIF_BIN2TERM_SAFE ) == 0 ) {
32553290 enif_free (data );
32563291 enif_free_env (tmp_env );
32573292 PyErr_SetString (PyExc_RuntimeError , "failed to decode term" );
@@ -4316,7 +4351,14 @@ static ERL_NIF_TERM nif_resume_callback(ErlNifEnv *env, int argc, const ERL_NIF_
43164351 /* Store the result in the suspended state */
43174352 pthread_mutex_lock (& state -> mutex );
43184353
4319- /* Copy result data */
4354+ /* Copy result data. Free any prior result first: a duplicate/raced resume
4355+ * would otherwise leak the previous buffer. (has_result is not a one-shot
4356+ * flag -- it toggles during nested replay -- so result_data is the real
4357+ * pending-result indicator.) */
4358+ if (state -> result_data != NULL ) {
4359+ enif_free (state -> result_data );
4360+ state -> result_data = NULL ;
4361+ }
43204362 state -> result_data = enif_alloc (result_bin .size );
43214363 if (state -> result_data == NULL ) {
43224364 pthread_mutex_unlock (& state -> mutex );
@@ -4364,6 +4406,12 @@ static ERL_NIF_TERM nif_resume_callback_dirty(ErlNifEnv *env, int argc, const ER
43644406 return make_error (env , "no_result" );
43654407 }
43664408
4409+ /* The worker is kept alive for the lifetime of the suspended state, but
4410+ * guard rather than dereference NULL in the replay below. */
4411+ if (state -> worker == NULL ) {
4412+ return make_error (env , "no_worker" );
4413+ }
4414+
43674415 /* Set up thread-local state for replay */
43684416 tl_current_worker = state -> worker ;
43694417 tl_callback_env = env ;
@@ -4430,6 +4478,11 @@ static ERL_NIF_TERM nif_resume_callback_dirty(ErlNifEnv *env, int argc, const ER
44304478 }
44314479
44324480 PyObject * args = PyTuple_New (args_len );
4481+ if (args == NULL ) {
4482+ Py_DECREF (func );
4483+ result = make_error (env , "alloc_failed" );
4484+ goto call_cleanup ;
4485+ }
44334486 ERL_NIF_TERM head , tail = state -> orig_args ;
44344487 for (unsigned int i = 0 ; i < args_len ; i ++ ) {
44354488 enif_get_list_cell (state -> orig_env , tail , & head , & tail );
0 commit comments