From d2b941f3455687cf2bbddf41659c1624d7de1400 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Thu, 7 May 2026 14:59:08 -0700 Subject: [PATCH] gh-149388: Make PipeHandle.close idempotent Clear _handle before calling CloseHandle so a stale handle (closed by another code path) does not leak OSError into _ProactorBasePipeTransport._call_connection_lost. --- Lib/asyncio/windows_utils.py | 3 ++- Lib/test/test_asyncio/test_windows_utils.py | 24 +++++++++++++++++++ ...-05-07-21-58-17.gh-issue-149388.DDBPeA.rst | 1 + 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-05-07-21-58-17.gh-issue-149388.DDBPeA.rst diff --git a/Lib/asyncio/windows_utils.py b/Lib/asyncio/windows_utils.py index acd49441131b042..d6393f0b1ffee5d 100644 --- a/Lib/asyncio/windows_utils.py +++ b/Lib/asyncio/windows_utils.py @@ -111,8 +111,9 @@ def fileno(self): def close(self, *, CloseHandle=_winapi.CloseHandle): if self._handle is not None: - CloseHandle(self._handle) + handle = self._handle self._handle = None + CloseHandle(handle) def __del__(self, _warn=warnings.warn): if self._handle is not None: diff --git a/Lib/test/test_asyncio/test_windows_utils.py b/Lib/test/test_asyncio/test_windows_utils.py index f9ee2f4f68150a1..509697613475953 100644 --- a/Lib/test/test_asyncio/test_windows_utils.py +++ b/Lib/test/test_asyncio/test_windows_utils.py @@ -77,6 +77,30 @@ def test_pipe_handle(self): else: raise RuntimeError('expected ERROR_INVALID_HANDLE') + def test_pipe_handle_close_after_external_close(self): + # gh-149388: PipeHandle.close() must clear ``_handle`` before calling + # CloseHandle so that if CloseHandle raises on a stale handle the + # PipeHandle is still marked closed and __del__ / subsequent close() + # calls are silent no-ops. + h1, h2 = windows_utils.pipe(overlapped=(False, False)) + try: + p = windows_utils.PipeHandle(h1) + # Simulate an external close of the underlying handle (e.g. + # a finalizer race or a concurrent close on the same object). + _winapi.CloseHandle(p.handle) + # First close() still propagates the OSError from CloseHandle, + # but must clear ``_handle`` first. + with self.assertRaises(OSError): + p.close() + self.assertIsNone(p.handle) + # Second close() is a no-op. + p.close() + # __del__ through GC is also a silent no-op — no unraisable. + del p + support.gc_collect() + finally: + _winapi.CloseHandle(h2) + class PopenTests(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2026-05-07-21-58-17.gh-issue-149388.DDBPeA.rst b/Misc/NEWS.d/next/Library/2026-05-07-21-58-17.gh-issue-149388.DDBPeA.rst new file mode 100644 index 000000000000000..4a1c6f3f5b4e579 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-05-07-21-58-17.gh-issue-149388.DDBPeA.rst @@ -0,0 +1 @@ +Make :class:`!asyncio.windows_utils.PipeHandle` closing idempotent.