From e178e426d4e3251ee0145779322a642acb4ff897 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 13 Oct 2025 21:16:15 +0900 Subject: [PATCH 1/6] Add lock with operations on _PyExecutorObject list --- Include/internal/pycore_interp_structs.h | 14 ++++++++++++++ Python/optimizer.c | 20 +++++++++++++++++++- Python/pystate.c | 3 +++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 2124e76514f1af..9a4bd270f36437 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -29,6 +29,17 @@ extern "C" { # define NUM_WEAKREF_LIST_LOCKS 127 #endif +// Executor list lock macros for thread-safe access to executor linked lists +#ifdef Py_GIL_DISABLED +# define EXECUTOR_LIST_LOCK(interp) \ + PyMutex_Lock(&(interp)->executor_list_lock) +# define EXECUTOR_LIST_UNLOCK(interp) \ + PyMutex_Unlock(&(interp)->executor_list_lock) +#else +# define EXECUTOR_LIST_LOCK(interp) ((void)0) +# define EXECUTOR_LIST_UNLOCK(interp) ((void)0) +#endif + typedef int (*_Py_pending_call_func)(void *); struct _pending_call { @@ -945,6 +956,9 @@ struct _is { struct _PyExecutorObject *executor_deletion_list_head; struct _PyExecutorObject *cold_executor; int executor_deletion_list_remaining_capacity; +#ifdef Py_GIL_DISABLED + PyMutex executor_list_lock; +#endif size_t trace_run_counter; _rare_events rare_events; PyDict_WatchCallback builtins_dict_watcher; diff --git a/Python/optimizer.c b/Python/optimizer.c index 83b0b1a5deba5c..48f1064b4eaae5 100644 --- a/Python/optimizer.c +++ b/Python/optimizer.c @@ -244,6 +244,7 @@ _Py_ClearExecutorDeletionList(PyInterpreterState *interp) ts = PyThreadState_Next(ts); HEAD_UNLOCK(runtime); } + EXECUTOR_LIST_LOCK(interp); _PyExecutorObject **prev_to_next_ptr = &interp->executor_deletion_list_head; _PyExecutorObject *exec = *prev_to_next_ptr; while (exec != NULL) { @@ -259,12 +260,14 @@ _Py_ClearExecutorDeletionList(PyInterpreterState *interp) exec = *prev_to_next_ptr; } interp->executor_deletion_list_remaining_capacity = EXECUTOR_DELETE_LIST_MAX; + EXECUTOR_LIST_UNLOCK(interp); } static void add_to_pending_deletion_list(_PyExecutorObject *self) { PyInterpreterState *interp = PyInterpreterState_Get(); + EXECUTOR_LIST_LOCK(interp); self->vm_data.links.next = interp->executor_deletion_list_head; interp->executor_deletion_list_head = self; if (interp->executor_deletion_list_remaining_capacity > 0) { @@ -273,6 +276,7 @@ add_to_pending_deletion_list(_PyExecutorObject *self) else { _Py_ClearExecutorDeletionList(interp); } + EXECUTOR_LIST_UNLOCK(interp); } static void @@ -1441,6 +1445,7 @@ static void link_executor(_PyExecutorObject *executor) { PyInterpreterState *interp = _PyInterpreterState_GET(); + EXECUTOR_LIST_LOCK(interp); _PyExecutorLinkListNode *links = &executor->vm_data.links; _PyExecutorObject *head = interp->executor_list_head; if (head == NULL) { @@ -1458,6 +1463,7 @@ link_executor(_PyExecutorObject *executor) executor->vm_data.linked = true; /* executor_list_head must be first in list */ assert(interp->executor_list_head->vm_data.links.previous == NULL); + EXECUTOR_LIST_UNLOCK(interp); } static void @@ -1466,6 +1472,8 @@ unlink_executor(_PyExecutorObject *executor) if (!executor->vm_data.linked) { return; } + PyInterpreterState *interp = PyInterpreterState_Get(); + EXECUTOR_LIST_LOCK(interp); _PyExecutorLinkListNode *links = &executor->vm_data.links; assert(executor->vm_data.valid); _PyExecutorObject *next = links->next; @@ -1478,11 +1486,11 @@ unlink_executor(_PyExecutorObject *executor) } else { // prev == NULL implies that executor is the list head - PyInterpreterState *interp = PyInterpreterState_Get(); assert(interp->executor_list_head == executor); interp->executor_list_head = next; } executor->vm_data.linked = false; + EXECUTOR_LIST_UNLOCK(interp); } /* This must be called by optimizers before using the executor */ @@ -1607,16 +1615,19 @@ _Py_Executors_InvalidateDependency(PyInterpreterState *interp, void *obj, int is } /* Clearing an executor can deallocate others, so we need to make a list of * executors to invalidate first */ + EXECUTOR_LIST_LOCK(interp); for (_PyExecutorObject *exec = interp->executor_list_head; exec != NULL;) { assert(exec->vm_data.valid); _PyExecutorObject *next = exec->vm_data.links.next; if (bloom_filter_may_contain(&exec->vm_data.bloom, &obj_filter) && PyList_Append(invalidate, (PyObject *)exec)) { + EXECUTOR_LIST_UNLOCK(interp); goto error; } exec = next; } + EXECUTOR_LIST_UNLOCK(interp); for (Py_ssize_t i = 0; i < PyList_GET_SIZE(invalidate); i++) { PyObject *exec = PyList_GET_ITEM(invalidate, i); executor_clear(exec); @@ -1637,6 +1648,7 @@ _Py_Executors_InvalidateDependency(PyInterpreterState *interp, void *obj, int is void _Py_Executors_InvalidateAll(PyInterpreterState *interp, int is_invalidation) { + EXECUTOR_LIST_LOCK(interp); while (interp->executor_list_head) { _PyExecutorObject *executor = interp->executor_list_head; assert(executor->vm_data.valid == 1 && executor->vm_data.linked == 1); @@ -1651,6 +1663,7 @@ _Py_Executors_InvalidateAll(PyInterpreterState *interp, int is_invalidation) OPT_STAT_INC(executors_invalidated); } } + EXECUTOR_LIST_UNLOCK(interp); } void @@ -1665,11 +1678,13 @@ _Py_Executors_InvalidateCold(PyInterpreterState *interp) /* Clearing an executor can deallocate others, so we need to make a list of * executors to invalidate first */ + EXECUTOR_LIST_LOCK(interp); for (_PyExecutorObject *exec = interp->executor_list_head; exec != NULL;) { assert(exec->vm_data.valid); _PyExecutorObject *next = exec->vm_data.links.next; if (!exec->vm_data.warm && PyList_Append(invalidate, (PyObject *)exec) < 0) { + EXECUTOR_LIST_UNLOCK(interp); goto error; } else { @@ -1678,6 +1693,7 @@ _Py_Executors_InvalidateCold(PyInterpreterState *interp) exec = next; } + EXECUTOR_LIST_UNLOCK(interp); for (Py_ssize_t i = 0; i < PyList_GET_SIZE(invalidate); i++) { PyObject *exec = PyList_GET_ITEM(invalidate, i); executor_clear(exec); @@ -1801,10 +1817,12 @@ _PyDumpExecutors(FILE *out) fprintf(out, "digraph ideal {\n\n"); fprintf(out, " rankdir = \"LR\"\n\n"); PyInterpreterState *interp = PyInterpreterState_Get(); + EXECUTOR_LIST_LOCK(interp); for (_PyExecutorObject *exec = interp->executor_list_head; exec != NULL;) { executor_to_gv(exec, out); exec = exec->vm_data.links.next; } + EXECUTOR_LIST_UNLOCK(interp); fprintf(out, "}\n\n"); return 0; } diff --git a/Python/pystate.c b/Python/pystate.c index dbed609f29aa07..b33e2e72cd46b9 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -575,6 +575,9 @@ init_interpreter(PyInterpreterState *interp, interp->executor_list_head = NULL; interp->executor_deletion_list_head = NULL; interp->executor_deletion_list_remaining_capacity = 0; +#ifdef Py_GIL_DISABLED + interp->executor_list_lock = (PyMutex){0}; +#endif interp->trace_run_counter = JIT_CLEANUP_THRESHOLD; if (interp != &runtime->_main_interpreter) { /* Fix the self-referential, statically initialized fields. */ From f3a9c74395d2c239f820d534bee9a938abc79d78 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 13 Oct 2025 23:13:54 +0900 Subject: [PATCH 2/6] Using FT_MUTEX_LOCK macro --- Include/internal/pycore_interp_structs.h | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 9a4bd270f36437..13f4fbd20c33d3 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -30,15 +30,10 @@ extern "C" { #endif // Executor list lock macros for thread-safe access to executor linked lists -#ifdef Py_GIL_DISABLED -# define EXECUTOR_LIST_LOCK(interp) \ - PyMutex_Lock(&(interp)->executor_list_lock) -# define EXECUTOR_LIST_UNLOCK(interp) \ - PyMutex_Unlock(&(interp)->executor_list_lock) -#else -# define EXECUTOR_LIST_LOCK(interp) ((void)0) -# define EXECUTOR_LIST_UNLOCK(interp) ((void)0) -#endif +#define EXECUTOR_LIST_LOCK(interp) \ + FT_MUTEX_LOCK(&(interp)->executor_list_lock) +#define EXECUTOR_LIST_UNLOCK(interp) \ + FT_MUTEX_UNLOCK(&(interp)->executor_list_lock) typedef int (*_Py_pending_call_func)(void *); From 68581614b8b2cde3fa98d8cc0dfbf69cfc15b71a Mon Sep 17 00:00:00 2001 From: AN Long Date: Sun, 9 Nov 2025 17:12:29 +0900 Subject: [PATCH 3/6] Free the executors outside the lock in _Py_ClearExecutorDeletionList --- Python/optimizer.c | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/Python/optimizer.c b/Python/optimizer.c index bc8e2918d4abe8..a1083bd524bdb7 100644 --- a/Python/optimizer.c +++ b/Python/optimizer.c @@ -245,6 +245,15 @@ _Py_ClearExecutorDeletionList(PyInterpreterState *interp) ts = PyThreadState_Next(ts); HEAD_UNLOCK(runtime); } + + /* Create a list to collect executors that need to be freed. + * This avoids calling _PyExecutor_Free while holding the lock, + * which could trigger destructors and cause deadlock. */ + PyObject *to_free = PyList_New(0); + if (to_free == NULL) { + goto error; + } + EXECUTOR_LIST_LOCK(interp); _PyExecutorObject **prev_to_next_ptr = &interp->executor_deletion_list_head; _PyExecutorObject *exec = *prev_to_next_ptr; @@ -256,12 +265,26 @@ _Py_ClearExecutorDeletionList(PyInterpreterState *interp) } else { *prev_to_next_ptr = exec->vm_data.links.next; - _PyExecutor_Free(exec); + if (PyList_Append(to_free, (PyObject *)exec)) { + EXECUTOR_LIST_UNLOCK(interp); + goto error; + } } exec = *prev_to_next_ptr; } interp->executor_deletion_list_remaining_capacity = EXECUTOR_DELETE_LIST_MAX; EXECUTOR_LIST_UNLOCK(interp); + + for (Py_ssize_t i = 0; i < PyList_GET_SIZE(to_free); i++) { + _PyExecutorObject *exec = (_PyExecutorObject *)PyList_GET_ITEM(to_free, i); + _PyExecutor_Free(exec); + } + Py_DECREF(to_free); + return; + +error: + PyErr_Clear(); + Py_XDECREF(to_free); } static void From fa0dd0be5d4b45660258c2fa8e5ebb5cb04dd694 Mon Sep 17 00:00:00 2001 From: AN Long Date: Sun, 9 Nov 2025 17:18:56 +0900 Subject: [PATCH 4/6] Fix a potential dead lock in add_to_pending_deletion_list --- Python/optimizer.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Python/optimizer.c b/Python/optimizer.c index a1083bd524bdb7..0dbb93462c72b2 100644 --- a/Python/optimizer.c +++ b/Python/optimizer.c @@ -296,11 +296,17 @@ add_to_pending_deletion_list(_PyExecutorObject *self) interp->executor_deletion_list_head = self; if (interp->executor_deletion_list_remaining_capacity > 0) { interp->executor_deletion_list_remaining_capacity--; + EXECUTOR_LIST_UNLOCK(interp); } else { + /* Release the lock before calling _Py_ClearExecutorDeletionList + * to avoid deadlock, since it also tries to acquire the same lock */ + EXECUTOR_LIST_UNLOCK(interp); _Py_ClearExecutorDeletionList(interp); + EXECUTOR_LIST_LOCK(interp); + interp->executor_deletion_list_head = self; + EXECUTOR_LIST_UNLOCK(interp); } - EXECUTOR_LIST_UNLOCK(interp); } static void From bf48bb461f6ee3131925fb21077b6883bac7ca97 Mon Sep 17 00:00:00 2001 From: AN Long Date: Sun, 9 Nov 2025 23:04:17 +0900 Subject: [PATCH 5/6] Using a _PyRecursiveMutex for executors lock --- Include/internal/pycore_interp_structs.h | 12 +++++++++--- Python/pystate.c | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 9d7f6bc7fe72e6..80b4702bd76f14 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -9,6 +9,7 @@ extern "C" { #include "pycore_ast_state.h" // struct ast_state #include "pycore_llist.h" // struct llist_node +#include "pycore_lock.h" // _PyRecursiveMutex #include "pycore_opcode_utils.h" // NUM_COMMON_CONSTANTS #include "pycore_pymath.h" // _PY_SHORT_FLOAT_REPR #include "pycore_structs.h" // PyHamtObject @@ -30,10 +31,15 @@ extern "C" { #endif // Executor list lock macros for thread-safe access to executor linked lists +#ifdef Py_GIL_DISABLED #define EXECUTOR_LIST_LOCK(interp) \ - FT_MUTEX_LOCK(&(interp)->executor_list_lock) + _PyRecursiveMutex_Lock(&(interp)->executor_list_lock) #define EXECUTOR_LIST_UNLOCK(interp) \ - FT_MUTEX_UNLOCK(&(interp)->executor_list_lock) + _PyRecursiveMutex_Unlock(&(interp)->executor_list_lock) +#else +#define EXECUTOR_LIST_LOCK(interp) +#define EXECUTOR_LIST_UNLOCK(interp) +#endif typedef int (*_Py_pending_call_func)(void *); @@ -946,7 +952,7 @@ struct _is { struct _PyExecutorObject *cold_executor; int executor_deletion_list_remaining_capacity; #ifdef Py_GIL_DISABLED - PyMutex executor_list_lock; + _PyRecursiveMutex executor_list_lock; #endif size_t executor_creation_counter; _rare_events rare_events; diff --git a/Python/pystate.c b/Python/pystate.c index 40951179d2e889..a7c6223b91807d 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -576,7 +576,7 @@ init_interpreter(PyInterpreterState *interp, interp->executor_deletion_list_head = NULL; interp->executor_deletion_list_remaining_capacity = 0; #ifdef Py_GIL_DISABLED - interp->executor_list_lock = (PyMutex){0}; + interp->executor_list_lock = (_PyRecursiveMutex){0}; #endif interp->executor_creation_counter = JIT_CLEANUP_THRESHOLD; if (interp != &runtime->_main_interpreter) { From 6a5a1a973ce209e454d2dd2230c1df85347fd90d Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 10 Nov 2025 00:06:28 +0900 Subject: [PATCH 6/6] Fix refcount --- Python/optimizer.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Python/optimizer.c b/Python/optimizer.c index 0dbb93462c72b2..a79780f871adae 100644 --- a/Python/optimizer.c +++ b/Python/optimizer.c @@ -277,6 +277,7 @@ _Py_ClearExecutorDeletionList(PyInterpreterState *interp) for (Py_ssize_t i = 0; i < PyList_GET_SIZE(to_free); i++) { _PyExecutorObject *exec = (_PyExecutorObject *)PyList_GET_ITEM(to_free, i); + PyList_SET_ITEM(to_free, i, NULL); _PyExecutor_Free(exec); } Py_DECREF(to_free);