From b1977fbfba101157924900752a3b18a5886f81cb Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 13 Dec 2024 18:32:48 -0500 Subject: [PATCH 01/19] Switch callbacks to a Python list. --- Include/internal/pycore_atexit.h | 13 +++---- Modules/atexitmodule.c | 60 +++++++++++--------------------- 2 files changed, 24 insertions(+), 49 deletions(-) diff --git a/Include/internal/pycore_atexit.h b/Include/internal/pycore_atexit.h index cde5b530baf00c..4e79daed388e4b 100644 --- a/Include/internal/pycore_atexit.h +++ b/Include/internal/pycore_atexit.h @@ -36,21 +36,16 @@ typedef struct atexit_callback { struct atexit_callback *next; } atexit_callback; -typedef struct { - PyObject *func; - PyObject *args; - PyObject *kwargs; -} atexit_py_callback; - struct atexit_state { atexit_callback *ll_callbacks; // XXX The rest of the state could be moved to the atexit module state // and a low-level callback added for it during module exec. // For the moment we leave it here. - atexit_py_callback **callbacks; - int ncallbacks; - int callback_len; + + // List containing tuples with callback information. + // e.g. [(func, args, kwargs), ...] + PyObject *callbacks; }; // Export for '_interpchannels' shared extension diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index c009235b7a36c2..b1488dd6e39dcb 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -53,32 +53,11 @@ PyUnstable_AtExit(PyInterpreterState *interp, } -static void -atexit_delete_cb(struct atexit_state *state, int i) -{ - atexit_py_callback *cb = state->callbacks[i]; - state->callbacks[i] = NULL; - - Py_DECREF(cb->func); - Py_DECREF(cb->args); - Py_XDECREF(cb->kwargs); - PyMem_Free(cb); -} - - /* Clear all callbacks without calling them */ static void atexit_cleanup(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); - } - state->ncallbacks = 0; + PyList_Clear(state->callbacks); } @@ -89,24 +68,16 @@ _PyAtExit_Init(PyInterpreterState *interp) // _PyAtExit_Init() must only be called once assert(state->callbacks == NULL); - state->callback_len = 32; - state->ncallbacks = 0; - state->callbacks = PyMem_New(atexit_py_callback*, state->callback_len); + state->callbacks = PyList_New(32); if (state->callbacks == NULL) { return _PyStatus_NO_MEMORY(); } return _PyStatus_OK(); } - -void -_PyAtExit_Fini(PyInterpreterState *interp) +static void +atexit_call_lowlevel_locked(struct atexit_state *state) { - struct atexit_state *state = &interp->atexit; - atexit_cleanup(state); - PyMem_Free(state->callbacks); - state->callbacks = NULL; - atexit_callback *next = state->ll_callbacks; state->ll_callbacks = NULL; while (next != NULL) { @@ -120,22 +91,31 @@ _PyAtExit_Fini(PyInterpreterState *interp) } } +void +_PyAtExit_Fini(PyInterpreterState *interp) +{ + struct atexit_state *state = &interp->atexit; + atexit_cleanup(state); + Py_CLEAR(state->callbacks); + atexit_call_lowlevel_locked(state); +} + static void atexit_callfuncs(struct atexit_state *state) { assert(!PyErr_Occurred()); - if (state->ncallbacks == 0) { + // Create a copy of the list for thread safety + PyObject *copy = PyList_GetSlice(state->callbacks, 0, Py_SIZE(state->callbacks)); + if (copy == NULL) + { + PyErr_WriteUnraisable(NULL); return; } - for (int i = state->ncallbacks - 1; i >= 0; i--) { - atexit_py_callback *cb = state->callbacks[i]; - if (cb == NULL) { - continue; - } - + for (Py_ssize_t i = 0; i < Py_SIZE(copy); ++i) { + PyObject *@s // bpo-46025: Increment the refcount of cb->func as the call itself may unregister it PyObject* the_func = Py_NewRef(cb->func); PyObject *res = PyObject_Call(cb->func, cb->args, cb->kwargs); From 965f489c2581462d2e9971d1b43cddb646627f08 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 13 Dec 2024 21:30:58 -0500 Subject: [PATCH 02/19] Use a list for atexit callbacks. --- Include/internal/pycore_list.h | 1 + Modules/atexitmodule.c | 94 ++++++++++++++++------------------ Objects/listobject.c | 69 +++++++++++++++++++------ 3 files changed, 98 insertions(+), 66 deletions(-) diff --git a/Include/internal/pycore_list.h b/Include/internal/pycore_list.h index 836ff30abfcedb..250a5cca9ff439 100644 --- a/Include/internal/pycore_list.h +++ b/Include/internal/pycore_list.h @@ -63,6 +63,7 @@ union _PyStackRef; PyAPI_FUNC(PyObject *)_PyList_FromStackRefSteal(const union _PyStackRef *src, Py_ssize_t n); PyAPI_FUNC(PyObject *)_PyList_AsTupleAndClear(PyListObject *v); +PyAPI_FUNC(int) _PyList_Remove(PyObject *self, PyObject *value); #ifdef __cplusplus } diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index b1488dd6e39dcb..0dba631c1a274a 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -68,16 +68,23 @@ _PyAtExit_Init(PyInterpreterState *interp) // _PyAtExit_Init() must only be called once assert(state->callbacks == NULL); - state->callbacks = PyList_New(32); + state->callbacks = PyList_New(0); if (state->callbacks == NULL) { + PyErr_WriteUnraisable(NULL); return _PyStatus_NO_MEMORY(); } return _PyStatus_OK(); } -static void -atexit_call_lowlevel_locked(struct atexit_state *state) +void +_PyAtExit_Fini(PyInterpreterState *interp) { + // In theory, there shouldn't be any threads left by now, so we + // won't lock this. + struct atexit_state *state = &interp->atexit; + atexit_cleanup(state); + Py_CLEAR(state->callbacks); + atexit_callback *next = state->ll_callbacks; state->ll_callbacks = NULL; while (next != NULL) { @@ -91,20 +98,12 @@ atexit_call_lowlevel_locked(struct atexit_state *state) } } -void -_PyAtExit_Fini(PyInterpreterState *interp) -{ - struct atexit_state *state = &interp->atexit; - atexit_cleanup(state); - Py_CLEAR(state->callbacks); - atexit_call_lowlevel_locked(state); -} - - static void atexit_callfuncs(struct atexit_state *state) { assert(!PyErr_Occurred()); + assert(state->callbacks != NULL); + assert(PyList_CheckExact(state->callbacks)); // Create a copy of the list for thread safety PyObject *copy = PyList_GetSlice(state->callbacks, 0, Py_SIZE(state->callbacks)); @@ -115,10 +114,17 @@ atexit_callfuncs(struct atexit_state *state) } for (Py_ssize_t i = 0; i < Py_SIZE(copy); ++i) { - PyObject *@s + // We don't have to worry about evil borrowed references, because + // no other threads can access this list. + PyObject *tuple = PyList_GET_ITEM(copy, i); + assert(PyTuple_CheckExact(tuple)); // bpo-46025: Increment the refcount of cb->func as the call itself may unregister it - PyObject* the_func = Py_NewRef(cb->func); - PyObject *res = PyObject_Call(cb->func, cb->args, cb->kwargs); + // XXX Is this still needed? + PyObject *the_func = Py_NewRef(PyTuple_GET_ITEM(tuple, 0)); + PyObject *kwargs = PyTuple_GET_ITEM(tuple, 2); + PyObject *res = PyObject_Call(the_func, + PyTuple_GET_ITEM(tuple, 1), + kwargs == Py_None ? NULL : kwargs); if (res == NULL) { PyErr_FormatUnraisable( "Exception ignored in atexit callback %R", the_func); @@ -129,6 +135,7 @@ atexit_callfuncs(struct atexit_state *state) Py_DECREF(the_func); } + Py_DECREF(copy); atexit_cleanup(state); assert(!PyErr_Occurred()); @@ -174,33 +181,25 @@ atexit_register(PyObject *module, PyObject *args, PyObject *kwargs) "the first argument must be callable"); return NULL; } + PyObject *rest_of_args = PyTuple_GetSlice(args, 1, PyTuple_GET_SIZE(args)); - struct atexit_state *state = get_atexit_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) { - return PyErr_NoMemory(); - } - state->callbacks = r; + if (kwargs == NULL) + { + kwargs = Py_None; } - - atexit_py_callback *callback = PyMem_Malloc(sizeof(atexit_py_callback)); - if (callback == NULL) { - return PyErr_NoMemory(); + PyObject *tuple = _PyTuple_FromArray((PyObject *[]){ func, rest_of_args, kwargs }, 3); + if (tuple == NULL) + { + return NULL; } - callback->args = PyTuple_GetSlice(args, 1, PyTuple_GET_SIZE(args)); - if (callback->args == NULL) { - PyMem_Free(callback); + struct atexit_state *state = get_atexit_state(); + if (PyList_Append(state->callbacks, tuple) < 0) + { + Py_DECREF(tuple); return NULL; } - callback->func = Py_NewRef(func); - callback->kwargs = Py_XNewRef(kwargs); - - state->callbacks[state->ncallbacks++] = callback; + Py_DECREF(tuple); return Py_NewRef(func); } @@ -244,9 +243,12 @@ static PyObject * atexit_ncallbacks(PyObject *module, PyObject *unused) { struct atexit_state *state = get_atexit_state(); - return PyLong_FromSsize_t(state->ncallbacks); + assert(state->callbacks != NULL); + assert(PyList_CheckExact(state->callbacks)); + return PyLong_FromSsize_t(Py_SIZE(state->callbacks)); } + PyDoc_STRVAR(atexit_unregister__doc__, "unregister($module, func, /)\n\ --\n\ @@ -260,20 +262,10 @@ static PyObject * atexit_unregister(PyObject *module, PyObject *func) { struct atexit_state *state = get_atexit_state(); - for (int i = 0; i < state->ncallbacks; i++) + if (_PyList_Remove(state->callbacks, func) < 0) { - atexit_py_callback *cb = state->callbacks[i]; - if (cb == NULL) { - continue; - } - - int eq = PyObject_RichCompareBool(cb->func, func, Py_EQ); - if (eq < 0) { - return NULL; - } - if (eq) { - atexit_delete_cb(state, i); - } + PyErr_Clear(); + Py_RETURN_NONE; } Py_RETURN_NONE; } diff --git a/Objects/listobject.c b/Objects/listobject.c index a877bad66be45f..fa92c6c2700d4a 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -3296,6 +3296,28 @@ list_count(PyListObject *self, PyObject *value) return PyLong_FromSsize_t(count); } +static int +list_remove_without_error(PyListObject *self, PyObject *value) +{ + Py_ssize_t i; + + for (i = 0; i < Py_SIZE(self); i++) { + PyObject *obj = self->ob_item[i]; + Py_INCREF(obj); + int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); + Py_DECREF(obj); + if (cmp > 0) { + if (list_ass_slice_lock_held(self, i, i+1, NULL) == 0) + return 1; + return -1; + } + else if (cmp < 0) + return -1; + } + + return 0; +} + /*[clinic input] @critical_section list.remove @@ -3312,23 +3334,40 @@ static PyObject * list_remove_impl(PyListObject *self, PyObject *value) /*[clinic end generated code: output=b9b76a6633b18778 input=26c813dbb95aa93b]*/ { - Py_ssize_t i; + int result = list_remove_without_error(self, value); + if (result < 0) + { + return NULL; + } + else if (result == 0) { + PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list"); + return NULL; + } - for (i = 0; i < Py_SIZE(self); i++) { - PyObject *obj = self->ob_item[i]; - Py_INCREF(obj); - int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); - Py_DECREF(obj); - if (cmp > 0) { - if (list_ass_slice_lock_held(self, i, i+1, NULL) == 0) - Py_RETURN_NONE; - return NULL; - } - else if (cmp < 0) - return NULL; + Py_RETURN_NONE; +} + +/* Thread-safe removal of a list item. */ +int +_PyList_Remove(PyObject *self, PyObject *value) +{ + assert(self != NULL); + assert(value != NULL); + assert(PyList_Check(self)); + PyObject *result; + + Py_BEGIN_CRITICAL_SECTION(self); + result = list_remove_impl((PyListObject *)self, value); + Py_END_CRITICAL_SECTION(); + + if (result == NULL) + { + return -1; } - PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list"); - return NULL; + + // Should be an immortal reference to None, no need to DECREF + assert(result == Py_None); + return 0; } static int From 1ff5c62b6bd33da4d1a716b0c9c1e44a69ebc5ec Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 13 Dec 2024 21:36:58 -0500 Subject: [PATCH 03/19] Lock the low level callback list. --- Include/internal/pycore_atexit.h | 11 +++++++++++ Modules/atexitmodule.c | 2 ++ 2 files changed, 13 insertions(+) diff --git a/Include/internal/pycore_atexit.h b/Include/internal/pycore_atexit.h index 4e79daed388e4b..13c4bbefb102f9 100644 --- a/Include/internal/pycore_atexit.h +++ b/Include/internal/pycore_atexit.h @@ -37,6 +37,9 @@ typedef struct atexit_callback { } atexit_callback; struct atexit_state { +#ifdef Py_GIL_DISABLED + PyMutex ll_callbacks_lock; +#endif atexit_callback *ll_callbacks; // XXX The rest of the state could be moved to the atexit module state @@ -48,6 +51,14 @@ struct atexit_state { PyObject *callbacks; }; +#ifdef Py_GIL_DISABLED +#define _PyAtExit_LockCallbacks(state) PyMutex_Lock(&state->ll_callbacks_lock); +#define _PyAtExit_UnlockCallbacks(state) PyMutex_Unlock(&state->ll_callbacks_lock); +#else +#define _PyAtExit_LockCallbacks(state) +#define _PyAtExit_UnlockCallbacks(state) +#endif + // Export for '_interpchannels' shared extension PyAPI_FUNC(int) _Py_AtExit( PyInterpreterState *interp, diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index 0dba631c1a274a..6f258a70b29864 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -41,6 +41,7 @@ PyUnstable_AtExit(PyInterpreterState *interp, callback->next = NULL; struct atexit_state *state = &interp->atexit; + _PyAtExit_LockCallbacks(state); atexit_callback *top = state->ll_callbacks; if (top == NULL) { state->ll_callbacks = callback; @@ -49,6 +50,7 @@ PyUnstable_AtExit(PyInterpreterState *interp, callback->next = top; state->ll_callbacks = callback; } + _PyAtExit_UnlockCallbacks(state); return 0; } From 5534c0a87b50501ec5e8f92ec69ce24d83785db6 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 13 Dec 2024 21:39:25 -0500 Subject: [PATCH 04/19] Add a test. --- Lib/test/test_atexit.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Lib/test/test_atexit.py b/Lib/test/test_atexit.py index 913b7556be83af..7e0231a77722c6 100644 --- a/Lib/test/test_atexit.py +++ b/Lib/test/test_atexit.py @@ -46,6 +46,38 @@ def test_atexit_instances(self): self.assertEqual(res.out.decode().splitlines(), ["atexit2", "atexit1"]) self.assertFalse(res.err) + @threading_helper.requires_working_threading() + @support.requires_resource("cpu") + @unittest.skipUnless(support.Py_GIL_DISABLED, "only meaningful without the GIL") + def test_atexit_thread_safety(self): + # GH-126907: atexit was not thread safe on the free-threaded build + source = """ + from threading import Thread + + def dummy(): + pass + + + def thready(): + for _ in range(100): + atexit.register(dummy) + atexit._clear() + atexit.register(dummy) + atexit.unregister(dummy) + + + threads = [Thread(target=thready) for _ in range(100)] + for thread in threads: + thread.start() + + for thread in threads: + thread.join() + """ + + # atexit._clear() has some evil side effects, and we don't + # want them to affect the rest of the tests. + assert_python_ok(textwrap.dedent(source)) + @support.cpython_only class SubinterpreterTest(unittest.TestCase): From 79d3adb10eea842cff5187b97c33f6488e0c978d Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 13 Dec 2024 22:04:11 -0500 Subject: [PATCH 05/19] Don't use an internal function for unregister. --- Include/internal/pycore_list.h | 1 - Lib/test/test_atexit.py | 6 +-- Modules/atexitmodule.c | 39 +++++++++++++++---- Objects/listobject.c | 69 ++++++++-------------------------- 4 files changed, 50 insertions(+), 65 deletions(-) diff --git a/Include/internal/pycore_list.h b/Include/internal/pycore_list.h index 250a5cca9ff439..836ff30abfcedb 100644 --- a/Include/internal/pycore_list.h +++ b/Include/internal/pycore_list.h @@ -63,7 +63,6 @@ union _PyStackRef; PyAPI_FUNC(PyObject *)_PyList_FromStackRefSteal(const union _PyStackRef *src, Py_ssize_t n); PyAPI_FUNC(PyObject *)_PyList_AsTupleAndClear(PyListObject *v); -PyAPI_FUNC(int) _PyList_Remove(PyObject *self, PyObject *value); #ifdef __cplusplus } diff --git a/Lib/test/test_atexit.py b/Lib/test/test_atexit.py index 7e0231a77722c6..f30daaf0edd4e9 100644 --- a/Lib/test/test_atexit.py +++ b/Lib/test/test_atexit.py @@ -4,7 +4,7 @@ import unittest from test import support from test.support import script_helper - +from test.support import threading_helper class GeneralTest(unittest.TestCase): def test_general(self): @@ -66,7 +66,7 @@ def thready(): atexit.unregister(dummy) - threads = [Thread(target=thready) for _ in range(100)] + threads = [Thread(target=thready) for _ in range(10)] for thread in threads: thread.start() @@ -76,7 +76,7 @@ def thready(): # atexit._clear() has some evil side effects, and we don't # want them to affect the rest of the tests. - assert_python_ok(textwrap.dedent(source)) + script_helper.assert_python_ok(textwrap.dedent(source)) @support.cpython_only diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index 6f258a70b29864..b915488feb15a5 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -196,7 +196,8 @@ atexit_register(PyObject *module, PyObject *args, PyObject *kwargs) } struct atexit_state *state = get_atexit_state(); - if (PyList_Append(state->callbacks, tuple) < 0) + // atexit callbacks go in a LIFO order + if (PyList_Insert(state->callbacks, 0, tuple) < 0) { Py_DECREF(tuple); return NULL; @@ -250,6 +251,31 @@ atexit_ncallbacks(PyObject *module, PyObject *unused) return PyLong_FromSsize_t(Py_SIZE(state->callbacks)); } +static int +atexit_unregister_locked(PyObject *callbacks, PyObject *func) +{ + for (Py_ssize_t i = 0; i < Py_SIZE(callbacks); ++i) { + PyObject *tuple = PyList_GET_ITEM(callbacks, i); + assert(PyTuple_CheckExact(tuple)); + PyObject *to_compare = PyTuple_GET_ITEM(tuple, 0); + + Py_INCREF(to_compare); + int cmp = PyObject_RichCompareBool(func, to_compare, Py_EQ); + Py_DECREF(to_compare); + + if (cmp == 1) { + if (PyList_SetSlice(callbacks, i, i + 1, NULL) < 0) { + return -1; + } + return 0; + } + else if (cmp < 0) { + return -1; + } + } + + return 0; +} PyDoc_STRVAR(atexit_unregister__doc__, "unregister($module, func, /)\n\ @@ -264,12 +290,11 @@ static PyObject * atexit_unregister(PyObject *module, PyObject *func) { struct atexit_state *state = get_atexit_state(); - if (_PyList_Remove(state->callbacks, func) < 0) - { - PyErr_Clear(); - Py_RETURN_NONE; - } - Py_RETURN_NONE; + int result; + Py_BEGIN_CRITICAL_SECTION(state->callbacks); + result = atexit_unregister_locked(state->callbacks, func); + Py_END_CRITICAL_SECTION(); + return result < 0 ? NULL : Py_None; } diff --git a/Objects/listobject.c b/Objects/listobject.c index fa92c6c2700d4a..a877bad66be45f 100644 --- a/Objects/listobject.c +++ b/Objects/listobject.c @@ -3296,28 +3296,6 @@ list_count(PyListObject *self, PyObject *value) return PyLong_FromSsize_t(count); } -static int -list_remove_without_error(PyListObject *self, PyObject *value) -{ - Py_ssize_t i; - - for (i = 0; i < Py_SIZE(self); i++) { - PyObject *obj = self->ob_item[i]; - Py_INCREF(obj); - int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); - Py_DECREF(obj); - if (cmp > 0) { - if (list_ass_slice_lock_held(self, i, i+1, NULL) == 0) - return 1; - return -1; - } - else if (cmp < 0) - return -1; - } - - return 0; -} - /*[clinic input] @critical_section list.remove @@ -3334,40 +3312,23 @@ static PyObject * list_remove_impl(PyListObject *self, PyObject *value) /*[clinic end generated code: output=b9b76a6633b18778 input=26c813dbb95aa93b]*/ { - int result = list_remove_without_error(self, value); - if (result < 0) - { - return NULL; - } - else if (result == 0) { - PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list"); - return NULL; - } - - Py_RETURN_NONE; -} - -/* Thread-safe removal of a list item. */ -int -_PyList_Remove(PyObject *self, PyObject *value) -{ - assert(self != NULL); - assert(value != NULL); - assert(PyList_Check(self)); - PyObject *result; - - Py_BEGIN_CRITICAL_SECTION(self); - result = list_remove_impl((PyListObject *)self, value); - Py_END_CRITICAL_SECTION(); + Py_ssize_t i; - if (result == NULL) - { - return -1; + for (i = 0; i < Py_SIZE(self); i++) { + PyObject *obj = self->ob_item[i]; + Py_INCREF(obj); + int cmp = PyObject_RichCompareBool(obj, value, Py_EQ); + Py_DECREF(obj); + if (cmp > 0) { + if (list_ass_slice_lock_held(self, i, i+1, NULL) == 0) + Py_RETURN_NONE; + return NULL; + } + else if (cmp < 0) + return NULL; } - - // Should be an immortal reference to None, no need to DECREF - assert(result == Py_None); - return 0; + PyErr_SetString(PyExc_ValueError, "list.remove(x): x not in list"); + return NULL; } static int From a66b20915b4bbf46fe8ac56f18fb988cfa603bd3 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 13 Dec 2024 22:15:54 -0500 Subject: [PATCH 06/19] Fix tests. --- Lib/test/test_atexit.py | 2 +- Modules/atexitmodule.c | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_atexit.py b/Lib/test/test_atexit.py index f30daaf0edd4e9..7c598cccda730a 100644 --- a/Lib/test/test_atexit.py +++ b/Lib/test/test_atexit.py @@ -76,7 +76,7 @@ def thready(): # atexit._clear() has some evil side effects, and we don't # want them to affect the rest of the tests. - script_helper.assert_python_ok(textwrap.dedent(source)) + script_helper.assert_python_ok("-c", textwrap.dedent(source)) @support.cpython_only diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index b915488feb15a5..5cb0e40787f302 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -258,16 +258,13 @@ atexit_unregister_locked(PyObject *callbacks, PyObject *func) PyObject *tuple = PyList_GET_ITEM(callbacks, i); assert(PyTuple_CheckExact(tuple)); PyObject *to_compare = PyTuple_GET_ITEM(tuple, 0); - - Py_INCREF(to_compare); int cmp = PyObject_RichCompareBool(func, to_compare, Py_EQ); - Py_DECREF(to_compare); if (cmp == 1) { if (PyList_SetSlice(callbacks, i, i + 1, NULL) < 0) { return -1; } - return 0; + --i; } else if (cmp < 0) { return -1; From 3f3f143ed35e5f00245dbde28d975ea514d7b169 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 13 Dec 2024 22:21:45 -0500 Subject: [PATCH 07/19] Add blurb. --- .../next/Library/2024-12-13-22-20-54.gh-issue-126907.fWRL_R.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2024-12-13-22-20-54.gh-issue-126907.fWRL_R.rst diff --git a/Misc/NEWS.d/next/Library/2024-12-13-22-20-54.gh-issue-126907.fWRL_R.rst b/Misc/NEWS.d/next/Library/2024-12-13-22-20-54.gh-issue-126907.fWRL_R.rst new file mode 100644 index 00000000000000..d33d2aa23b21b3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-12-13-22-20-54.gh-issue-126907.fWRL_R.rst @@ -0,0 +1,2 @@ +Fix crash when using :mod:`atexit` concurrently on the :term:`free-threaded +` build. From 172a9f544ded9fed3f66111fd76bdf50ff6b53ce Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 13 Dec 2024 22:26:21 -0500 Subject: [PATCH 08/19] Add some extra sauce to the tests. --- Lib/test/test_atexit.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_atexit.py b/Lib/test/test_atexit.py index 7c598cccda730a..eb01da6e88a8bc 100644 --- a/Lib/test/test_atexit.py +++ b/Lib/test/test_atexit.py @@ -64,6 +64,7 @@ def thready(): atexit._clear() atexit.register(dummy) atexit.unregister(dummy) + atexit._run_exitfuncs() threads = [Thread(target=thready) for _ in range(10)] From 2ead4f6f1e508de5a702c8899c97ff9d8aa0b002 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 13 Dec 2024 22:30:05 -0500 Subject: [PATCH 09/19] Remove old INCREF shenanigans. --- Modules/atexitmodule.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index 5cb0e40787f302..3e48de2854ba93 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -120,9 +120,8 @@ atexit_callfuncs(struct atexit_state *state) // no other threads can access this list. PyObject *tuple = PyList_GET_ITEM(copy, i); assert(PyTuple_CheckExact(tuple)); - // bpo-46025: Increment the refcount of cb->func as the call itself may unregister it - // XXX Is this still needed? - PyObject *the_func = Py_NewRef(PyTuple_GET_ITEM(tuple, 0)); + + PyObject *the_func = PyTuple_GET_ITEM(tuple, 0); PyObject *kwargs = PyTuple_GET_ITEM(tuple, 2); PyObject *res = PyObject_Call(the_func, PyTuple_GET_ITEM(tuple, 1), @@ -134,7 +133,6 @@ atexit_callfuncs(struct atexit_state *state) else { Py_DECREF(res); } - Py_DECREF(the_func); } Py_DECREF(copy); From 522e56e7910d1fb0f759d7479ae8386f74fab113 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 16 Dec 2024 08:16:04 -0500 Subject: [PATCH 10/19] Apply suggestions from code review Co-authored-by: Victor Stinner Co-authored-by: Kumar Aditya --- Include/internal/pycore_atexit.h | 8 ++++---- Modules/atexitmodule.c | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Include/internal/pycore_atexit.h b/Include/internal/pycore_atexit.h index 13c4bbefb102f9..db1e5568e09413 100644 --- a/Include/internal/pycore_atexit.h +++ b/Include/internal/pycore_atexit.h @@ -52,11 +52,11 @@ struct atexit_state { }; #ifdef Py_GIL_DISABLED -#define _PyAtExit_LockCallbacks(state) PyMutex_Lock(&state->ll_callbacks_lock); -#define _PyAtExit_UnlockCallbacks(state) PyMutex_Unlock(&state->ll_callbacks_lock); +# define _PyAtExit_LockCallbacks(state) PyMutex_Lock(&state->ll_callbacks_lock); +# define _PyAtExit_UnlockCallbacks(state) PyMutex_Unlock(&state->ll_callbacks_lock); #else -#define _PyAtExit_LockCallbacks(state) -#define _PyAtExit_UnlockCallbacks(state) +# define _PyAtExit_LockCallbacks(state) +# define _PyAtExit_UnlockCallbacks(state) #endif // Export for '_interpchannels' shared extension diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index 3e48de2854ba93..c1de154758a900 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -121,9 +121,9 @@ atexit_callfuncs(struct atexit_state *state) PyObject *tuple = PyList_GET_ITEM(copy, i); assert(PyTuple_CheckExact(tuple)); - PyObject *the_func = PyTuple_GET_ITEM(tuple, 0); + PyObject *func = PyTuple_GET_ITEM(tuple, 0); PyObject *kwargs = PyTuple_GET_ITEM(tuple, 2); - PyObject *res = PyObject_Call(the_func, + PyObject *res = PyObject_Call(func, PyTuple_GET_ITEM(tuple, 1), kwargs == Py_None ? NULL : kwargs); if (res == NULL) { @@ -187,7 +187,7 @@ atexit_register(PyObject *module, PyObject *args, PyObject *kwargs) { kwargs = Py_None; } - PyObject *tuple = _PyTuple_FromArray((PyObject *[]){ func, rest_of_args, kwargs }, 3); + PyObject *callback = PyTuple_Pack(3, func, func_args, func_kwargs); if (tuple == NULL) { return NULL; @@ -246,13 +246,13 @@ atexit_ncallbacks(PyObject *module, PyObject *unused) struct atexit_state *state = get_atexit_state(); assert(state->callbacks != NULL); assert(PyList_CheckExact(state->callbacks)); - return PyLong_FromSsize_t(Py_SIZE(state->callbacks)); + return PyLong_FromSsize_t(PyList_GET_SIZE(state->callbacks)); } static int atexit_unregister_locked(PyObject *callbacks, PyObject *func) { - for (Py_ssize_t i = 0; i < Py_SIZE(callbacks); ++i) { + for (Py_ssize_t i = 0; i < PyList_GET_SIZE(callbacks); ++i) { PyObject *tuple = PyList_GET_ITEM(callbacks, i); assert(PyTuple_CheckExact(tuple)); PyObject *to_compare = PyTuple_GET_ITEM(tuple, 0); From d7f3454fe954b4348ab9d99db8f4e6e64092a3d2 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 16 Dec 2024 13:19:30 +0000 Subject: [PATCH 11/19] Remove obsolete PyErr_WriteUnraisable() --- Modules/atexitmodule.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index c1de154758a900..393ea81f629f39 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -72,7 +72,6 @@ _PyAtExit_Init(PyInterpreterState *interp) state->callbacks = PyList_New(0); if (state->callbacks == NULL) { - PyErr_WriteUnraisable(NULL); return _PyStatus_NO_MEMORY(); } return _PyStatus_OK(); From bd426efaa5ad9c70a25d846612b62e7c5bc0b8dd Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 16 Dec 2024 13:20:09 +0000 Subject: [PATCH 12/19] rest_of_args -> func_args --- Modules/atexitmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index 393ea81f629f39..e79e35d433e44a 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -180,7 +180,7 @@ atexit_register(PyObject *module, PyObject *args, PyObject *kwargs) "the first argument must be callable"); return NULL; } - PyObject *rest_of_args = PyTuple_GetSlice(args, 1, PyTuple_GET_SIZE(args)); + PyObject *func_args = PyTuple_GetSlice(args, 1, PyTuple_GET_SIZE(args)); if (kwargs == NULL) { From 2bec259dcbfd949a5192fac85d51bfe7efcc40b6 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 16 Dec 2024 13:20:51 +0000 Subject: [PATCH 13/19] kwargs -> func_kwargs --- Modules/atexitmodule.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index e79e35d433e44a..94fe4ffe2ce079 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -181,10 +181,11 @@ atexit_register(PyObject *module, PyObject *args, PyObject *kwargs) return NULL; } PyObject *func_args = PyTuple_GetSlice(args, 1, PyTuple_GET_SIZE(args)); + PyObject *func_kwargs = kwargs; - if (kwargs == NULL) + if (func_kwargs == NULL) { - kwargs = Py_None; + func_kwargs = Py_None; } PyObject *callback = PyTuple_Pack(3, func, func_args, func_kwargs); if (tuple == NULL) From b94d45d740d23e4b44436444205300d7b8138cf2 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 16 Dec 2024 13:21:49 +0000 Subject: [PATCH 14/19] Change ordering of cmp checks --- Modules/atexitmodule.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index 94fe4ffe2ce079..a5443f373114b8 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -257,16 +257,17 @@ atexit_unregister_locked(PyObject *callbacks, PyObject *func) assert(PyTuple_CheckExact(tuple)); PyObject *to_compare = PyTuple_GET_ITEM(tuple, 0); int cmp = PyObject_RichCompareBool(func, to_compare, Py_EQ); - - if (cmp == 1) { + if (cmp < 0) + { + return -1; + } + else if (cmp == 1) { + // We found a callback! if (PyList_SetSlice(callbacks, i, i + 1, NULL) < 0) { return -1; } --i; } - else if (cmp < 0) { - return -1; - } } return 0; From 59d645078115b760534b4d06ee5cfb81c14452dd Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 16 Dec 2024 13:23:15 +0000 Subject: [PATCH 15/19] Use an args variable --- Modules/atexitmodule.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index a5443f373114b8..ff3c5c6fc28b8b 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -121,9 +121,11 @@ atexit_callfuncs(struct atexit_state *state) assert(PyTuple_CheckExact(tuple)); PyObject *func = PyTuple_GET_ITEM(tuple, 0); + PyObject *args = PyTuple_GET_ITEM(tuple, 1); PyObject *kwargs = PyTuple_GET_ITEM(tuple, 2); + PyObject *res = PyObject_Call(func, - PyTuple_GET_ITEM(tuple, 1), + args, kwargs == Py_None ? NULL : kwargs); if (res == NULL) { PyErr_FormatUnraisable( From be3a41128590309cee8debadc745e5e54724b951 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 16 Dec 2024 13:25:46 +0000 Subject: [PATCH 16/19] Fix build failures from renamed variables. --- Modules/atexitmodule.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index ff3c5c6fc28b8b..39ef0256ebbeba 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -129,7 +129,7 @@ atexit_callfuncs(struct atexit_state *state) kwargs == Py_None ? NULL : 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); @@ -190,19 +190,19 @@ atexit_register(PyObject *module, PyObject *args, PyObject *kwargs) func_kwargs = Py_None; } PyObject *callback = PyTuple_Pack(3, func, func_args, func_kwargs); - if (tuple == NULL) + if (callback == NULL) { return NULL; } struct atexit_state *state = get_atexit_state(); // atexit callbacks go in a LIFO order - if (PyList_Insert(state->callbacks, 0, tuple) < 0) + if (PyList_Insert(state->callbacks, 0, callback) < 0) { - Py_DECREF(tuple); + Py_DECREF(callback); return NULL; } - Py_DECREF(tuple); + Py_DECREF(callback); return Py_NewRef(func); } From ac9299d87769a71b2cc961b662394da86003693c Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 16 Dec 2024 08:34:29 -0500 Subject: [PATCH 17/19] Update Modules/atexitmodule.c Co-authored-by: Victor Stinner --- Modules/atexitmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index 39ef0256ebbeba..28966d4df8e32b 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -263,7 +263,7 @@ atexit_unregister_locked(PyObject *callbacks, PyObject *func) { return -1; } - else if (cmp == 1) { + if (cmp == 1) { // We found a callback! if (PyList_SetSlice(callbacks, i, i + 1, NULL) < 0) { return -1; From 68d2086804885cffb7b032846127e99feba6e191 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 17 Dec 2024 00:29:27 +0530 Subject: [PATCH 18/19] Update Modules/atexitmodule.c --- Modules/atexitmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index 28966d4df8e32b..f1cf3f55c9bcf4 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -107,7 +107,7 @@ atexit_callfuncs(struct atexit_state *state) assert(PyList_CheckExact(state->callbacks)); // Create a copy of the list for thread safety - PyObject *copy = PyList_GetSlice(state->callbacks, 0, Py_SIZE(state->callbacks)); + PyObject *copy = PyList_GetSlice(state->callbacks, 0, PyList_GET_SIZE(state->callbacks)); if (copy == NULL) { PyErr_WriteUnraisable(NULL); From b4d741fca2551b2e71a76035b31d6be175a12ba1 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Tue, 17 Dec 2024 00:29:35 +0530 Subject: [PATCH 19/19] Update Modules/atexitmodule.c --- Modules/atexitmodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/atexitmodule.c b/Modules/atexitmodule.c index f1cf3f55c9bcf4..1b89b32ba907d7 100644 --- a/Modules/atexitmodule.c +++ b/Modules/atexitmodule.c @@ -114,7 +114,7 @@ atexit_callfuncs(struct atexit_state *state) return; } - for (Py_ssize_t i = 0; i < Py_SIZE(copy); ++i) { + for (Py_ssize_t i = 0; i < PyList_GET_SIZE(copy); ++i) { // We don't have to worry about evil borrowed references, because // no other threads can access this list. PyObject *tuple = PyList_GET_ITEM(copy, i);