From a71a282c69bee0794d33c7c145cdc0e28b18886a Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:25:46 +0200 Subject: [PATCH] [3.14] gh-103847: fix some asyncio subprocess cancellation bugs (GH-146571) (#154632) gh-103847: fix some asyncio subprocess cancellation bugs (GH-146571) (cherry picked from commit f429fb36a16134a2a0e839d0f385eb8fee280de4) Co-authored-by: Kumar Aditya --- Lib/asyncio/base_subprocess.py | 18 +++++++++++++++--- Lib/asyncio/unix_events.py | 2 +- Lib/asyncio/windows_events.py | 2 +- Lib/test/test_asyncio/test_subprocess.py | 5 +---- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Lib/asyncio/base_subprocess.py b/Lib/asyncio/base_subprocess.py index 98e72f212aa2..0423bc100b2d 100644 --- a/Lib/asyncio/base_subprocess.py +++ b/Lib/asyncio/base_subprocess.py @@ -22,7 +22,7 @@ class BaseSubprocessTransport(transports.SubprocessTransport): self._proc = None self._pid = None self._returncode = None - self._exit_waiters = [] + self._exit_waiters = set() self._pending_calls = collections.deque() self._pipes = {} self._finished = False @@ -211,6 +211,14 @@ class BaseSubprocessTransport(transports.SubprocessTransport): except (SystemExit, KeyboardInterrupt): raise except BaseException as exc: + # Close any pipes that were already connected before the + # error/cancellation to avoid leaking file descriptors. + for proto in self._pipes.values(): + if proto is not None: + proto.pipe.close() + for raw_pipe in (proc.stdin, proc.stdout, proc.stderr): + if raw_pipe is not None: + raw_pipe.close() if waiter is not None and not waiter.cancelled(): waiter.set_exception(exc) else: @@ -260,8 +268,12 @@ class BaseSubprocessTransport(transports.SubprocessTransport): return self._returncode waiter = self._loop.create_future() - self._exit_waiters.append(waiter) - return await waiter + self._exit_waiters.add(waiter) + try: + return await waiter + finally: + if self._exit_waiters is not None: + self._exit_waiters.discard(waiter) def _try_finish(self): assert not self._finished diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index af33d7ac860c..36de51970c6b 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -212,7 +212,7 @@ class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop): raise except BaseException: transp.close() - await transp._wait() + await tasks.shield(transp._wait()) raise return transp diff --git a/Lib/asyncio/windows_events.py b/Lib/asyncio/windows_events.py index 0bf7732136f1..c905db101680 100644 --- a/Lib/asyncio/windows_events.py +++ b/Lib/asyncio/windows_events.py @@ -408,7 +408,7 @@ class ProactorEventLoop(proactor_events.BaseProactorEventLoop): raise except BaseException: transp.close() - await transp._wait() + await tasks.shield(transp._wait()) raise return transp diff --git a/Lib/test/test_asyncio/test_subprocess.py b/Lib/test/test_asyncio/test_subprocess.py index 4ab4315f1efe..aab92db872d6 100644 --- a/Lib/test/test_asyncio/test_subprocess.py +++ b/Lib/test/test_asyncio/test_subprocess.py @@ -11,7 +11,7 @@ from asyncio import base_subprocess from asyncio import subprocess from test.test_asyncio import utils as test_utils from test import support -from test.support import os_helper, warnings_helper, gc_collect +from test.support import os_helper, gc_collect if not support.has_subprocess_support: raise unittest.SkipTest("test module requires subprocess") @@ -919,7 +919,6 @@ class SubprocessMixin: self.loop.run_until_complete(main()) - @warnings_helper.ignore_warnings(category=ResourceWarning) def test_subprocess_read_pipe_cancelled(self): async def main(): loop = asyncio.get_running_loop() @@ -930,7 +929,6 @@ class SubprocessMixin: asyncio.run(main()) gc_collect() - @warnings_helper.ignore_warnings(category=ResourceWarning) def test_subprocess_write_pipe_cancelled(self): async def main(): loop = asyncio.get_running_loop() @@ -941,7 +939,6 @@ class SubprocessMixin: asyncio.run(main()) gc_collect() - @warnings_helper.ignore_warnings(category=ResourceWarning) def test_subprocess_read_write_pipe_cancelled(self): async def main(): loop = asyncio.get_running_loop() -- 2.47.3