]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.15] gh-149388: Make asyncio `PipeHandle.close` idempotent (GH-149518) (#149605)
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Sat, 9 May 2026 15:08:34 +0000 (17:08 +0200)
committerGitHub <noreply@github.com>
Sat, 9 May 2026 15:08:34 +0000 (20:38 +0530)
gh-149388: Make asyncio `PipeHandle.close` idempotent (GH-149518)
(cherry picked from commit 7241f2739c4bbdf4519238689e5e4ea9268b411e)

Co-authored-by: Max Schmitt <max@schmitt.mx>
Lib/asyncio/windows_utils.py
Lib/test/test_asyncio/test_windows_utils.py
Misc/NEWS.d/next/Library/2026-05-07-21-58-17.gh-issue-149388.DDBPeA.rst [new file with mode: 0644]

index acd49441131b04252b31c8dbef846fac35800d95..d6393f0b1ffee5dd84a2172db7823e7ae490506a 100644 (file)
@@ -111,8 +111,9 @@ class PipeHandle:
 
     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:
index f9ee2f4f68150a17974843134ce41be8953b1486..50969761347595332d3597a86dcabac84b24a79a 100644 (file)
@@ -77,6 +77,30 @@ class PipeTests(unittest.TestCase):
         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 (file)
index 0000000..4a1c6f3
--- /dev/null
@@ -0,0 +1 @@
+Make :class:`!asyncio.windows_utils.PipeHandle` closing idempotent.