Skip to content

Commit fdec193

Browse files
committed
gh-159485: Hold an interpreter guard while using the warnings lock
1 parent 37baa8f commit fdec193

9 files changed

Lines changed: 103 additions & 108 deletions

File tree

‎Include/internal/pycore_interp_structs.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,8 @@ struct _warnings_runtime_state {
652652
_PyRecursiveMutex lock;
653653
long filters_version;
654654
PyObject *context;
655+
// The lock owner's guard must outlive the mutex unlock.
656+
PyInterpreterGuard *lock_guard;
655657
};
656658

657659
struct _Py_mem_interp_free_queue {

‎Include/internal/pycore_lock.h‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,6 @@ typedef struct {
168168
size_t level;
169169
} _PyRecursiveMutex;
170170

171-
// All other threads that use the mutex must be barred from reattaching.
172-
extern void _PyRecursiveMutex_at_shutdown_reinit(_PyRecursiveMutex *m);
173-
174171
PyAPI_FUNC(int) _PyRecursiveMutex_IsLockedByCurrentThread(_PyRecursiveMutex *m);
175172
PyAPI_FUNC(void) _PyRecursiveMutex_Lock(_PyRecursiveMutex *m);
176173
extern PyLockStatus _PyRecursiveMutex_LockTimed(_PyRecursiveMutex *m, PyTime_t timeout, _PyLockFlags flags);

‎Include/internal/pycore_warnings.h‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ extern "C" {
88
# error "this header requires Py_BUILD_CORE define"
99
#endif
1010

11-
extern void _PyWarnings_PrepareForFinalization(PyInterpreterState *interp);
12-
1311
extern int _PyWarnings_InitState(PyInterpreterState *interp);
1412

1513
extern PyObject* _PyWarnings_Init(void);

‎Lib/test/test_embed.py‎

Lines changed: 25 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -508,65 +508,46 @@ def test_datetime_reset_strptime(self):
508508
self.assertEqual(out, '20000101\n' * INIT_LOOPS)
509509

510510
@threading_helper.requires_working_threading()
511-
@unittest.skipUnless(_testinternalcapi, "requires _testinternalcapi")
512511
def test_warnings_at_shutdown(self):
513-
import_helper.import_module('_testcapi')
514512
code = textwrap.dedent('''
515-
import _testinternalcapi
516-
import _warnings
517513
import threading
514+
import time
518515
import warnings
519-
from test import support
520516
521-
warnings.simplefilter('ignore')
522-
local = threading.local()
517+
warnings.simplefilter('always', ResourceWarning)
523518
ready = threading.Event()
524519
525520
class WarnOnDelete:
521+
def __init__(self):
522+
self.self_ref = self
523+
526524
def __del__(self):
527-
warnings.warn('during shutdown', ResourceWarning)
528-
if not ready.is_set():
529-
# The finalizing thread still owns all three levels.
530-
for _ in range(3):
531-
_warnings._release_lock()
532-
try:
533-
_warnings._release_lock()
534-
except RuntimeError:
535-
pass
536-
else:
537-
raise AssertionError('unexpected recursion depth')
538-
print('finalized')
525+
warnings.warn('late', ResourceWarning)
526+
527+
def showwarning(message, *args, **kwargs):
528+
if str(message) == 'hold':
529+
warnings.warn('nested', ResourceWarning)
530+
ready.set()
531+
# A Python event cannot synchronize with the point where
532+
# shutdown stops daemon threads, so hold the lock briefly.
533+
time.sleep(0.1)
534+
else:
535+
print(message)
536+
537+
warnings.showwarning = showwarning
539538
540539
def worker():
541-
local.obj = WarnOnDelete()
542-
_warnings._acquire_lock()
543-
ready.set()
544-
threading.Event().wait()
540+
warnings.warn('hold', ResourceWarning)
545541
546-
for _ in range(3):
547-
_warnings._acquire_lock()
548542
threading.Thread(target=worker, daemon=True).start()
549-
for _ in support.sleeping_retry(4):
550-
if _testinternalcapi.warnings_lock_has_waiters():
551-
break
552-
''')
553-
release = textwrap.dedent('''
554-
for _ in range(3):
555-
_warnings._release_lock()
556-
assert ready.wait(4), 'lock not acquired'
543+
assert ready.wait(4), 'daemon did not start warning'
544+
# Collect the cycle during shutdown GC, after daemon threads stop.
545+
WarnOnDelete()
557546
''')
558-
subinterp = (
559-
'from test import support\n'
560-
f'assert support.run_in_subinterp({code + release!r}) == 0'
561-
)
562-
# Cover Py_EndInterpreter(), then shut down with a waiter. Reuse the
563-
# main interpreter's mutex and shut down with a daemon owning it.
564-
# All three thread-local finalizers must warn.
565547
out, err = self.run_embedded_interpreter(
566-
"test_repeated_init_exec", subinterp + '\n' + code, code + release,
567-
env=remove_python_envvars(),
568-
timeout=10)
569-
self.assertEqual(out, 'finalized\n' * 3)
548+
"test_repeated_init_exec", code, "pass",
549+
env=remove_python_envvars(), timeout=10)
550+
self.assertEqual(out, 'nested\nlate\n')
570551
self.assertEqual(err, '--- Loop #1 ---\n--- Loop #2 ---\n')
571552

572553
def test_static_types_inherited_slots(self):
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
Prevent a deadlock when emitting warnings during late interpreter shutdown
2-
if a daemon thread holds the warnings lock or is waiting to acquire it.
1+
Prevent a shutdown deadlock when a daemon thread emits a warning. The
2+
interpreter now waits for the thread to release the warnings lock.

‎Modules/_testinternalcapi/test_lock.c‎

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
// C Extension module to test pycore_lock.h API
22

33
#include "parts.h"
4-
#include "pycore_interp.h" // PyInterpreterState.warnings
54
#include "pycore_lock.h"
6-
#include "pycore_pystate.h" // _PyEval_StopTheWorld()
75
#include "pycore_pythread.h" // PyThread_get_thread_ident_ex()
86

97
#include "clinic/test_lock.c.h"
@@ -551,21 +549,7 @@ test_lock_recursive(PyObject *self, PyObject *obj)
551549
Py_RETURN_NONE;
552550
}
553551

