Skip to content

Commit ed144b7

Browse files
authored
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.
1 parent 13c758d commit ed144b7

3 files changed

Lines changed: 19 additions & 11 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a data race on the garbage collector debug flag in free-threading builds:
2+
:func:`gc.set_debug` and :func:`gc.get_debug` now access it atomically.

‎Modules/gcmodule.c‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "Python.h"
88
#include "pycore_gc.h"
99
#include "pycore_object.h" // _PyObject_IS_GC()
10+
#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_INT_RELAXED()
1011
#include "pycore_pystate.h" // _PyInterpreterState_GET()
1112

1213
typedef struct _gc_runtime_state GCState;
@@ -116,7 +117,7 @@ gc_set_debug_impl(PyObject *module, int flags)
116117
/*[clinic end generated code: output=7c8366575486b228 input=5e5ce15e84fbed15]*/
117118
{
118119
GCState *gcstate = get_gc_state();
119-
gcstate->debug = flags;
120+
FT_ATOMIC_STORE_INT_RELAXED(gcstate->debug, flags);
120121
Py_RETURN_NONE;
121122
}
122123

@@ -131,7 +132,7 @@ gc_get_debug_impl(PyObject *module)
131132
/*[clinic end generated code: output=91242f3506cd1e50 input=91a101e1c3b98366]*/
132133
{
133134
GCState *gcstate = get_gc_state();
134-
return gcstate->debug;
135+
return FT_ATOMIC_LOAD_INT_RELAXED(gcstate->debug);
135136
}
136137

137138
/*[clinic input]

‎Python/gc_free_threading.c‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "pycore_interpframe.h" // _PyFrame_GetLocalsArray()
1212
#include "pycore_list.h" // _PyList_GetItemRef()
1313
#include "pycore_object_alloc.h" // _PyObject_MallocWithType()
14+
#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_INT_RELAXED()
1415
#include "pycore_pystate.h" // _PyThreadState_GET()
1516
#include "pycore_tstate.h" // _PyThreadStateImpl
1617
#include "pycore_tuple.h" // _PyTuple_MaybeUntrack()
@@ -1738,6 +1739,7 @@ delete_garbage(struct collection_state *state)
17381739
{
17391740
PyThreadState *tstate = _PyThreadState_GET();
17401741
GCState *gcstate = state->gcstate;
1742+
int debug = FT_ATOMIC_LOAD_INT_RELAXED(gcstate->debug);
17411743

17421744
assert(!_PyErr_Occurred(tstate));
17431745

@@ -1760,7 +1762,7 @@ delete_garbage(struct collection_state *state)
17601762

17611763
state->collected++;
17621764

1763-
if (gcstate->debug & _PyGC_DEBUG_SAVEALL) {
1765+
if (debug & _PyGC_DEBUG_SAVEALL) {
17641766
assert(gcstate->garbage != NULL);
17651767
if (PyList_Append(gcstate->garbage, op) < 0) {
17661768
_PyErr_Clear(tstate);
@@ -1786,16 +1788,17 @@ handle_legacy_finalizers(struct collection_state *state)
17861788
{
17871789
GCState *gcstate = state->gcstate;
17881790
assert(gcstate->garbage != NULL);
1791+
int debug = FT_ATOMIC_LOAD_INT_RELAXED(gcstate->debug);
17891792

17901793
PyObject *op;
17911794
while ((op = worklist_pop(&state->legacy_finalizers)) != NULL) {
17921795
state->uncollectable++;
17931796

1794-
if (gcstate->debug & _PyGC_DEBUG_UNCOLLECTABLE) {
1797+
if (debug & _PyGC_DEBUG_UNCOLLECTABLE) {
17951798
debug_cycle("uncollectable", op);
17961799
}
17971800

1798-
if ((gcstate->debug & _PyGC_DEBUG_SAVEALL) || has_legacy_finalizer(op)) {
1801+
if ((debug & _PyGC_DEBUG_SAVEALL) || has_legacy_finalizer(op)) {
17991802
if (PyList_Append(gcstate->garbage, op) < 0) {
18001803
PyErr_Clear();
18011804
}
@@ -2150,7 +2153,7 @@ gc_collect_internal(PyInterpreterState *interp, struct collection_state *state,
21502153
bool needs_resurrection_check = state->objs_to_decref.head != 0;
21512154

21522155
// Print debugging information.
2153-
if (interp->gc.debug & _PyGC_DEBUG_COLLECTABLE) {
2156+
if (FT_ATOMIC_LOAD_INT_RELAXED(interp->gc.debug) & _PyGC_DEBUG_COLLECTABLE) {
21542157
needs_resurrection_check = true;
21552158
PyObject *op;
21562159
WORKSTACK_FOR_EACH(&state->unreachable, op) {
@@ -2264,7 +2267,8 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
22642267
invoke_gc_callback(tstate, "start", generation, 0, 0, 0, 0.0);
22652268
}
22662269

2267-
if (gcstate->debug & _PyGC_DEBUG_STATS) {
2270+
int debug = FT_ATOMIC_LOAD_INT_RELAXED(gcstate->debug);
2271+
if (debug & _PyGC_DEBUG_STATS) {
22682272
PySys_WriteStderr("gc: collecting generation %d...\n", generation);
22692273
show_stats_each_generations(gcstate);
22702274
}
@@ -2291,7 +2295,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
22912295
(void)PyTime_PerfCounterRaw(&stop);
22922296
double duration = PyTime_AsSecondsDouble(stop - start);
22932297

2294-
if (gcstate->debug & _PyGC_DEBUG_STATS) {
2298+
if (debug & _PyGC_DEBUG_STATS) {
22952299
PySys_WriteStderr(
22962300
"gc: done, %zd unreachable, %zd uncollectable, %.4fs elapsed\n",
22972301
n+m, n, duration);
@@ -2624,10 +2628,11 @@ void
26242628
_PyGC_DumpShutdownStats(PyInterpreterState *interp)
26252629
{
26262630
GCState *gcstate = &interp->gc;
2627-
if (!(gcstate->debug & _PyGC_DEBUG_SAVEALL)
2631+
int debug = FT_ATOMIC_LOAD_INT_RELAXED(gcstate->debug);
2632+
if (!(debug & _PyGC_DEBUG_SAVEALL)
26282633
&& gcstate->garbage != NULL && PyList_GET_SIZE(gcstate->garbage) > 0) {
26292634
const char *message;
2630-
if (gcstate->debug & _PyGC_DEBUG_UNCOLLECTABLE) {
2635+
if (debug & _PyGC_DEBUG_UNCOLLECTABLE) {
26312636
message = "gc: %zd uncollectable objects at shutdown";
26322637
}
26332638
else {
@@ -2643,7 +2648,7 @@ _PyGC_DumpShutdownStats(PyInterpreterState *interp)
26432648
{
26442649
PyErr_FormatUnraisable("Exception ignored in GC shutdown");
26452650
}
2646-
if (gcstate->debug & _PyGC_DEBUG_UNCOLLECTABLE) {
2651+
if (debug & _PyGC_DEBUG_UNCOLLECTABLE) {
26472652
PyObject *repr = NULL, *bytes = NULL;
26482653
repr = PyObject_Repr(gcstate->garbage);
26492654
if (!repr || !(bytes = PyUnicode_EncodeFSDefault(repr))) {

0 commit comments

Comments
 (0)