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 |