554-
static PyObject *
555-
warnings_lock_has_waiters(PyObject *self, PyObject *Py_UNUSED(ignored))
556-
{
557-
PyInterpreterState *interp = PyInterpreterState_Get();
558-
PyMutex *mutex = &interp->warnings.lock.mutex;
559-
// A contender sets the parked bit before enqueuing, but cannot detach
560-
// until it has enqueued. Stop the world to close that window on nogil.
561-
_PyEval_StopTheWorld(interp);
562-
uint8_t bits = _Py_atomic_load_uint8_relaxed(&mutex->_bits);
563-
_PyEval_StartTheWorld(interp);
564-
return PyBool_FromLong((bits & _Py_HAS_PARKED) != 0);
565-
}
566-
567552
static PyMethodDef test_methods[] = {
568-
{"warnings_lock_has_waiters", warnings_lock_has_waiters, METH_NOARGS},
569553
{"test_lock_basic", test_lock_basic, METH_NOARGS},
570554
{"test_lock_two_threads", test_lock_two_threads, METH_NOARGS},
571555
{"test_lock_counter", test_lock_counter, METH_NOARGS},

‎Python/_warnings.c‎

Lines changed: 74 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "pycore_frame.h" // PyFrameObject
33
#include "pycore_genobject.h" // PyAsyncGenObject
44
#include "pycore_import.h" // _PyImport_GetModules()
5+
#include "pycore_interp.h" // _PyInterpreterState_GetFinalizing()
56
#include "pycore_interpframe.h" // _PyFrame_GetCode()
67
#include "pycore_long.h" // _PyLong_GetZero()
78
#include "pycore_pylifecycle.h" // _Py_IsInterpreterFinalizing()
@@ -254,27 +255,66 @@ get_warnings_attr(PyInterpreterState *interp, PyObject *attr, int try_import)
254255
return obj;
255256
}
256257

