8000 [3.11] gh-106883 Fix deadlock in threaded application (#117332) · python/cpython@6b37486 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6b37486

Browse files
authored
[3.11] gh-106883 Fix deadlock in threaded application (#117332)
When using threaded applications, there is a high risk of a deadlock in the interpreter. It's a lock ordering deadlock with HEAD_LOCK(&_PyRuntime); and the GIL. By disabling the GC during the _PyThread_CurrentFrames() and _PyThread_CurrentExceptions() calls fixes the issue.
1 parent f1897d3 commit 6b37486

File tree

3 files changed

+93
-0
lines changed

3 files changed

+93
-0
lines changed

Lib/test/test_sys.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from test.support.script_helper import assert_python_ok, assert_python_failure
1515
from test.support import threading_helper
1616
from test.support import import_helper
17+
from test.support import skip_if_sanitizer
1718
import textwrap
1819
import unittest
1920
import warnings
@@ -471,6 +472,79 @@ def g456():
471472
leave_g.set()
472473
t.join()
473474

475+
@skip_if_sanitizer(memory=True, address=True, reason= "Test too slow "
476+
"when the address sanitizer is enabled.")
477+
@threading_helper.reap_threads
478+
@threading_helper.requires_working_threading()
479+
@support.requires_fork()
480+
def test_current_frames_exceptions_deadlock(self):
481+
"""
482+
Reproduce the bug raised in GH-106883 and GH-116969.
483+
"""
484+
import threading
485+
import time
486+
import signal
487+
488+
class MockObject:
489+
def __init__(self):
490+
# Create some garbage
491+
self._list = list(range(10000))
492+
# Call the functions under test
493+
self._trace = sys._current_frames()
494+
self._exceptions = sys._current_exceptions()
495+
496+
def __del__(self):
497+
# The presence of the __del__ method causes the deadlock when
498+
# there is one thread executing the _current_frames or
499+
# _current_exceptions functions and the other thread is
500+
# running the GC:
501+
# thread 1 has the interpreter lock and it is trying to
502+
# acquire the GIL; thread 2 holds the GIL but is trying to
503+
# acquire the interpreter lock.
504+
# When the GC is running and it finds that an
505+
# object has the __del__ method, it needs to execute the
506+
# Python code in it and it requires the GIL to execute it
507+
# (which will never happen because it is held by another thread
508+
# blocked on the acquisition of the interpreter lock)
509+
pass
510+
511+
def thread_function(num_objects):
512+
obj = None
513+
for _ in range(num_objects):
514+
obj = MockObject()
515+
516+
# The number of objects should be big enough to increase the
517+
# chances to call the GC.
518+
NUM_OBJECTS = 1000
519+
NUM_THREADS = 10
520+
521+
# 40 seconds should be enough for the test to be executed: if it
522+
# is more than 40 seconds it means that the process is in deadlock
523+
# hence the test fails
524+
TIMEOUT = 40
525+
526+
# Test the sys._current_frames and sys._current_exceptions calls
527+
pid = os.fork()
528+
if pid: # parent process
529+
try:
530+
support.wait_process(pid, exitcode=0, timeout=TIMEOUT)
531+
except KeyboardInterrupt:
532+
# When pressing CTRL-C kill the deadlocked process
533+
os.kill(pid, signal.SIGTERM)
534+
raise
535+
else: # child process
536+
# Run the actual test in the forked process.
537+
threads = []
538+
for i in range(NUM_THREADS):
539+
thread = threading.Thread(
540+
target=thread_function, args=(NUM_OBJECTS,)
541+
)
542+
threads.append(thread)
543+
thread.start()
544+
for t in threads:
545+
t.join()
546+
os._exit(0)
547+
474548
@threading_helper.reap_threads
475549
@threading_helper.requires_working_threading()
476550
def test_current_exceptions(self):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Disable GC during the _PyThread_CurrentFrames() and _PyThread_CurrentExceptions() calls to avoid the interpreter to deadlock.

Python/pystate.c

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,9 @@ _PyThread_CurrentFrames(void)
13981398
return NULL;
13991399
}
14001400

1401+
// gh-106883: Disable the GC as this can cause the interpreter to deadlock
1402+
int gc_was_enabled = PyGC_Disable();
1403+
14011404
/* for i in all interpreters:
14021405
* for t in all of i's thread states:
14031406
* if t's frame isn't NULL, map t's id to its frame
@@ -1440,6 +1443,12 @@ _PyThread_CurrentFrames(void)
14401443

14411444
done:
14421445
HEAD_UNLOCK(runtime);
1446+
1447+
// Once the runtime is released, the GC can be reenabled.
1448+
if (gc_was_enabled) {
1449+
PyGC_Enable();
1450+
}
1451+
14431452
return result;
14441453
}
14451454

@@ -1459,6 +1468,9 @@ _PyThread_CurrentExceptions(void)
14591468
return NULL;
14601469
}
14611470

1471+
// gh-106883: Disable the GC as this can cause the interpreter to deadlock
1472+
int gc_was_enabled = PyGC_Disable();
1473+
14621474
/* for i in all interpreters:
14631475
* for t in all of i's thread states:
14641476
* if t's frame isn't NULL, map t's id to its frame
@@ -1499,6 +1511,12 @@ _PyThread_CurrentExceptions(void)
14991511

15001512
done:
15011513
HEAD_UNLOCK(runtime);
1514+
1515+
// Once the runtime is released, the GC can be reenabled.
1516+
if (gc_was_enabled) {
1517+
PyGC_Enable();
1518+
}
1519+
15021520
return result;
15031521
}
15041522

0 commit comments

Comments
 (0)
0