]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-146613: Fix re-entrant use-after-free in `itertools._grouper` (GH-147962...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Tue, 7 Apr 2026 10:24:54 +0000 (12:24 +0200)
committerGitHub <noreply@github.com>
Tue, 7 Apr 2026 10:24:54 +0000 (12:24 +0200)
gh-146613: Fix re-entrant use-after-free in `itertools._grouper` (GH-147962)
(cherry picked from commit fc7a188fe70a7b98696b4fcee8db9eb8398aeb7b)

Co-authored-by: Ma Yukun <68433685+TheSkyC@users.noreply.github.com>
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 093aa48d48a4bba04247b8e59bc55b22028912dc..899e984268066e9c261260ae92fd6be5885f3470 100644 (file)
@@ -1018,6 +1018,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 d967904069e988f82d8b03f39528c6cc1b724329..5add3c216b3858f70d3dd22fdf9f538b00f71923 100644 (file)
@@ -712,7 +712,16 @@ _grouper_next(_grouperobject *igo)
     }
 
     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;