Skip to content

gh-153014: Fix data race on the GC debug flag in free-threading builds - #153015

Merged
nascheme merged 10 commits into
python:mainfrom
tonghuaroot:gc-debug-ft-race
Oct 21, 2026
Merged

nascheme merged 10 commits into
python:mainfrom
tonghuaroot:gc-debug-ft-race

Conversation

@tonghuaroot

@tonghuaroot tonghuaroot commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

In a free-threading build, gc.set_debug() stored gcstate->debug with a
plain write while gc.get_debug() and the collector read it without
synchronisation. gc.set_debug() takes no lock and, unlike
gc_set_threshold_impl(), does not run under _PyEval_StopTheWorld(), so
ThreadSanitizer reports a write/write race on gcstate->debug.

This accesses the flag with FT_ATOMIC_STORE_INT_RELAXED /
FT_ATOMIC_LOAD_INT_RELAXED in gc_set_debug_impl() / gc_get_debug_impl()
and in the collector reads in Python/gc_free_threading.c. Four of those
collector reads run outside stop-the-world (in delete_garbage(),
handle_legacy_finalizers(), and two stats reads in gc_collect_main()) and
race the write directly; the read in gc_collect_internal() runs under
stop-the-world and is made atomic only for consistency. Relaxed ordering is
correct for an independent int flag, matching how gcstate->enabled and
sys dlopenflags (gh-151644) are already handled.

Python/gc.c (the default GIL build) is left unchanged: it has no concurrent
access, and FT_ATOMIC_* compiles to a plain load/store there anyway.

Verified with a --with-thread-sanitizer --disable-gil build stressing
concurrent gc.set_debug() / gc.get_debug() plus a thread churning cyclic
garbage:

Before: WARNING: ThreadSanitizer: data race ... Write of size 4 ... gc_set_debug (2 reports)
After:  0 data races

A regression test is added under Lib/test/test_free_threading/.

… builds

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.
Comment thread Python/gc_free_threading.c Outdated
@sergey-miryanov

Copy link
Copy Markdown
Contributor

cc @nascheme as a GC-FT expert

@nascheme

nascheme commented Jul 8, 2026

Copy link
Copy Markdown
Member

This looks worth fixing. Some suggested improvements:

  • Use threading_helper.run_concurrently()
  • Remove the thread that runs GC collections. I don't think that's useful since it stops-the-world.
  • I'd use one writer and one reader. It would be enough to trigger the TSAN warnings.
  • If gc->debug is read more than once inside a function, read it into a local variable.

@github-actions

Copy link
Copy Markdown

This PR is stale because it has been open for 90 days with no activity.

@github-actions github-actions Bot added the stale Stale PR or inactive for long period of time. label Oct 11, 2026
- Read gcstate->debug once into a local in delete_garbage,
  handle_legacy_finalizers, gc_collect_main and _PyGC_DumpShutdownStats.
- Simplify the regression test to one gc.set_debug() writer racing one
  gc.get_debug() reader via run_concurrently(), dropping the collector
  thread.
@tonghuaroot

Copy link
Copy Markdown
Contributor Author

Done: gcstate->debug is now read once into a local in the four collector functions, and the test is one gc.set_debug() writer racing one gc.get_debug() reader via run_concurrently() (no collector thread). Verified under --disable-gil --with-thread-sanitizer: TSAN flags the race without the fix and is clean with it. Also merged main to clear the conflict.

I have made the requested changes; please review again

@bedevere-app

bedevere-app Bot commented Oct 12, 2026

Copy link
Copy Markdown

Thanks for making the requested changes!

: please review the changes made to this pull request.

@nascheme

Copy link
Copy Markdown
Member

Hi tonghuaroot, thanks for making the changes requested. This looks good to merge now. After reconsidering the unit test, I think it's better we just remove it (I pushed a change). In terms of CI cost vs what this test buys us, I think it's not worth it. This race is not very serious. Fixing the code so we don't get a TSAN warning is good. Sorry for asking you to fix the test on the first review, normally we do want unit tests when bugs are fixed.

@nascheme
nascheme enabled auto-merge (squash) October 12, 2026 18:22
@tonghuaroot

Copy link
Copy Markdown
Contributor Author

looks like this PR needs your approval @nascheme

@sergey-miryanov sergey-miryanov removed the stale Stale PR or inactive for long period of time. label Oct 15, 2026
@nascheme
nascheme disabled auto-merge October 20, 2026 05:52
@StanFromIreland

Copy link
Copy Markdown
Member

@nascheme you'll have to approve the PR for the bot to change the label, or change it manually.

@nascheme nascheme left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@nascheme nascheme added type-bug An unexpected behavior, bug, or error topic-free-threading labels Oct 21, 2026
@nascheme
nascheme merged commit ed144b7 into python:main Oct 21, 2026
63 of 64 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

topic-free-threading type-bug An unexpected behavior, bug, or error

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants