Skip to content

Commit 91d71dd

Browse files
authored
gh-149816: #96 Fix a race condition in invoke_gc_callback with free threading (GH-150029)
1 parent e8158d1 commit 91d71dd

3 files changed

Lines changed: 44 additions & 17 deletions

File tree

Lib/test/test_free_threading/test_gc.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import threading
44
from threading import Thread
5+
import time
56
from unittest import TestCase
67
import gc
78

@@ -94,6 +95,26 @@ def evil():
9495
thread.start()
9596
thread.join()
9697

98+
def test_gc_callbacks_race_with_mutation(self):
99+
def collect():
100+
b.wait()
101+
while not stop.is_set():
102+
gc.collect()
103+
104+
def mutate():
105+
b.wait()
106+
while not stop.is_set():
107+
gc.callbacks[:] = [lambda *_: _ for _ in range(16)]
108+
time.sleep(0)
109+
gc.callbacks.clear()
110+
111+
threads = [threading.Thread(target=f) for f in (collect, mutate) * 4]
112+
b = threading.Barrier(len(threads) + 1)
113+
stop = threading.Event()
114+
115+
with threading_helper.start_threads(threads, stop.set):
116+
b.wait()
117+
time.sleep(0.2)
97118
def test_set_threshold(self):
98119
# GH-148613: Setting the GC threshold from another thread could cause a
99120
# race between the `gc_should_collect` and `gc_set_threshold` functions.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix race conditions in ``invoke_gc_callback`` iterating ``gc.callbacks``
2+
in free-threading mode.

Python/gc_free_threading.c

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "pycore_initconfig.h" // _PyStatus_NO_MEMORY()
1010
#include "pycore_interp.h" // PyInterpreterState.gc
1111
#include "pycore_interpframe.h" // _PyFrame_GetLocalsArray()
12+
#include "pycore_list.h" // _PyList_GetItemRef()
1213
#include "pycore_object_alloc.h" // _PyObject_MallocWithType()
1314
#include "pycore_pystate.h" // _PyThreadState_GET()
1415
#include "pycore_tstate.h" // _PyThreadStateImpl
@@ -1940,33 +1941,36 @@ invoke_gc_callback(PyThreadState *tstate, const char *phase,
19401941

19411942
/* The local variable cannot be rebound, check it for sanity */
19421943
assert(PyList_CheckExact(gcstate->callbacks));
1943-
PyObject *info = NULL;
1944-
if (PyList_GET_SIZE(gcstate->callbacks) != 0) {
1945-
info = Py_BuildValue("{sisnsnsnsd}",
1946-
"generation", generation,
1947-
"collected", collected,
1948-
"uncollectable", uncollectable,
1949-
"candidates", candidates,
1950-
"duration", duration);
1951-
if (info == NULL) {
1952-
PyErr_FormatUnraisable("Exception ignored while "
1953-
"invoking gc callbacks");
1954-
return;
1955-
}
1944+
if (PyList_GET_SIZE(gcstate->callbacks) == 0) {
1945+
return;
1946+
}
1947+
1948+
PyObject *info = Py_BuildValue("{sisnsnsnsd}",
1949+
"generation", generation,
1950+
"collected", collected,
1951+
"uncollectable", uncollectable,
1952+
"candidates", candidates,
1953+
"duration", duration);
1954+
if (info == NULL) {
1955+
PyErr_FormatUnraisable("Exception ignored while "
1956+
"invoking gc callbacks");
1957+
return;
19561958
}
19571959

19581960
PyObject *phase_obj = PyUnicode_FromString(phase);
19591961
if (phase_obj == NULL) {
1960-
Py_XDECREF(info);
1962+
Py_DECREF(info);
19611963
PyErr_FormatUnraisable("Exception ignored while "
19621964
"invoking gc callbacks");
19631965
return;
19641966
}
19651967

19661968
PyObject *stack[] = {phase_obj, info};
19671969
for (Py_ssize_t i=0; i<PyList_GET_SIZE(gcstate->callbacks); i++) {
1968-
PyObject *r, *cb = PyList_GET_ITEM(gcstate->callbacks, i);
1969-
Py_INCREF(cb); /* make sure cb doesn't go away */
1970+
PyObject *r, *cb = _PyList_GetItemRef((PyListObject *)gcstate->callbacks, i);
1971+
if (cb == NULL) {
1972+
break;
1973+
}
19701974
r = PyObject_Vectorcall(cb, stack, 2, NULL);
19711975
if (r == NULL) {
19721976
PyErr_FormatUnraisable("Exception ignored while "
@@ -1978,7 +1982,7 @@ invoke_gc_callback(PyThreadState *tstate, const char *phase,
19781982
Py_DECREF(cb);
19791983
}
19801984
Py_DECREF(phase_obj);
1981-
Py_XDECREF(info);
1985+
Py_DECREF(info);
19821986
assert(!_PyErr_Occurred(tstate));
19831987
}
19841988

0 commit comments

Comments
 (0)