diff --git a/Lib/test/audit-tests.py b/Lib/test/audit-tests.py index 53bd369fabe21f..aaf8f03b78d6c9 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 e9e546fdbfd6fb..71138639f4e0f2 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. diff --git a/Python/sysmodule.c b/Python/sysmodule.c index ce75027f67e9f4..462462d14ec2e2 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -239,8 +239,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 - || interp->audit_hooks + || interp_has_audit_hooks || PyDTrace_AUDIT_ENABLED()); } @@ -310,7 +314,8 @@ sys_audit_tstate(PyThreadState *ts, const char *event, } /* Call interpreter hooks */ - if (is->audit_hooks) { + PyObject *audit_hooks = is->audit_hooks; + if (audit_hooks != NULL && PyList_GET_SIZE(audit_hooks) > 0) { eventName = PyUnicode_FromString(event); if (!eventName) { goto exit; @@ -540,15 +545,7 @@ sys_addaudithook_impl(PyObject *module, PyObject *hook) } PyInterpreterState *interp = tstate->interp; - if (interp->audit_hooks == NULL) { - interp->audit_hooks = PyList_New(0); - if (interp->audit_hooks == NULL) { - return NULL; - } - /* Avoid having our list of hooks show up in the GC module */ - PyObject_GC_UnTrack(interp->audit_hooks); - } - + assert(interp->audit_hooks != NULL); if (PyList_Append(interp->audit_hooks, hook) < 0) { return NULL; } @@ -4367,6 +4364,13 @@ _PySys_Create(PyThreadState *tstate, PyObject **sysmod_p) PyInterpreterState *interp = tstate->interp; + PyObject *audit_hooks = PyList_New(0); + if (audit_hooks == NULL) { + goto error; + } + _PyObject_GC_UNTRACK(audit_hooks); + interp->audit_hooks = audit_hooks; + PyObject *modules = _PyImport_InitModules(interp); if (modules == NULL) { goto error;