From abdbe0b80724c5d12886b78ec0959b05b7352f9d Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Sat, 3 Jan 2026 13:27:02 +0530 Subject: [PATCH 1/3] gh-142615: disallow multiple initializations of `asyncio.Task` and `asyncio.Future` (#142616) --- Lib/asyncio/futures.py | 4 ++ Lib/test/test_asyncio/test_futures.py | 31 ++------------ Lib/test/test_asyncio/test_tasks.py | 42 ++++--------------- ...-12-12-08-51-29.gh-issue-142615.GoJ6el.rst | 3 ++ Modules/_asynciomodule.c | 24 +++-------- 5 files changed, 26 insertions(+), 78 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-12-12-08-51-29.gh-issue-142615.GoJ6el.rst diff --git a/Lib/asyncio/futures.py b/Lib/asyncio/futures.py index 29652295218a22..11858a0274a69f 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: diff --git a/Lib/test/test_asyncio/test_futures.py b/Lib/test/test_asyncio/test_futures.py index 666f9c9ee18783..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') @@ -1091,33 +1095,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..dc179acd86e8a6 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -2776,28 +2776,17 @@ 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 + 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 index 00000000000000..3413f9a5ac6db6 --- /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. diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index 0b2a5d7093e1ea..8eb8e191530a33 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -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; } From e7c542de5f069a4b83e8eded3067613e4d59a529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Sat, 3 Jan 2026 09:03:04 +0100 Subject: [PATCH 2/3] gh-140648: Make asyncio REPL respect the `-I` flag (isolated mode) (#143045) --- Lib/asyncio/__main__.py | 2 +- Lib/test/test_repl.py | 35 ++++++++++++------- ...-12-21-17-24-29.gh-issue-140648.i8dca6.rst | 3 ++ 3 files changed, 27 insertions(+), 13 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-12-21-17-24-29.gh-issue-140648.i8dca6.rst diff --git a/Lib/asyncio/__main__.py b/Lib/asyncio/__main__.py index afbb70bbcab930..44667efc522556 100644 --- a/Lib/asyncio/__main__.py +++ b/Lib/asyncio/__main__.py @@ -96,7 +96,7 @@ def run(self): console.write(banner) - if startup_path := os.getenv("PYTHONSTARTUP"): + if not sys.flags.isolated and (startup_path := os.getenv("PYTHONSTARTUP")): sys.audit("cpython.run_startup", startup_path) import tokenize diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 0fa1df40e44c5f..6cdb1ca65c6aed 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -28,7 +28,7 @@ raise unittest.SkipTest("test module requires subprocess") -def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=False, **kw): +def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=False, isolated=True, **kw): """Run the Python REPL with the given arguments. kw is extra keyword args to pass to subprocess.Popen. Returns a Popen @@ -42,7 +42,10 @@ def spawn_repl(*args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, custom=F # path may be used by PyConfig_Get("module_search_paths") to build the # default module search path. stdin_fname = os.path.join(os.path.dirname(sys.executable), "") - cmd_line = [stdin_fname, '-I'] + cmd_line = [stdin_fname] + # Isolated mode implies -EPs and ignores PYTHON* variables. + if isolated: + cmd_line.append('-I') # Don't re-run the built-in REPL from interactive mode # if we're testing a custom REPL (such as the asyncio REPL). if not custom: @@ -215,7 +218,7 @@ def make_repl(env): with os_helper.temp_dir() as tmpdir: script = os.path.join(tmpdir, "pythonstartup.py") with open(script, "w") as f: - f.write("print('from pythonstartup')" + os.linesep) + f.write("print('from pythonstartup')\n") env = os.environ.copy() env['PYTHONSTARTUP'] = script @@ -296,19 +299,27 @@ def test_asyncio_repl_reaches_python_startup_script(self): with os_helper.temp_dir() as tmpdir: script = os.path.join(tmpdir, "pythonstartup.py") with open(script, "w") as f: - f.write("print('pythonstartup done!')" + os.linesep) - f.write("exit(0)" + os.linesep) + f.write("print('pythonstartup done!')\n") + env = os.environ.copy() + env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".asyncio_history") + env["PYTHONSTARTUP"] = script + p = spawn_asyncio_repl(isolated=False, env=env) + output = kill_python(p) + self.assertEqual(p.returncode, 0) + self.assertIn("pythonstartup done!", output) + def test_asyncio_repl_respects_isolated_mode(self): + with os_helper.temp_dir() as tmpdir: + script = os.path.join(tmpdir, "pythonstartup.py") + with open(script, "w") as f: + f.write("print('should not print')\n") env = os.environ.copy() env["PYTHON_HISTORY"] = os.path.join(tmpdir, ".asyncio_history") env["PYTHONSTARTUP"] = script - subprocess.check_call( - [sys.executable, "-m", "asyncio"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=env, - timeout=SHORT_TIMEOUT, - ) + p = spawn_asyncio_repl(isolated=True, env=env) + output = kill_python(p) + self.assertEqual(p.returncode, 0) + self.assertNotIn("should not print", output) @unittest.skipUnless(pty, "requires pty") def test_asyncio_repl_is_ok(self): diff --git a/Misc/NEWS.d/next/Library/2025-12-21-17-24-29.gh-issue-140648.i8dca6.rst b/Misc/NEWS.d/next/Library/2025-12-21-17-24-29.gh-issue-140648.i8dca6.rst new file mode 100644 index 00000000000000..9e56f096b938f1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-21-17-24-29.gh-issue-140648.i8dca6.rst @@ -0,0 +1,3 @@ +The :mod:`asyncio` REPL now respects the :option:`-I` flag (isolated mode). +Previously, it would load and execute :envvar:`PYTHONSTARTUP` even if the +flag was set. Contributed by Bartosz Sławecki. From 6d05e55de0f9c0b07fb14b4d2b9cf9c8eee2042c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Sat, 3 Jan 2026 11:08:42 +0100 Subject: [PATCH 3/3] Fix dunder name typo in compiler code comment (#143374) --- Python/compile.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/compile.c b/Python/compile.c index 6951c98500dfec..1ab96a37f683f2 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -634,7 +634,7 @@ _PyCompile_EnterScope(compiler *c, identifier name, int scope_type, } } if (u->u_ste->ste_has_conditional_annotations) { - /* Cook up an implicit __conditional__annotations__ cell */ + /* Cook up an implicit __conditional_annotations__ cell */ Py_ssize_t res; assert(u->u_scope_type == COMPILE_SCOPE_CLASS || u->u_scope_type == COMPILE_SCOPE_MODULE); res = _PyCompile_DictAddObj(u->u_metadata.u_cellvars, &_Py_ID(__conditional_annotations__));