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
{{ message }}
Repository navigation
itertoolsmodule: free-threading use after free bug in counting slow mode #153981
During next we swap out the pointer for lz->long_cnt inside count_nextlong. This can cause a use after free bug as shown with a new test. Running this is TSAN mode exposes this reliably.
ThreadSanitizer can not provide additional info.
SUMMARY: ThreadSanitizer: SEGV object.c:766 in PyObject_Repr
==83915==ABORTING
[1] 83915 abort PYTHON_GIL=0 ./python.exe -m unittest -v
class TestCountConcurrent(unittest.TestCase):
@staticmethod
def _spin_next(it, n=2000):
for _ in range(n):
next(it)
@staticmethod
def _spin_repr(it, n=2000):
for _ in range(n):
repr(it)
@threading_helper.reap_threads
def test_repr_racing_next_fast_mode(self):
for _ in range(10):
it = count()
workers = [self._spin_next] * 2 + [self._spin_repr] * 4
threading_helper.run_concurrently(workers, args=(it,))
@threading_helper.reap_threads
def test_repr_racing_next_slow_mode(self):
for _ in range(10):
# Large count to trigger "slow mode"
it = count(10**18, 2)
workers = [self._spin_next] * 2 + [self._spin_repr] * 4
threading_helper.run_concurrently(workers, args=(it,))
To fix I believe we need to hold a critical section reference in repr to fetching a reference to long_cnt
Confirming this independently. fusil --tsan (a ThreadSanitizer fuzzer) surfaced the same slow-mode use-after-free, which I'd reported as a follow-up on the fast-mode issue (gh-153908) just before being pointed at this issue and your PR.
The mechanism matches: in slow (big-int) mode count_repr borrows lz->long_cnt and repr()s it (the if (lz->long_cnt == NULL) mode check + PyUnicode_FromFormat("%R", …, lz->long_cnt)) with no synchronization, while count_nextlong does lz->long_cnt = stepped_up under Py_BEGIN_CRITICAL_SECTION(lz) and returns the old value — which the next() caller then DECREFs, so count_repr can repr() a freed object. TSan flags it as a data race on lz->long_cnt (count_nextlong vs count_repr), and when the free wins it's a SEGV in PyObject_Repr (matching your SEGV object.c:766). Reproduced deterministically on a free-threaded --with-thread-sanitizer build of current main; fast-mode count() is clean.
PR #153983 looks like the correct fix: snapshotting long_cnt/long_step with Py_XNewRef inside Py_BEGIN_CRITICAL_SECTION(lz) and formatting from the locals addresses both halves — the data race (the reads are now under the same critical section count_nextlong writes under) and the use-after-free (the strong reference keeps long_cnt alive across the repr()). A bare atomic load of the pointer would have closed the race but not the UAF, so the strong-ref snapshot is the key part.
(Found by fusil --tsan, a ThreadSanitizer fuzzer; comment + reproducer by Claude Code, reviewed by hand.)
Activity
pythongh-153981: Fix use after free bug itertools.count()
devdanzin commentedon Jul 19, 2026
Confirming this independently.
fusil --tsan(a ThreadSanitizer fuzzer) surfaced the same slow-mode use-after-free, which I'd reported as a follow-up on the fast-mode issue (gh-153908) just before being pointed at this issue and your PR.The mechanism matches: in slow (big-int) mode
count_reprborrowslz->long_cntandrepr()s it (theif (lz->long_cnt == NULL)mode check +PyUnicode_FromFormat("%R", …, lz->long_cnt)) with no synchronization, whilecount_nextlongdoeslz->long_cnt = stepped_upunderPy_BEGIN_CRITICAL_SECTION(lz)and returns the old value — which thenext()caller then DECREFs, socount_reprcanrepr()a freed object. TSan flags it as a data race onlz->long_cnt(count_nextlongvscount_repr), and when the free wins it's aSEGVinPyObject_Repr(matching yourSEGV object.c:766). Reproduced deterministically on a free-threaded--with-thread-sanitizerbuild of currentmain; fast-modecount()is clean.Repro:
PR #153983 looks like the correct fix: snapshotting
long_cnt/long_stepwithPy_XNewRefinsidePy_BEGIN_CRITICAL_SECTION(lz)and formatting from the locals addresses both halves — the data race (the reads are now under the same critical sectioncount_nextlongwrites under) and the use-after-free (the strong reference keepslong_cntalive across therepr()). A bare atomic load of the pointer would have closed the race but not the UAF, so the strong-ref snapshot is the key part.(Found by fusil --tsan, a ThreadSanitizer fuzzer; comment + reproducer by Claude Code, reviewed by hand.)