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]

Reply via email to