Race condition in `itertools.islice` under free-threading
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 77.2k
- Forks
- 35.9k
- PR merge metrics
- PR metrics pending
Description
Bug report
Bug description:
islice_next reads and writes three fields -- lz->cnt, lz->next, and lz->it -- and do operations on them without a critical section.
Two threads calling next on the same islice object concurrently can race:
- Both read
lz->cnt = N < lz->next, both execute the skip-loop body for the same slot, advancing the underlying iterator twice for one step. In that case, the step arithmetic gets corrupted. - Both read
lz->cnt = N < stop, both pass the stop check, both calliternext(it)and return an item -- the total number of yielded items exceedsstop. - There's a race on
lz->cnt++andlz->next += step: both threads can read the same value, then both can increment locally, and both store the same result. After that, the counter is permanently incorrect (such that subsequent skip/stop decisions use a wrong baseline). - One of the threads reaches exhaustion (
goto empty) and callsPy_CLEAR(lz->it), which setslz->it = NULLand DECREF's the iterator, which frees it (since each thread has readyit = lz->it. and not INCREF'd it). Another thread can have readlz->itbefore the clear and be in the middle ofiternext(it)on the now-freed object in which case there is a use-after-free.
chain_next for example wraps its body in Py_BEGIN_CRITICAL_SECTION(op), which I believe is what islice should do as well.
Reproducer
import itertools
import threading
STOP = 100
NTHREADS = 8
data = iter(range(STOP + NTHREADS * 2))
sl = itertools.islice(data, STOP)
results: list[int] = []
lock = threading.Lock()
def consume() -> None:
while True:
v = next(sl, None)
if v is None:
break
with lock:
results.append(v)
threads = [threading.Thread(target=consume) for _ in range(NTHREADS)]
for t in threads: t.start()
for t in threads: t.join()
The reproducer, on a free-threaded build, gets reports like this one.
WARNING: ThreadSanitizer: data race (pid=49886)
Read of size 8 at 0x00030275f8b0 by thread T2:
#0 islice_next itertoolsmodule.c:1663 (python.exe:arm64+0x10042bce4)
#1 builtin_next bltinmodule.c:1770 (python.exe:arm64+0x10028a900)
#2 cfunction_vectorcall_FASTCALL methodobject.c:449 (python.exe:arm64+0x1001379dc)
#3 _PyObject_VectorcallTstate pycore_call.h:144 (python.exe:arm64+0x100090e80)
#4 PyObject_Vectorcall call.c:327 (python.exe:arm64+0x100090e80)
...
CPython versions tested on:
CPython main branch
Operating systems tested on:
macOS
Linked PRs
- gh-151410
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in Modules/itertoolsmodule.c at islice_next and compare chain_next's critical-section handling. Run the supplied multithreaded reproducer on a free-threaded CPython build and inspect the relevant itertools tests. Done means concurrent next() calls no longer trigger the reported race or exceed islice's stop boundary.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100