Skip to content

Data race on a shared types.GenericAlias iterator in ga_iter_reduce under free-threading (follow-up to gh-154043) #154916

Description

@Naserume

Bug report

Bug description:

This is a follow-up to #154043 (segfault in ga_iternext when a types.GenericAlias iterator is shared across threads), which was fixed by gh-154108 by consuming gi->obj atomically in ga_iternext:

ga_iternext(PyObject*op)
{
gaiterobject*gi= (gaiterobject*)op;
#ifdefPy_GIL_DISABLED
PyObject*obj=_Py_atomic_exchange_ptr(&gi->obj, NULL);
#else
PyObject*obj=gi->obj;
gi->obj=NULL;
#endif
if (obj==NULL) {
PyErr_SetNone(PyExc_StopIteration);
returnNULL;
}
gaobject*alias= (gaobject*)obj;
PyObject*starred_alias=Py_GenericAlias(alias->origin, alias->args);
Py_DECREF(obj);
if (starred_alias==NULL) {
returnNULL;
}
((gaobject*)starred_alias)->starred= true;
returnstarred_alias;
}

That fix only covers ga_iternext. The iterator's __reduce__ implementation, ga_iter_reduce, still reads the same gi->obj field with plain (non-atomic) loads and no critical section:

staticPyObject*
ga_iter_reduce(PyObject*self, PyObject*Py_UNUSED(ignored))
{
PyObject*iter=_PyEval_GetBuiltin(&_Py_ID(iter));
gaiterobject*gi= (gaiterobject*)self;
/* _PyEval_GetBuiltin can invoke arbitrary code,
* call must be before access of iterator pointers.
* see issue #101765 */
if (gi->obj)
returnPy_BuildValue("N(O)", iter, gi->obj);
else
returnPy_BuildValue("N(())", iter);
}

So sharing one iter(list[int]) across threads and calling __reduce__ concurrently with next() is still a data race on gi->obj, the same field the fix made atomic in ga_iternext. ga_iternext does _Py_atomic_exchange_ptr(&gi->obj, NULL) (atomic write) while ga_iter_reduce does if (gi->obj) return Py_BuildValue("N(O)", iter, gi->obj); (plain read).

fromthreadingimportThread, Barrier_shared_iter=iter(list[int])
defchain1_thread():
for_inrange(20000):
try:
next(_shared_iter)
exceptStopIteration:
passdefchain2_thread():
for_inrange(20000):
try:
_shared_iter.__reduce__()
exceptException:
passN_C1=4N_C2=4barrier=Barrier(N_C1+N_C2)
def_c1():
barrier.wait()
chain1_thread()
def_c2():
barrier.wait()
chain2_thread()
threads= [Thread(target=_c1) for_inrange(N_C1)]
threads+= [Thread(target=_c2) for_inrange(N_C2)]
fortinthreads: t.start()
fortinthreads: t.join()

TSAN Report:

WARNING: ThreadSanitizer: data race (pid=650295)
Write of size 8 at 0x7fffb6161af0 by thread T1:
#0 _Py_atomic_exchange_ptr /home/fuzz_cpython/cpython-latest/cpython/./Include/cpython/pyatomic_gcc.h:195:10 #1 ga_iternext /home/fuzz_cpython/cpython-latest/cpython/Objects/genericaliasobject.c:946:21 #2 builtin_next /home/fuzz_cpython/cpython-latest/cpython/Python/bltinmodule.c:1776:11 #3 cfunction_vectorcall_FASTCALL /home/fuzz_cpython/cpython-latest/cpython/Objects/methodobject.c:449:24 #4 _PyObject_VectorcallTstate /home/fuzz_cpython/cpython-latest/cpython/./Include/internal/pycore_call.h:144:11 #5 PyObject_Vectorcall /home/fuzz_cpython/cpython-latest/cpython/Objects/call.c:327:12 #6 _Py_VectorCallInstrumentation_StackRefSteal /home/fuzz_cpython/cpython-latest/cpython/Python/ceval.c:768:11 #7 _PyEval_EvalFrameDefault /home/fuzz_cpython/cpython-latest/cpython/Python/generated_cases.c.h:1906:35
...
Previous read of size 8 at 0x7fffb6161af0 by thread T8:
#0 ga_iter_reduce /cpython/Objects/genericaliasobject.c:1000:13 #1 _PyEval_EvalFrameDefault /cpython/Python/generated_cases.c.h:4330:35 ...
SUMMARY: ThreadSanitizer: data race /cpython/./Include/cpython/pyatomic_gcc.h:195:10 in _Py_atomic_exchange_ptr

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Linked PRs

Activity

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)type-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions