GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Data race between `gc.get_stats()` and a concurrent collection under free-threading · Issue #151646 · python/cpython

Data race between gc.get_stats() and a concurrent collection under free-threading #151646

New issue
Copy link
New issue
Copy link
Closed
Closed
Data race between gc.get_stats() and a concurrent collection under free-threading#151646
Copy link
Labels
interpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-free-threadingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@Naserume
Naserume
opened on Jun 18, 2026
Issue body actions

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,

cpython/Modules/gcmodule.c

Lines 377 to 379 in 9e863fa

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,

cpython/Python/gc_free_threading.c

Lines 2284 to 2292 in 9e863fa

/* Update stats */
struct gc_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:

import gc
from threading import Thread

def collector():
    for _ in range(50000):
        a = {}; a['self'] = a
        gc.collect()

def reader():
    for _ in range(50000):
        gc.get_stats()

threads  = [Thread(target=collector) for _ in range(1)]
threads += [Thread(target=reader)    for _ in range(8)]
for t in threads: t.start()
for t in threads: 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

  • gh-151646: Fix data race between gc.get_stats() and GC in free-threading builds #151766

Activity

  1. Naserume
    added
    type-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
    on Jun 18, 2026
  2. ZeroIntensity
    added
    interpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)
    topic-free-threading
    on Jun 18, 2026
  3. zangjiucheng commented on Jun 20, 2026

    @zangjiucheng
    zangjiucheng
    on Jun 20, 2026
    Contributor
    More actions

    Hello everyone. I can reproduce the TSAN report from the compiled Linux version.

    Root cause (By my understanding)

    gc_get_stats_impl copies three gc_generation_stats structs (including double duration and PyTime_t timestamp fields) without any synchronization, while gc_collect_main writes the same fields at the end of each collection cycle — after stop-the-world has resumed, so both threads can be running concurrently.

    Fix proposal

    Add a PyMutex stats_mutex to _gc_runtime_state (compiled only under Py_GIL_DISABLED) and hold it around both the write in gc_collect_main and the read in gc_get_stats_impl.

    A per-field atomic approach won't work here because the reader needs a consistent snapshot across all fields of a single generation — mixing a new collections count with an old duration would yield logically incoherent output. A short mutex is appropriate: the collector is already serialized (only one collection runs at a time), gc.get_stats() is not a hot path, and the critical section is just 7 field writes / 3 struct copies.

    The mutex in gc_collect_main is acquired after _Py_StartTheWorld(), so there is no risk of world pause.

    Please let me know your suggestion :), and I will attach a PR for this proposal.

  4. zangjiucheng
    added a commit that references this issue on Jul 4, 2026

    pythongh-151646: Lighten gc.get_stats race test and trim mutex comments

    08a114a
  5. nascheme
    added a commit that references this issue on Jul 4, 2026

    gh-151646: Fix data race between gc.get_stats() and GC in free-thread…

    8b1dbb1
  6. nascheme
    closed this as completedon Jul 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)topic-free-threadingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.