From 241b9de253e0caa909c6f3f511df76f577f7f3ad Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Fri, 25 Sep 2026 17:14:26 -0700 Subject: [PATCH 1/2] gh-146065: Fix NULL dereference in FutureIter_am_send This is based on #146304, but fixes a concurrency issue where `future` is accessed unsafely. Co-authored-by: VanshAgarwal24036 <148854295+VanshAgarwal24036@users.noreply.github.com> Co-authored-by: Victor Stinner --- Lib/test/test_asyncio/test_futures.py | 18 ++++++++++++++ ...-03-23-00-04-13.gh-issue-146065.FIdn8D.rst | 2 ++ Modules/_asynciomodule.c | 24 +++++++++++++++---- 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-03-23-00-04-13.gh-issue-146065.FIdn8D.rst diff --git a/Lib/test/test_asyncio/test_futures.py b/Lib/test/test_asyncio/test_futures.py index a217177e1deb06d..5ed3ec582224c61 100644 --- a/Lib/test/test_asyncio/test_futures.py +++ b/Lib/test/test_asyncio/test_futures.py @@ -763,6 +763,24 @@ def test_future_disallow_multiple_initialization(self): with self.assertRaises(RuntimeError, msg="is already initialized"): f.__init__(loop=self.loop) + def test_futureiter_send_after_throw_no_crash(self): + fut = self._new_future(loop=self.loop) + it = fut.__await__() + next(it) + with self.assertRaises(RuntimeError): + it.throw(RuntimeError) + with self.assertRaises(StopIteration): + it.send(None) + + def test_futureiter_send_after_close_no_crash(self): + fut = self._new_future(loop=self.loop) + it = fut.__await__() + next(it) + it.close() + with self.assertRaises(StopIteration): + it.send(None) + + @unittest.skipUnless(hasattr(futures, '_CFuture'), 'requires the C _asyncio module') class CFutureTests(BaseFutureTests, test_utils.TestCase): diff --git a/Misc/NEWS.d/next/Library/2026-03-23-00-04-13.gh-issue-146065.FIdn8D.rst b/Misc/NEWS.d/next/Library/2026-03-23-00-04-13.gh-issue-146065.FIdn8D.rst new file mode 100644 index 000000000000000..db91ce76198c01e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-23-00-04-13.gh-issue-146065.FIdn8D.rst @@ -0,0 +1,2 @@ +Fix a crash in asyncio.Future iterator when send() is +called after throw() or close(). diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 18e731336d6da1d..40dc0cfa4d06492 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -1795,10 +1795,9 @@ FutureIter_dealloc(PyObject *it) } static PySendResult -FutureIter_am_send_lock_held(futureiterobject *it, PyObject **result) +FutureIter_am_send_lock_held(FutureObj *fut, PyObject **result) { PyObject *res; - FutureObj *fut = it->future; _Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(fut); *result = NULL; @@ -1829,11 +1828,24 @@ FutureIter_am_send(PyObject *op, PyObject **result) { futureiterobject *it = (futureiterobject*)op; + + FutureObj *fut; + Py_BEGIN_CRITICAL_SECTION(op); + fut = (FutureObj*)Py_XNewRef(it->future); + Py_END_CRITICAL_SECTION(); + + if (fut == NULL) { + PyErr_SetNone(PyExc_StopIteration); + *result = NULL; + return PYGEN_ERROR; + } + /* arg is unused, see the comment on FutureIter_send for clarification */ PySendResult res; - Py_BEGIN_CRITICAL_SECTION(it->future); - res = FutureIter_am_send_lock_held(it, result); + Py_BEGIN_CRITICAL_SECTION(fut); + res = FutureIter_am_send_lock_held(fut, result); Py_END_CRITICAL_SECTION(); + Py_DECREF(fut); return res; } @@ -1927,7 +1939,9 @@ FutureIter_throw(PyObject *op, PyObject *const *args, Py_ssize_t nargs) goto fail; } + Py_BEGIN_CRITICAL_SECTION(self); Py_CLEAR(self->future); + Py_END_CRITICAL_SECTION(); PyErr_Restore(type, val, tb); @@ -1944,7 +1958,9 @@ static int FutureIter_clear(PyObject *op) { futureiterobject *it = (futureiterobject*)op; + Py_BEGIN_CRITICAL_SECTION(op); Py_CLEAR(it->future); + Py_END_CRITICAL_SECTION(); return 0; } From d51862b39151f4265346dc20621b2450aa646d69 Mon Sep 17 00:00:00 2001 From: "Michael J. Sullivan" Date: Wed, 30 Sep 2026 10:07:25 -0700 Subject: [PATCH 2/2] Make FutureIter_close use Py_CLEAR directly This just gets rid of any confusion about why FutureIter_clear is using a critical section --- Modules/_asynciomodule.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 40dc0cfa4d06492..5569f7a3670378b 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -1954,21 +1954,22 @@ FutureIter_throw(PyObject *op, PyObject *const *args, Py_ssize_t nargs) return NULL; } -static int -FutureIter_clear(PyObject *op) +static PyObject * +FutureIter_close(PyObject *self, PyObject *arg) { - futureiterobject *it = (futureiterobject*)op; - Py_BEGIN_CRITICAL_SECTION(op); + futureiterobject *it = (futureiterobject*)self; + Py_BEGIN_CRITICAL_SECTION(self); Py_CLEAR(it->future); Py_END_CRITICAL_SECTION(); - return 0; + Py_RETURN_NONE; } -static PyObject * -FutureIter_close(PyObject *self, PyObject *arg) +static int +FutureIter_clear(PyObject *op) { - (void)FutureIter_clear(self); - Py_RETURN_NONE; + futureiterobject *it = (futureiterobject*)op; + Py_CLEAR(it->future); + return 0; } static int