GeezerVison is showing github.com — tsan-006: itertoolsmodule.c: count.__repr__ plain-reads cnt · Issue #153908 · python/cpython
tsan-006: itertoolsmodule.c: count.__repr__ plain-reads cnt #153908DescriptionBug description:
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
CPython versions tested on:
CPython main branch
Operating systems tested on:
Macos 26.3.2
Linked PRs
ActivityZeroIntensity commented on Jul 18, 2026 devdanzin commented on 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 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):
import itertools, threading
NT = 8
for _ in range(3000):
it = itertools.count(10**18, 2) # big-int -> slow (long_cnt) mode
bar = threading.Barrier(NT)
def work(advance, it=it, bar=bar):
bar.wait()
for _ in range(400):
next(it) if advance else repr(it)
ts = [threading.Thread(target=work, args=(i % 2 == 0,)) for i in range(NT)]
for t in ts: t.start()
for t in ts: t.join()
TSan reports count_nextlong (lz->long_cnt = stepped_up) vs count_repr (lz->long_cnt read). Fast-mode count() is clean.
The fix would mirror count_next's slow path: take Py_BEGIN_CRITICAL_SECTION(lz) around count_repr's long_cnt access (or read long_cnt atomically).
Should I open a new issue for this, or is it fine to handle it here? johng commented on Jul 19, 2026 ContributorAuthor More actions devdanzin commented on Jul 19, 2026 @johng Ah, sorry, missed that! Your fix fully addresses the issues described above.
BTW, great work finding interesting bugs, much appreciated!
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|