Skip to content

segfault in ga_iternext: sharing a types.GenericAlias iterator across threads double-frees under free-threading #154043

Description

@devdanzin

Crash report

What happened?

On a free-threaded (--disable-gil) build, iterating a single, shared types.GenericAlias iterator (e.g. iter(list[int])) from multiple threads segfaults. ga_iternext (Objects/genericaliasobject.c) is a one-shot iterator with no synchronization:

static PyObject *
ga_iternext(PyObject *op)
{
    gaiterobject *gi = (gaiterobject *)op;
    if (gi->obj == NULL) {                               // read gi->obj
        PyErr_SetNone(PyExc_StopIteration);
        return NULL;
    }
    gaobject *alias = (gaobject *)gi->obj;
    PyObject *starred_alias = Py_GenericAlias(alias->origin, alias->args);   // use gi->obj
    if (starred_alias == NULL)
        return NULL;
    ((gaobject *)starred_alias)->starred = true;
    Py_SETREF(gi->obj, NULL);                            // Py_DECREF(gi->obj); gi->obj = NULL
    return starred_alias;
}

Two threads both observe gi->obj != NULL, both build the starred alias from it, and both reach Py_SETREF(gi->obj, NULL) (which is tmp = gi->obj; gi->obj = NULL; Py_DECREF(tmp)). With gi->obj's refcount at 1 (the iterator is its only holder), the two Py_DECREFs free it twice — a double-free / refcount underflow — and the reads of alias->origin/alias->args become a use-after-free once the other thread frees it. The process crashes.

Reproducer

import threading

NT = 16

def worker(it, barrier):
    barrier.wait()
    try:
        next(it)
    except StopIteration:
        pass

for _round in range(20000):
    shared = iter(list[int])      # ONE shared GenericAlias iterator; gi->obj refcount == 1
    bar = threading.Barrier(NT)
    threads = [threading.Thread(target=worker, args=(shared, bar)) for _ in range(NT)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()
print("done")

PYTHON_GIL=0 ./python repro.py segfaults within the first few rounds — 5/5 runs on both a debug and a release (-O0, no sanitizer) free-threaded build, so it is neither debug-only nor sanitizer-only:

Thread NNNN received signal SIGSEGV, Segmentation fault.
#0  ga_iternext (op=...) at Objects/genericaliasobject.c:952        (Py_SETREF(gi->obj, NULL))
#1  builtin_next                Python/bltinmodule.c:1776
#2  _Py_BuiltinCallFast_StackRef Python/ceval.c:817
#3  _PyEval_EvalFrameDefault     Python/generated_cases.c.h:2510    (next(it))
...

ThreadSanitizer reports the same as a data race on gi->obj (WARNING: ThreadSanitizer: data race... in ga_iternext, exit 66).

Suggested fix

Consume gi->obj atomically so exactly one thread takes it:

PyObject *obj = _Py_atomic_exchange_ptr(&gi->obj, NULL);
if (obj == NULL) {
    PyErr_SetNone(PyExc_StopIteration);
    return NULL;
}
gaobject *alias = (gaobject *)obj;
PyObject *starred_alias = Py_GenericAlias(alias->origin, alias->args);
if (starred_alias == NULL) {
    Py_DECREF(obj);
    return NULL;
}
((gaobject *)starred_alias)->starred = true;
Py_DECREF(obj);
return starred_alias;

(Or wrap the body in Py_BEGIN_CRITICAL_SECTION(gi).) This matches the "consume once" semantics and keeps the GIL build unchanged.

Per the iterator free-threading strategy (gh-124397), concurrent iteration may return duplicate or skipped values or raise — but it must not crash; this crashes. Distinct from gh-153298, which is the GenericAlias.__parameters__ lazy-init race, not the iterator.

(Found by fusil --tsan, a ThreadSanitizer fuzzer; crash confirmed by re-running the reproducer without a sanitizer on a plain free-threaded build. Draft and reproducer by Claude Code, minimized and reviewed by hand.)

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Output from running 'python -VV' on the command line:

Python 3.16.0a0 free-threading build (heads/main:a1d580430c8, Jul 18 2026, 20:36:44) [Clang 21.1.8 (6ubuntu1)]

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)topic-free-threadingtype-crashA hard crash of the interpreter, possibly with a core dump

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions