bpo-29942: Fix the use of recursion in itertools.chain.from_iterable. (#889)
authorT. Wouters <thomas@python.org>
Thu, 30 Mar 2017 16:58:35 +0000 (09:58 -0700)
committerGitHub <noreply@github.com>
Thu, 30 Mar 2017 16:58:35 +0000 (09:58 -0700)
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.

Lib/test/test_itertools.py
Misc/NEWS
Modules/itertoolsmodule.c

index ea1f57caade12b91bf7c51f7eed93e33d9d76267..c431f0dc6e1d7f4896906313dae62d1d05582dc9 100644 (file)
@@ -1976,6 +1976,14 @@ class RegressionTests(unittest.TestCase):
         self.assertRaises(AssertionError, list, cycle(gen1()))
         self.assertEqual(hist, [0,1])
 
+    def test_long_chain_of_empty_iterables(self):
+        # Make sure itertools.chain doesn't run into recursion limits when
+        # dealing with long chains of empty iterables. Even with a high
+        # number this would probably only fail in Py_DEBUG mode.
+        it = chain.from_iterable(() for unused in range(10000000))
+        with self.assertRaises(StopIteration):
+            next(it)
+
 class SubclassWithKwargsTest(unittest.TestCase):
     def test_keywords_in_subclass(self):
         # count is not subclassable...
index 8646b8ce452b93bdf99191a5598c35fe02fa202b..42a09b6d7b0d109f86f05d5b15e1826c446ac174 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -301,6 +301,9 @@ Extension Modules
 Library
 -------
 
+- bpo-29942: Fix a crash in itertools.chain.from_iterable when encountering
+  long runs of empty iterables.
+
 - bpo-10030: Sped up reading encrypted ZIP files by 2 times.
 
 - bpo-29204: Element.getiterator() and the html parameter of XMLParser() were
index e91a0299c4a56639a1dcca3560d6f024c1697743..9823c77f776b2340b8ed665fa1a17b7fc21216a2 100644 (file)
@@ -1864,33 +1864,37 @@ chain_next(chainobject *lz)
 {
     PyObject *item;
 
-    if (lz->source == NULL)
-        return NULL;                    /* already stopped */
-
-    if (lz->active == NULL) {
-        PyObject *iterable = PyIter_Next(lz->source);
-        if (iterable == NULL) {
-            Py_CLEAR(lz->source);
-            return NULL;                /* no more input sources */
-        }
-        lz->active = PyObject_GetIter(iterable);
-        Py_DECREF(iterable);
+    /* lz->source is the iterator of iterables. If it's NULL, we've already
+     * consumed them all. lz->active is the current iterator. If it's NULL,
+     * we should grab a new one from lz->source. */
+    while (lz->source != NULL) {
         if (lz->active == NULL) {
-            Py_CLEAR(lz->source);
-            return NULL;                /* input not iterable */
+            PyObject *iterable = PyIter_Next(lz->source);
+            if (iterable == NULL) {
+                Py_CLEAR(lz->source);
+                return NULL;            /* no more input sources */
+            }
+            lz->active = PyObject_GetIter(iterable);
+            Py_DECREF(iterable);
+            if (lz->active == NULL) {
+                Py_CLEAR(lz->source);
+                return NULL;            /* input not iterable */
+            }
         }
+        item = (*Py_TYPE(lz->active)->tp_iternext)(lz->active);
+        if (item != NULL)
+            return item;
+        if (PyErr_Occurred()) {
+            if (PyErr_ExceptionMatches(PyExc_StopIteration))
+                PyErr_Clear();
+            else
+                return NULL;            /* input raised an exception */
+        }
+        /* lz->active is consumed, try with the next iterable. */
+        Py_CLEAR(lz->active);
     }
-    item = (*Py_TYPE(lz->active)->tp_iternext)(lz->active);
-    if (item != NULL)
-        return item;
-    if (PyErr_Occurred()) {
-        if (PyErr_ExceptionMatches(PyExc_StopIteration))
-            PyErr_Clear();
-        else
-            return NULL;                /* input raised an exception */
-    }
-    Py_CLEAR(lz->active);
-    return chain_next(lz);              /* recurse and use next active */
+    /* Everything had been consumed already. */
+    return NULL;
 }
 
 static PyObject *