https://github.com/python/cpython/commit/801377967fd7bfa573c0b4c48e31b69d50c1da2f
commit: 801377967fd7bfa573c0b4c48e31b69d50c1da2f
branch: main
author: Neil Schemenauer <[email protected]>
committer: nascheme <[email protected]>
date: 2026-10-16T20:11:09-07:00
summary:
gh-158973: Reset the warnings lock after fork in the child (gh-158974)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-10-07-00-00-00.gh-issue-158973.warnings-fork.rst
M Include/internal/pycore_interp_structs.h
M Include/internal/pycore_lock.h
M Include/internal/pycore_warnings.h
M Lib/test/test_warnings/__init__.py
M Modules/posixmodule.c
M Python/_warnings.c
M Python/lock.c
diff --git a/Include/internal/pycore_interp_structs.h
b/Include/internal/pycore_interp_structs.h
index 9cc25eda44f9de1..dca86dec8be7979 100644
--- a/Include/internal/pycore_interp_structs.h
+++ b/Include/internal/pycore_interp_structs.h
@@ -650,6 +650,10 @@ struct _warnings_runtime_state {
PyObject *once_registry; /* Dict */
PyObject *default_action; /* String */
_PyRecursiveMutex lock;
+#ifdef HAVE_FORK
+ // Whether the thread calling fork() owns the warnings lock.
+ bool lock_held_at_fork;
+#endif
long filters_version;
PyObject *context;
};
diff --git a/Include/internal/pycore_lock.h b/Include/internal/pycore_lock.h
index 7dd2419ececf299..db7141d5dc710ac 100644
--- a/Include/internal/pycore_lock.h
+++ b/Include/internal/pycore_lock.h
@@ -173,6 +173,9 @@ PyAPI_FUNC(void) _PyRecursiveMutex_Lock(_PyRecursiveMutex
*m);
extern PyLockStatus _PyRecursiveMutex_LockTimed(_PyRecursiveMutex *m, PyTime_t
timeout, _PyLockFlags flags);
PyAPI_FUNC(void) _PyRecursiveMutex_Unlock(_PyRecursiveMutex *m);
extern int _PyRecursiveMutex_TryUnlock(_PyRecursiveMutex *m);
+#ifdef HAVE_FORK
+extern void _PyRecursiveMutex_at_fork_reinit(_PyRecursiveMutex *m, int owned);
+#endif
// A readers-writer (RW) lock. The lock supports multiple concurrent readers or
// a single writer. The lock is write-preferring: if a writer is waiting while
diff --git a/Include/internal/pycore_warnings.h
b/Include/internal/pycore_warnings.h
index e1348d529a22463..d3c0c549373257e 100644
--- a/Include/internal/pycore_warnings.h
+++ b/Include/internal/pycore_warnings.h
@@ -12,6 +12,11 @@ extern int _PyWarnings_InitState(PyInterpreterState *interp);
extern PyObject* _PyWarnings_Init(void);
+#ifdef HAVE_FORK
+extern void _PyWarnings_BeforeFork(PyInterpreterState *interp);
+extern void _PyWarnings_AfterFork(PyInterpreterState *interp);
+#endif
+
extern void _PyErr_WarnUnawaitedCoroutine(PyObject *coro);
extern void _PyErr_WarnUnawaitedAgenMethod(PyAsyncGenObject *agen, PyObject
*method);
diff --git a/Lib/test/test_warnings/__init__.py
b/Lib/test/test_warnings/__init__.py
index 5b5d5c2f70ffd7a..7262fb0d38344c0 100644
--- a/Lib/test/test_warnings/__init__.py
+++ b/Lib/test/test_warnings/__init__.py
@@ -15,6 +15,7 @@
from test.support import import_helper
from test.support import isolation
from test.support import os_helper
+from test.support import threading_helper
from test.support import warnings_helper
from test.support import force_not_colorized
from test.support.script_helper import assert_python_ok, assert_python_failure
@@ -1632,6 +1633,100 @@ def test_release_lock_no_lock(self):
):
c_warnings._release_lock()
+ @support.cpython_only
+ @unittest.skipUnless(support.has_fork_support, 'requires working os.fork')
+ @threading_helper.requires_working_threading()
+ def test_fork_other_thread_holds_lock(self):
+ code = textwrap.dedent('''
+ import _warnings
+ import os
+ import threading
+ import warnings
+ from test import support
+
+ parent_pid = os.getpid()
+ read_fd, write_fd = os.pipe()
+ local = threading.local()
+ ready = threading.Event()
+ release = threading.Event()
+ warnings.simplefilter('ignore', DeprecationWarning)
+ warnings.simplefilter('ignore', ResourceWarning)
+
+ class WarnOnDelete:
+ def __del__(self):
+ if os.getpid() != parent_pid:
+ # Even an ignored warning must acquire the lock.
+ warnings.warn('child cleanup', ResourceWarning)
+ os.write(write_fd, b'finalized')
+
+ def worker():
+ local.obj = WarnOnDelete()
+ _warnings._acquire_lock()
+ _warnings._acquire_lock()
+ try:
+ ready.set()
+ release.wait()
+ finally:
+ _warnings._release_lock()
+ _warnings._release_lock()
+
+ thread = threading.Thread(target=worker)
+ thread.start()
+ # Release the worker before fork's parent-side warning attempts
+ # to acquire the warnings lock.
+ os.register_at_fork(after_in_parent=release.set)
+ try:
+ assert ready.wait(support.LONG_TIMEOUT)
+ pid = os.fork()
+ if pid == 0:
+ # The finalizer must run before os.fork() returns.
+ os._exit(0)
+ support.wait_process(pid, exitcode=0)
+ finally:
+ release.set()
+ thread.join()
+ os.close(write_fd)
+ assert os.read(read_fd, 100) == b'finalized'
+ os.close(read_fd)
+ ''')
+ assert_python_ok('-c', code)
+
+ @support.cpython_only
+ @unittest.skipUnless(support.has_fork_support, 'requires working os.fork')
+ def test_fork_current_thread_holds_lock(self):
+ code = textwrap.dedent('''
+ import _warnings
+ import os
+ import warnings
+ from test import support
+
+ warnings.simplefilter('ignore')
+ for _ in range(3):
+ _warnings._acquire_lock()
+ pid = os.fork()
+ try:
+ # A warning must not change the inherited recursion depth.
+ warnings.warn('after fork')
+ for _ in range(3):
+ _warnings._release_lock()
+ try:
+ _warnings._release_lock()
+ except RuntimeError:
+ pass
+ else:
+ raise AssertionError('unexpected recursion depth')
+ _warnings._acquire_lock()
+ _warnings._release_lock()
+ except BaseException:
+ if pid == 0:
+ os._exit(1)
+ raise
+ if pid == 0:
+ os._exit(0)
+ support.wait_process(pid, exitcode=0)
+ ''')
+ assert_python_ok('-c', code)
+
class _DeprecatedTest(BaseTest, unittest.TestCase):
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-07-00-00-00.gh-issue-158973.warnings-fork.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-07-00-00-00.gh-issue-158973.warnings-fork.rst
new file mode 100644
index 000000000000000..9a98cf6e0333845
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-07-00-00-00.gh-issue-158973.warnings-fork.rst
@@ -0,0 +1,3 @@
+Reset the warnings lock in the child after :func:`os.fork`, before clearing
+other threads' states, to avoid deadlocks when their finalizers issue warnings.
+Preserve the lock's recursion depth if the forking thread owns it.
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
index dfc5409cf18f2d2..301afe1fa27be68 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -31,6 +31,7 @@
#include "pycore_time.h" // _PyLong_FromTime_t()
#include "pycore_tuple.h" // _PyTuple_FromPairSteal
#include "pycore_typeobject.h" // _PyType_AddMethod()
+#include "pycore_warnings.h" // _PyWarnings_AfterFork()
#ifndef MS_WINDOWS
# include "posixmodule.h" // _PyLong_FromUid()
@@ -722,6 +723,7 @@ PyOS_BeforeFork(void)
_PyImport_AcquireLock(interp);
_PyEval_StopTheWorldAll(&_PyRuntime);
HEAD_LOCK(&_PyRuntime);
+ _PyWarnings_BeforeFork(interp);
}
void
@@ -792,6 +794,9 @@ PyOS_AfterFork_Child(void)
_Py_jit_debug_mutex = (PyMutex){0};
#endif
+ // Thread-state cleanup can run finalizers that issue warnings.
+ _PyWarnings_AfterFork(tstate->interp);
+
reset_remotedebug_data(tstate);
reset_asyncio_state((_PyThreadStateImpl *)tstate);
diff --git a/Python/_warnings.c b/Python/_warnings.c
index 586cec195fac680..8fecff060fdad93 100644
--- a/Python/_warnings.c
+++ b/Python/_warnings.c
@@ -9,6 +9,7 @@
#include "pycore_traceback.h" // _Py_DisplaySourceLine()
#include "pycore_tuple.h" // _PyTuple_FromPair
#include "pycore_unicodeobject.h" // _PyUnicode_EqualToASCIIString()
+#include "pycore_warnings.h" // _PyWarnings_AfterFork()
#include <stdbool.h>
#include "clinic/_warnings.c.h"
@@ -276,6 +277,23 @@ warnings_lock_held(WarningsState *st)
return PyMutex_IsLocked(&st->lock.mutex);
}
+#ifdef HAVE_FORK
+void
+_PyWarnings_BeforeFork(PyInterpreterState *interp)
+{
+ WarningsState *st = warnings_get_state(interp);
+ st->lock_held_at_fork =
_PyRecursiveMutex_IsLockedByCurrentThread(&st->lock);
+}
+
+void
+_PyWarnings_AfterFork(PyInterpreterState *interp)
+{
+ WarningsState *st = warnings_get_state(interp);
+ _PyRecursiveMutex_at_fork_reinit(&st->lock, st->lock_held_at_fork);
+ st->lock_held_at_fork = false;
+}
+#endif
+
static PyObject *
get_warnings_context(PyInterpreterState *interp)
{
diff --git a/Python/lock.c b/Python/lock.c
index b669e3dcdbc9993..a2d47eea64a006b 100644
--- a/Python/lock.c
+++ b/Python/lock.c
@@ -453,6 +453,25 @@ _PyRecursiveMutex_TryUnlock(_PyRecursiveMutex *m)
return 0;
}
+#ifdef HAVE_FORK
+// Reinitialize the mutex in the child process after fork(). The caller must
+// record before fork() whether the forking thread owns the mutex, since the
+// thread ident can change in the child (gh-126688). If it does, the child
+// keeps the mutex with the same recursion level; otherwise the owner and any
+// waiters no longer exist in the child and the mutex is reset.
+void
+_PyRecursiveMutex_at_fork_reinit(_PyRecursiveMutex *m, int owned)
+{
+ if (owned) {
+ m->mutex = (PyMutex){._bits = _Py_LOCKED};
+ _Py_atomic_store_ullong_relaxed(&m->thread,
PyThread_get_thread_ident_ex());
+ }
+ else {
+ memset(m, 0, sizeof(*m));
+ }
+}
+#endif
+
#define _Py_WRITE_LOCKED 1
#define _PyRWMutex_READER_SHIFT 2
#define _Py_RWMUTEX_MAX_READERS (UINTPTR_MAX >> _PyRWMutex_READER_SHIFT)
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]