]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-142615: disallow multiple initializations of `asyncio.Task` and `asyncio.Future...
authorKumar Aditya <kumaraditya@python.org>
Sat, 3 Jan 2026 07:57:02 +0000 (13:27 +0530)
committerGitHub <noreply@github.com>
Sat, 3 Jan 2026 07:57:02 +0000 (13:27 +0530)
Lib/asyncio/futures.py
Lib/test/test_asyncio/test_futures.py
Lib/test/test_asyncio/test_tasks.py
Misc/NEWS.d/next/Library/2025-12-12-08-51-29.gh-issue-142615.GoJ6el.rst [new file with mode: 0644]
Modules/_asynciomodule.c

index 29652295218a2251ddc494e753fa5a7de9b35668..11858a0274a69fdd66b4e7a3c7b885443c91a576 100644 (file)
@@ -79,6 +79,10 @@ class Future:
         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:
index 666f9c9ee187836853894c5400e27c42b3036f7e..9385a65e52813e6cb46c2df1b7077c55f4fe34d0 100644 (file)
@@ -750,6 +750,10 @@ class BaseFutureTests:
         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')
@@ -1091,33 +1095,6 @@ class BaseFutureDoneCallbackTests():
             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,
index a3c5351fed02522deb6ddc9cbe20f132269b61d9..dc179acd86e8a646df418a187af22a8c9c2eb66f 100644 (file)
@@ -2776,28 +2776,17 @@ class BaseTaskTests:
         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
+    def test_task_disallow_multiple_initialization(self):
+        async def foo():
+            pass
 
-            self.assertEqual(sys.getrefcount(obj), initial_refcount)
+        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
@@ -2921,19 +2910,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/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 (file)
index 0000000..3413f9a
--- /dev/null
@@ -0,0 +1,3 @@
+Fix possible crashes when initializing :class:`asyncio.Task` or :class:`asyncio.Future` multiple times.\r
+These classes can now be initialized only once and any subsequent initialization attempt will raise a RuntimeError.\r
+Patch by Kumar Aditya.
index 0b2a5d7093e1eaaba2445d4a9c918d362b018201..8eb8e191530a3326a82bd3e9422d18b5c90426df 100644 (file)
@@ -498,21 +498,13 @@ 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;
@@ -3008,11 +3000,7 @@ task_call_step_soon(asyncio_state *state, TaskObj *task, PyObject *arg)
         return -1;
     }
 
-    // Beware: An evil call_soon could alter task_context.
-    // See: https://github.com/python/cpython/issues/126080.
-    PyObject *task_context = Py_NewRef(task->task_context);
-    int ret = call_soon(state, task->task_loop, cb, NULL, task_context);
-    Py_DECREF(task_context);
+    int ret = call_soon(state, task->task_loop, cb, NULL, task->task_context);
     Py_DECREF(cb);
     return ret;
 }