From 003ae58b08ea71b2e9a03509d34c6d8e73789a2f Mon Sep 17 00:00:00 2001 From: lul-cas Date: Wed, 23 Sep 2026 20:08:27 -0300 Subject: [PATCH 1/2] gh-158033: Keep the aiter sentinel alive during comparison --- Lib/test/test_asyncgen.py | 41 +++++++++++++++++++++++++++++++++++++++ Objects/iterobject.c | 4 +++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_asyncgen.py b/Lib/test/test_asyncgen.py index b5e0891feb794e8..e05dcd6203bd3af 100644 --- a/Lib/test/test_asyncgen.py +++ b/Lib/test/test_asyncgen.py @@ -905,6 +905,47 @@ def test_aiter_callable_exhausted(self): with self.assertRaises(StopAsyncIteration): self.loop.run_until_complete(anext(it)) + def test_aiter_callable_reentrant_sentinel_compare(self): + stop = False + destroyed = [] + compared = [] + + async def produce(): + return Result() + + def fetch(): + nonlocal stop + if stop: + raise StopAsyncIteration + return produce() + + class Sentinel: + def __del__(self): + destroyed.append(True) + + def __eq__(self, other): + nonlocal stop + stop = True + aw = ait.__anext__() + try: + aw.__await__().send(None) + except StopAsyncIteration: + pass + return NotImplemented + + class Result: + def __eq__(self, other): + compared.append(isinstance(other, Sentinel) and not destroyed) + return NotImplemented + + ait = aiter(fetch, Sentinel()) + with self.assertRaises(StopIteration) as cm: + ait.__anext__().__await__().send(None) + self.assertIsInstance(cm.exception.value, Result) + self.assertEqual(compared, [True]) + with self.assertRaises(StopAsyncIteration): + ait.__anext__().__await__().send(None) + def test_aiter_callable_lazy(self): # The callable is only called when the awaitable is awaited calls = [] diff --git a/Objects/iterobject.c b/Objects/iterobject.c index 2d5e3709a27dfb0..a8278d81713146c 100644 --- a/Objects/iterobject.c +++ b/Objects/iterobject.c @@ -641,7 +641,9 @@ acallawaitable_handle_error(acallawaitableobject *aw) } int ok = 0; if (it->it_sentinel != NULL) { - ok = PyObject_RichCompareBool(it->it_sentinel, value, Py_EQ); + PyObject *sentinel = Py_NewRef(it->it_sentinel); + ok = PyObject_RichCompareBool(sentinel, value, Py_EQ); + Py_DECREF(sentinel); } if (ok == 0) { (void)_PyGen_SetStopIterationValue(value); From 7af8fa75bc40e74e0ff97c61c87bd89b7bd03f24 Mon Sep 17 00:00:00 2001 From: lul-cas Date: Wed, 23 Sep 2026 21:48:19 -0300 Subject: [PATCH 2/2] gh-158033: Add the NEWS entry for the aiter sentinel fix --- .../2026-09-23-21-41-01.gh-issue-158033.mK8rLt.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-09-23-21-41-01.gh-issue-158033.mK8rLt.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-23-21-41-01.gh-issue-158033.mK8rLt.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-23-21-41-01.gh-issue-158033.mK8rLt.rst new file mode 100644 index 000000000000000..3dd5aac5625d87b --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-23-21-41-01.gh-issue-158033.mK8rLt.rst @@ -0,0 +1,3 @@ +Fix a use-after-free in the two-argument form of :func:`aiter` when comparing +the sentinel. A reentrant equality comparison could exhaust the iterator and +free the sentinel while the comparison was still using it.