]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-146613: Fix re-entrant use-after-free in `itertools._grouper` (#147962)
authorMa Yukun <68433685+TheSkyC@users.noreply.github.com>
Thu, 2 Apr 2026 13:34:58 +0000 (21:34 +0800)
committerGitHub <noreply@github.com>
Thu, 2 Apr 2026 13:34:58 +0000 (19:04 +0530)
Lib/test/test_itertools.py
Misc/NEWS.d/next/Library/2026-04-01-11-05-36.gh-issue-146613.GzjUFK.rst [new file with mode: 0644]
Modules/itertoolsmodule.c

index dc64288085fa74d698fd20ff715038f885c995ad..cf579d4da4e0dfb0ab31e4a1a8a5efc36e51331b 100644 (file)
@@ -754,6 +754,38 @@ class TestBasicOps(unittest.TestCase):
         next(g)
         next(g)  # must pass with address sanitizer
 
+    def test_grouper_reentrant_eq_does_not_crash(self):
+        # regression test for gh-146613
+        grouper_iter = None
+
+        class Key:
+            __hash__ = None
+
+            def __init__(self, do_advance):
+                self.do_advance = do_advance
+
+            def __eq__(self, other):
+                nonlocal grouper_iter
+                if self.do_advance:
+                    self.do_advance = False
+                    if grouper_iter is not None:
+                        try:
+                            next(grouper_iter)
+                        except StopIteration:
+                            pass
+                    return NotImplemented
+                return True
+
+        def keyfunc(element):
+            if element == 0:
+                return Key(do_advance=True)
+            return Key(do_advance=False)
+
+        g = itertools.groupby(range(4), keyfunc)
+        key, grouper_iter = next(g)
+        items = list(grouper_iter)
+        self.assertEqual(len(items), 1)
+
     def test_filter(self):
         self.assertEqual(list(filter(isEven, range(6))), [0,2,4])
         self.assertEqual(list(filter(None, [0,1,0,2,0])), [1,2])
diff --git a/Misc/NEWS.d/next/Library/2026-04-01-11-05-36.gh-issue-146613.GzjUFK.rst b/Misc/NEWS.d/next/Library/2026-04-01-11-05-36.gh-issue-146613.GzjUFK.rst
new file mode 100644 (file)
index 0000000..94e198e
--- /dev/null
@@ -0,0 +1,2 @@
+:mod:`itertools`: Fix a crash in :func:`itertools.groupby` when\r
+the grouper iterator is concurrently mutated.\r
index cf49724b8861c25fcf6bd3cf4a360b8adc8ade0e..0453a166c843ad08e92ca5461a1efda03b26dc0c 100644 (file)
@@ -678,7 +678,16 @@ _grouper_next(PyObject *op)
     }
 
     assert(gbo->currkey != NULL);
-    rcmp = PyObject_RichCompareBool(igo->tgtkey, gbo->currkey, Py_EQ);
+    /* A user-defined __eq__ can re-enter the grouper and advance the iterator,
+       mutating gbo->currkey while we are comparing them.
+       Take local snapshots and hold strong references so INCREF/DECREF
+       apply to the same objects even under re-entrancy. */
+    PyObject *tgtkey = Py_NewRef(igo->tgtkey);
+    PyObject *currkey = Py_NewRef(gbo->currkey);
+    rcmp = PyObject_RichCompareBool(tgtkey, currkey, Py_EQ);
+    Py_DECREF(tgtkey);
+    Py_DECREF(currkey);
+
     if (rcmp <= 0)
         /* got any error or current group is end */
         return NULL;