https://github.com/python/cpython/commit/6b37486184590d19c6f24e620545ec8f8f65e4c7
commit: 6b37486184590d19c6f24e620545ec8f8f65e4c7
branch: 3.11
author: Diego Russo <diego.ru...@arm.com>
committer: pablogsal <pablog...@gmail.com>
date: 2025-03-11T15:31:03Z
summary:

[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.

files:
A Misc/NEWS.d/next/C API/2024-04-05-14-32-21.gh-issue-106883.OKmc0Q.rst
M Lib/test/test_sys.py
M Python/pystate.c

diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index 86cf1a794f973c..28931039f3792e 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -14,6 +14,7 @@
 from test.support.script_helper import assert_python_ok, assert_python_failure
 from test.support import threading_helper
 from test.support import import_helper
+from test.support import skip_if_sanitizer
 import textwrap
 import unittest
 import warnings
@@ -471,6 +472,79 @@ def g456():
         leave_g.set()
         t.join()
 
+    @skip_if_sanitizer(memory=True, address=True, reason= "Test too slow "
+                       "when the address sanitizer is enabled.")
+    @threading_helper.reap_threads
+    @threading_helper.requires_working_threading()
+    @support.requires_fork()
+    def test_current_frames_exceptions_deadlock(self):
+        """
+        Reproduce the bug raised in GH-106883 and GH-116969.
+        """
+        import threading
+        import time
+        import signal
+
+        class MockObject:
+            def __init__(self):
+                # Create some garbage
+                self._list = list(range(10000))
+                # Call the functions under test
+                self._trace = sys._current_frames()
+                self._exceptions = sys._current_exceptions()
+
+            def __del__(self):
+                # The presence of the __del__ method causes the deadlock when
+                # there is one thread executing the _current_frames or
+                # _current_exceptions functions and the other thread is
+                # running the GC:
+                # thread 1 has the interpreter lock and it is trying to
+                # acquire the GIL; thread 2 holds the GIL but is trying to
+                # acquire the interpreter lock.
+                # When the GC is running and it finds that an
+                # object has the __del__ method, it needs to execute the
+                # Python code in it and it requires the GIL to execute it
+                # (which will never happen because it is held by another thread
+                # blocked on the acquisition of the interpreter lock)
+                pass
+
+        def thread_function(num_objects):
+            obj = None
+            for _ in range(num_objects):
+                obj = MockObject()
+
+        # The number of objects should be big enough to increase the
+        # chances to call the GC.
+        NUM_OBJECTS = 1000
+        NUM_THREADS = 10
+
+        # 40 seconds should be enough for the test to be executed: if it
+        # is more than 40 seconds it means that the process is in deadlock
+        # hence the test fails
+        TIMEOUT = 40
+
+        # Test the sys._current_frames and sys._current_exceptions calls
+        pid = os.fork()
+        if pid:  # parent process
+            try:
+                support.wait_process(pid, exitcode=0, timeout=TIMEOUT)
+            except KeyboardInterrupt:
+                # When pressing CTRL-C kill the deadlocked process
+                os.kill(pid, signal.SIGTERM)
+                raise
+        else:  # child process
+            # Run the actual test in the forked process.
+            threads = []
+            for i in range(NUM_THREADS):
+                thread = threading.Thread(
+                    target=thread_function, args=(NUM_OBJECTS,)
+                )
+                threads.append(thread)
+                thread.start()
+            for t in threads:
+                t.join()
+            os._exit(0)
+
     @threading_helper.reap_threads
     @threading_helper.requires_working_threading()
     def test_current_exceptions(self):
diff --git a/Misc/NEWS.d/next/C 
API/2024-04-05-14-32-21.gh-issue-106883.OKmc0Q.rst b/Misc/NEWS.d/next/C 
API/2024-04-05-14-32-21.gh-issue-106883.OKmc0Q.rst
new file mode 100644
index 00000000000000..01c7fb0c790708
--- /dev/null
+++ b/Misc/NEWS.d/next/C API/2024-04-05-14-32-21.gh-issue-106883.OKmc0Q.rst     
@@ -0,0 +1 @@
+Disable GC during the _PyThread_CurrentFrames() and 
_PyThread_CurrentExceptions() calls to avoid the interpreter to deadlock.
diff --git a/Python/pystate.c b/Python/pystate.c
index db2ce878af64ec..a45116665fe195 100644
--- a/Python/pystate.c
+++ b/Python/pystate.c
@@ -1398,6 +1398,9 @@ _PyThread_CurrentFrames(void)
         return NULL;
     }
 
+    // gh-106883: Disable the GC as this can cause the interpreter to deadlock
+    int gc_was_enabled = PyGC_Disable();
+
     /* for i in all interpreters:
      *     for t in all of i's thread states:
      *          if t's frame isn't NULL, map t's id to its frame
@@ -1440,6 +1443,12 @@ _PyThread_CurrentFrames(void)
 
 done:
     HEAD_UNLOCK(runtime);
+
+    // Once the runtime is released, the GC can be reenabled.
+    if (gc_was_enabled) {
+        PyGC_Enable();
+    }
+
     return result;
 }
 
@@ -1459,6 +1468,9 @@ _PyThread_CurrentExceptions(void)
         return NULL;
     }
 
+    // gh-106883: Disable the GC as this can cause the interpreter to deadlock
+    int gc_was_enabled = PyGC_Disable();
+
     /* for i in all interpreters:
      *     for t in all of i's thread states:
      *          if t's frame isn't NULL, map t's id to its frame
@@ -1499,6 +1511,12 @@ _PyThread_CurrentExceptions(void)
 
 done:
     HEAD_UNLOCK(runtime);
+
+    // Once the runtime is released, the GC can be reenabled.
+    if (gc_was_enabled) {
+        PyGC_Enable();
+    }
+
     return result;
 }
 

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to