Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions doc/modules/ROOT/pages/5.testing/5b.socket-pair.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,10 @@ xref:5.testing/5c.patterns.adoc[Testing Patterns].
* Slower than `mocket` for byte-level assertions.
* Timing is not deterministic; use stop_tokens or timers for cancellation
paths.
* On Linux, `SO_LINGER(true, 0)` causes `close()` to send `RST`, which
may surface as `cond::connection_reset` on the other end depending on
the order of operations. If a test asserts on a clean shutdown
sequence, set `Linger=false`.
* `SO_LINGER(true, 0)` (the default `Linger=true`) causes `close()` to send
`RST`, which surfaces on the other end as `std::errc::connection_reset` on
every backend. Exact timing is not deterministic, so assert on the error
condition rather than a clean shutdown sequence, or set `Linger=false`.

== Next Steps

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ local_stream_accept_op::do_complete(
if (op->cancelled.load(std::memory_order_acquire))
*op->ec_out = capy::error::canceled;
else if (op->dwError != 0)
*op->ec_out = make_err(op->dwError);
*op->ec_out = iocp_make_err(op->dwError, /*accept_path=*/true);
else
*op->ec_out = {};
}
Expand Down Expand Up @@ -325,6 +325,10 @@ win_local_stream_acceptor_internal::wait(
inline void
win_local_stream_acceptor_internal::close_socket() noexcept
{
// Flag the accept op cancelled before closing so a closesocket-delivered
// ERROR_NETNAME_DELETED is short-circuited to canceled rather than mapped
// to connection_aborted by iocp_make_err (see win_tcp_socket close_socket).
acc_.request_cancel();
wt_.request_cancel();
svc_.scheduler().cancel_wait_if_constructed(&wt_);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,12 @@ win_local_stream_socket_internal::cancel() noexcept
inline void
win_local_stream_socket_internal::close_socket() noexcept
{
// Flag every op cancelled before closing so a closesocket-delivered
// ERROR_NETNAME_DELETED is short-circuited to canceled rather than mapped
// to connection_reset by iocp_make_err (see win_tcp_socket close_socket).
conn_.request_cancel();
rd_.request_cancel();
wr_.request_cancel();
wt_.request_cancel();
svc_.scheduler().cancel_wait_if_constructed(&wt_);

Expand Down
69 changes: 68 additions & 1 deletion include/boost/corosio/native/detail/iocp/win_overlapped_op.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,72 @@

namespace boost::corosio::detail {

/** Convert an IOCP completion's raw error to std::error_code, disambiguating
ERROR_NETNAME_DELETED by operation kind and surfacing the remote-connection
error family as portable std::errc conditions.

IOCP delivers ERROR_NETNAME_DELETED both when a local closesocket() cancels
a pending op (already handled by the cancelled flag before this is reached)
and when the peer hard-closes with a RST. A NETNAME_DELETED that survives
the cancelled check is therefore a genuine remote reset, and the correct
surface depends on the operation: connection_reset for stream read/write,
connection_aborted for accept (mirroring Asio's per-op mapping).

Why generic_category / std::errc rather than a native code: which raw
Windows code std::system_category maps to a given std::errc condition
depends on the standard library. MSVC's STL maps the WSA-range socket
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 but not the WSA range.
So no single system_category code compares equal to e.g.
std::errc::connection_refused on both toolchains. Returning the condition
itself via std::make_error_code (generic_category) sidesteps the
divergence: it compares equal to the matching std::errc on every standard
library, mirroring what the POSIX backends yield from errno. The trade-off
is a generic (less Windows-specific) message string.

Both the WSA and Win32 forms are accepted because the delivered form
varies by operation: a WSASend/WSARecv completion surfaces the Winsock
code directly (e.g. WSAECONNRESET 10054), whereas a ConnectEx failure is
completed with an NTSTATUS and GetQueuedCompletionStatus reports the Win32
code RtlNtStatusToDosError derives from it (e.g. ERROR_CONNECTION_REFUSED
1225). This normalization lives here, in the IOCP layer, rather than in
the platform-neutral make_err. All other codes defer to make_err.

@param dwError The Windows error code (DWORD).
@param accept_path True on the accept completion path.
@return The corresponding std::error_code.
*/
inline std::error_code
iocp_make_err(DWORD dwError, bool accept_path) noexcept
{
// A pending op hit by a remote RST completes with ERROR_NETNAME_DELETED;
// its portable meaning depends on the operation (reset vs aborted).
if (dwError == ERROR_NETNAME_DELETED)
return std::make_error_code(accept_path
? std::errc::connection_aborted
: std::errc::connection_reset);

switch (dwError)
{
case WSAECONNRESET: // 10054
return std::make_error_code(std::errc::connection_reset);
case WSAECONNREFUSED: case ERROR_CONNECTION_REFUSED: // 10061 / 1225
return std::make_error_code(std::errc::connection_refused);
case WSAECONNABORTED: case ERROR_CONNECTION_ABORTED: // 10053 / 1236
return std::make_error_code(std::errc::connection_aborted);
case WSAENETUNREACH: case ERROR_NETWORK_UNREACHABLE: // 10051 / 1231
return std::make_error_code(std::errc::network_unreachable);
case WSAEHOSTUNREACH: case ERROR_HOST_UNREACHABLE: // 10065 / 1232
return std::make_error_code(std::errc::host_unreachable);
case WSAETIMEDOUT: case ERROR_SEM_TIMEOUT: // 10060 / 121
return std::make_error_code(std::errc::timed_out);
default:
break;
}
return make_err(dwError);
}

/** Base class for IOCP overlapped operations.

Derives from both OVERLAPPED (for Windows IOCP) and scheduler_op
Expand Down Expand Up @@ -117,7 +183,8 @@ struct overlapped_op
decode_io_result(
ec_out,
cancelled.load(std::memory_order_acquire),
dwError != 0 ? make_err(dwError) : std::error_code{},
dwError != 0 ? iocp_make_err(dwError, /*accept_path=*/false)
: std::error_code{},
is_read, static_cast<std::size_t>(bytes_transferred),
empty_buffer);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
// Copyright (c) 2026 Steve Gerbino
// Copyright (c) 2026 Michael Vandeberg
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
Expand Down Expand Up @@ -235,7 +236,7 @@ accept_op::do_complete(
if (op->cancelled.load(std::memory_order_acquire))
*op->ec_out = capy::error::canceled;
else if (op->dwError != 0)
*op->ec_out = make_err(op->dwError);
*op->ec_out = iocp_make_err(op->dwError, /*accept_path=*/true);
else
*op->ec_out = {};
}
Expand Down Expand Up @@ -774,11 +775,19 @@ win_tcp_socket_internal::cancel() noexcept
inline void
win_tcp_socket_internal::close_socket() noexcept
{
// Tear down any aux-reactor-parked wait op before closing the
// SOCKET handle. Otherwise the reactor would keep polling a
// dangling fd (and on a Winsock SOCKET-id reuse the wrong fd
// could be polled briefly). The cancel happens-before the
// closesocket below.
// Flag every op cancelled before tearing down the handle. closesocket()
// can complete a pending overlapped op with ERROR_NETNAME_DELETED (not the
// MSDN-documented ERROR_OPERATION_ABORTED), which iocp_make_err now maps to
// connection_reset -- so a locally-closed op must be short-circuited to
// canceled via the flag rather than relying on the raw error, exactly as
// cancel() does. The store happens-before the completion is decoded.
conn_.request_cancel();
rd_.request_cancel();
wr_.request_cancel();
// wt_ is parked in the aux reactor (no overlapped outstanding), so it also
// needs an explicit reactor deregister before the SOCKET handle is closed;
// otherwise the reactor would keep polling a dangling fd (and on a Winsock
// SOCKET-id reuse the wrong fd could be polled briefly).
wt_.request_cancel();
svc_.scheduler().cancel_wait_if_constructed(&wt_);

Expand Down Expand Up @@ -1433,6 +1442,10 @@ win_tcp_acceptor_internal::cancel() noexcept
inline void
win_tcp_acceptor_internal::close_socket() noexcept
{
// Flag the accept op cancelled before closing so a closesocket-delivered
// ERROR_NETNAME_DELETED is short-circuited to canceled rather than mapped
// to connection_aborted by iocp_make_err (see win_tcp_socket close_socket).
acc_.request_cancel();
// Tear down any aux-reactor-parked wait op first.
wt_.request_cancel();
svc_.scheduler().cancel_wait_if_constructed(&wt_);
Expand Down
8 changes: 8 additions & 0 deletions include/boost/corosio/native/detail/iocp/win_udp_service.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,14 @@ win_udp_socket_internal::cancel() noexcept
inline void
win_udp_socket_internal::close_socket() noexcept
{
// Flag every op cancelled before closing so a closesocket-delivered
// ERROR_NETNAME_DELETED is short-circuited to canceled rather than mapped
// to connection_reset by iocp_make_err (see win_tcp_socket close_socket).
wr_.request_cancel();
rd_.request_cancel();
conn_.request_cancel();
send_wr_.request_cancel();
recv_rd_.request_cancel();
wt_.request_cancel();
svc_.scheduler().cancel_wait_if_constructed(&wt_);

Expand Down
49 changes: 10 additions & 39 deletions include/boost/corosio/native/detail/kqueue/kqueue_traits.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,16 @@ struct kqueue_traits

static constexpr bool needs_write_notification = false;

/* macOS kqueue workaround: RST doesn't reliably trigger EV_EOF.
If the user sets SO_LINGER, we clear it before close so the
destructor doesn't block and close() sends FIN instead of RST.

The hook tracks whether the user explicitly set SO_LINGER via
set_option(). On pre_shutdown/pre_destroy, if the flag is set,
we reset linger to off before the fd is closed.
*/
// No per-socket state or lifecycle hooks: the stream socket hook is a
// plain setsockopt passthrough, like epoll/select. SO_LINGER in particular
// is passed through unmodified. An abortive close (SO_LINGER{on,0}) thus
// sends a RST, which the peer's kqueue reports as EV_EOF carrying
// ECONNRESET in fflags -- so the peer surfaces connection_reset rather than
// a graceful eof, consistent with the other backends (#304). A positive
// linger timeout is likewise honored, so close() with unsent data blocks
// up to that timeout, as POSIX specifies.
struct stream_socket_hook
{
bool user_set_linger_ = false;

std::error_code on_set_option(
int fd, int level, int optname,
void const* data, std::size_t size) noexcept
Expand All @@ -65,37 +63,10 @@ struct kqueue_traits
fd, level, optname, data,
static_cast<socklen_t>(size)) != 0)
return make_err(errno);

if (level == SOL_SOCKET && optname == SO_LINGER &&
size >= sizeof(struct ::linger))
user_set_linger_ =
static_cast<struct ::linger const*>(data)->l_onoff != 0;

return {};
}

void pre_shutdown(int fd) noexcept
{
reset_linger(fd);
}

void pre_destroy(int fd) noexcept
{
reset_linger(fd);
}

private:
void reset_linger(int fd) noexcept
{
if (user_set_linger_ && fd >= 0)
{
struct ::linger lg;
lg.l_onoff = 0;
lg.l_linger = 0;
::setsockopt(fd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg));
}
user_set_linger_ = false;
}
static void pre_shutdown(int) noexcept {}
static void pre_destroy(int) noexcept {}
};

struct write_policy
Expand Down
17 changes: 9 additions & 8 deletions include/boost/corosio/native/detail/make_err.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
// Copyright (c) 2026 Steve Gerbino
// Copyright (c) 2026 Michael Vandeberg
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
Expand Down Expand Up @@ -52,13 +53,14 @@ make_err(int errn) noexcept

/** Convert a Windows error code to std::error_code.

Maps ERROR_OPERATION_ABORTED, ERROR_CANCELLED, and
ERROR_NETNAME_DELETED to capy::error::canceled.
Maps ERROR_HANDLE_EOF to capy::error::eof.
Maps ERROR_OPERATION_ABORTED and ERROR_CANCELLED to
capy::error::canceled, and ERROR_HANDLE_EOF to capy::error::eof.
Every other code passes through std::system_category().

ERROR_NETNAME_DELETED (64) is what IOCP actually delivers
when closesocket() cancels pending overlapped I/O, despite
MSDN documenting ERROR_OPERATION_ABORTED for that case.
ERROR_NETNAME_DELETED (64) is deliberately not mapped here: IOCP
delivers it both for a local closesocket() that cancels pending I/O
and for a remote RST, so it can only be disambiguated per operation
kind at the IOCP decode sites (see iocp_make_err in win_overlapped_op).

@param dwError The Windows error code (DWORD).
@return The corresponding std::error_code.
Expand All @@ -69,8 +71,7 @@ make_err(unsigned long dwError) noexcept
if (dwError == 0)
return {};

if (dwError == ERROR_OPERATION_ABORTED || dwError == ERROR_CANCELLED ||
dwError == ERROR_NETNAME_DELETED)
if (dwError == ERROR_OPERATION_ABORTED || dwError == ERROR_CANCELLED)
return capy::error::canceled;

if (dwError == ERROR_HANDLE_EOF)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ class reactor_socket_service : public ServiceBase
}

protected:
// Override in derived to add pre-close logic (e.g. kqueue linger reset)
// Override in derived to add pre-close logic. No backend currently needs
// it; the hooks exist so a trait can run fd-level teardown before close.
void pre_shutdown(Impl*) noexcept {}
void pre_destroy(Impl*) noexcept {}

Expand Down
Loading
Loading