Skip to content

[Windows] Read SO_ERROR as a winsock error - #3710

Open
jakepetroules wants to merge 3 commits into
apple:mainfrom
jakepetroules:windows-so-error
Open

[Windows] Read SO_ERROR as a winsock error#3710
jakepetroules wants to merge 3 commits into
apple:mainfrom
jakepetroules:windows-so-error

Conversation

@jakepetroules

@jakepetroules jakepetroules commented Aug 15, 2026

Copy link
Copy Markdown
Member

Motivation:

Depends on #3699 and #3700, which are the first two commits on this branch. Please review only the last commit, or wait until those are merged.

On Windows, SO_ERROR reports a Winsock error code, not an errno. Three places read it and put the value into an errno IOError. The codes then made no sense. ChannelTests.testConnectWithECONNREFUSEDGetsTheRightError saw 10061 (WSAECONNREFUSED) where it expected 107 (ECONNREFUSED).

There is a second effect. SocketChannel.error() compares that value with ECONNREFUSED and ENOMEM to decide if the error can be recovered. On Windows the comparison never matched, so every such error was treated as fatal and reset the channel.

Modifications:

  • Add IOError(socketError:reason:). It records the value in the domain it belongs to: winsock on Windows, errno elsewhere. Use it in Socket.finishConnect, BaseSocketChannel and SocketChannel.error().
  • Classify the error through IOError.errnoCode. On Windows that reports the matching errno ([Windows] Report the errno equivalent of winsock errors #3700), so the comparisons work on every platform. The #if os(Windows) branch in shouldCloseOnError is no longer needed.

The comment about ECONNREFUSED on Linux moves next to the cases it describes, so it is not lost.

Result:

Connect failures report the correct error on Windows, and a recoverable socket error is no longer treated as fatal there.

This fixes ChannelTests.testConnectWithECONNREFUSEDGetsTheRightError. That test runs once ChannelTests is enabled on Windows, which another PR does.

Tested on a Windows ARM64 machine (Swift 6.3.2): builds, NIOPosix still builds on macOS, and part of a larger branch where a full swift test passes (2360 tests, 0 failures).

`CNIOWindows_sendmmsg` was a stub that asserted and then called `abort()`, so any
code path reaching it took the whole process down. That is the datagram write
path whenever more than one write is pending, which made
`DatagramChannelTests` and `RawSocketBootstrapTests` unrunnable: both aborted
partway through, taking the rest of the test process with them.

Winsock has no `sendmmsg`, so emulate it by sending the messages one at a time,
which is what the Darwin shim does for the same reason. Implement it in Swift
alongside the existing `sendmsg`, rather than filling in the C shim, since
everything needed is callable directly from Swift; the now-unused C stub and its
declaration are removed. The `WSASendMsg` extension function is resolved once
per batch rather than once per message, so the lookup is hoisted into a helper
shared with `sendmsg`.

The error semantics match the other implementations: a failure on the first
message is reported to the caller, with `WSAEWOULDBLOCK` reported as
`.wouldBlock` the way the POSIX implementation's `syscall(blocking: true)`
wrapper does, while a failure after some messages have been sent is reported as
a short send for the caller to retry.

This does not yet make either test suite pass, so no skips are removed here.
Both now get further before failing for unrelated reasons: the datagram tests
reach a `fatalError` in `PendingDatagramWritesManager`, which reads `errnoCode`
on an `IOError` that carries a winsock-domain code, and the raw socket tests
fail with "Already closed". Crucially, neither aborts the test process any more.
Depends on apple#3699.

`IOError.errnoCode` trapped with `fatalError("IOError domain is not errno")`
whenever the error carried a winsock- or windows-domain code, which NIO's own
error classification then walked straight into. `PendingDatagramWritesManager`
compares against `EMSGSIZE` and `EHOSTUNREACH` to decide whether a datagram
write error is recoverable, and `BaseSocket` compares against `EAFNOSUPPORT`
when it turns off `IPV6_V6ONLY` -- the latter on the path of every IPv6 socket
NIO creates.

Winsock's error codes mean the same things as their errno counterparts, so
translate them rather than trapping: `WSAEMSGSIZE` reports `EMSGSIZE`, and so
on. Error handling written against errno then keeps working on Windows, which
also matters for the tests, where there are ~38 comparisons against errno
constants.

Codes with no errno counterpart, and windows-domain codes, are reported
unchanged rather than trapping. That is unambiguous: Windows CRT errno values
never exceed 140, while winsock and Win32 codes are far larger, so such a value
cannot be mistaken for an errno.

Note that the Windows CRT gives `EWOULDBLOCK` and `EAGAIN` distinct values,
unlike the platforms where they are synonyms, so `WSAEWOULDBLOCK` is paired with
`EWOULDBLOCK`.

With this and apple#3699, `DatagramChannelTests` runs to completion on Windows for
the first time (74 tests: 38 pass, 19 fail, 17 skipped) rather than aborting the
test process. The remaining failures are unrelated gaps, so the suite stays
skipped for now and no skips are removed here.
Depends on apple#3700.

`SO_ERROR` reports a Winsock error code on Windows, not an `errno`, but the
three places that read it wrapped the value in an errno-domain `IOError`. The
codes then made no sense: `ChannelTests.testConnectWithECONNREFUSEDGetsTheRightError`
saw 10061 (`WSAECONNREFUSED`) where it expected 107 (`ECONNREFUSED`).

`SocketChannel.error()` also compared the raw value against `ECONNREFUSED` and
`ENOMEM` to decide whether the error is recoverable, so on Windows a recoverable
error was always classified as fatal and reset the channel.

Add an `IOError(socketError:reason:)` initialiser that records the value in the
domain it actually belongs to, and use it in `Socket.finishConnect`,
`BaseSocketChannel` and `SocketChannel.error()`. Classification now goes through
`IOError.errnoCode`, which reports the corresponding `errno` on Windows, so the
comparisons hold on every platform and the `#if os(Windows)` branch in
`shouldCloseOnError` is no longer needed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant