From 37f8e990ac4819e20018f39c91623265fe17ffcc Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 12 Dec 2025 13:59:41 +0530 Subject: [PATCH 1/7] disallow multiple initializations of asyncio tasks and futures --- Lib/test/test_asyncio/test_futures.py | 27 -------------------- Lib/test/test_asyncio/test_tasks.py | 36 --------------------------- Modules/_asynciomodule.c | 19 +++++--------- 3 files changed, 6 insertions(+), 76 deletions(-) diff --git a/Lib/test/test_asyncio/test_futures.py b/Lib/test/test_asyncio/test_futures.py index 666f9c9ee18783..8fdd04154a7f6d 100644 --- a/Lib/test/test_asyncio/test_futures.py +++ b/Lib/test/test_asyncio/test_futures.py @@ -1091,33 +1091,6 @@ def __getattribute__(self, name): fut.add_done_callback(fut_callback_0) self.assertRaises(ReachableCode, fut.set_result, "boom") - def test_use_after_free_on_fut_context_0_with_evil__getattribute__(self): - # see: https://github.com/python/cpython/issues/125984 - - class EvilEventLoop(SimpleEvilEventLoop): - def call_soon(self, *args, **kwargs): - super().call_soon(*args, **kwargs) - raise ReachableCode - - def __getattribute__(self, name): - if name == 'call_soon': - # resets the future's event loop - fut.__init__(loop=SimpleEvilEventLoop()) - return object.__getattribute__(self, name) - - evil_loop = EvilEventLoop() - with mock.patch.object(self, 'loop', evil_loop): - fut = self._new_future() - self.assertIs(fut.get_loop(), evil_loop) - - fut_callback_0 = mock.Mock() - fut_context_0 = mock.Mock() - fut.add_done_callback(fut_callback_0, context=fut_context_0) - del fut_context_0 - del fut_callback_0 - self.assertRaises(ReachableCode, fut.set_result, "boom") - - @unittest.skipUnless(hasattr(futures, '_CFuture'), 'requires the C _asyncio module') class CFutureDoneCallbackTests(BaseFutureDoneCallbackTests, diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index a3c5351fed0252..7e90d261d1955c 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -2776,29 +2776,6 @@ def test_get_context(self): finally: loop.close() - def test_proper_refcounts(self): - # see: https://github.com/python/cpython/issues/126083 - class Break: - def __str__(self): - raise RuntimeError("break") - - obj = object() - initial_refcount = sys.getrefcount(obj) - - coro = coroutine_function() - with contextlib.closing(asyncio.EventLoop()) as loop: - task = asyncio.Task.__new__(asyncio.Task) - for _ in range(5): - with self.assertRaisesRegex(RuntimeError, 'break'): - task.__init__(coro, loop=loop, context=obj, name=Break()) - - coro.close() - task._log_destroy_pending = False - del task - - self.assertEqual(sys.getrefcount(obj), initial_refcount) - - def add_subclass_tests(cls): BaseTask = cls.Task BaseFuture = cls.Future @@ -2921,19 +2898,6 @@ class CTask_CFuture_Tests(BaseTaskTests, SetMethodsTest, all_tasks = getattr(tasks, '_c_all_tasks', None) current_task = staticmethod(getattr(tasks, '_c_current_task', None)) - @support.refcount_test - def test_refleaks_in_task___init__(self): - gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount') - async def coro(): - pass - task = self.new_task(self.loop, coro()) - self.loop.run_until_complete(task) - refs_before = gettotalrefcount() - for i in range(100): - task.__init__(coro(), loop=self.loop) - self.loop.run_until_complete(task) - self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10) - def test_del__log_destroy_pending_segfault(self): async def coro(): pass diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 0e6a1e93e04f33..46ecc39678cdfe 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -498,21 +498,14 @@ future_schedule_callbacks(asyncio_state *state, FutureObj *fut) static int future_init(FutureObj *fut, PyObject *loop) { + if (fut->fut_loop != NULL) { + PyErr_Format(PyExc_RuntimeError, + "%T object is already initialized", fut); + return -1; + } + PyObject *res; int is_true; - - Py_CLEAR(fut->fut_loop); - Py_CLEAR(fut->fut_callback0); - Py_CLEAR(fut->fut_context0); - Py_CLEAR(fut->fut_callbacks); - Py_CLEAR(fut->fut_result); - Py_CLEAR(fut->fut_exception); - Py_CLEAR(fut->fut_exception_tb); - Py_CLEAR(fut->fut_source_tb); - Py_CLEAR(fut->fut_cancel_msg); - Py_CLEAR(fut->fut_cancelled_exc); - Py_CLEAR(fut->fut_awaited_by); - fut->fut_state = STATE_PENDING; fut->fut_log_tb = 0; fut->fut_blocking = 0; From ffa9ec9b1103ffbc9ee5a83f0662effa56c7cdb4 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 12 Dec 2025 14:05:54 +0530 Subject: [PATCH 2/7] do the same for python tasks --- Lib/asyncio/futures.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/asyncio/futures.py b/Lib/asyncio/futures.py index 29652295218a22..c5fddc42355ab6 100644 --- a/Lib/asyncio/futures.py +++ b/Lib/asyncio/futures.py @@ -79,6 +79,10 @@ def __init__(self, *, loop=None): loop object used by the future. If it's not provided, the future uses the default event loop. """ + if self._loop is not None: + raise RuntimeError(f'{self.__class__.__name__} object is already ' + 'initialized.') + if loop is None: self._loop = events.get_event_loop() else: From 4eb0119744ef4eccbcaa863c3fb07a0e0800c228 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 12 Dec 2025 14:14:22 +0530 Subject: [PATCH 3/7] add tests --- Lib/test/test_asyncio/test_futures.py | 4 ++++ Lib/test/test_asyncio/test_tasks.py | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/Lib/test/test_asyncio/test_futures.py b/Lib/test/test_asyncio/test_futures.py index 8fdd04154a7f6d..9385a65e52813e 100644 --- a/Lib/test/test_asyncio/test_futures.py +++ b/Lib/test/test_asyncio/test_futures.py @@ -750,6 +750,10 @@ def test_future_cancelled_exception_refcycles(self): self.assertIsNotNone(exc) self.assertListEqual(gc.get_referrers(exc), []) + def test_future_disallow_multiple_initialization(self): + f = self._new_future(loop=self.loop) + with self.assertRaises(RuntimeError, msg="is already initialized"): + f.__init__(loop=self.loop) @unittest.skipUnless(hasattr(futures, '_CFuture'), 'requires the C _asyncio module') diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 7e90d261d1955c..dc179acd86e8a6 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -2776,6 +2776,18 @@ def test_get_context(self): finally: loop.close() + def test_task_disallow_multiple_initialization(self): + async def foo(): + pass + + coro = foo() + self.addCleanup(coro.close) + task = self.new_task(self.loop, coro) + task._log_destroy_pending = False + + with self.assertRaises(RuntimeError, msg="is already initialized"): + task.__init__(coro, loop=self.loop) + def add_subclass_tests(cls): BaseTask = cls.Task BaseFuture = cls.Future From a457ba1dcc49364ed4d9090bd5b6fa52ea4c0728 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 08:51:38 +0000 Subject: [PATCH 4/7] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Library/2025-12-12-08-51-29.gh-issue-142615.GoJ6el.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-12-12-08-51-29.gh-issue-142615.GoJ6el.rst diff --git a/Misc/NEWS.d/next/Library/2025-12-12-08-51-29.gh-issue-142615.GoJ6el.rst b/Misc/NEWS.d/next/Library/2025-12-12-08-51-29.gh-issue-142615.GoJ6el.rst new file mode 100644 index 00000000000000..958d03c0e69883 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-12-08-51-29.gh-issue-142615.GoJ6el.rst @@ -0,0 +1,3 @@ +Fix possible crashes when initializing :class:`asyncio.Task` or :class:`asyncio.Future` multiple times. +These classes can now be initialized only once and any subsequent initialization attempt will raise a RuntimeError. +Patch by Kumar Aditya. From 82a7fd020cc6e5ed5b9e1ad78ecea99fe92784c7 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 12 Dec 2025 14:36:15 +0530 Subject: [PATCH 5/7] fix news --- .../next/Library/2025-12-12-08-51-29.gh-issue-142615.GoJ6el.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-12-12-08-51-29.gh-issue-142615.GoJ6el.rst b/Misc/NEWS.d/next/Library/2025-12-12-08-51-29.gh-issue-142615.GoJ6el.rst index 958d03c0e69883..3413f9a5ac6db6 100644 --- a/Misc/NEWS.d/next/Library/2025-12-12-08-51-29.gh-issue-142615.GoJ6el.rst +++ b/Misc/NEWS.d/next/Library/2025-12-12-08-51-29.gh-issue-142615.GoJ6el.rst @@ -1,3 +1,3 @@ Fix possible crashes when initializing :class:`asyncio.Task` or :class:`asyncio.Future` multiple times. -These classes can now be initialized only once and any subsequent initialization attempt will raise a RuntimeError. +These classes can now be initialized only once and any subsequent initialization attempt will raise a RuntimeError. Patch by Kumar Aditya. From 5ae2e226ca735e630d4c3d3a458f354534925589 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 12 Dec 2025 14:42:32 +0530 Subject: [PATCH 6/7] fmt --- Modules/_asynciomodule.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 46ecc39678cdfe..2a0cccbb14de32 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -499,8 +499,7 @@ static int future_init(FutureObj *fut, PyObject *loop) { if (fut->fut_loop != NULL) { - PyErr_Format(PyExc_RuntimeError, - "%T object is already initialized", fut); + PyErr_Format(PyExc_RuntimeError, "%T object is already initialized", fut); return -1; } From 3e697f6a5f9784210f397b3d9d1e5179010c17d0 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Fri, 12 Dec 2025 14:43:41 +0530 Subject: [PATCH 7/7] make error message same for both implementations --- Lib/asyncio/futures.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/asyncio/futures.py b/Lib/asyncio/futures.py index c5fddc42355ab6..11858a0274a69f 100644 --- a/Lib/asyncio/futures.py +++ b/Lib/asyncio/futures.py @@ -80,8 +80,8 @@ def __init__(self, *, loop=None): the default event loop. """ if self._loop is not None: - raise RuntimeError(f'{self.__class__.__name__} object is already ' - 'initialized.') + raise RuntimeError(f"{self.__class__.__name__} object is already " + "initialized") if loop is None: self._loop = events.get_event_loop()