https://github.com/python/cpython/commit/8e648a9c6b29c65ac6e290d6453cd69cedb3b1b4
commit: 8e648a9c6b29c65ac6e290d6453cd69cedb3b1b4
branch: 3.14
author: Neil Schemenauer <[email protected]>
committer: nascheme <[email protected]>
date: 2026-07-10T09:47:33-07:00
summary:

[3.14] gh-153062: Fix a crash iterating itertools.tee on the free-threaded 
build (gh-153063) (gh-153476)

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.

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.

(cherry picked from commit 6c389c446e487bcb9d09e85de30aa42052990f1c)

Co-authored-by: tonghuaroot (童话) <[email protected]>

files:
A Lib/test/test_free_threading/test_itertools.py
A Misc/NEWS.d/next/Library/2026-07-05-14-30-00.gh-issue-153062.teeFT1.rst
M Modules/itertoolsmodule.c

diff --git a/Lib/test/test_free_threading/test_itertools.py 
b/Lib/test/test_free_threading/test_itertools.py
new file mode 100644
index 00000000000000..2d2d4ff1ca1f6f
--- /dev/null
+++ b/Lib/test/test_free_threading/test_itertools.py
@@ -0,0 +1,64 @@
+import unittest
+from itertools import (
+    tee,
+)
+from test.support import threading_helper
+
+
+threading_helper.requires_working_threading(module=True)
+
+
+class TestTeeConcurrent(unittest.TestCase):
+    # itertools.tee branches share a linked list of internal data cells.
+    # Concurrent iteration must not corrupt that shared state or crash the
+    # free-threaded build.  A crash shows up as the interpreter dying (not as a
+    # caught exception); tee is documented as not thread-safe, so a
+    # ``RuntimeError`` from the re-entrancy guard is an allowed outcome and is
+    # tolerated here.
+
+    def test_same_branch(self):
+        # Many threads consume the same tee branch.
+        errors = []
+
+        def consume(it):
+            try:
+                for _ in it:
+                    pass
+            except RuntimeError:
+                pass
+            except Exception as e:
+                errors.append(e)
+
+        for _ in range(100):
+            a, _ = tee(iter(range(2000)), 2)
+            threading_helper.run_concurrently(consume, nthreads=8, args=(a,))
+
+        self.assertEqual(errors, [], msg=f"unexpected errors: {errors}")
+
+    def test_sibling_branches(self):
+        # Each thread consumes a different sibling branch of the same tee.
+        errors = []
+
+        def make_worker(it):
+            def consume():
+                try:
+                    for _ in it:
+                        pass
+                except RuntimeError:
+                    pass
+                except Exception as e:
+                    errors.append(e)
+
+            return consume
+
+        for _ in range(100):
+            branches = tee(iter(range(4000)), 8)
+            threading_helper.run_concurrently(
+                [make_worker(it) for it in branches]
+            )
+
+        self.assertEqual(errors, [], msg=f"unexpected errors: {errors}")
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git 
a/Misc/NEWS.d/next/Library/2026-07-05-14-30-00.gh-issue-153062.teeFT1.rst 
b/Misc/NEWS.d/next/Library/2026-07-05-14-30-00.gh-issue-153062.teeFT1.rst
new file mode 100644
index 00000000000000..ed1aaa15d37bd9
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-05-14-30-00.gh-issue-153062.teeFT1.rst
@@ -0,0 +1,2 @@
+Fix a crash when concurrently iterating an :func:`itertools.tee` iterator on
+the free-threaded build.
diff --git a/Modules/itertoolsmodule.c b/Modules/itertoolsmodule.c
index 5fad09b1d39312..ffe44595ac59e7 100644
--- a/Modules/itertoolsmodule.c
+++ b/Modules/itertoolsmodule.c
@@ -772,13 +772,17 @@ teedataobject_newinternal(itertools_state *state, 
PyObject *it)
 static PyObject *
 teedataobject_jumplink(itertools_state *state, teedataobject *tdo)
 {
+    PyObject *link;
+    Py_BEGIN_CRITICAL_SECTION(tdo);
     if (tdo->nextlink == NULL)
         tdo->nextlink = teedataobject_newinternal(state, tdo->it);
-    return Py_XNewRef(tdo->nextlink);
+    link = Py_XNewRef(tdo->nextlink);
+    Py_END_CRITICAL_SECTION();
+    return link;
 }
 
 static PyObject *
-teedataobject_getitem(teedataobject *tdo, int i)
+teedataobject_getitem_lock_held(teedataobject *tdo, int i)
 {
     PyObject *value;
 
@@ -804,6 +808,16 @@ teedataobject_getitem(teedataobject *tdo, int i)
     return Py_NewRef(value);
 }
 
+static PyObject *
+teedataobject_getitem(teedataobject *tdo, int i)
+{
+    PyObject *result;
+    Py_BEGIN_CRITICAL_SECTION(tdo);
+    result = teedataobject_getitem_lock_held(tdo, i);
+    Py_END_CRITICAL_SECTION();
+    return result;
+}
+
 static int
 teedataobject_traverse(PyObject *op, visitproc visit, void * arg)
 {
@@ -823,8 +837,11 @@ teedataobject_safe_decref(PyObject *obj)
 {
     while (obj && _PyObject_IsUniquelyReferenced(obj)) {
         teedataobject *tmp = teedataobject_CAST(obj);
-        PyObject *nextlink = tmp->nextlink;
+        PyObject *nextlink;
+        Py_BEGIN_CRITICAL_SECTION(obj);
+        nextlink = tmp->nextlink;
         tmp->nextlink = NULL;
+        Py_END_CRITICAL_SECTION();
         Py_SETREF(obj, nextlink);
     }
     Py_XDECREF(obj);
@@ -837,11 +854,13 @@ teedataobject_clear(PyObject *op)
     PyObject *tmp;
     teedataobject *tdo = teedataobject_CAST(op);
 
+    Py_BEGIN_CRITICAL_SECTION(op);
     Py_CLEAR(tdo->it);
     for (i=0 ; i<tdo->numread ; i++)
         Py_CLEAR(tdo->values[i]);
     tmp = tdo->nextlink;
     tdo->nextlink = NULL;
+    Py_END_CRITICAL_SECTION();
     teedataobject_safe_decref(tmp);
     return 0;
 }
@@ -934,20 +953,67 @@ static PyObject *
 tee_next(PyObject *op)
 {
     teeobject *to = teeobject_CAST(op);
-    PyObject *value, *link;
+    PyObject *value;
 
+#ifndef Py_GIL_DISABLED
+    /* The GIL already serializes access, so keep the simple path without the
+       snapshot and revalidation that the free-threaded build needs. */
     if (to->index >= LINKCELLS) {
-        link = teedataobject_jumplink(to->state, to->dataobj);
-        if (link == NULL)
+        PyObject *link = teedataobject_jumplink(to->state, to->dataobj);
+        if (link == NULL) {
             return NULL;
+        }
         Py_SETREF(to->dataobj, (teedataobject *)link);
         to->index = 0;
     }
     value = teedataobject_getitem(to->dataobj, to->index);
-    if (value == NULL)
+    if (value == NULL) {
         return NULL;
+    }
     to->index++;
     return value;
+#else
+    for (;;) {
+        teedataobject *dataobj;
+        int index;
+
+        /* Snapshot the branch position (strong ref to the shared data object)
+           under the tee lock; the data object is locked separately, not 
nested,
+           then the advance is revalidated. */
+        Py_BEGIN_CRITICAL_SECTION(op);
+        dataobj = (teedataobject *)Py_NewRef((PyObject *)to->dataobj);
+        index = to->index;
+        Py_END_CRITICAL_SECTION();
+
+        if (index < LINKCELLS) {
+            value = teedataobject_getitem(dataobj, index);
+            if (value != NULL) {
+                Py_BEGIN_CRITICAL_SECTION(op);
+                if (to->dataobj == dataobj && to->index == index) {
+                    to->index = index + 1;
+                }
+                Py_END_CRITICAL_SECTION();
+            }
+            Py_DECREF(dataobj);
+            return value;
+        }
+
+        PyObject *link = teedataobject_jumplink(to->state, dataobj);
+        if (link == NULL) {
+            Py_DECREF(dataobj);
+            return NULL;
+        }
+        Py_BEGIN_CRITICAL_SECTION(op);
+        if (to->dataobj == dataobj) {
+            Py_SETREF(to->dataobj, (teedataobject *)link);
+            to->index = 0;
+            link = NULL;
+        }
+        Py_END_CRITICAL_SECTION();
+        Py_XDECREF(link);
+        Py_DECREF(dataobj);
+    }
+#endif
 }
 
 static int
@@ -966,8 +1032,10 @@ tee_copy_impl(teeobject *to)
     if (newto == NULL) {
         return NULL;
     }
+    Py_BEGIN_CRITICAL_SECTION(to);
     newto->dataobj = (teedataobject *)Py_NewRef(to->dataobj);
     newto->index = to->index;
+    Py_END_CRITICAL_SECTION();
     newto->weakreflist = NULL;
     newto->state = to->state;
     PyObject_GC_Track(newto);

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to