Skip to content

Commit 1dd28d0

Browse files
Fix segmentation fault caused by using libraries depending on pybind11 (#9)
1 parent 15af1c8 commit 1dd28d0

File tree

4 files changed

+70
-49
lines changed

4 files changed

+70
-49
lines changed

c_src/pythonx/python.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ DEF_SYMBOL(PyErr_Fetch)
3131
DEF_SYMBOL(PyErr_Occurred)
3232
DEF_SYMBOL(PyEval_GetBuiltins)
3333
DEF_SYMBOL(PyEval_EvalCode)
34+
DEF_SYMBOL(PyEval_RestoreThread)
3435
DEF_SYMBOL(PyEval_SaveThread)
3536
DEF_SYMBOL(PyFloat_AsDouble)
3637
DEF_SYMBOL(PyFloat_FromDouble)
37-
DEF_SYMBOL(PyGILState_Ensure)
38-
DEF_SYMBOL(PyGILState_Release)
3938
DEF_SYMBOL(PyImport_AddModule)
4039
DEF_SYMBOL(PyImport_ImportModule)
40+
DEF_SYMBOL(PyInterpreterState_Get)
4141
DEF_SYMBOL(PyIter_Next)
4242
DEF_SYMBOL(PyList_Append)
4343
DEF_SYMBOL(PyList_GetItem)
@@ -60,6 +60,7 @@ DEF_SYMBOL(PyObject_Str)
6060
DEF_SYMBOL(PySet_Add)
6161
DEF_SYMBOL(PySet_New)
6262
DEF_SYMBOL(PySet_Size)
63+
DEF_SYMBOL(PyThreadState_New)
6364
DEF_SYMBOL(PyTuple_GetItem)
6465
DEF_SYMBOL(PyTuple_New)
6566
DEF_SYMBOL(PyTuple_Pack)
@@ -70,7 +71,6 @@ DEF_SYMBOL(PyUnicode_FromStringAndSize)
7071
DEF_SYMBOL(Py_BuildValue)
7172
DEF_SYMBOL(Py_CompileString)
7273
DEF_SYMBOL(Py_DecRef)
73-
DEF_SYMBOL(Py_FinalizeEx)
7474
DEF_SYMBOL(Py_IncRef)
7575
DEF_SYMBOL(Py_InitializeEx)
7676
DEF_SYMBOL(Py_IsFalse)
@@ -105,13 +105,13 @@ void load_python_library(std::string path) {
105105
LOAD_SYMBOL(python_library, PyErr_Occurred)
106106
LOAD_SYMBOL(python_library, PyEval_GetBuiltins)
107107
LOAD_SYMBOL(python_library, PyEval_EvalCode)
108+
LOAD_SYMBOL(python_library, PyEval_RestoreThread)
108109
LOAD_SYMBOL(python_library, PyEval_SaveThread)
109110
LOAD_SYMBOL(python_library, PyFloat_AsDouble)
110111
LOAD_SYMBOL(python_library, PyFloat_FromDouble)
111-
LOAD_SYMBOL(python_library, PyGILState_Ensure)
112-
LOAD_SYMBOL(python_library, PyGILState_Release)
113112
LOAD_SYMBOL(python_library, PyImport_AddModule)
114113
LOAD_SYMBOL(python_library, PyImport_ImportModule)
114+
LOAD_SYMBOL(python_library, PyInterpreterState_Get)
115115
LOAD_SYMBOL(python_library, PyIter_Next)
116116
LOAD_SYMBOL(python_library, PyList_Append)
117117
LOAD_SYMBOL(python_library, PyList_GetItem)
@@ -134,6 +134,7 @@ void load_python_library(std::string path) {
134134
LOAD_SYMBOL(python_library, PySet_Add)
135135
LOAD_SYMBOL(python_library, PySet_New)
136136
LOAD_SYMBOL(python_library, PySet_Size)
137+
LOAD_SYMBOL(python_library, PyThreadState_New)
137138
LOAD_SYMBOL(python_library, PyTuple_GetItem)
138139
LOAD_SYMBOL(python_library, PyTuple_New)
139140
LOAD_SYMBOL(python_library, PyTuple_Pack)
@@ -144,7 +145,6 @@ void load_python_library(std::string path) {
144145
LOAD_SYMBOL(python_library, Py_BuildValue)
145146
LOAD_SYMBOL(python_library, Py_CompileString)
146147
LOAD_SYMBOL(python_library, Py_DecRef)
147-
LOAD_SYMBOL(python_library, Py_FinalizeEx)
148148
LOAD_SYMBOL(python_library, Py_IncRef)
149149
LOAD_SYMBOL(python_library, Py_InitializeEx)
150150
LOAD_SYMBOL(python_library, Py_IsFalse)

c_src/pythonx/python.hpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ namespace pythonx::python {
6262

6363
// Opaque types
6464

65+
using PyInterpreterStatePtr = void *;
6566
using PyObjectPtr = void *;
6667
using PyThreadStatePtr = void *;
67-
using PyGILState_STATE = unsigned char;
6868
using Py_ssize_t = ssize_t;
6969

7070
// Functions
@@ -85,13 +85,13 @@ extern void (*PyErr_Fetch)(PyObjectPtr *, PyObjectPtr *, PyObjectPtr *);
8585
extern PyObjectPtr (*PyErr_Occurred)();
8686
extern PyObjectPtr (*PyEval_GetBuiltins)();
8787
extern PyObjectPtr (*PyEval_EvalCode)(PyObjectPtr, PyObjectPtr, PyObjectPtr);
88+
extern void (*PyEval_RestoreThread)(PyThreadStatePtr);
8889
extern PyThreadStatePtr (*PyEval_SaveThread)();
8990
extern double (*PyFloat_AsDouble)(PyObjectPtr);
9091
extern PyObjectPtr (*PyFloat_FromDouble)(double);
91-
extern PyGILState_STATE (*PyGILState_Ensure)();
92-
extern void (*PyGILState_Release)(PyGILState_STATE);
9392
extern PyObjectPtr (*PyImport_AddModule)(const char *);
9493
extern PyObjectPtr (*PyImport_ImportModule)(const char *);
94+
extern PyInterpreterStatePtr (*PyInterpreterState_Get)();
9595
extern PyObjectPtr (*PyIter_Next)(PyObjectPtr);
9696
extern int (*PyList_Append)(PyObjectPtr, PyObjectPtr);
9797
extern PyObjectPtr (*PyList_GetItem)(PyObjectPtr, Py_ssize_t);
@@ -114,6 +114,7 @@ extern PyObjectPtr (*PyObject_Str)(PyObjectPtr);
114114
extern int (*PySet_Add)(PyObjectPtr, PyObjectPtr);
115115
extern PyObjectPtr (*PySet_New)(PyObjectPtr);
116116
extern Py_ssize_t (*PySet_Size)(PyObjectPtr);
117+
extern PyThreadStatePtr (*PyThreadState_New)(PyInterpreterStatePtr);
117118
extern PyObjectPtr (*PyTuple_GetItem)(PyObjectPtr, Py_ssize_t);
118119
extern PyObjectPtr (*PyTuple_New)(Py_ssize_t);
119120
extern PyObjectPtr (*PyTuple_Pack)(Py_ssize_t, ...);
@@ -124,7 +125,6 @@ extern PyObjectPtr (*PyUnicode_FromStringAndSize)(const char *, Py_ssize_t);
124125
extern PyObjectPtr (*Py_BuildValue)(const char *, ...);
125126
extern PyObjectPtr (*Py_CompileString)(const char *, const char *, int);
126127
extern void (*Py_DecRef)(PyObjectPtr);
127-
extern int (*Py_FinalizeEx)();
128128
extern void (*Py_IncRef)(PyObjectPtr);
129129
extern void (*Py_InitializeEx)(int);
130130
extern int (*Py_IsFalse)(PyObjectPtr);

c_src/pythonx/pythonx.cpp

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ std::wstring python_home_path_w;
2626
std::wstring python_executable_path_w;
2727
std::map<std::string, std::tuple<PyObjectPtr, PyObjectPtr>> compilation_cache;
2828
std::mutex compilation_cache_mutex;
29+
PyInterpreterStatePtr interpreter_state;
30+
std::map<std::thread::id, PyThreadStatePtr> thread_states;
31+
std::mutex thread_states_mutex;
2932

3033
// Wrapper around the Python Global Interpreter Lock (GIL).
3134
//
@@ -36,12 +39,63 @@ std::mutex compilation_cache_mutex;
3639
//
3740
// [1]: https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization
3841
class PyGILGuard {
39-
PyGILState_STATE state;
42+
// The simplest way to implement this guard is to use `PyGILState_Ensure`
43+
// and `PyGILState_Release`, however this can lead to segfaults when
44+
// using libraries depending on pybind11.
45+
//
46+
// pybind11 is a popular library for writing C extensions in Python
47+
// packages. It provies convenient C++ API on top of the Python C
48+
// API. In particular, it provides conveniences for dealing with
49+
// GIL, one of them being `gil_scoped_acquire`. The implementation
50+
// has a bug that results in a dangling pointer being used. This
51+
// bug only appears when the code runs in a non-main thread that
52+
// manages the `gil_scoped_acquire` checks if the calling thread
53+
// alread holds GIL with `PyGILState_Ensure` and `PyGILState_Release`.
54+
// Specifically, the GIL, in which case it stores the pointer to
55+
// the corresponding `PyThreadState`. After `PyGILState_Release`,
56+
// the thread state is freed, but subsequent usage of `gil_scoped_acquire`
57+
// still re-uses the pointer. This issues has been reported in [1].
58+
//
59+
// In our case, we evaluate Python code dirty scheduler threads.
60+
// This means that the threads are reused and we acquire the GIL
61+
// every time. In order to avoid the pybind11 bug, we want to avoid
62+
// using `PyGILState_Release`, and instead have a permanent `PyThreadState`
63+
// for each of the dirty scheduler threads. We do this by creating
64+
// new state when the given scheduler thread obtains the GIL for
65+
// the first time. Then, we use `PyEval_RestoreThread` and `PyEval_SaveThread`
66+
// to acquire and release the GIL respectively.
67+
//
68+
// NOTE: the dirty scheduler thread pool is fixed, so the map does
69+
// not grow beyond that. If we ever need to acquire the GIL from
70+
// other threads, we should extend this implementation to either
71+
// allow removing the state on destruction, or have a variant with
72+
// `PyGILState_Ensure` and `PyGILState_Release`, as long as it does
73+
// not fall into the bug described above.
74+
//
75+
// [1]: https://github.com/pybind/pybind11/issues/2888
4076

4177
public:
42-
PyGILGuard() { this->state = PyGILState_Ensure(); }
78+
PyGILGuard() {
79+
auto thread_id = std::this_thread::get_id();
80+
81+
PyThreadStatePtr state;
82+
83+
{
84+
auto guard = std::lock_guard<std::mutex>(thread_states_mutex);
85+
86+
if (thread_states.find(thread_id) == thread_states.end()) {
87+
// Note that PyThreadState_New does not require GIL to be held.
88+
state = PyThreadState_New(interpreter_state);
89+
thread_states[thread_id] = state;
90+
} else {
91+
state = thread_states[thread_id];
92+
}
93+
}
94+
95+
PyEval_RestoreThread(state);
96+
}
4397

44-
~PyGILGuard() { PyGILState_Release(this->state); }
98+
~PyGILGuard() { PyEval_SaveThread(); }
4599
};
46100

47101
// Ensures the given object refcount is decremented when the guard
@@ -275,6 +329,8 @@ fine::Ok<> init(ErlNifEnv *env, std::string python_dl_path,
275329

276330
Py_InitializeEx(0);
277331

332+
interpreter_state = PyInterpreterState_Get();
333+
278334
// In order to use any of the Python C API functions, the calling
279335
// thread must hold the GIL. Since every NIF call may run on a
280336
// different dirty scheduler thread, we need to acquire the GIL at
@@ -285,7 +341,7 @@ fine::Ok<> init(ErlNifEnv *env, std::string python_dl_path,
285341
// See pyo3 [1] for an extra reference.
286342
//
287343
// [1]: https://github.com/PyO3/pyo3/blob/v0.23.3/src/gil.rs#L63-L74
288-
PyEval_SaveThread();
344+
thread_states[std::this_thread::get_id()] = PyEval_SaveThread();
289345

290346
is_initialized = true;
291347

@@ -405,40 +461,6 @@ sys.stdin = Stdin()
405461

406462
FINE_NIF(init, ERL_NIF_DIRTY_JOB_CPU_BOUND);
407463

408-
// Note that this NIF is here for the reference, but currently we do
409-
// not support deinitialization. While in principle it should be
410-
// possible to reinitialize Python, it can lead to issues in practice.
411-
// For example, doing so while using numpy simply does not work, see
412-
// [1] for discussion points.
413-
//
414-
// [1]: https://bugs.python.org/issue34309
415-
fine::Ok<> terminate(ErlNifEnv *env) {
416-
ensure_initialized();
417-
418-
auto init_guard = std::lock_guard<std::mutex>(init_mutex);
419-
420-
// Here we only acquire the GIL, since releasing after finalization
421-
// makes no sense
422-
PyGILState_Ensure();
423-
424-
if (Py_FinalizeEx() == -1) {
425-
throw std::runtime_error("failed to finalize Python interpreter");
426-
}
427-
428-
is_initialized = false;
429-
430-
auto compilation_cache_guard =
431-
std::lock_guard<std::mutex>(compilation_cache_mutex);
432-
compilation_cache.clear();
433-
434-
// Raises runtime error on failure, which is propagated automatically
435-
unload_python_library();
436-
437-
return fine::Ok<>();
438-
}
439-
440-
FINE_NIF(terminate, ERL_NIF_DIRTY_JOB_CPU_BOUND);
441-
442464
fine::Ok<> janitor_decref(ErlNifEnv *env, uint64_t ptr) {
443465
auto init_guard = std::lock_guard<std::mutex>(init_mutex);
444466

lib/pythonx/nif.ex

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ defmodule Pythonx.NIF do
1313
end
1414

1515
def init(_python_dl_path, _python_home_path, _python_executable_path, _sys_paths), do: err!()
16-
def terminate(), do: err!()
1716
def janitor_decref(_ptr), do: err!()
1817
def none_new(), do: err!()
1918
def false_new(), do: err!()

0 commit comments

Comments
 (0)