Use "overlapped I/O" for Windows named pipe communication
Marco Ricci

Marco Ricci commited on 2025-12-28 18:53:33
Zeige 1 geänderte Dateien mit 118 Einfügungen und 12 Löschungen.


(... or, as the rest of the world calls it: non-blocking I/O.)

We previously avoided using the overlapped I/O interface, for multiple
reasons:

  - It requires C structs which are not already natively wrapped by
    `ctypes` on The Annoying OS, and are fairly verbose to implement
    manually.

  - The notification that the I/O operation has completed requires
    further types and functions to be wrapped.

  - The C structs are documented reasonably well, but the bit flags used
    in this interface are sometimes only given by name, not by value,
    and thus need to be looked up in actual pre-existing code.

However, Pageant also internally uses overlapped I/O, and the behavior
observed in 5bcd2c39308880309286a2243e3795833817d1a5 may be due to
a mismatch between the I/O channel we see (non-overlapped) and the
channel Pageant sees (overlapped).  So, for consistency and
compatibility, we also use the overlapped I/O interface.
... ...
@@ -28,7 +28,14 @@ if TYPE_CHECKING:
28 28
     from collections.abc import Callable
29 29
     from typing import ClassVar
30 30
 
31
-    from typing_extensions import Any, Buffer, Literal, Self, TypeVar
31
+    from typing_extensions import (
32
+        Any,
33
+        Buffer,
34
+        Literal,
35
+        Self,
36
+        TypeAlias,
37
+        TypeVar,
38
+    )
32 39
 
33 40
     from derivepassphrase import _types
34 41
 
... ...
@@ -53,9 +60,43 @@ if TYPE_CHECKING:
53 60
         ],
54 61
         HANDLE,
55 62
     ]
56
-    ReadFile: Callable[[HANDLE, LPCVOID, DWORD, LPDWORD, None], BOOL]
57
-    WriteFile: Callable[[HANDLE, LPCVOID, DWORD, LPDWORD, None], BOOL]
63
+    ReadFile: Callable[[HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED], BOOL]
64
+    WriteFile: Callable[[HANDLE, LPCVOID, DWORD, LPDWORD, LPOVERLAPPED], BOOL]
58 65
     CloseHandle: Callable[[HANDLE], BOOL]
66
+    GetOverlappedResult: Callable[[HANDLE, LPOVERLAPPED, LPDWORD, BOOL], BOOL]
67
+    CreateEvent: Callable[[None, BOOL, BOOL, None], HANDLE]
68
+
69
+
70
+class _OverlappedDummyStruct(ctypes.Structure):
71
+    """The DUMMYSTRUCTNAME structure in the definition of OVERLAPPED."""
72
+
73
+    _fields_ = (("Offset", DWORD), ("OffsetHigh", DWORD))
74
+
75
+
76
+class _OverlappedDummyUnion(ctypes.Union):
77
+    """The DUMMYUNIONNAME union in the definition of OVERLAPPED."""
78
+
79
+    _fields_ = (
80
+        ("DUMMYSTRUCTNAME", _OverlappedDummyStruct),
81
+        ("Pointer", ctypes.c_void_p),
82
+    )
83
+
84
+
85
+class OVERLAPPED(ctypes.Structure):
86
+    """The data structure for overlapped (async) I/O on The Annoying OS."""
87
+
88
+    _fields_ = (
89
+        ("Internal", ctypes.POINTER(ctypes.c_ulong)),
90
+        ("InternalHigh", ctypes.POINTER(ctypes.c_ulong)),
91
+        ("DUMMYUNIONNAME", _OverlappedDummyUnion),
92
+        ("hEvent", HANDLE),
93
+    )
94
+
95
+
96
+if TYPE_CHECKING:
97
+    LPOVERLAPPED: TypeAlias = ctypes._Pointer[OVERLAPPED]  # noqa: SLF001
98
+else:
99
+    LPOVERLAPPED = ctypes.POINTER(OVERLAPPED)
59 100
 
60 101
 __all__ = ("SocketProvider",)
61 102
 
... ...
@@ -90,6 +131,7 @@ GENERIC_WRITE = 0x40000000
90 131
 
91 132
 FILE_SHARE_READ = 0x00000001
92 133
 FILE_SHARE_WRITE = 0x00000002
134
+FILE_FLAG_OVERLAPPED = 0x40000000
93 135
 
94 136
 OPEN_EXISTING = 0x3
95 137
 
... ...
@@ -101,6 +143,7 @@ CRYPTPROTECTMEMORY_CROSS_PROCESS = 0x1
101 143
 PIPE_PREFIX = "//./pipe/".replace("/", "\\")
102 144
 
103 145
 ERROR_PIPE_BUSY = 231
146
+ERROR_IO_PENDING = 997
104 147
 
105 148
 
