Skip to content

Commit 6e3809c

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 cc1bdf9 commit 6e3809c

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
@@ -645,6 +645,10 @@ loops that truncate the stream.
645645
used anywhere else; otherwise, the *iterable* could get advanced without
646646
the tee objects being informed.
647647

648+
``tee`` iterators are not threadsafe. A :exc:`RuntimeError` may be
649+
raised when using simultaneously iterators returned by the same :func:`tee`
650+
call, even if the original *iterable* is threadsafe.
651+
648652
This itertool may require significant auxiliary storage (depending on how
649653
much temporary data needs to be stored). In general, if one iterator uses
650654
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

@@ -1494,6 +1495,42 @@ def test_tee_del_backward(self):
14941495
del forward, backward
14951496
raise
14961497

1498+
def test_tee_reenter(self):
1499+
class I:
1500+
first = True
1501+
def __iter__(self):
1502+
return self
1503+
def __next__(self):
1504+
first = self.first
1505+
self.first = False
1506+
if first:
1507+
return next(b)
1508+
1509+
a, b = tee(I())
1510+
with self.assertRaisesRegex(RuntimeError, "tee"):
1511+
next(a)
1512+
1513+
def test_tee_concurrent(self):
1514+
start = threading.Event()
1515+
finish = threading.Event()
1516+
class I:
1517+
def __iter__(self):
1518+
return self
1519+
def __next__(self):
1520+
start.set()
1521+
finish.wait()
1522+
1523+
a, b = tee(I())
1524+
thread = threading.Thread(target=next, args=[a])
1525+
thread.start()
1526+
try:
1527+
start.wait()
1528+
with self.assertRaisesRegex(RuntimeError, "tee"):
1529+
next(b)
1530+
finally:
1531+
finish.set()
1532+
thread.join()
1533+
14971534
def test_StopIteration(self):
14981535
self.assertRaises(StopIteration, next, zip())
14991536

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
@@ -443,6 +443,7 @@ typedef struct {
443443
PyObject_HEAD
444444
PyObject *it;
445445
int numread; /* 0 <= numread <= LINKCELLS */
446+
int running;
446447
PyObject *nextlink;
447448
PyObject *(values[LINKCELLS]);
448449
} teedataobject;
@@ -465,6 +466,7 @@ teedataobject_newinternal(PyObject *it)
465466
if (tdo == NULL)
466467
return NULL;
467468

469+
tdo->running = 0;
468470
tdo->numread = 0;
469471
tdo->nextlink = NULL;
470472
Py_INCREF(it);
@@ -493,7 +495,14 @@ teedataobject_getitem(teedataobject *tdo, int i)
493495
else {
494496
/* this is the lead iterator, so fetch more data */
495497
assert(i == tdo->numread);
498+
if (tdo->running) {
499+
PyErr_SetString(PyExc_RuntimeError,
500+
"cannot re-enter the tee iterator");
501+
return NULL;
502+
}
503+
tdo->running = 1;
496504
value = PyIter_Next(tdo->it);
505+
tdo->running = 0;
497506
if (value == NULL)
498507
return NULL;
499508
tdo->numread++;

0 commit comments

Comments
 (0)