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 adf73d79a46661..8b5c04dd793d08 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(); } @@ -2146,7 +2149,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) { @@ -2260,7 +2263,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); } @@ -2287,7 +2291,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); @@ -2618,10 +2622,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 { @@ -2637,7 +2642,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))) {