106 149
 # The type checker and the standard library use different
... ...
@@ -172,6 +215,8 @@ try:  # pragma: unless the-annoying-os no cover
172 215
     ReadFile = kernel32.ReadFile  # type: ignore[attr-defined]
173 216
     WriteFile = kernel32.WriteFile  # type: ignore[attr-defined]
174 217
     CloseHandle = kernel32.CloseHandle  # type: ignore[attr-defined]
218
+    GetOverlappedResult = kernel32.GetOverlappedResult  # type: ignore[attr-defined]
219
+    CreateEvent = kernel32.CreateEventW  # type: ignore[attr-defined]
175 220
 except (
176 221
     AttributeError,
177 222
     FileNotFoundError,
... ...
@@ -203,7 +248,7 @@ except (
203 248
         lpBuffer: LPVOID,  # noqa: N803
204 249
         nNumberOfBytesToRead: DWORD,  # noqa: N803
205 250
         lpNumberOfBytesRead: LPDWORD,  # noqa: N803
206
-        lpOverlapped: None,  # noqa: N803
251
+        lpOverlapped: LPOVERLAPPED,  # noqa: N803
207 252
     ) -> BOOL:  # pragma: no cover [external]
208 253
         del (
209 254
             hFile,
... ...
@@ -219,7 +264,7 @@ except (
219 264
         lpBuffer: LPVOID,  # noqa: N803
220 265
         nNumberOfBytesToWrite: DWORD,  # noqa: N803
221 266
         lpNumberOfBytesWritten: LPDWORD,  # noqa: N803
222
-        lpOverlapped: None,  # noqa: N803
267
+        lpOverlapped: LPOVERLAPPED,  # noqa: N803
223 268
     ) -> BOOL:  # pragma: no cover [external]
224 269
         del (
225 270
             hFile,
... ...
@@ -236,6 +281,24 @@ except (
236 281
         del hHandle
237 282
         raise OSError(errno.ENOTSUP, os.strerror(errno.ENOTSUP))
238 283
 
284
+    def GetOverlappedResult(  # noqa: N802
285
+        hFile: HANDLE,  # noqa: N803
286
+        lpOverlapped: LPOVERLAPPED,  # noqa: N803
287
+        lpNumberOfBytesTransferred: LPDWORD,  # noqa: N803
288
+        bWait: BOOL,  # noqa: N803
289
+    ) -> BOOL:  # pragma: no cover [external]
290
+        del hFile, lpOverlapped, lpNumberOfBytesTransferred, bWait
291
+        raise OSError(errno.ENOTSUP, os.strerror(errno.ENOTSUP))
292
+
293
+    def CreateEvent(  # noqa: N802
294
+        lpEventAttributes: None,  # noqa: N803
295
+        bManualReset: BOOL,  # noqa: N803
296
+        bInitialState: BOOL,  # noqa: N803
297
+        lpName: None,  # noqa: N803
298
+    ) -> HANDLE:
299
+        del lpEventAttributes, bManualReset, bInitialState, lpName
300
+        raise OSError(errno.ENOTSUP, os.strerror(errno.ENOTSUP))
301
+
239 302
 
240 303
 else:  # pragma: unless the-annoying-os no cover
241 304
     CreateFile.argtypes = [  # type: ignore[attr-defined]
... ...
@@ -250,28 +313,45 @@ else:  # pragma: unless the-annoying-os no cover
250 313
         # We always pass None/NULL.
251 314
         HANDLE,
252 315
     ]
316
+    CreateEvent.argtypes = [
317
+        # Actually, LPSECURITY_ATTRIBUTES, but we always pass
318
+        # None/NULL.
319
+        ctypes.c_void_p,
320
+        BOOL,
321
+        BOOL,
322
+        # We always pass None/NULL.
323
+        LPCWSTR,
324
+    ]
253 325
     CreateFile.restype = HANDLE  # type: ignore[attr-defined]
326
+    CreateEvent.restype = HANDLE  # type: ignore[attr-defined]
254 327
     CreateFile.errcheck = _errcheck  # type: ignore[assignment, attr-defined]
328
+    CreateEvent.errcheck = _errcheck  # type: ignore[assignment, attr-defined]
255 329
     ReadFile.argtypes = [  # type: ignore[attr-defined]
256 330
         HANDLE,
257 331
         LPVOID,
258 332
         DWORD,
259 333
         LPDWORD,
260
-        # Actually, LPOVERLAPPED, but we always pass None/NULL.
261
-        ctypes.c_void_p,
334
+        LPOVERLAPPED,
262 335
     ]
263 336
     WriteFile.argtypes = [  # type: ignore[attr-defined]
264 337
         HANDLE,
265 338
         LPVOID,
266 339
         DWORD,
267 340
         LPDWORD,
268
-        # Actually, LPOVERLAPPED, but we always pass None/NULL.
269
-        ctypes.c_void_p,
341
+        LPOVERLAPPED,
270 342
     ]
271 343
     CloseHandle.argtypes = [HANDLE]  # type: ignore[attr-defined]
344
+    GetOverlappedResult.argtypes = [
345
+        HANDLE,
346
+        LPOVERLAPPED,
347
+        LPDWORD,
348
+        # We always pass True.
349
+        BOOL,
350
+    ]
272 351
     ReadFile.restype = BOOL  # type: ignore[attr-defined]
273 352
     WriteFile.restype = BOOL  # type: ignore[attr-defined]
274 353
     CloseHandle.restype = BOOL  # type: ignore[attr-defined]
354
+    GetOverlappedResult.restype = BOOL  # type: ignore[attr-defined]
275 355
 
276 356
 
277 357
 class WindowsNamedPipeHandle:
... ...
@@ -314,7 +394,7 @@ class WindowsNamedPipeHandle:
314 394
                     ctypes.c_ulong(FILE_SHARE_READ | FILE_SHARE_WRITE),
315 395
                     None,
316 396
                     ctypes.c_ulong(OPEN_EXISTING),
317
-                    ctypes.c_ulong(0),
397
+                    ctypes.c_ulong(FILE_FLAG_OVERLAPPED),
318 398
                     None,
319 399
                 )
320 400
             except BlockingIOError:  # pragma: no cover [external]
... ...
@@ -344,17 +424,31 @@ class WindowsNamedPipeHandle:
344 424
         buffer = (ctypes.c_char * 65536)()
345 425
         while data > 0:
346 426
             block_size = min(max(0, data), 65536)
427
+            wait_event = CreateEvent(None, BOOL(True), BOOL(False), None)  # noqa: FBT003
428
+            try:
429
+                overlapped_struct = OVERLAPPED(hEvent=wait_event)
347 430
                 success = ReadFile(
348 431
                     self.handle,
349 432
                     ctypes.cast(ctypes.byref(buffer), ctypes.c_void_p),
350 433
                     DWORD(block_size),
351 434
                     ctypes.cast(ctypes.byref(read_count), LPDWORD),
352
-                None,
435
+                    ctypes.cast(ctypes.byref(overlapped_struct), LPOVERLAPPED),
436
+                )
437
+                if not success and ctypes.GetLastError() == ERROR_IO_PENDING:
438
+                    success = GetOverlappedResult(
439
+                        self.handle,
440
+                        ctypes.cast(
441
+                            ctypes.byref(overlapped_struct), LPOVERLAPPED
442
+                        ),
443
+                        ctypes.cast(ctypes.byref(read_count), LPDWORD),
444
+                        BOOL(True),  # noqa: FBT003
353 445
                     )
354 446
                 if (
355 447
                     not success or read_count.value == 0
356 448
                 ):  # pragma: no cover [external]
357 449
                     raise ctypes.WinError()  # type: ignore[attr-defined]
450
+            finally:
451
+                CloseHandle(wait_event)
358 452
             result.extend(buffer.raw[:block_size])
359 453
             data -= read_count.value
360 454
             read_count.value = 0
... ...
@@ -369,17 +463,29 @@ class WindowsNamedPipeHandle:
369 463
         for i, x in enumerate(data):
370 464
             databuf[i] = ctypes.c_char(x)
371 465
         write_count = DWORD(0)
466
+        wait_event = CreateEvent(None, BOOL(True), BOOL(False), None)  # noqa: FBT003
467
+        try:
468
+            overlapped_struct = OVERLAPPED(hEvent=wait_event)
372 469
             success = WriteFile(
373 470
                 self.handle,
374 471
                 ctypes.cast(ctypes.byref(databuf), ctypes.c_void_p),
375 472
                 DWORD(len(data)),
376 473
                 ctypes.cast(ctypes.byref(write_count), LPDWORD),
377
-            None,
474
+                ctypes.cast(ctypes.byref(overlapped_struct), LPOVERLAPPED),
475
+            )
476
+            if not success and ctypes.GetLastError() == ERROR_IO_PENDING:
477
+                success = GetOverlappedResult(
478
+                    self.handle,
479
+                    ctypes.cast(ctypes.byref(overlapped_struct), LPOVERLAPPED),
480
+                    ctypes.cast(ctypes.byref(write_count), LPDWORD),
481
+                    BOOL(True),  # noqa: FBT003
378 482
                 )
379 483
             if (
380 484
                 not success or write_count.value == 0
381 485
             ):  # pragma: no cover [external]
382 486
                 raise ctypes.WinError()  # type: ignore[attr-defined]
487
+        finally:
488
+            CloseHandle(wait_event)
383 489
 
384 490
     @classmethod
385 491
     def for_openssh(cls) -> Self:
386 492