Skip to content

fix(iocp)!: map remote RST to connection_reset, not cond::canceled (#…#319

Draft
mvandeberg wants to merge 3 commits into
cppalliance:develop-2from
mvandeberg:pr/304-normalize-errors
Draft

fix(iocp)!: map remote RST to connection_reset, not cond::canceled (#…#319
mvandeberg wants to merge 3 commits into
cppalliance:develop-2from
mvandeberg:pr/304-normalize-errors

Conversation

@mvandeberg

Copy link
Copy Markdown
Contributor

…304)

IOCP delivers ERROR_NETNAME_DELETED both when a local closesocket() cancels a pending overlapped op and when the peer hard-closes with a RST. detail::make_err mapped it unconditionally to capy::error::canceled, so a remote reset on Windows surfaced as cond::canceled -- conflating remote termination with deliberate local cancellation. POSIX already mapped RST to errc::connection_reset.

The cancelled atomic (set by the stop-token, cancel(), and close() paths) is consulted before the raw error at every decode site, so a surviving ERROR_NETNAME_DELETED is always a genuine remote reset. Remove it from make_err's canceled branch and remap it per operation kind at the IOCP decode sites via a new iocp_make_err helper: connection_reset for stream read/write, connection_aborted for accept (mirroring Asio). make_err stays generic; the winsock-dependent remap lives in the IOCP layer.

Add test/unit/error_conditions.cpp: a backend-parameterized audit of the remote/connection error family (FIN->eof, RST->connection_reset, write-to-dead-peer->connection_reset|broken_pipe, connect-refused-> connection_refused), asserting portable conditions on every backend so the cross-platform contract is enforced. The cancellation family is already covered across all backends elsewhere and is not duplicated.

normalize_openssl_shutdown_read_error is left unchanged: it already excludes canceled (#301), and reset-during-shutdown genuinely means no close_notify was received -> stream_truncated.

…ppalliance#304)

IOCP delivers ERROR_NETNAME_DELETED both when a local closesocket()
cancels a pending overlapped op and when the peer hard-closes with a
RST. detail::make_err mapped it unconditionally to capy::error::canceled,
so a remote reset on Windows surfaced as cond::canceled -- conflating
remote termination with deliberate local cancellation. POSIX already
mapped RST to errc::connection_reset.

The cancelled atomic (set by the stop-token, cancel(), and close() paths)
is consulted before the raw error at every decode site, so a surviving
ERROR_NETNAME_DELETED is always a genuine remote reset. Remove it from
make_err's canceled branch and remap it per operation kind at the IOCP
decode sites via a new iocp_make_err helper: connection_reset for stream
read/write, connection_aborted for accept (mirroring Asio). make_err
stays generic; the winsock-dependent remap lives in the IOCP layer.

Add test/unit/error_conditions.cpp: a backend-parameterized audit of the
remote/connection error family (FIN->eof, RST->connection_reset,
write-to-dead-peer->connection_reset|broken_pipe, connect-refused->
connection_refused), asserting portable conditions on every backend so
the cross-platform contract is enforced. The cancellation family is
already covered across all backends elsewhere and is not duplicated.

normalize_openssl_shutdown_read_error is left unchanged: it already
excludes canceled (cppalliance#301), and reset-during-shutdown genuinely means no
close_notify was received -> stream_truncated.
@cppalliance-bot

cppalliance-bot commented Jul 16, 2026

Copy link
Copy Markdown

An automated preview of the documentation is available at https://319.corosio.prtest3.cppalliance.org/index.html

If more commits are pushed to the pull request, the docs will rebuild at the same URL.

2026-07-16 23:52:01 UTC

…iance#304)

The kqueue backend stripped a user-set SO_LINGER before close, forcing
close() to send FIN instead of RST. A peer parked in read_some then saw
a graceful cond::eof instead of errc::connection_reset, diverging from
epoll/select/IOCP (the cppalliance#304 cross-backend invariant).

The strip was a macOS workaround premised on "RST doesn't reliably
trigger EV_EOF, so pending reads hang." A standalone probe on macOS
(Darwin 25.5, ARM64) disproves this: an abortive close with SO_LINGER
{1,0} reliably wakes the peer's kqueue with EV_EOF, carries ECONNRESET
in the kevent fflags, and a subsequent recv() returns ECONNRESET --
20/20 runs, including the race where the RST lands before EVFILT_READ
is registered (level-triggered EV_EOF persists). The scheduler already
maps EV_EOF to a read wakeup, so no read hangs, and with l_linger == 0
the abortive close never blocks the caller.

Make the kqueue stream_socket_hook a plain option passthrough matching
epoll/select. macOS kqueue now reports connection_reset like every other
backend; FreeBSD kqueue is expected to follow (BSD kevent fflags carry
the socket error identically) and will be confirmed by CI.
@mvandeberg
mvandeberg force-pushed the pr/304-normalize-errors branch from bfa1e65 to fee9573 Compare July 16, 2026 22:55
…ppalliance#304)

The remote-connection error family (refused connect, peer RST, write to a
dead peer, unreachable host/net, connect timeout) must compare equal to the
portable std::errc / capy::cond conditions on every backend. On IOCP it did
not, and the failure mode differed by standard library:

  - MSVC's STL maps the WSA-range codes (WSAECONNRESET 10054, WSAECONNREFUSED
    10061, ...) to the std::errc conditions but not their 12xx Win32
    counterparts.
  - libstdc++ (MinGW) does the exact opposite: it maps the Win32 12xx codes
    (ERROR_CONNECTION_REFUSED 1225, ...) but not the WSA range.

So there is NO single native Windows code that std::system_category maps to,
say, std::errc::connection_refused on both toolchains. The prior approach of
stuffing a WSA code into system_category fixed MSVC but left MinGW CI red
(error_conditions.cpp cppalliance#4 reset, cppalliance#7 write-to-dead-peer, cppalliance#9 refused connect),
and a Win32 code would fail the reverse way.

Fix: in iocp_make_err, return the condition itself via std::make_error_code
(generic_category), which compares equal to the matching std::errc on every
standard library -- mirroring what the POSIX backends yield from errno. Both
the WSA and Win32 forms of each code are accepted as input because the
delivered form varies by operation: WSASend/WSARecv completions surface the
Winsock code directly (e.g. WSAECONNRESET), while a ConnectEx failure is
completed with an NTSTATUS that GetQueuedCompletionStatus reports as the
RtlNtStatusToDosError-derived Win32 code (e.g. ERROR_CONNECTION_REFUSED). The
normalization stays in the IOCP layer; the platform-neutral make_err is
untouched. Trade-off: a generic, less Windows-specific message string.

Observed raw codes (this environment): refused connect -> 1225, network
unreachable -> 1231, connect timeout -> 121, write to reset peer -> 10054.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants