https://github.com/python/cpython/commit/6e9a900916e32f4565f0ecebf7b6f48f5a60bc66
commit: 6e9a900916e32f4565f0ecebf7b6f48f5a60bc66
branch: main
author: John <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-11-09T18:54:57+05:30
summary:

gh-161366: Fix deadlock with FT GC debug collectables print (#161444)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-11-05-21-00-57.gh-issue-161366.ouxAvv.rst
M Lib/test/test_free_threading/test_gc.py
M Python/gc_free_threading.c

diff --git a/Lib/test/test_free_threading/test_gc.py 
b/Lib/test/test_free_threading/test_gc.py
index b287b51826999c..8c97b04ed4f71b 100644
--- a/Lib/test/test_free_threading/test_gc.py
+++ b/Lib/test/test_free_threading/test_gc.py
@@ -9,6 +9,7 @@
 
 from test import support
 from test.support import threading_helper
+from test.support.script_helper import assert_python_ok
 
 
 class MyObj:
@@ -110,6 +111,35 @@ def run(self):
 
         self.assertTrue(C().run())
 
+    def test_debug_collectable_python_stderr(self):
+        # The DEBUG_COLLECTABLE output is written with sys.stderr.write(),
+        # which may run Python code or wait for another thread (e.g. for the
+        # lock of a buffered stream), so it must not be written while the
+        # world is stopped.  Debug builds assert that the world is not stopped
+        # when Python code runs, so use a sys.stderr written in Python.
+        code = """if 1:
+            import gc
+            import io
+            import sys
+
+            class Stderr(io.StringIO):
+                def write(self, s):
+                    return super().write(s)
+
+            sys.stderr = Stderr()
+            gc.set_debug(gc.DEBUG_COLLECTABLE)
+            a = []
+            a.append(a)
+            del a
+            gc.collect()
+            gc.set_debug(0)
+            output = sys.stderr.getvalue()
+            sys.stderr = sys.__stderr__
+            print(output)
+            """
+        _, out, _ = assert_python_ok("-c", code)
+        self.assertRegex(out, b"gc: collectable <list 0x[0-9A-Fa-f]+>")
+
     def test_merge_brc_queue_of_detached_thread(self):
         # GH-157838: objects queued for merging by a thread that is detached
         # (blocked in a lock acquire, sleep, etc.) are merged and freed on its
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-11-05-21-00-57.gh-issue-161366.ouxAvv.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-05-21-00-57.gh-issue-161366.ouxAvv.rst
new file mode 100644
index 00000000000000..94098a0cf5195c
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-11-05-21-00-57.gh-issue-161366.ouxAvv.rst
@@ -0,0 +1,4 @@
+Fix a deadlock in the garbage collector of the :term:`free-threaded build`
+when :data:`gc.DEBUG_COLLECTABLE` is set and another thread is writing to
+:data:`sys.stderr`. The debug output is now written after the other threads
+are resumed.
diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c
index 6eaeeea8848cdb..f4f1a7777f3522 100644
--- a/Python/gc_free_threading.c
+++ b/Python/gc_free_threading.c
@@ -2158,20 +2158,24 @@ gc_collect_internal(PyInterpreterState *interp, struct 
collection_state *state,
     // Deallocations after the refcount merge may run Python code.
     bool needs_resurrection_check = state->objs_to_decref.head != 0;
 
-    // Print debugging information.
-    if (FT_ATOMIC_LOAD_INT_RELAXED(interp->gc.debug) & 
_PyGC_DEBUG_COLLECTABLE) {
-        needs_resurrection_check = true;
-        PyObject *op;
-        WORKSTACK_FOR_EACH(&state->unreachable, op) {
-            debug_cycle("collectable", op);
-        }
-    }
+    // The debug output is written with sys.stderr.write(), which may run
+    // Python code, so it is printed below after the world is started.
+    bool debug_print = (FT_ATOMIC_LOAD_INT_RELAXED(interp->gc.debug) &
+                        _PyGC_DEBUG_COLLECTABLE) != 0;
 
     // Find weakref callbacks and pending finalizers (but do not call them).
     bool has_finalizers = prepare_finalization(state, 
&needs_resurrection_check);
-    if (needs_resurrection_check) {
+    if (needs_resurrection_check || debug_print) {
         _PyEval_StartTheWorld(interp);
 
+        // Print debugging information.
+        if (debug_print) {
+            PyObject *op;
+            WORKSTACK_FOR_EACH(&state->unreachable, op) {
+                debug_cycle("collectable", op);
+            }
+        }
+
         // Deallocate any object from the refcount merge step
         cleanup_worklist(&state->objs_to_decref);
 

_______________________________________________
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]

Reply via email to