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]