-
-
Notifications
You must be signed in to change notification settings - Fork 32.1k
gh-126907: Lock the atexit
state on the free-threaded build
#126908
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b8e4ae8
d54bca9
0601a6d
2fcee90
9eeea2e
60bb43f
fd8a0df
cca1bd5
e4ce835
d1acb86
801c4f1
a48f2ac
9688361
826dbe3
48c1b11
a2d4afa
5cc26fe
32e7415
11f7e16
9a8edf9
f4beed7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
Fix crash when using :mod:`atexit` concurrently on the :term:`free-threaded | ||
<free threading>` build. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,18 @@ | |
#include "pycore_interp.h" // PyInterpreterState.atexit | ||
#include "pycore_pystate.h" // _PyInterpreterState_GET | ||
|
||
#ifdef Py_GIL_DISABLED | ||
# define _PyAtExit_LOCK(state) PyMutex_Lock(&state->lock); | ||
# define _PyAtExit_UNLOCK(state) PyMutex_Unlock(&state->lock); | ||
# define _PyAtExit_ASSERT_LOCKED(state) assert(PyMutex_IsLocked(&state->lock)); | ||
# define _PyAtExit_ASSERT_UNLOCKED(state) assert(!PyMutex_IsLocked(&state->lock)); | ||
#else | ||
# define _PyAtExit_LOCK(state) | ||
# define _PyAtExit_UNLOCK(state) | ||
# define _PyAtExit_ASSERT_LOCKED(state) | ||
# define _PyAtExit_ASSERT_UNLOCKED(state) | ||
#endif | ||
|
||
/* ===================================================================== */ | ||
/* Callback machinery. */ | ||
|
||
|
@@ -38,41 +50,49 @@ PyUnstable_AtExit(PyInterpreterState *interp, | |
callback->next = NULL; | ||
|
||
struct atexit_state *state = &interp->atexit; | ||
_PyAtExit_LOCK(state); | ||
if (state->ll_callbacks == NULL) { | ||
state->ll_callbacks = callback; | ||
state->last_ll_callback = callback; | ||
} | ||
else { | ||
state->last_ll_callback->next = callback; | ||
ZeroIntensity marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
_PyAtExit_UNLOCK(state); | ||
return 0; | ||
} | ||
|
||
|
||
static void | ||
atexit_delete_cb(struct atexit_state *state, int i) | ||
atexit_delete_cb_locked(struct atexit_state *state, int i) | ||
{ | ||
atexit_py_callback *cb = state->callbacks[i]; | ||
state->callbacks[i] = NULL; | ||
|
||
// Finalizers might be re-entrant. Technically, we don't need | ||
// the lock anymore, but the caller assumes that it still holds the lock. | ||
_PyAtExit_UNLOCK(state); | ||
|
||
Py_DECREF(cb->func); | ||
Py_DECREF(cb->args); | ||
Py_XDECREF(cb->kwargs); | ||
PyMem_Free(cb); | ||
|
||
_PyAtExit_LOCK(state); | ||
} | ||
|
||
|
||
/* Clear all callbacks without calling them */ | ||
static void | ||
atexit_cleanup(struct atexit_state *state) | ||
atexit_cleanup_locked(struct atexit_state *state) | ||
{ | ||
atexit_py_callback *cb; | ||
for (int i = 0; i < state->ncallbacks; i++) { | ||
cb = state->callbacks[i]; | ||
if (cb == NULL) | ||
continue; | ||
|
||
atexit_delete_cb(state, i); | ||
atexit_delete_cb_locked(state, i); | ||
} | ||
state->ncallbacks = 0; | ||
} | ||
|
@@ -84,6 +104,7 @@ _PyAtExit_Init(PyInterpreterState *interp) | |
struct atexit_state *state = &interp->atexit; | ||
// _PyAtExit_Init() must only be called once | ||
assert(state->callbacks == NULL); | ||
_PyAtExit_ASSERT_UNLOCKED(state); | ||
|
||
state->callback_len = 32; | ||
state->ncallbacks = 0; | ||
|
@@ -99,7 +120,12 @@ void | |
_PyAtExit_Fini(PyInterpreterState *interp) | ||
{ | ||
struct atexit_state *state = &interp->atexit; | ||
atexit_cleanup(state); | ||
// Only one thread can call this, but atexit_cleanup_locked() assumes | ||
// that the lock is held, so let's hold it anyway. | ||
_PyAtExit_ASSERT_UNLOCKED(state); | ||
_PyAtExit_LOCK(state); | ||
atexit_cleanup_locked(state); | ||
ZeroIntensity marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_PyAtExit_UNLOCK(state); | ||
PyMem_Free(state->callbacks); | ||
state->callbacks = NULL; | ||
|
||
|
@@ -118,8 +144,9 @@ _PyAtExit_Fini(PyInterpreterState *interp) | |
|
||
|
||
static void | ||
atexit_callfuncs(struct atexit_state *state) | ||
atexit_callfuncs_locked(struct atexit_state *state) | ||
{ | ||
_PyAtExit_ASSERT_LOCKED(state); | ||
assert(!PyErr_Occurred()); | ||
|
||
if (state->ncallbacks == 0) { | ||
|
@@ -133,20 +160,26 @@ atexit_callfuncs(struct atexit_state *state) | |
} | ||
|
||
// bpo-46025: Increment the refcount of cb->func as the call itself may unregister it | ||
ZeroIntensity marked this conversation as resolved.
Show resolved
Hide resolved
|
||
PyObject* the_func = Py_NewRef(cb->func); | ||
PyObject *res = PyObject_Call(cb->func, cb->args, cb->kwargs); | ||
// No need to hold a strong reference to the arguments though | ||
PyObject *func = Py_NewRef(cb->func); | ||
PyObject *args = cb->args; | ||
PyObject *kwargs = cb->kwargs; | ||
|
||
// Unlock for re-entrancy problems | ||
_PyAtExit_UNLOCK(state); | ||
picnixz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
PyObject *res = PyObject_Call(func, args, kwargs); | ||
if (res == NULL) { | ||
PyErr_FormatUnraisable( | ||
"Exception ignored in atexit callback %R", the_func); | ||
"Exception ignored in atexit callback %R", func); | ||
} | ||
else { | ||
Py_DECREF(res); | ||
} | ||
Py_DECREF(the_func); | ||
Py_DECREF(func); | ||
_PyAtExit_LOCK(state); | ||
} | ||
|
||
atexit_cleanup(state); | ||
|
||
atexit_cleanup_locked(state); | ||
assert(!PyErr_Occurred()); | ||
} | ||
|
||
|
@@ -155,7 +188,9 @@ void | |
_PyAtExit_Call(PyInterpreterState *interp) | ||
{ | ||
struct atexit_state *state = &interp->atexit; | ||
atexit_callfuncs(state); | ||
_PyAtExit_LOCK(state); | ||
atexit_callfuncs_locked(state); | ||
_PyAtExit_UNLOCK(state); | ||
} | ||
|
||
|
||
|
@@ -192,31 +227,40 @@ atexit_register(PyObject *module, PyObject *args, PyObject *kwargs) | |
} | ||
|
||
struct atexit_state *state = get_atexit_state(); | ||
/* (Peter Bierma/ZeroIntensity) In theory, we could hold the | ||
* lock for a shorter amount of time using some fancy compare-exchanges | ||
* with the length. However, I'm lazy. | ||
*/ | ||
_PyAtExit_LOCK(state); | ||
if (state->ncallbacks >= state->callback_len) { | ||
atexit_py_callback **r; | ||
state->callback_len += 16; | ||
size_t size = sizeof(atexit_py_callback*) * (size_t)state->callback_len; | ||
r = (atexit_py_callback**)PyMem_Realloc(state->callbacks, size); | ||
if (r == NULL) { | ||
_PyAtExit_UNLOCK(state); | ||
ZeroIntensity marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return PyErr_NoMemory(); | ||
} | ||
state->callbacks = r; | ||
} | ||
|
||
atexit_py_callback *callback = PyMem_Malloc(sizeof(atexit_py_callback)); | ||
if (callback == NULL) { | ||
_PyAtExit_UNLOCK(state); | ||
return PyErr_NoMemory(); | ||
} | ||
|
||
callback->args = PyTuple_GetSlice(args, 1, PyTuple_GET_SIZE(args)); | ||
if (callback->args == NULL) { | ||
PyMem_Free(callback); | ||
_PyAtExit_UNLOCK(state); | ||
return NULL; | ||
} | ||
callback->func = Py_NewRef(func); | ||
callback->kwargs = Py_XNewRef(kwargs); | ||
|
||
state->callbacks[state->ncallbacks++] = callback; | ||
_PyAtExit_UNLOCK(state); | ||
|
||
return Py_NewRef(func); | ||
} | ||
|
@@ -233,7 +277,9 @@ static PyObject * | |
atexit_run_exitfuncs(PyObject *module, PyObject *unused) | ||
{ | ||
struct atexit_state *state = get_atexit_state(); | ||
atexit_callfuncs(state); | ||
_PyAtExit_LOCK(state); | ||
atexit_callfuncs_locked(state); | ||
_PyAtExit_UNLOCK(state); | ||
Py_RETURN_NONE; | ||
} | ||
|
||
|
@@ -246,7 +292,10 @@ Clear the list of previously registered exit functions."); | |
static PyObject * | ||
atexit_clear(PyObject *module, PyObject *unused) | ||
{ | ||
atexit_cleanup(get_atexit_state()); | ||
struct atexit_state *state = get_atexit_state(); | ||
_PyAtExit_LOCK(state); | ||
atexit_cleanup_locked(state); | ||
_PyAtExit_UNLOCK(state); | ||
Py_RETURN_NONE; | ||
} | ||
|
||
|
@@ -260,7 +309,10 @@ static PyObject * | |
atexit_ncallbacks(PyObject *module, PyObject *unused) | ||
{ | ||
struct atexit_state *state = get_atexit_state(); | ||
return PyLong_FromSsize_t(state->ncallbacks); | ||
_PyAtExit_LOCK(state); | ||
PyObject *res = PyLong_FromSsize_t(state->ncallbacks); | ||
_PyAtExit_UNLOCK(state); | ||
return res; | ||
} | ||
|
||
PyDoc_STRVAR(atexit_unregister__doc__, | ||
|
@@ -276,21 +328,33 @@ static PyObject * | |
atexit_unregister(PyObject *module, PyObject *func) | ||
{ | ||
struct atexit_state *state = get_atexit_state(); | ||
_PyAtExit_LOCK(state); | ||
for (int i = 0; i < state->ncallbacks; i++) | ||
{ | ||
atexit_py_callback *cb = state->callbacks[i]; | ||
if (cb == NULL) { | ||
continue; | ||
} | ||
PyObject *to_compare = cb->func; | ||
|
||
int eq = PyObject_RichCompareBool(cb->func, func, Py_EQ); | ||
// Unlock for custom __eq__ causing re-entrancy | ||
_PyAtExit_UNLOCK(state); | ||
int eq = PyObject_RichCompareBool(to_compare, func, Py_EQ); | ||
if (eq < 0) { | ||
return NULL; | ||
} | ||
_PyAtExit_LOCK(state); | ||
if (state->callbacks[i] == NULL) | ||
Comment on lines
+346
to
+347
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The explicit unlocking means that this still has re-entrancy and concurrency issues if the callback list is modified during the comparison. Using critical sections doesn't completely avoid the problem if |
||
{ | ||
// Edge case: another thread might have | ||
// unregistered the function while we released the lock. | ||
continue; | ||
} | ||
if (eq) { | ||
atexit_delete_cb(state, i); | ||
atexit_delete_cb_locked(state, i); | ||
} | ||
} | ||
_PyAtExit_UNLOCK(state); | ||
Py_RETURN_NONE; | ||
} | ||
|
||
|
Uh oh!
There was an error while loading. Please reload this page.