257-
void
258-
_PyWarnings_PrepareForFinalization(PyInterpreterState *interp)
259-
{
260-
WarningsState *st = warnings_get_state(interp);
261-
_PyRecursiveMutex_at_shutdown_reinit(&st->lock);
262-
}
258+
static _Py_thread_local PyInterpreterGuard warnings_guard;
263259

264-
static inline void
260+
static inline int
265261
warnings_lock(PyInterpreterState *interp)
266262
{
267263
WarningsState *st = warnings_get_state(interp);
268264
assert(st != NULL);
265+
if (_PyRecursiveMutex_IsLockedByCurrentThread(&st->lock)) {
266+
_PyRecursiveMutex_Lock(&st->lock);
267+
return 0;
268+
}
269+
PyInterpreterGuard *guard = NULL;
270+
if (_PyInterpreterState_GetFinalizing(interp) != _PyThreadState_GET()) {
271+
guard = &warnings_guard;
272+
if (guard->interp != NULL) {
273+
guard = PyMem_RawMalloc(sizeof(*guard));
274+
if (guard == NULL) {
275+
PyErr_NoMemory();
276+
return -1;
277+
}
278+
}
279+
if (_PyInterpreterGuard_TryAcquire(interp, guard) < 0) {
280+
if (guard != &warnings_guard) {
281+
PyMem_RawFree(guard);
282+
}
283+
// Finalization has begun; this thread must stop before it takes
284+
// the lock.
285+
_PyThreadState_HangThread(_PyThreadState_GET());
286+
}
287+
}
269288
_PyRecursiveMutex_Lock(&st->lock);
289+
st->lock_guard = guard;
290+
return 0;
270291
}
271292

272293
static inline int
273294
warnings_unlock(PyInterpreterState *interp)
274295
{
275296
WarningsState *st = warnings_get_state(interp);
276297
assert(st != NULL);
277-
return _PyRecursiveMutex_TryUnlock(&st->lock);
298+
if (!_PyRecursiveMutex_IsLockedByCurrentThread(&st->lock)) {
299+
return -1;
300+
}
301+
if (st->lock.level > 0) {
302+
_PyRecursiveMutex_Unlock(&st->lock);
303+
return 0;
304+
}
305+
PyInterpreterGuard *guard = st->lock_guard;
306+
st->lock_guard = NULL;
307+
_PyRecursiveMutex_Unlock(&st->lock);
308+
if (guard != NULL) {
309+
_PyInterpreterGuard_Release(guard);
310+
if (guard == &warnings_guard) {
311+
guard->interp = NULL;
312+
}
313+
else {
314+
PyMem_RawFree(guard);
315+
}
316+
}
317+
return 0;
278318
}
279319

280320
static inline bool
@@ -358,7 +398,9 @@ warnings_acquire_lock_impl(PyObject *module)
358398
if (interp == NULL) {
359399
return NULL;
360400
}
361-
warnings_lock(interp);
401+
if (warnings_lock(interp) < 0) {
402+
return NULL;
403+
}
362404
Py_RETURN_NONE;
363405
}
364406

@@ -1140,10 +1182,14 @@ do_warn(PyObject *message, PyObject *category, Py_ssize_t stack_level,
11401182
&filename, &lineno, &module, &registry))
11411183
return NULL;
11421184

1143-
warnings_lock(tstate->interp);
1144-
res = warn_explicit(tstate, category, message, filename, lineno, module, registry,
1145-
NULL, source);
1146-
warnings_unlock(tstate->interp);
1185+
if (warnings_lock(tstate->interp) < 0) {
1186+
res = NULL;
1187+
}
1188+
else {
1189+
res = warn_explicit(tstate, category, message, filename, lineno, module, registry,
1190+
NULL, source);
1191+
warnings_unlock(tstate->interp);
1192+
}
11471193
Py_DECREF(filename);
11481194
Py_DECREF(registry);
11491195
Py_DECREF(module);
@@ -1322,10 +1368,14 @@ warnings_warn_explicit_impl(PyObject *module, PyObject *message,
13221368
}
13231369
}
13241370

