Bug report
Bug description:
gc.get_stats() (gc_get_stats_impl) plain-copies the per-generation stats structs
gcstate->generation_stats->{young,old}.items[index] without any lock or atomics,
| stats[0] =gcstate->generation_stats->young.items[gcstate->generation_stats->young.index]; |
| stats[1] =gcstate->generation_stats->old[0].items[gcstate->generation_stats->old[0].index]; |
| stats[2] =gcstate->generation_stats->old[1].items[gcstate->generation_stats->old[1].index]; |
while a collection on another thread writes those same items[index] fields (collections++, collected += m, ...) at the end of gc_collect_main,
| /* Update stats */ |
| structgc_generation_stats*stats=get_stats(gcstate, generation); |
| stats->ts_start=start; |
| stats->ts_stop=stop; |
| stats->collections++; |
| stats->collected+=m; |
| stats->uncollectable+=n; |
| stats->duration+=duration; |
| stats->candidates+=state.candidates; |
so gc.get_stats() races the collector.
Reproducer:
importgcfromthreadingimportThreaddefcollector():
for_inrange(50000):
a= {}; a['self'] =agc.collect()
defreader():
for_inrange(50000):
gc.get_stats()
threads= [Thread(target=collector) for_inrange(1)]
threads+= [Thread(target=reader) for_inrange(8)]
fortinthreads: t.start()
fortinthreads: t.join()TSAN Report:
==================
WARNING: ThreadSanitizer: data race (pid=2301505)
Read of size 8 at 0x7fffb41a0180 by thread T2:
#0 gc_get_stats_impl /cpython/Modules/gcmodule.c:379:16 #1 gc_get_stats /cpython/Modules/clinic/gcmodule.c.h:456:12 #2 cfunction_vectorcall_NOARGS /cpython/Objects/methodobject.c:508:24 #3 _PyObject_VectorcallTstate /cpython/./Include/internal/pycore_call.h:144:11 #4 PyObject_Vectorcall /cpython/Objects/call.c:327:12 #5 _Py_VectorCallInstrumentation_StackRefSteal /cpython/Python/ceval.c:768:11 #6 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:1846:35 ...
Previous write of size 8 at 0x7fffb41a0180 by thread T1:
#0 gc_collect_main /cpython/Python/gc_free_threading.c:2288:23 #1 _PyGC_Collect /cpython/Python/gc_free_threading.c:2568:12 #2 gc_collect_impl /cpython/Modules/gcmodule.c:93:12 #3 gc_collect /cpython/Modules/clinic/gcmodule.c.h:143:21 #4 _Py_BuiltinCallFastWithKeywords_StackRef /cpython/Python/ceval.c:841:11 #5 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:2508:35 ...
SUMMARY: ThreadSanitizer: data race /cpython/Modules/gcmodule.c:379:16 in gc_get_stats_impl
==================
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Linked PRs
Bug report
Bug description:
gc.get_stats()(gc_get_stats_impl) plain-copies the per-generation stats structsgcstate->generation_stats->{young,old}.items[index]without any lock or atomics,cpython/Modules/gcmodule.c
Lines 377 to 379 in 9e863fa
while a collection on another thread writes those same
items[index]fields (collections++,collected += m, ...) at the end ofgc_collect_main,cpython/Python/gc_free_threading.c
Lines 2284 to 2292 in 9e863fa
so
gc.get_stats()races the collector.Reproducer:
TSAN Report:
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Linked PRs