You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follows from #153852 for data race on plain read of cnt in repr
itertoolsmodule.c: count.repr plain-reads cnt while count_next writes it with an atomic CAS — the writer was hardened, the reader missed
import itertools
import threading
STOP = False
def advance(it):
while not STOP:
next(it)
def read_repr(it):
while not STOP:
repr(it)
def main():
global STOP
it = itertools.count()
threads = [threading.Thread(target=advance, args=(it,)) for _ in range(2)]
threads += [threading.Thread(target=read_repr, args=(it,)) for _ in range(4)]
for t in threads:
t.start()
# Let the race run briefly; TSan reports on the first conflicting pair.
threading.Event().wait(2.0)
STOP = True
for t in threads:
t.join()
print("done (no race detected)")
if __name__ == "__main__":
main()
PYTHON_GIL=0 ./python.exe repro_tsan_0006_count.py
SUMMARY: ThreadSanitizer: data race itertoolsmodule.c:3680 in count_repr
==================
done (no race detected)
ThreadSanitizer: reported 2 warnings
[1] 55630 abort PYTHON_GIL=0 ./python.exe repro_tsan_0006_count.py
After GH-153917, itertools.count.__repr__ still data-races in slow (big-int) mode.
The fix hardened count_next (fast mode: atomic CAS on lz->cnt; slow mode: count_nextlong under Py_BEGIN_CRITICAL_SECTION(lz)) and the fast-mode read in count_repr. But count_repr's slow-mode path reads lz->long_cnt with no synchronization — starting at the if (lz->long_cnt == NULL) mode check and again in the PyUnicode_FromFormat("%R", ..., lz->long_cnt) — while count_nextlong does lz->long_cnt = stepped_up under the critical section. A critical section only serializes against other critical sections, so count_repr (which never takes it) still races the write. It's also a use-after-free: count_repr borrows lz->long_cnt and repr()s it, while count_nextlong replaces it and the old value returned by next() is later decref'd and freed.
Repro on a free-threaded --with-thread-sanitizer build (PYTHON_GIL=0):
Activity
gh-153908: Fix data race in `itertools.count.__repr__` (GH-153917)
ZeroIntensity commentedon Jul 18, 2026
Backports on automerge
Umbrella #153852: mark the first picked-up findings in the table
[3.14] gh-153908: Fix data race in `itertools.count.__repr__` (GH-153917
[3.15] gh-153908: Fix data race in `itertools.count.__repr__` (GH-153917
devdanzin commentedon Jul 19, 2026
After GH-153917,
itertools.count.__repr__still data-races in slow (big-int) mode.The fix hardened
count_next(fast mode: atomic CAS onlz->cnt; slow mode:count_nextlongunderPy_BEGIN_CRITICAL_SECTION(lz)) and the fast-mode read incount_repr. Butcount_repr's slow-mode path readslz->long_cntwith no synchronization — starting at theif (lz->long_cnt == NULL)mode check and again in thePyUnicode_FromFormat("%R", ..., lz->long_cnt)— whilecount_nextlongdoeslz->long_cnt = stepped_upunder the critical section. A critical section only serializes against other critical sections, socount_repr(which never takes it) still races the write. It's also a use-after-free:count_reprborrowslz->long_cntandrepr()s it, whilecount_nextlongreplaces it and the old value returned bynext()is later decref'd and freed.Repro on a free-threaded
--with-thread-sanitizerbuild (PYTHON_GIL=0):TSan reports
count_nextlong(lz->long_cnt = stepped_up) vscount_repr(lz->long_cntread). Fast-modecount()is clean.The fix would mirror
count_next's slow path: takePy_BEGIN_CRITICAL_SECTION(lz)aroundcount_repr'slong_cntaccess (or readlong_cntatomically).Should I open a new issue for this, or is it fine to handle it here?
johng commentedon Jul 19, 2026
@devdanzin I had already raised #153983 shortly after working on the fast mode race
devdanzin commentedon Jul 19, 2026
@johng Ah, sorry, missed that! Your fix fully addresses the issues described above.
BTW, great work finding interesting bugs, much appreciated!