From 9b7c1b00c87c677aeb88b7c0f359a8cdfdf0b12e Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 23 Jul 2026 13:26:26 -0400 Subject: [PATCH 1/4] Eagerly initialize audit hooks. --- Include/internal/pycore_interp_structs.h | 1 - Python/pystate.c | 1 - Python/sysmodule.c | 37 ++++++++++-------------- 3 files changed, 16 insertions(+), 23 deletions(-) diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index 3d577ff717cc6e..0623adce693d46 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -977,7 +977,6 @@ struct _is { struct _obmalloc_state *obmalloc; PyObject *audit_hooks; - PyMutex audit_hooks_mutex; PyType_WatchCallback type_watchers[TYPE_MAX_WATCHERS]; PyCode_WatchCallback code_watchers[CODE_MAX_WATCHERS]; PyContext_WatchCallback context_watchers[CONTEXT_MAX_WATCHERS]; diff --git a/Python/pystate.c b/Python/pystate.c index b6c34552e94337..d10b38def32911 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -578,7 +578,6 @@ init_interpreter(PyInterpreterState *interp, llist_init(&interp->mem_free_queue.head); llist_init(&interp->asyncio_tasks_head); interp->asyncio_tasks_lock = (PyMutex){0}; - interp->audit_hooks_mutex = (PyMutex){0}; for (int i = 0; i < _PY_MONITORING_UNGROUPED_EVENTS; i++) { interp->monitors.tools[i] = 0; } diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 207ccbce41d71d..2625830cfc6b1c 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -235,8 +235,12 @@ should_audit(PyInterpreterState *interp) if (!interp) { return 0; } + // interp->audit_hooks can only ever be NULL very early during initialization + // or very late during finalization. + int interp_has_audit_hooks = (interp->audit_hooks != NULL + && PyList_GET_SIZE(interp->audit_hooks) > 0); return (interp->runtime->audit_hooks.head - || FT_ATOMIC_LOAD_PTR_ACQUIRE(interp->audit_hooks) + || interp_has_audit_hooks || PyDTrace_AUDIT_ENABLED()); } @@ -306,8 +310,9 @@ sys_audit_tstate(PyThreadState *ts, const char *event, } /* Call interpreter hooks */ - PyObject *audit_hooks = FT_ATOMIC_LOAD_PTR_ACQUIRE(is->audit_hooks); - if (audit_hooks) { + PyObject *audit_hooks = is->audit_hooks; + assert(audit_hooks != NULL); + if (PyList_GET_SIZE(audit_hooks) > 0) { eventName = PyUnicode_FromString(event); if (!eventName) { goto exit; @@ -537,29 +542,13 @@ sys_addaudithook_impl(PyObject *module, PyObject *hook) } PyInterpreterState *interp = tstate->interp; - PyMutex mutex = interp->audit_hooks_mutex; - PyMutex_Lock(&mutex); - - if (interp->audit_hooks == NULL) { - PyObject *new_list = PyList_New(0); - if (new_list == NULL) { - goto error; - } - /* Avoid having our list of hooks show up in the GC module */ - PyObject_GC_UnTrack(new_list); - FT_ATOMIC_STORE_PTR_RELEASE(interp->audit_hooks, new_list); - } + assert(interp->audit_hooks != NULL); if (PyList_Append(interp->audit_hooks, hook) < 0) { - goto error; + return NULL; } - PyMutex_Unlock(&mutex); Py_RETURN_NONE; - -error: - PyMutex_Unlock(&mutex); - return NULL; } /*[clinic input] @@ -4323,6 +4312,12 @@ _PySys_Create(PyThreadState *tstate, PyObject **sysmod_p) PyInterpreterState *interp = tstate->interp; + PyObject *audit_hooks = PyList_New(0); + if (audit_hooks == NULL) { + goto error; + } + interp->audit_hooks = audit_hooks; + PyObject *modules = _PyImport_InitModules(interp); if (modules == NULL) { goto error; From e90adbba7635818c6aa49d597e2514cd5697a842 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 27 Jul 2026 11:20:09 -0400 Subject: [PATCH 2/4] Fix test failures. --- Python/pystate.c | 2 -- Python/sysmodule.c | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Python/pystate.c b/Python/pystate.c index d10b38def32911..358328fe18db58 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -857,8 +857,6 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate) */ // XXX Make sure we properly deal with problematic finalizers. - Py_CLEAR(interp->audit_hooks); - // gh-140257: Threads have already been cleared, but daemon threads may // still access eval_breaker atomically via take_gil() right before they // hang. Use an atomic store to prevent data races during finalization. diff --git a/Python/sysmodule.c b/Python/sysmodule.c index ba5f79a7f7dd35..3ad587da0c4d9e 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -453,6 +453,8 @@ _PySys_ClearAuditHooks(PyThreadState *ts) PyMem_RawFree(e); e = n; } + + Py_CLEAR(ts->interp->audit_hooks); } static void @@ -4315,6 +4317,7 @@ _PySys_Create(PyThreadState *tstate, PyObject **sysmod_p) if (audit_hooks == NULL) { goto error; } + _PyObject_GC_UNTRACK(audit_hooks); interp->audit_hooks = audit_hooks; PyObject *modules = _PyImport_InitModules(interp); From 64de3204ec00e2028d5400fa32b4f99c343df814 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 20 Oct 2026 17:57:43 -0400 Subject: [PATCH 3/4] Fix test failures. --- Python/pystate.c | 2 ++ Python/sysmodule.c | 5 +---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Python/pystate.c b/Python/pystate.c index 358328fe18db58..d10b38def32911 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -857,6 +857,8 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate) */ // XXX Make sure we properly deal with problematic finalizers. + Py_CLEAR(interp->audit_hooks); + // gh-140257: Threads have already been cleared, but daemon threads may // still access eval_breaker atomically via take_gil() right before they // hang. Use an atomic store to prevent data races during finalization. diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 3ad587da0c4d9e..8ddf5f3ed4cf70 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -311,8 +311,7 @@ sys_audit_tstate(PyThreadState *ts, const char *event, /* Call interpreter hooks */ PyObject *audit_hooks = is->audit_hooks; - assert(audit_hooks != NULL); - if (PyList_GET_SIZE(audit_hooks) > 0) { + if (audit_hooks != NULL && PyList_GET_SIZE(audit_hooks) > 0) { eventName = PyUnicode_FromString(event); if (!eventName) { goto exit; @@ -453,8 +452,6 @@ _PySys_ClearAuditHooks(PyThreadState *ts) PyMem_RawFree(e); e = n; } - - Py_CLEAR(ts->interp->audit_hooks); } static void From 40ce26712e709b6a20d55a74116523fb0affa768 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 20 Oct 2026 18:04:13 -0400 Subject: [PATCH 4/4] Add tests and blurb. --- Lib/test/audit-tests.py | 25 +++++++++++++++++++ Lib/test/test_audit.py | 5 ++++ ...-10-20-21-58-53.gh-issue-154431.pR4x6T.rst | 2 ++ 3 files changed, 32 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-20-21-58-53.gh-issue-154431.pR4x6T.rst diff --git a/Lib/test/audit-tests.py b/Lib/test/audit-tests.py index 8be5bf8aa4f546..8b7e05e4a469b1 100644 --- a/Lib/test/audit-tests.py +++ b/Lib/test/audit-tests.py @@ -88,6 +88,31 @@ def test_basic(): assertEqual(hook.seen[0][1], (1, 2, 3)) +def test_concurrent_add_hook(): + from test.support import threading_helper + + # gh-154431: Race the first hook registrations against audit events. + seen = [] + + def add_hook(index): + def hook(event, args): + if event == "test.concurrent_add_hook": + seen.append(index) + + sys.addaudithook(hook) + + def audit(): + for _ in range(2000): + sys.audit("test.concurrent_audit") + + workers = [lambda i=i: add_hook(i) for i in range(24)] + workers.extend([audit] * 8) + threading_helper.run_concurrently(workers) + + sys.audit("test.concurrent_add_hook") + assertEqual(sorted(seen), list(range(24))) + + def test_block_add_hook(): # Raising an exception should prevent a new hook from being added, # but will not propagate out. diff --git a/Lib/test/test_audit.py b/Lib/test/test_audit.py index db4e1eb9999c1f..e3484c08c9ffdb 100644 --- a/Lib/test/test_audit.py +++ b/Lib/test/test_audit.py @@ -7,6 +7,7 @@ from test import support from test.support import import_helper from test.support import os_helper +from test.support import threading_helper if not hasattr(sys, "addaudithook") or not hasattr(sys, "audit"): @@ -52,6 +53,10 @@ def run_python(self, *args, expect_stderr=False): def test_basic(self): self.do_test("test_basic") + @threading_helper.requires_working_threading() + def test_concurrent_add_hook(self): + self.do_test("test_concurrent_add_hook") + def test_block_add_hook(self): self.do_test("test_block_add_hook") diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-20-21-58-53.gh-issue-154431.pR4x6T.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-20-21-58-53.gh-issue-154431.pR4x6T.rst new file mode 100644 index 00000000000000..0ef063d9874818 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-20-21-58-53.gh-issue-154431.pR4x6T.rst @@ -0,0 +1,2 @@ +Fix a data race when :func:`sys.addaudithook` is called concurrently in the +free-threaded build.