1325-
warnings_lock(tstate->interp);
1326-
returned = warn_explicit(tstate, category, message, filename, lineno,
1327-
mod, registry, source_line, sourceobj);
1328-
warnings_unlock(tstate->interp);
1371+
if (warnings_lock(tstate->interp) < 0) {
1372+
returned = NULL;
1373+
}
1374+
else {
1375+
returned = warn_explicit(tstate, category, message, filename, lineno,
1376+
mod, registry, source_line, sourceobj);
1377+
warnings_unlock(tstate->interp);
1378+
}
13291379
Py_XDECREF(source_line);
13301380
return returned;
13311381
}
@@ -1472,7 +1522,9 @@ PyErr_WarnExplicitObject(PyObject *category, PyObject *message,
14721522
return -1;
14731523
}
14741524

1475-
warnings_lock(tstate->interp);
1525+
if (warnings_lock(tstate->interp) < 0) {
1526+
return -1;
1527+
}
14761528
res = warn_explicit(tstate, category, message, filename, lineno,
14771529
module, registry, NULL, NULL);
14781530
warnings_unlock(tstate->interp);
@@ -1537,19 +1589,17 @@ PyErr_WarnExplicitFormat(PyObject *category,
15371589
va_start(vargs, format);
15381590
message = PyUnicode_FromFormatV(format, vargs);
15391591
if (message != NULL) {
1540-
PyObject *res;
15411592
PyThreadState *tstate = get_current_tstate();
1542-
if (tstate != NULL) {
1543-
warnings_lock(tstate->interp);
1544-
res = warn_explicit(tstate, category, message, filename, lineno,
1545-
module, registry, NULL, NULL);
1593+
if (tstate != NULL && warnings_lock(tstate->interp) == 0) {
1594+
PyObject *res = warn_explicit(tstate, category, message, filename, lineno,
1595+
module, registry, NULL, NULL);
15461596
warnings_unlock(tstate->interp);
1547-
Py_DECREF(message);
15481597
if (res != NULL) {
15491598
Py_DECREF(res);
15501599
ret = 0;
15511600
}
15521601
}
1602+
Py_DECREF(message);
15531603
}
15541604
va_end(vargs);
15551605
exit:

‎Python/lock.c‎

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -427,21 +427,6 @@ _PyRecursiveMutex_LockTimed(_PyRecursiveMutex *m, PyTime_t timeout, _PyLockFlags
427427
return s;
428428
}
429429

430-
void
431-
_PyRecursiveMutex_at_shutdown_reinit(_PyRecursiveMutex *m)
432-
{
433-
// Preserve the finalizing thread's ownership and recursion depth.
434-
if (_PyRecursiveMutex_IsLockedByCurrentThread(m)) {
435-
m->mutex = (PyMutex){._bits = _Py_LOCKED};
436-
}
437-
else {
438-
memset(m, 0, sizeof(*m));
439-
}
440-
// Remove parked waiters from the queue. After waking, they will hang
441-
// when they try to reattach their thread state.
442-
_PyParkingLot_UnparkAll(&m->mutex._bits);
443-
}
444-
445430
void
446431
_PyRecursiveMutex_Unlock(_PyRecursiveMutex *m)
447432
{

‎Python/pylifecycle.c‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2452,7 +2452,6 @@ _Py_Finalize(_PyRuntimeState *runtime)
24522452
for (PyThreadState *p = list; p != NULL; p = p->next) {
24532453
_PyThreadState_SetShuttingDown(p);
24542454
}
2455-
_PyWarnings_PrepareForFinalization(tstate->interp);
24562455
_PyEval_StartTheWorldAll(runtime);
24572456
PyMutex_Unlock(&tstate->interp->ceval.pending.mutex);
24582457

@@ -2835,7 +2834,6 @@ Py_EndInterpreter(PyThreadState *tstate)
28352834
_PyThreadState_SetShuttingDown(p);
28362835
}
28372836

2838-
_PyWarnings_PrepareForFinalization(interp);
28392837
_PyEval_StartTheWorldAll(interp->runtime);
28402838
PyMutex_Unlock(&interp->ceval.pending.mutex);
28412839
_PyThreadState_DeleteList(list, /*is_after_fork=*/0);

0 commit comments

Comments
 (0)