Skip to content

Commit 599bb18

Browse files
authored
bpo-29942: Fix the use of recursion in itertools.chain.from_iterable. (#911)
* bpo-29942: Fix the use of recursion in itertools.chain.from_iterable. Fix the use of recursion in itertools.chain.from_iterable. Using recursion is unnecessary, and can easily cause stack overflows, especially when building in low optimization modes or with Py_DEBUG enabled. (cherry picked from commit 5466d4a)
1 parent 7b5b137 commit 599bb18

File tree

3 files changed

+39
-24
lines changed

3 files changed

+39
-24
lines changed

Lib/test/test_itertools.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1976,6 +1976,14 @@ def gen2(x):
19761976
self.assertRaises(AssertionError, list, cycle(gen1()))
19771977
self.assertEqual(hist, [0,1])
19781978

1979+
def test_long_chain_of_empty_iterables(self):
1980+
# Make sure itertools.chain doesn't run into recursion limits when
1981+
# dealing with long chains of empty iterables. Even with a high
1982+
# number this would probably only fail in Py_DEBUG mode.
1983+
it = chain.from_iterable(() for unused in range(10000000))
1984+
with self.assertRaises(StopIteration):
1985+
next(it)
1986+
19791987
class SubclassWithKwargsTest(unittest.TestCase):
19801988
def test_keywords_in_subclass(self):
19811989
# count is not subclassable...

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ Core and Builtins
3030
Library
3131
-------
3232

33+
- bpo-29942: Fix a crash in itertools.chain.from_iterable when encountering
34+
long runs of empty iterables.
35+
3336
- bpo-27863: Fixed multiple crashes in ElementTree caused by race conditions
3437
and wrong types.
3538

Modules/itertoolsmodule.c

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1864,33 +1864,37 @@ chain_next(chainobject *lz)
18641864
{
18651865
PyObject *item;
18661866

1867-
if (lz->source == NULL)
1868-
return NULL; /* already stopped */
1869-
1870-
if (lz->active == NULL) {
1871-
PyObject *iterable = PyIter_Next(lz->source);
1872-
if (iterable == NULL) {
1873-
Py_CLEAR(lz->source);
1874-
return NULL; /* no more input sources */
1875-
}
1876-
lz->active = PyObject_GetIter(iterable);
1877-
Py_DECREF(iterable);
1867+
/* lz->source is the iterator of iterables. If it's NULL, we've already
1868+
* consumed them all. lz->active is the current iterator. If it's NULL,
1869+
* we should grab a new one from lz->source. */
1870+
while (lz->source != NULL) {
18781871
if (lz->active == NULL) {
1879-
Py_CLEAR(lz->source);
1880-
return NULL; /* input not iterable */
1872+
PyObject *iterable = PyIter_Next(lz->source);
1873+
if (iterable == NULL) {
1874+
Py_CLEAR(lz->source);
1875+
return NULL; /* no more input sources */
1876+
}
1877+
lz->active = PyObject_GetIter(iterable);
1878+
Py_DECREF(iterable);
1879+
if (lz->active == NULL) {
1880+
Py_CLEAR(lz->source);
1881+
return NULL; /* input not iterable */
1882+
}
18811883
}
1884+
item = (*Py_TYPE(lz->active)->tp_iternext)(lz->active);
1885+
if (item != NULL)
1886+
return item;
1887+
if (PyErr_Occurred()) {
1888+
if (PyErr_ExceptionMatches(PyExc_StopIteration))
1889+
PyErr_Clear();
1890+
else
1891+
return NULL; /* input raised an exception */
1892+
}
1893+
/* lz->active is consumed, try with the next iterable. */
1894+
Py_CLEAR(lz->active);
18821895
}
1883-
item = (*Py_TYPE(lz->active)->tp_iternext)(lz->active);
1884-
if (item != NULL)
1885-
return item;
1886-
if (PyErr_Occurred()) {
1887-
if (PyErr_ExceptionMatches(PyExc_StopIteration))
1888-
PyErr_Clear();
1889-
else
1890-
return NULL; /* input raised an exception */
1891-
}
1892-
Py_CLEAR(lz->active);
1893-
return chain_next(lz); /* recurse and use next active */
1896+
/* Everything had been consumed already. */
1897+
return NULL;
18941898
}
18951899

18961900
static PyObject *

0 commit comments

Comments
 (0)