gh-153062: Fix a crash iterating itertools.tee on the free-threaded build#153063
Conversation
…aded build itertools.tee branches share a linked list of teedataobject cells. On the free-threaded build, iterating one branch from multiple threads, or iterating sibling branches concurrently, raced on the shared cells and each branch's position, corrupting refcounts and crashing. Lock each teedataobject while reading, extending, or clearing it, and snapshot each branch's position under the tee object's own lock, revalidating before advancing, so the two locks are never nested. Concurrent iteration of one tee is undefined and may raise RuntimeError as documented, but no longer crashes.
Drop scratch files accidentally staged in the previous commit.
The free-threading snapshot and revalidation add per-element overhead on the default build, where the GIL already serializes access. Guard that path under Py_GIL_DISABLED so the default build keeps the original iteration and the free-threaded path is unchanged.
|
I took a look at this. I think critical sections are a reasonable tool in this case. Using them here is similar to what other itertools iterators do. One gap, need to make tee_copy_impl safe as well: I think after that change, this PR would be okay to merge. |
|
Applied in 3dab9e0, thanks. |
|
Thanks @tonghuaroot for the PR, and @nascheme for merging it 🌮🎉.. I'm working now to backport this PR to: 3.14. |
|
Thanks @tonghuaroot for the PR, and @nascheme for merging it 🌮🎉.. I'm working now to backport this PR to: 3.15. |
|
Sorry, @tonghuaroot and @nascheme, I could not cleanly backport this to |
|
GH-153475 is a backport of this pull request to the 3.15 branch. |
|
GH-153476 is a backport of this pull request to the 3.14 branch. |
Iterating an
itertools.teeiterator concurrently crashes the free-threaded build. tee iterators are documented as not thread-safe, so concurrent use returning undefined results or raisingRuntimeErroris by design and is preserved here; this fixes only the memory-unsafety (corrupted reference counts and a segfault), which the free-threaded build must not permit. The otheritertoolsiterators already lock their__next__;teewas the remaining one.The shared
teedataobjectcells are locked while read, extended, or cleared. Each branch's position (dataobjandindex) is snapshotted under the tee object's own critical section and revalidated before it is advanced, so the tee lock and the data-object lock are never nested: a nested critical section can be suspended under contention, which would break the atomicity the position fetch depends on (this is whyPy_BEGIN_CRITICAL_SECTION2on the two objects does not work here).Tests in
Lib/test/test_free_threading/cover both a single branch consumed by many threads and sibling branches each consumed by their own thread;run_concurrentlyis barrier-synchronized so the threads actually contend. The re-entrancy guard'sRuntimeErroris tolerated, since it is a documented outcome. Single-threaded behavior is unchanged andtest_itertoolsstill passes.Backport labels left to a maintainer.