https://github.com/python/cpython/commit/ed144b722f881dd639ed6a7c3dbd0f6aed2b4a7c
commit: ed144b722f881dd639ed6a7c3dbd0f6aed2b4a7c
branch: main
author: tonghuaroot (童话) <[email protected]>
committer: nascheme <[email protected]>
date: 2026-10-21T16:16:13-07:00
summary:
gh-153014: Fix data race on the GC debug flag in free-threading builds
(gh-153015)
In free-threading builds gc.set_debug() stored gcstate->debug with a plain
write while gc.get_debug() and the collector read it without synchronisation,
which ThreadSanitizer reports as a data race. Access the flag with
FT_ATOMIC_STORE/LOAD_INT_RELAXED, consistent with how gc.enable()/disable()
already handle gcstate->enabled.
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-07-04-18-00-00.gh-issue-153014.gCdBg1.rst
M Modules/gcmodule.c
M Python/gc_free_threading.c
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-04-18-00-00.gh-issue-153014.gCdBg1.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-04-18-00-00.gh-issue-153014.gCdBg1.rst
new file mode 100644
index 00000000000000..dec0bfc572118d
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-04-18-00-00.gh-issue-153014.gCdBg1.rst
@@ -0,0 +1,2 @@
+Fix a data race on the garbage collector debug flag in free-threading builds:
+:func:`gc.set_debug` and :func:`gc.get_debug` now access it atomically.
diff --git a/Modules/gcmodule.c b/Modules/gcmodule.c
index fea3897ceb4c8c..af90a56b912172 100644
--- a/Modules/gcmodule.c
+++ b/Modules/gcmodule.c
@@ -7,6 +7,7 @@
#include "Python.h"
#include "pycore_gc.h"
#include "pycore_object.h" // _PyObject_IS_GC()
+#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_INT_RELAXED()
#include "pycore_pystate.h" // _PyInterpreterState_GET()
typedef struct _gc_runtime_state GCState;
@@ -116,7 +117,7 @@ gc_set_debug_impl(PyObject *module, int flags)
/*[clinic end generated code: output=7c8366575486b228 input=5e5ce15e84fbed15]*/
{
GCState *gcstate = get_gc_state();
- gcstate->debug = flags;
+ FT_ATOMIC_STORE_INT_RELAXED(gcstate->debug, flags);
Py_RETURN_NONE;
}
@@ -131,7 +132,7 @@ gc_get_debug_impl(PyObject *module)
/*[clinic end generated code: output=91242f3506cd1e50 input=91a101e1c3b98366]*/
{
GCState *gcstate = get_gc_state();
- return gcstate->debug;
+ return FT_ATOMIC_LOAD_INT_RELAXED(gcstate->debug);
}
/*[clinic input]
diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c
index e8ed866312c94b..be06ceba5ca7b2 100644
--- a/Python/gc_free_threading.c
+++ b/Python/gc_free_threading.c
@@ -11,6 +11,7 @@
#include "pycore_interpframe.h" // _PyFrame_GetLocalsArray()
#include "pycore_list.h" // _PyList_GetItemRef()
#include "pycore_object_alloc.h" // _PyObject_MallocWithType()
+#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_INT_RELAXED()
#include "pycore_pystate.h" // _PyThreadState_GET()
#include "pycore_tstate.h" // _PyThreadStateImpl
#include "pycore_tuple.h" // _PyTuple_MaybeUntrack()
@@ -1738,6 +1739,7 @@ delete_garbage(struct collection_state *state)
{
PyThreadState *tstate = _PyThreadState_GET();
GCState *gcstate = state->gcstate;
+ int debug = FT_ATOMIC_LOAD_INT_RELAXED(gcstate->debug);
assert(!_PyErr_Occurred(tstate));
@@ -1760,7 +1762,7 @@ delete_garbage(struct collection_state *state)
state->collected++;
- if (gcstate->debug & _PyGC_DEBUG_SAVEALL) {
+ if (debug & _PyGC_DEBUG_SAVEALL) {
assert(gcstate->garbage != NULL);
if (PyList_Append(gcstate->garbage, op) < 0) {
_PyErr_Clear(tstate);
@@ -1786,16 +1788,17 @@ handle_legacy_finalizers(struct collection_state *state)
{
GCState *gcstate = state->gcstate;
assert(gcstate->garbage != NULL);
+ int debug = FT_ATOMIC_LOAD_INT_RELAXED(gcstate->debug);
PyObject *op;
while ((op = worklist_pop(&state->legacy_finalizers)) != NULL) {
state->uncollectable++;
- if (gcstate->debug & _PyGC_DEBUG_UNCOLLECTABLE) {
+ if (debug & _PyGC_DEBUG_UNCOLLECTABLE) {
debug_cycle("uncollectable", op);
}
- if ((gcstate->debug & _PyGC_DEBUG_SAVEALL) ||
has_legacy_finalizer(op)) {
+ if ((debug & _PyGC_DEBUG_SAVEALL) || has_legacy_finalizer(op)) {
if (PyList_Append(gcstate->garbage, op) < 0) {
PyErr_Clear();
}
@@ -2150,7 +2153,7 @@ gc_collect_internal(PyInterpreterState *interp, struct
collection_state *state,
bool needs_resurrection_check = state->objs_to_decref.head != 0;
// Print debugging information.
- if (interp->gc.debug & _PyGC_DEBUG_COLLECTABLE) {
+ if (FT_ATOMIC_LOAD_INT_RELAXED(interp->gc.debug) &
_PyGC_DEBUG_COLLECTABLE) {
needs_resurrection_check = true;
PyObject *op;
WORKSTACK_FOR_EACH(&state->unreachable, op) {
@@ -2264,7 +2267,8 @@ gc_collect_main(PyThreadState *tstate, int generation,
_PyGC_Reason reason)
invoke_gc_callback(tstate, "start", generation, 0, 0, 0, 0.0);
}
- if (gcstate->debug & _PyGC_DEBUG_STATS) {
+ int debug = FT_ATOMIC_LOAD_INT_RELAXED(gcstate->debug);
+ if (debug & _PyGC_DEBUG_STATS) {
PySys_WriteStderr("gc: collecting generation %d...\n", generation);
show_stats_each_generations(gcstate);
}
@@ -2291,7 +2295,7 @@ gc_collect_main(PyThreadState *tstate, int generation,
_PyGC_Reason reason)
(void)PyTime_PerfCounterRaw(&stop);
double duration = PyTime_AsSecondsDouble(stop - start);
- if (gcstate->debug & _PyGC_DEBUG_STATS) {
+ if (debug & _PyGC_DEBUG_STATS) {
PySys_WriteStderr(
"gc: done, %zd unreachable, %zd uncollectable, %.4fs elapsed\n",
n+m, n, duration);
@@ -2624,10 +2628,11 @@ void
_PyGC_DumpShutdownStats(PyInterpreterState *interp)
{
GCState *gcstate = &interp->gc;
- if (!(gcstate->debug & _PyGC_DEBUG_SAVEALL)
+ int debug = FT_ATOMIC_LOAD_INT_RELAXED(gcstate->debug);
+ if (!(debug & _PyGC_DEBUG_SAVEALL)
&& gcstate->garbage != NULL && PyList_GET_SIZE(gcstate->garbage) > 0) {
const char *message;
- if (gcstate->debug & _PyGC_DEBUG_UNCOLLECTABLE) {
+ if (debug & _PyGC_DEBUG_UNCOLLECTABLE) {
message = "gc: %zd uncollectable objects at shutdown";
}
else {
@@ -2643,7 +2648,7 @@ _PyGC_DumpShutdownStats(PyInterpreterState *interp)
{
PyErr_FormatUnraisable("Exception ignored in GC shutdown");
}
- if (gcstate->debug & _PyGC_DEBUG_UNCOLLECTABLE) {
+ if (debug & _PyGC_DEBUG_UNCOLLECTABLE) {
PyObject *repr = NULL, *bytes = NULL;
repr = PyObject_Repr(gcstate->garbage);
if (!repr || !(bytes = PyUnicode_EncodeFSDefault(repr))) {
_______________________________________________
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]