Skip to content

Commit 19cf4d9

Browse files
authored
Fix Windows IPC race condition when using parallel checking (#21228)
Cancellation is asynchronous, so some data could be read after calling `cancel()`. Rework `ready_to_read` to first cancel all operations and then check for each if data is available. See discussion in #21221 for more context. I heavily relied on coding agent assist for this, but I did multiple review iterations and refinements.
1 parent 8e3c99a commit 19cf4d9

File tree

1 file changed

+25
-5
lines changed

1 file changed

+25
-5
lines changed

mypy/ipc.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -421,16 +421,36 @@ def ready_to_read(conns: Sequence[IPCBase], timeout: float | None = None) -> lis
421421
ov.cancel()
422422
raise IPCException(f"Failed to wait for connections: {_winapi.GetLastError()}")
423423

424-
# Check which pending operations completed, cancel the rest
424+
# Cancel all pending operations. CancelIoEx is asynchronous, so an
425+
# operation may have completed before the cancel took effect. We then
426+
# wait for all operations to finalize and check each result: completed
427+
# reads get their data saved and are marked ready; cancelled ones are
428+
# simply skipped. This avoids a race between checking if an operation
429+
# is signaled and cancelling it.
430+
for _, ov in pending:
431+
ov.cancel()
425432
for i, ov in pending:
426-
if _winapi.WaitForSingleObject(ov.event, 0) == _winapi.WAIT_OBJECT_0:
433+
try:
427434
_, err = ov.GetOverlappedResult(True)
435+
except OSError as e:
436+
err = e.winerror
437+
# Cancellation is expected here; broken/disconnected pipes should be
438+
# surfaced as readable so the follow-up receive observes EOF/closure.
439+
if err not in (
440+
_winapi.ERROR_OPERATION_ABORTED,
441+
_winapi.ERROR_BROKEN_PIPE,
442+
_winapi.ERROR_NETNAME_DELETED,
443+
):
444+
# Anything else is a real IPC failure, not part of the probe race.
445+
raise
446+
if err == _winapi.ERROR_OPERATION_ABORTED:
447+
# Operation was successfully cancelled -- no data consumed.
448+
continue
449+
if err in (0, _winapi.ERROR_MORE_DATA):
428450
data = ov.getbuffer()
429451
if data:
430452
conns[i].buffer.extend(data)
431-
ready.append(i)
432-
else:
433-
ov.cancel()
453+
ready.append(i)
434454

435455
return ready
436456

0 commit comments

Comments
 (0)