diff --git a/Include/internal/pycore_tstate.h b/Include/internal/pycore_tstate.h index 58dd66b69efc9da..2af09ad388256be 100644 --- a/Include/internal/pycore_tstate.h +++ b/Include/internal/pycore_tstate.h @@ -54,6 +54,9 @@ typedef struct _PyThreadStateImpl { // Reused for the thread's lifetime; owns placeholders while they resolve. PyObject *lazy_imports; + // Held while this thread owns the warnings lock. + PyInterpreterGuard *warnings_lock_guard; + // Distinguishes between yield and return from PyEval_EvalFrame(). // See gen_send_ex2() in Objects/genobject.c enum { diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py index 02fb352502b54dc..0c1f05b1f73beae 100644 --- a/Lib/test/test_embed.py +++ b/Lib/test/test_embed.py @@ -507,6 +507,49 @@ def test_datetime_reset_strptime(self): out, err = self.run_embedded_interpreter("test_repeated_init_exec", code) self.assertEqual(out, '20000101\n' * INIT_LOOPS) + @threading_helper.requires_working_threading() + def test_warnings_at_shutdown(self): + code = textwrap.dedent(''' + import threading + import time + import warnings + + warnings.simplefilter('always', ResourceWarning) + ready = threading.Event() + + class WarnOnDelete: + def __init__(self): + self.self_ref = self + + def __del__(self): + warnings.warn('late', ResourceWarning) + + def showwarning(message, *args, **kwargs): + if str(message) == 'hold': + warnings.warn('nested', ResourceWarning) + ready.set() + # A Python event cannot synchronize with the point where + # shutdown stops daemon threads, so hold the lock briefly. + time.sleep(0.1) + else: + print(message) + + warnings.showwarning = showwarning + + def worker(): + warnings.warn('hold', ResourceWarning) + + threading.Thread(target=worker, daemon=True).start() + assert ready.wait(4), 'daemon did not start warning' + # Collect the cycle during shutdown GC, after daemon threads stop. + WarnOnDelete() + ''') + out, err = self.run_embedded_interpreter( + "test_repeated_init_exec", code, "pass", + env=remove_python_envvars(), timeout=10) + self.assertEqual(out, 'nested\nlate\n') + self.assertEqual(err, '--- Loop #1 ---\n--- Loop #2 ---\n') + def test_static_types_inherited_slots(self): script = textwrap.dedent(""" import test.support diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-14-41-28.gh-issue-159485.S3IOyb.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-14-41-28.gh-issue-159485.S3IOyb.rst new file mode 100644 index 000000000000000..f49806a97164bc8 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-14-41-28.gh-issue-159485.S3IOyb.rst @@ -0,0 +1,2 @@ +Prevent a shutdown deadlock when a daemon thread emits a warning. The +interpreter now waits for the thread to release the warnings lock. diff --git a/Python/_warnings.c b/Python/_warnings.c index 586cec195fac680..8a7d5956be1a193 100644 --- a/Python/_warnings.c +++ b/Python/_warnings.c @@ -2,10 +2,12 @@ #include "pycore_frame.h" // PyFrameObject #include "pycore_genobject.h" // PyAsyncGenObject #include "pycore_import.h" // _PyImport_GetModules() +#include "pycore_interp.h" // _PyInterpreterState_GetFinalizing() #include "pycore_interpframe.h" // _PyFrame_GetCode() #include "pycore_long.h" // _PyLong_GetZero() #include "pycore_pylifecycle.h" // _Py_IsInterpreterFinalizing() #include "pycore_pystate.h" // _PyThreadState_GET() +#include "pycore_tstate.h" // _PyThreadStateImpl #include "pycore_traceback.h" // _Py_DisplaySourceLine() #include "pycore_tuple.h" // _PyTuple_FromPair #include "pycore_unicodeobject.h" // _PyUnicode_EqualToASCIIString() @@ -254,12 +256,25 @@ get_warnings_attr(PyInterpreterState *interp, PyObject *attr, int try_import) return obj; } -static inline void +static inline int warnings_lock(PyInterpreterState *interp) { WarningsState *st = warnings_get_state(interp); assert(st != NULL); + PyThreadState *tstate = _PyThreadState_GET(); + _PyThreadStateImpl *tstate_impl = (_PyThreadStateImpl *)tstate; + if (!_PyRecursiveMutex_IsLockedByCurrentThread(&st->lock) && + _PyInterpreterState_GetFinalizing(interp) != tstate) + { + assert(tstate_impl->warnings_lock_guard == NULL); + PyInterpreterGuard *guard = PyInterpreterGuard_FromCurrent(); + if (guard == NULL) { + return -1; + } + tstate_impl->warnings_lock_guard = guard; + } _PyRecursiveMutex_Lock(&st->lock); + return 0; } static inline int @@ -267,7 +282,21 @@ warnings_unlock(PyInterpreterState *interp) { WarningsState *st = warnings_get_state(interp); assert(st != NULL); - return _PyRecursiveMutex_TryUnlock(&st->lock); + if (!_PyRecursiveMutex_IsLockedByCurrentThread(&st->lock)) { + return -1; + } + if (st->lock.level > 0) { + _PyRecursiveMutex_Unlock(&st->lock); + return 0; + } + _PyThreadStateImpl *tstate = (_PyThreadStateImpl *)_PyThreadState_GET(); + PyInterpreterGuard *guard = tstate->warnings_lock_guard; + tstate->warnings_lock_guard = NULL; + _PyRecursiveMutex_Unlock(&st->lock); + if (guard != NULL) { + PyInterpreterGuard_Close(guard); + } + return 0; } static inline bool @@ -351,7 +380,9 @@ warnings_acquire_lock_impl(PyObject *module) if (interp == NULL) { return NULL; } - warnings_lock(interp); + if (warnings_lock(interp) < 0) { + return NULL; + } Py_RETURN_NONE; } @@ -1133,10 +1164,14 @@ do_warn(PyObject *message, PyObject *category, Py_ssize_t stack_level, &filename, &lineno, &module, ®istry)) return NULL; - warnings_lock(tstate->interp); - res = warn_explicit(tstate, category, message, filename, lineno, module, registry, - NULL, source); - warnings_unlock(tstate->interp); + if (warnings_lock(tstate->interp) < 0) { + res = NULL; + } + else { + res = warn_explicit(tstate, category, message, filename, lineno, module, registry, + NULL, source); + warnings_unlock(tstate->interp); + } Py_DECREF(filename); Py_DECREF(registry); Py_DECREF(module); @@ -1315,10 +1350,14 @@ warnings_warn_explicit_impl(PyObject *module, PyObject *message, } } - warnings_lock(tstate->interp); - returned = warn_explicit(tstate, category, message, filename, lineno, - mod, registry, source_line, sourceobj); - warnings_unlock(tstate->interp); + if (warnings_lock(tstate->interp) < 0) { + returned = NULL; + } + else { + returned = warn_explicit(tstate, category, message, filename, lineno, + mod, registry, source_line, sourceobj); + warnings_unlock(tstate->interp); + } Py_XDECREF(source_line); return returned; } @@ -1465,7 +1504,9 @@ PyErr_WarnExplicitObject(PyObject *category, PyObject *message, return -1; } - warnings_lock(tstate->interp); + if (warnings_lock(tstate->interp) < 0) { + return -1; + } res = warn_explicit(tstate, category, message, filename, lineno, module, registry, NULL, NULL); warnings_unlock(tstate->interp); @@ -1530,19 +1571,17 @@ PyErr_WarnExplicitFormat(PyObject *category, va_start(vargs, format); message = PyUnicode_FromFormatV(format, vargs); if (message != NULL) { - PyObject *res; PyThreadState *tstate = get_current_tstate(); - if (tstate != NULL) { - warnings_lock(tstate->interp); - res = warn_explicit(tstate, category, message, filename, lineno, - module, registry, NULL, NULL); + if (tstate != NULL && warnings_lock(tstate->interp) == 0) { + PyObject *res = warn_explicit(tstate, category, message, filename, lineno, + module, registry, NULL, NULL); warnings_unlock(tstate->interp); - Py_DECREF(message); if (res != NULL) { Py_DECREF(res); ret = 0; } } + Py_DECREF(message); } va_end(vargs); exit: