Skip to content

Commit 5190b71

Browse files
bpo-34410: Fix a crash in the tee iterator when re-enter it. (GH-15625)
RuntimeError is now raised in this case. (cherry picked from commit 526a014) Co-authored-by: Serhiy Storchaka <[email protected]>
1 parent 021e5db commit 5190b71

File tree

4 files changed

+52
-0
lines changed

4 files changed

+52
-0
lines changed

Doc/library/itertools.rst

+4
Original file line numberDiff line numberDiff line change
@@ -628,6 +628,10 @@ loops that truncate the stream.
628628
used anywhere else; otherwise, the *iterable* could get advanced without
629629
the tee objects being informed.
630630

631+
``tee`` iterators are not threadsafe. A :exc:`RuntimeError` may be
632+
raised when using simultaneously iterators returned by the same :func:`tee`
633+
call, even if the original *iterable* is threadsafe.
634+
631635
This itertool may require significant auxiliary storage (depending on how
632636
much temporary data needs to be stored). In general, if one iterator uses
633637
most or all of the data before another iterator starts, it is faster to use

Lib/test/test_itertools.py

+37
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from functools import reduce
1212
import sys
1313
import struct
14+
import threading
1415
maxsize = support.MAX_Py_ssize_t
1516
minsize = -maxsize-1
1617

@@ -1476,6 +1477,42 @@ def test_tee_del_backward(self):
14761477
del forward, backward
14771478
raise
14781479

1480+
def test_tee_reenter(self):
1481+
class I:
1482+
first = True
1483+
def __iter__(self):
1484+
return self
1485+
def __next__(self):
1486+
first = self.first
1487+
self.first = False
1488+
if first:
1489+
return next(b)
1490+
1491+
a, b = tee(I())
1492+
with self.assertRaisesRegex(RuntimeError, "tee"):
1493+
next(a)
1494+
1495+
def test_tee_concurrent(self):
1496+
start = threading.Event()
1497+
finish = threading.Event()
1498+
class I:
1499+
def __iter__(self):
1500+
return self
1501+
def __next__(self):
1502+
start.set()
1503+
finish.wait()
1504+
1505+
a, b = tee(I())
1506+
thread = threading.Thread(target=next, args=[a])
1507+
thread.start()
1508+
try:
1509+
start.wait()
1510+
with self.assertRaisesRegex(RuntimeError, "tee"):
1511+
next(b)
1512+
finally:
1513+
finish.set()
1514+
thread.join()
1515+
14791516
def test_StopIteration(self):
14801517
self.assertRaises(StopIteration, next, zip())
14811518

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed a crash in the :func:`tee` iterator when re-enter it. RuntimeError is
2+
now raised in this case.

Modules/itertoolsmodule.c

+9
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ typedef struct {
395395
PyObject_HEAD
396396
PyObject *it;
397397
int numread; /* 0 <= numread <= LINKCELLS */
398+
int running;
398399
PyObject *nextlink;
399400
PyObject *(values[LINKCELLS]);
400401
} teedataobject;
@@ -417,6 +418,7 @@ teedataobject_newinternal(PyObject *it)
417418
if (tdo == NULL)
418419
return NULL;
419420

421+
tdo->running = 0;
420422
tdo->numread = 0;
421423
tdo->nextlink = NULL;
422424
Py_INCREF(it);
@@ -445,7 +447,14 @@ teedataobject_getitem(teedataobject *tdo, int i)
445447
else {
446448
/* this is the lead iterator, so fetch more data */
447449
assert(i == tdo->numread);
450+
if (tdo->running) {
451+
PyErr_SetString(PyExc_RuntimeError,
452+
"cannot re-enter the tee iterator");
453+
return NULL;
454+
}
455+
tdo->running = 1;
448456
value = PyIter_Next(tdo->it);
457+
tdo->running = 0;
449458
if (value == NULL)
450459
return NULL;
451460
tdo->numread++;

0 commit comments

Comments
 (0)