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
Bug report
Bug description:
This is a follow-up to #154043 (segfault in
ga_iternextwhen atypes.GenericAliasiterator is shared across threads), which was fixed by gh-154108 by consuminggi->objatomically inga_iternext:cpython/Objects/genericaliasobject.c
Lines 942 to 963 in 8b048eb
That fix only covers
ga_iternext. The iterator's__reduce__implementation,ga_iter_reduce, still reads the samegi->objfield with plain (non-atomic) loads and no critical section:cpython/Objects/genericaliasobject.c
Lines 990 to 1004 in 8b048eb
So sharing one
iter(list[int])across threads and calling__reduce__concurrently withnext()is still a data race ongi->obj, the same field the fix made atomic inga_iternext.ga_iternextdoes_Py_atomic_exchange_ptr(&gi->obj, NULL)(atomic write) whilega_iter_reducedoesif (gi->obj) return Py_BuildValue("N(O)", iter, gi->obj);(plain read).TSAN Report:
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux
Linked PRs
types.GenericAliasiterator inga_iter_reduceunder free-threading #155476