Skip to content

Commit 798a844

Browse files
mxschmittclaude
andcommitted
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. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f0daba1 commit 798a844

3 files changed

Lines changed: 37 additions & 2 deletions

File tree

Lib/asyncio/windows_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ def fileno(self):
111111

112112
def close(self, *, CloseHandle=_winapi.CloseHandle):
113113
if self._handle is not None:
114-
CloseHandle(self._handle)
115-
self._handle = None
114+
handle, self._handle = self._handle, None
115+
CloseHandle(handle)
116116

117117
def __del__(self, _warn=warnings.warn):
118118
if self._handle is not None:

Lib/test/test_asyncio/test_windows_utils.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,34 @@ def test_pipe_handle(self):
7777
else:
7878
raise RuntimeError('expected ERROR_INVALID_HANDLE')
7979

80+
def test_pipe_handle_close_after_external_close(self):
81+
# gh-149388: PipeHandle.close() must clear ``_handle`` before calling
82+
# CloseHandle so that a handle already closed by another code path
83+
# does not leak an OSError into the caller (and into
84+
# _ProactorBasePipeTransport._call_connection_lost on the proactor
85+
# loop, where the error is not caught).
86+
h1, h2 = windows_utils.pipe(overlapped=(False, False))
87+
try:
88+
p = windows_utils.PipeHandle(h1)
89+
# Simulate an external close of the underlying handle (e.g.
90+
# a finalizer race or a concurrent close on the same object).
91+
_winapi.CloseHandle(p.handle)
92+
# The OSError from CloseHandle may still surface to the caller,
93+
# but ``_handle`` must be cleared first so that __del__ and any
94+
# subsequent close() are silent no-ops.
95+
try:
96+
p.close()
97+
except OSError:
98+
pass
99+
self.assertIsNone(p.handle)
100+
# Second close is a no-op.
101+
p.close()
102+
# __del__ through GC is a no-op too — no unraisable warning.
103+
del p
104+
support.gc_collect()
105+
finally:
106+
_winapi.CloseHandle(h2)
107+
80108

81109
class PopenTests(unittest.TestCase):
82110

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Make :meth:`!asyncio.windows_utils.PipeHandle.close` idempotent by clearing
2+
``_handle`` before calling :c:func:`!CloseHandle`. Previously, if the
3+
underlying Win32 handle had already been closed by another code path, the
4+
``OSError`` raised by :c:func:`!CloseHandle` would escape through
5+
:meth:`!_ProactorBasePipeTransport._call_connection_lost` on Windows
6+
proactor loops and be reported as an unhandled exception to the event
7+
loop's exception handler.

0 commit comments

Comments
 (0)