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

encukou <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
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]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.