https://github.com/python/cpython/commit/91d71dd67074d4599b6bd49cc933f41f8bd57058
commit: 91d71dd67074d4599b6bd49cc933f41f8bd57058
branch: main
author: dzaramelcone <[email protected]>
committer: encukou <[email protected]>
date: 2026-08-20T16:22:24+02:00
summary:

gh-149816: #96 Fix a race condition in invoke_gc_callback with free threading 
(GH-150029)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-12-32-33.gh-issue-149816.v18Ypf.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 39901023450940..d1522a1d6da14e 100644
--- a/Lib/test/test_free_threading/test_gc.py
+++ b/Lib/test/test_free_threading/test_gc.py
@@ -2,6 +2,7 @@
 
 import threading
 from threading import Thread
+import time
 from unittest import TestCase
 import gc
 
@@ -94,6 +95,26 @@ def evil():
         thread.start()
         thread.join()
 
+    def test_gc_callbacks_race_with_mutation(self):
+        def collect():
+            b.wait()
+            while not stop.is_set():
+                gc.collect()
+
+        def mutate():
+            b.wait()
+            while not stop.is_set():
+                gc.callbacks[:] = [lambda *_: _ for _ in range(16)]
+                time.sleep(0)
+                gc.callbacks.clear()
+
+        threads = [threading.Thread(target=f) for f in (collect, mutate) * 4]
+        b = threading.Barrier(len(threads) + 1)
+        stop = threading.Event()
+
+        with threading_helper.start_threads(threads, stop.set):
+            b.wait()
+            time.sleep(0.2)
     def test_set_threshold(self):
         # GH-148613: Setting the GC threshold from another thread could cause a
         # race between the `gc_should_collect` and `gc_set_threshold` 
functions.
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-12-32-33.gh-issue-149816.v18Ypf.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-12-32-33.gh-issue-149816.v18Ypf.rst
new file mode 100644
index 00000000000000..bf7e4a624250e3
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-18-12-32-33.gh-issue-149816.v18Ypf.rst
@@ -0,0 +1,2 @@
+Fix race conditions in ``invoke_gc_callback`` iterating ``gc.callbacks``
+in free-threading mode.
diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c
index 99f1a1eb47e3dd..fbd13d1e4d87f2 100644
--- a/Python/gc_free_threading.c
+++ b/Python/gc_free_threading.c
@@ -9,6 +9,7 @@
 #include "pycore_initconfig.h"    // _PyStatus_NO_MEMORY()
 #include "pycore_interp.h"        // PyInterpreterState.gc
 #include "pycore_interpframe.h"   // _PyFrame_GetLocalsArray()
+#include "pycore_list.h"          // _PyList_GetItemRef()
 #include "pycore_object_alloc.h"  // _PyObject_MallocWithType()
 #include "pycore_pystate.h"       // _PyThreadState_GET()
 #include "pycore_tstate.h"        // _PyThreadStateImpl
@@ -1940,24 +1941,25 @@ invoke_gc_callback(PyThreadState *tstate, const char 
*phase,
 
     /* The local variable cannot be rebound, check it for sanity */
     assert(PyList_CheckExact(gcstate->callbacks));
-    PyObject *info = NULL;
-    if (PyList_GET_SIZE(gcstate->callbacks) != 0) {
-        info = Py_BuildValue("{sisnsnsnsd}",
-            "generation", generation,
-            "collected", collected,
-            "uncollectable", uncollectable,
-            "candidates", candidates,
-            "duration", duration);
-        if (info == NULL) {
-            PyErr_FormatUnraisable("Exception ignored while "
-                                   "invoking gc callbacks");
-            return;
-        }
+    if (PyList_GET_SIZE(gcstate->callbacks) == 0) {
+        return;
+    }
+
+    PyObject *info = Py_BuildValue("{sisnsnsnsd}",
+        "generation", generation,
+        "collected", collected,
+        "uncollectable", uncollectable,
+        "candidates", candidates,
+        "duration", duration);
+    if (info == NULL) {
+        PyErr_FormatUnraisable("Exception ignored while "
+                               "invoking gc callbacks");
+        return;
     }
 
     PyObject *phase_obj = PyUnicode_FromString(phase);
     if (phase_obj == NULL) {
-        Py_XDECREF(info);
+        Py_DECREF(info);
         PyErr_FormatUnraisable("Exception ignored while "
                                "invoking gc callbacks");
         return;
@@ -1965,8 +1967,10 @@ invoke_gc_callback(PyThreadState *tstate, const char 
*phase,
 
     PyObject *stack[] = {phase_obj, info};
     for (Py_ssize_t i=0; i<PyList_GET_SIZE(gcstate->callbacks); i++) {
-        PyObject *r, *cb = PyList_GET_ITEM(gcstate->callbacks, i);
-        Py_INCREF(cb); /* make sure cb doesn't go away */
+        PyObject *r, *cb = _PyList_GetItemRef((PyListObject 
*)gcstate->callbacks, i);
+        if (cb == NULL) {
+            break;
+        }
         r = PyObject_Vectorcall(cb, stack, 2, NULL);
         if (r == NULL) {
             PyErr_FormatUnraisable("Exception ignored while "
@@ -1978,7 +1982,7 @@ invoke_gc_callback(PyThreadState *tstate, const char 
*phase,
         Py_DECREF(cb);
     }
     Py_DECREF(phase_obj);
-    Py_XDECREF(info);
+    Py_DECREF(info);
     assert(!_PyErr_Occurred(tstate));
 }
 

_______________________________________________
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