From 40dddcb0d7e4ead757af8b463dd0e378f1de5dc8 Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Thu, 6 Aug 2026 18:32:17 +0200 Subject: [PATCH 01/10] refactor(timer): drop unused timer constructors and default the moves detail::timer's consumers construct it only from an execution context and set expiry separately, so the time-point, duration, and executor convenience constructors had no callers. The move operations only exist so a disengaged std::optional can be moved; io_object already implements the transfer, so defaulting them replaces the out-of-line definitions. --- include/boost/corosio/detail/timer.hpp | 94 ++------------------------ src/corosio/src/timer.cpp | 15 ---- 2 files changed, 5 insertions(+), 104 deletions(-) diff --git a/include/boost/corosio/detail/timer.hpp b/include/boost/corosio/detail/timer.hpp index f038ba5e7..7b5d7d33a 100644 --- a/include/boost/corosio/detail/timer.hpp +++ b/include/boost/corosio/detail/timer.hpp @@ -21,18 +21,15 @@ #include #include #include -#include #include #include -#include #include #include #include #include #include #include -#include namespace boost::corosio::detail { @@ -204,105 +201,24 @@ class BOOST_COROSIO_DECL timer : public io_object */ explicit timer(capy::execution_context& ctx); - /** Construct a timer with an initial absolute expiry time. - - @param ctx The execution context that will own this timer. It - must be a corosio io_context; otherwise the constructor - throws (a timer service is required). - @param t The initial expiry time point. - - @throws std::logic_error if @p ctx is not an io_context. - */ - timer(capy::execution_context& ctx, time_point t); - - /** Construct a timer with an initial relative expiry time. - - @param ctx The execution context that will own this timer. It - must be a corosio io_context; otherwise the constructor - throws (a timer service is required). - @param d The initial expiry duration relative to now. - - @throws std::logic_error if @p ctx is not an io_context. - */ - template - timer(capy::execution_context& ctx, std::chrono::duration d) - : timer(ctx) - { - expires_after(d); - } - - /** Construct a timer from an executor. - - The timer is associated with the executor's context, which must - be a corosio io_context. - - @param ex The executor whose context will own this timer. - - @throws std::logic_error if the executor's context is not an - io_context. - */ - template - requires(!std::same_as, timer>) && - capy::Executor - explicit timer(Ex const& ex) : timer(ex.context()) - { - } - - /** Construct a timer from an executor with an absolute expiry time. - - @param ex The executor whose context will own this timer. - @param t The initial expiry time point. - - @throws std::logic_error if the executor's context is not an - io_context. - */ - template - requires capy::Executor - timer(Ex const& ex, time_point t) : timer(ex.context(), t) - { - } - - /** Construct a timer from an executor with a relative expiry time. - - @param ex The executor whose context will own this timer. - @param d The initial expiry duration relative to now. - - @throws std::logic_error if the executor's context is not an - io_context. - */ - template - requires capy::Executor - timer(Ex const& ex, std::chrono::duration d) - : timer(ex.context(), d) - { - } - /** Move constructor. - Transfers ownership of the timer resources. - - @param other The timer to move from. + Transfers ownership of the timer resources. Required so a + disengaged `std::optional` is movable; a timer is never + moved while a wait is published. @pre No awaitables returned by @p other's methods exist. - @pre The execution context associated with @p other must - outlive this timer. */ - timer(timer&& other) noexcept; + timer(timer&&) noexcept = default; /** Move assignment operator. Closes any existing timer and transfers ownership. - @param other The timer to move from. - @pre No awaitables returned by either `*this` or @p other's methods exist. - @pre The execution context associated with @p other must - outlive this timer. - - @return Reference to this timer. */ - timer& operator=(timer&& other) noexcept; + timer& operator=(timer&&) noexcept = default; timer(timer const&) = delete; timer& operator=(timer const&) = delete; diff --git a/src/corosio/src/timer.cpp b/src/corosio/src/timer.cpp index 669284d8e..aecab9d61 100644 --- a/src/corosio/src/timer.cpp +++ b/src/corosio/src/timer.cpp @@ -20,21 +20,6 @@ timer::timer(capy::execution_context& ctx) { } -timer::timer(capy::execution_context& ctx, time_point t) : timer(ctx) -{ - expires_at(t); -} - -timer::timer(timer&& other) noexcept : io_object(std::move(other)) {} - -timer& -timer::operator=(timer&& other) noexcept -{ - if (this != &other) - h_ = std::move(other.h_); - return *this; -} - // Not inline: wait_awaitable::await_suspend (defined in timer.hpp) calls // this from translation units that may never include timer_service.hpp, // so this must be the one strong definition the linker can always find From 136f52a71c2a3c951b28130d8e3e38ff9aeb4e16 Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Thu, 6 Aug 2026 19:50:38 +0200 Subject: [PATCH 02/10] refactor(iocp): remove the disabled NT wait-packet timer implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit make_win_timers has always returned the thread-based timers; the NtAssociateWaitCompletionPacket path was compiled but unreachable behind #if 0. The reason it lost — one-shot packets force a full re-association on every scheduler wakeup — now lives at the selection site. --- .../corosio/native/detail/iocp/win_timers.hpp | 15 +- .../native/detail/iocp/win_timers_nt.hpp | 253 ------------------ 2 files changed, 6 insertions(+), 262 deletions(-) delete mode 100644 include/boost/corosio/native/detail/iocp/win_timers_nt.hpp diff --git a/include/boost/corosio/native/detail/iocp/win_timers.hpp b/include/boost/corosio/native/detail/iocp/win_timers.hpp index e157ccfd0..41d1ea9e8 100644 --- a/include/boost/corosio/native/detail/iocp/win_timers.hpp +++ b/include/boost/corosio/native/detail/iocp/win_timers.hpp @@ -53,7 +53,6 @@ make_win_timers(void* iocp_handle, long* dispatch_required); } // namespace boost::corosio::detail // Include concrete implementations needed by make_win_timers -#include #include namespace boost::corosio::detail { @@ -61,15 +60,13 @@ namespace boost::corosio::detail { inline std::unique_ptr make_win_timers(void* iocp_handle, long* dispatch_required) { - // Thread-based is faster; NT API requires one-shot re-association per - // wakeup which tanks performance. See timers_nt.hpp for details. + // Thread-based over NtAssociateWaitCompletionPacket: the NT wait + // packet is one-shot, so it must be re-associated (SetWaitableTimer + // + NtAssociateWaitCompletionPacket) after every scheduler wakeup + // even in timer-free workloads, costing ~60% CPU overhead. Skipping + // the re-association is not a fix: a spent packet never fires again + // and pending timers hang the scheduler. return std::make_unique(iocp_handle, dispatch_required); - -#if 0 - // NT native API (Windows 8+) - if (auto p = win_timers_nt::try_create(iocp_handle, dispatch_required)) - return p; -#endif } } // namespace boost::corosio::detail diff --git a/include/boost/corosio/native/detail/iocp/win_timers_nt.hpp b/include/boost/corosio/native/detail/iocp/win_timers_nt.hpp deleted file mode 100644 index b760afd64..000000000 --- a/include/boost/corosio/native/detail/iocp/win_timers_nt.hpp +++ /dev/null @@ -1,253 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) -// Copyright (c) 2026 Steve Gerbino -// -// 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) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_TIMERS_NT_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_TIMERS_NT_HPP - -#include - -#if BOOST_COROSIO_HAS_IOCP - -#include -#include -#include -#include - -namespace boost::corosio::detail { - -// NT API type definitions -using NTSTATUS = LONG; - -using NtAssociateWaitCompletionPacketFn = NTSTATUS(NTAPI*)( - void* WaitCompletionPacketHandle, - void* IoCompletionHandle, - void* TargetObjectHandle, - void* KeyContext, - void* ApcContext, - NTSTATUS IoStatus, - ULONG_PTR IoStatusInformation, - BOOLEAN* AlreadySignaled); - -using NtCancelWaitCompletionPacketFn = NTSTATUS(NTAPI*)( - void* WaitCompletionPacketHandle, BOOLEAN RemoveSignaledPacket); - -class win_timers_nt final : public win_timers -{ - void* iocp_; - void* waitable_timer_ = nullptr; - void* wait_packet_ = nullptr; - NtAssociateWaitCompletionPacketFn nt_associate_; - NtCancelWaitCompletionPacketFn nt_cancel_; - - win_timers_nt( - void* iocp_handle, - long* dispatch_required, - NtAssociateWaitCompletionPacketFn nt_assoc, - NtCancelWaitCompletionPacketFn nt_cancel); - -public: - // Returns nullptr if NT APIs unavailable (pre-Windows 8) - static std::unique_ptr - try_create(void* iocp_handle, long* dispatch_required); - - ~win_timers_nt(); - - win_timers_nt(win_timers_nt const&) = delete; - win_timers_nt& operator=(win_timers_nt const&) = delete; - - void start() override; - void stop() override; - void update_timeout(time_point next_expiry) override; - -private: - void associate_timer(); -}; - -/* - NT Wait Completion Packet Timer Implementation - ============================================== - - This uses undocumented NT APIs to integrate waitable timers directly with - IOCP, avoiding the need for a dedicated timer thread. - - CRITICAL: THE ASSOCIATION IS ONE-SHOT - ------------------------------------- - - When NtAssociateWaitCompletionPacket associates a timer with IOCP, the - association is consumed when the timer fires. After the completion packet - is posted to IOCP, the wait packet is "spent" and must be re-associated - before it can fire again. - - This means update_timeout() MUST be called after every timer wakeup to - re-associate the wait packet, even if the timer expiry hasn't changed. - The scheduler calls update_timeout() unconditionally in do_one() after - processing expired timers for this reason. - - WHY THIS IMPLEMENTATION IS SLOW - -------------------------------- - - The re-association must happen on every scheduler iteration, even for - timer-free workloads. This causes ~60% CPU overhead in benchmarks because - SetWaitableTimer + NtAssociateWaitCompletionPacket are called repeatedly. - - DO NOT OPTIMIZE BY SKIPPING RE-ASSOCIATION - ------------------------------------------ - - It may seem obvious to skip re-association when no timers exist or when the - expiry hasn't changed. However, skipping breaks the timer mechanism: - - 1. Timer fires -> posts key_wake_dispatch to IOCP - 2. do_one() processes the completion, calls process_expired() - 3. If update_timeout() is skipped, the wait packet is not re-associated - 4. Future timers will never fire -> scheduler hangs - - The correct optimization (if needed) would be at the waitable timer level - (caching due_time to avoid redundant SetWaitableTimer calls), but the - NtAssociateWaitCompletionPacket call cannot be skipped after any wakeup. -*/ - -inline constexpr NTSTATUS STATUS_SUCCESS = 0; - -using NtCreateWaitCompletionPacketFn = NTSTATUS(NTAPI*)( - void** WaitCompletionPacketHandle, - ULONG DesiredAccess, - void* ObjectAttributes); - -inline win_timers_nt::win_timers_nt( - void* iocp_handle, - long* dispatch_required, - NtAssociateWaitCompletionPacketFn nt_assoc, - NtCancelWaitCompletionPacketFn nt_cancel) - : win_timers(dispatch_required) - , iocp_(iocp_handle) - , nt_associate_(nt_assoc) - , nt_cancel_(nt_cancel) -{ - waitable_timer_ = ::CreateWaitableTimerW(nullptr, FALSE, nullptr); -} - -inline std::unique_ptr -win_timers_nt::try_create(void* iocp_handle, long* dispatch_required) -{ - HMODULE ntdll = ::GetModuleHandleW(L"ntdll.dll"); - if (!ntdll) - return nullptr; - - // GetProcAddress returns FARPROC; cast through void* to the specific NT - // entry-point signature (same idiom as win_file_service and - // win_random_access_file_service). The void* hop avoids GCC/Clang's - // -Wcast-function-type without a compiler-specific pragma. - auto nt_create = reinterpret_cast( - reinterpret_cast( - ::GetProcAddress(ntdll, "NtCreateWaitCompletionPacket"))); - auto nt_assoc = reinterpret_cast( - reinterpret_cast( - ::GetProcAddress(ntdll, "NtAssociateWaitCompletionPacket"))); - auto nt_cancel = reinterpret_cast( - reinterpret_cast( - ::GetProcAddress(ntdll, "NtCancelWaitCompletionPacket"))); - - if (!nt_create || !nt_assoc || !nt_cancel) - return nullptr; - - auto p = std::unique_ptr( - new win_timers_nt(iocp_handle, dispatch_required, nt_assoc, nt_cancel)); - - if (!p->waitable_timer_) - return nullptr; - - // Create the wait completion packet - NTSTATUS status = nt_create(&p->wait_packet_, MAXIMUM_ALLOWED, nullptr); - if (status != STATUS_SUCCESS || !p->wait_packet_) - return nullptr; - - return p; -} - -inline win_timers_nt::~win_timers_nt() -{ - if (wait_packet_) - ::CloseHandle(wait_packet_); - if (waitable_timer_) - ::CloseHandle(waitable_timer_); -} - -inline void -win_timers_nt::start() -{ - associate_timer(); -} - -inline void -win_timers_nt::stop() -{ - nt_cancel_(wait_packet_, TRUE); -} - -inline void -win_timers_nt::update_timeout(time_point next_expiry) -{ - BOOST_COROSIO_ASSERT(waitable_timer_); - - // Cancel pending association - nt_cancel_(wait_packet_, FALSE); - - auto now = std::chrono::steady_clock::now(); - LARGE_INTEGER due_time; - - if (next_expiry <= now) - { - // Already expired - fire immediately - due_time.QuadPart = 0; - } - else if (next_expiry == (time_point::max)()) - { - // No timers - set far future - due_time.QuadPart = -LONGLONG(49) * 24 * 60 * 60 * 10000000LL; - } - else - { - // Convert duration to 100ns units (negative = relative) - auto duration = next_expiry - now; - auto ns = std::chrono::duration_cast(duration) - .count(); - due_time.QuadPart = -(ns / 100); - if (due_time.QuadPart == 0) - due_time.QuadPart = -1; - } - - ::SetWaitableTimer(waitable_timer_, &due_time, 0, nullptr, nullptr, FALSE); - associate_timer(); -} - -inline void -win_timers_nt::associate_timer() -{ - // Set dispatch flag before associating - ::InterlockedExchange(dispatch_required_, 1); - - BOOLEAN already_signaled = FALSE; - NTSTATUS status = nt_associate_( - wait_packet_, iocp_, waitable_timer_, - reinterpret_cast(key_wake_dispatch), nullptr, STATUS_SUCCESS, 0, - &already_signaled); - - if (status == STATUS_SUCCESS && already_signaled) - { - ::PostQueuedCompletionStatus( - static_cast(iocp_), 0, key_wake_dispatch, nullptr); - } -} - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_IOCP - -#endif // BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_TIMERS_NT_HPP From bbca5c219554c47da458cfee603812581e2b872a Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Thu, 6 Aug 2026 20:58:15 +0200 Subject: [PATCH 03/10] fix(tls): fail closed on an unparseable entity certificate or key Both engines latched a setup error for a bad trust anchor but silently dropped an entity certificate or private key that failed to parse or decrypt, deferring the misconfiguration to an opaque handshake failure on the peer. Latch the same flag on those paths: the OpenSSL engine refuses the handshake through check_context(), wolfSSL refuses directly from init. Cover the credential-parsing paths from both sides: a full engine handshake from DER-converted fixtures, plus setup refusal for a garbage DER certificate and for a key whose oversize callback password is truncated to OpenSSL's buffer. --- src/openssl/src/detail/engine.cpp | 23 +++++++ src/wolfssl/src/detail/engine.cpp | 68 +++++++++++-------- test/unit/openssl_engine.cpp | 104 ++++++++++++++++++++++++++++++ test/unit/wolfssl_engine.cpp | 18 ++++++ 4 files changed, 187 insertions(+), 26 deletions(-) diff --git a/src/openssl/src/detail/engine.cpp b/src/openssl/src/detail/engine.cpp index 02894ee29..babdb51ba 100644 --- a/src/openssl/src/detail/engine.cpp +++ b/src/openssl/src/detail/engine.cpp @@ -442,6 +442,9 @@ class openssl_native_context : public native_context_base if (cd.pkcs12_data.empty() && !cd.entity_certificate.empty()) { + // An entity certificate that fails to parse must not pass + // silently: the handshake would run without the identity the + // caller configured and fail remotely instead of at setup. BIO* bio = BIO_new_mem_buf( cd.entity_certificate.data(), static_cast(cd.entity_certificate.size())); @@ -457,8 +460,16 @@ class openssl_native_context : public native_context_base SSL_CTX_use_certificate(ctx_, cert); X509_free(cert); } + else + { + setup_failed_ = true; + } BIO_free(bio); } + else + { + setup_failed_ = true; + } } if (cd.pkcs12_data.empty() && !cd.certificate_chain.empty()) @@ -506,12 +517,24 @@ class openssl_native_context : public native_context_base } else pkey = d2i_PrivateKey_bio(bio, nullptr); + // A key that fails to parse or decrypt (wrong or missing + // password) must fail setup, not surface later as an + // inexplicable handshake error. if (pkey) { SSL_CTX_use_PrivateKey(ctx_, pkey); EVP_PKEY_free(pkey); } + else + { + setup_failed_ = true; + } BIO_free(bio); + ERR_clear_error(); + } + else + { + setup_failed_ = true; } } diff --git a/src/wolfssl/src/detail/engine.cpp b/src/wolfssl/src/detail/engine.cpp index 89395ffde..c85a70b8d 100644 --- a/src/wolfssl/src/detail/engine.cpp +++ b/src/wolfssl/src/detail/engine.cpp @@ -382,13 +382,18 @@ class wolfssl_native_context : public native_context_base // These discrete PEM/DER fields are an alternative credential source // to a PKCS#12 bundle; when a bundle was supplied it already loaded // the credential above, so skip them. + // An entity credential that fails to load must not pass silently: + // the handshake would run without the identity the caller + // configured and fail remotely instead of at setup. if (cd.pkcs12_data.empty() && !cd.certificate_chain.empty()) { - wolfSSL_CTX_use_certificate_chain_buffer( - ctx, - reinterpret_cast( - cd.certificate_chain.data()), - static_cast(cd.certificate_chain.size())); + if (wolfSSL_CTX_use_certificate_chain_buffer( + ctx, + reinterpret_cast( + cd.certificate_chain.data()), + static_cast(cd.certificate_chain.size())) != + WOLFSSL_SUCCESS) + setup_error_ = setup_error_ ? setup_error_ : 1; } else if (cd.pkcs12_data.empty() && !cd.entity_certificate.empty()) { @@ -396,11 +401,13 @@ class wolfssl_native_context : public native_context_base int format = (cd.entity_cert_format == tls_file_format::pem) ? WOLFSSL_FILETYPE_PEM : WOLFSSL_FILETYPE_ASN1; - wolfSSL_CTX_use_certificate_buffer( - ctx, - reinterpret_cast( - cd.entity_certificate.data()), - static_cast(cd.entity_certificate.size()), format); + if (wolfSSL_CTX_use_certificate_buffer( + ctx, + reinterpret_cast( + cd.entity_certificate.data()), + static_cast(cd.entity_certificate.size()), + format) != WOLFSSL_SUCCESS) + setup_error_ = setup_error_ ? setup_error_ : 1; } // Apply private key if provided (skipped when a PKCS#12 bundle @@ -422,10 +429,11 @@ class wolfssl_native_context : public native_context_base static_cast(cd.private_key.size()), der_buf.data(), static_cast(der_buf.size()), password.c_str()); - if (der_len > 0) + if (der_len <= 0 || wolfSSL_CTX_use_PrivateKey_buffer( ctx, der_buf.data(), der_len, - WOLFSSL_FILETYPE_ASN1); + WOLFSSL_FILETYPE_ASN1) != WOLFSSL_SUCCESS) + setup_error_ = setup_error_ ? setup_error_ : 1; } else { @@ -437,17 +445,23 @@ class wolfssl_native_context : public native_context_base password.c_str(), static_cast(password.size())); if (dec_len > 0) - wolfSSL_CTX_use_PrivateKey_buffer( - ctx, der_buf.data(), dec_len, - WOLFSSL_FILETYPE_ASN1); + { + if (wolfSSL_CTX_use_PrivateKey_buffer( + ctx, der_buf.data(), dec_len, + WOLFSSL_FILETYPE_ASN1) != WOLFSSL_SUCCESS) + setup_error_ = setup_error_ ? setup_error_ : 1; + } else + { // Not encrypted or decryption failed - try loading directly - wolfSSL_CTX_use_PrivateKey_buffer( - ctx, - reinterpret_cast( - cd.private_key.data()), - static_cast(cd.private_key.size()), - WOLFSSL_FILETYPE_ASN1); + if (wolfSSL_CTX_use_PrivateKey_buffer( + ctx, + reinterpret_cast( + cd.private_key.data()), + static_cast(cd.private_key.size()), + WOLFSSL_FILETYPE_ASN1) != WOLFSSL_SUCCESS) + setup_error_ = setup_error_ ? setup_error_ : 1; + } } } else @@ -455,11 +469,13 @@ class wolfssl_native_context : public native_context_base int format = (cd.private_key_format == tls_file_format::pem) ? WOLFSSL_FILETYPE_PEM : WOLFSSL_FILETYPE_ASN1; - wolfSSL_CTX_use_PrivateKey_buffer( - ctx, - reinterpret_cast( - cd.private_key.data()), - static_cast(cd.private_key.size()), format); + if (wolfSSL_CTX_use_PrivateKey_buffer( + ctx, + reinterpret_cast( + cd.private_key.data()), + static_cast(cd.private_key.size()), format) != + WOLFSSL_SUCCESS) + setup_error_ = setup_error_ ? setup_error_ : 1; } } diff --git a/test/unit/openssl_engine.cpp b/test/unit/openssl_engine.cpp index a27671cd6..1f24f19b0 100644 --- a/test/unit/openssl_engine.cpp +++ b/test/unit/openssl_engine.cpp @@ -23,6 +23,7 @@ // The engine header is vendor-free; this single-vendor TU pulls in // the real headers itself for the native-handle tests (key update, // renegotiation). +#include #include #include "engine_shuttle.hpp" @@ -541,6 +542,109 @@ struct openssl_engine_test testTruncatedRecordWaitsForInput(); testCorruptRecordMapsError(); testPartialPutInputNoLoss(); + testDerCertificateAndKey(); + testPasswordTruncation(); + testGarbageDerCertificateFailsSetup(); + } + + // Convert a PEM cert/key fixture to DER with OpenSSL itself so the + // engine's DER decode branch handles real input. + static std::string + pem_cert_to_der(char const* pem) + { + BIO* bio = BIO_new_mem_buf(pem, -1); + X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); + BIO_free(bio); + if (!cert) + return {}; + unsigned char* der = nullptr; + int len = i2d_X509(cert, &der); + std::string out; + if (len > 0) + out.assign(reinterpret_cast(der), std::size_t(len)); + OPENSSL_free(der); + X509_free(cert); + return out; + } + + static std::string + pem_key_to_der(char const* pem) + { + BIO* bio = BIO_new_mem_buf(pem, -1); + EVP_PKEY* key = + PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + BIO_free(bio); + if (!key) + return {}; + unsigned char* der = nullptr; + int len = i2d_PrivateKey(key, &der); + std::string out; + if (len > 0) + out.assign(reinterpret_cast(der), std::size_t(len)); + OPENSSL_free(der); + EVP_PKEY_free(key); + return out; + } + + void + testDerCertificateAndKey() + { + auto cert_der = pem_cert_to_der(test::server_cert_pem); + auto key_der = pem_key_to_der(test::server_key_pem); + BOOST_TEST(!cert_der.empty()); + BOOST_TEST(!key_der.empty()); + + tls_context server_ctx; + BOOST_TEST(!server_ctx.use_certificate( + cert_der, tls_file_format::der)); + BOOST_TEST(!server_ctx.use_private_key( + key_der, tls_file_format::der)); + BOOST_TEST(!server_ctx.set_verify_mode(tls_verify_mode::none)); + + auto client_ctx = test::make_client_context(); + ossl_engine client; + ossl_engine server; + BOOST_TEST(init_pair(client, server, client_ctx, server_ctx)); + BOOST_TEST(test::run_engine_handshake(client, server)); + } + + // A password longer than OpenSSL's callback buffer is truncated, + // which then fails the key decrypt; the context build must latch + // the failure so the driver refuses the handshake. + void + testPasswordTruncation() + { + tls_context ctx; + // NOLINTNEXTLINE(bugprone-unused-return-value) + ctx.use_certificate(test::server_cert_pem, tls_file_format::pem); + // NOLINTNEXTLINE(bugprone-unused-return-value) + ctx.set_password_callback( + [](std::size_t, tls_password_purpose) { + return std::string(4096, 'x'); + }); + // NOLINTNEXTLINE(bugprone-unused-return-value) + ctx.use_private_key( + test::encrypted_server_key_pem, tls_file_format::pem); + + ossl_engine eng; + BOOST_TEST(!eng.init(ctx)); + BOOST_TEST(!!eng.check_context()); + } + + // A certificate that does not parse in the declared format must + // fail context setup instead of handshaking without an identity. + void + testGarbageDerCertificateFailsSetup() + { + tls_context ctx; + // NOLINTNEXTLINE(bugprone-unused-return-value) + ctx.use_certificate("\x30\x82\x00\x00", tls_file_format::der); + // NOLINTNEXTLINE(bugprone-unused-return-value) + ctx.use_private_key(test::server_key_pem, tls_file_format::pem); + + ossl_engine eng; + BOOST_TEST(!eng.init(ctx)); + BOOST_TEST(!!eng.check_context()); } }; diff --git a/test/unit/wolfssl_engine.cpp b/test/unit/wolfssl_engine.cpp index 083278748..e0b8c9d84 100644 --- a/test/unit/wolfssl_engine.cpp +++ b/test/unit/wolfssl_engine.cpp @@ -586,6 +586,24 @@ struct wolfssl_engine_test testShutdownWantWrite(); testShutdownZeroReturnNoOutput(); testHandshakeFatalGarbageInput(); + testGarbageDerCertificateFailsSetup(); + } + + // A certificate that does not parse in the declared format must + // fail context setup instead of handshaking without an identity. + void + testGarbageDerCertificateFailsSetup() + { + tls_context ctx; + // NOLINTNEXTLINE(bugprone-unused-return-value) + ctx.use_certificate("\x30\x82\x00\x00", tls_file_format::der); + // NOLINTNEXTLINE(bugprone-unused-return-value) + ctx.use_private_key(test::server_key_pem, tls_file_format::pem); + + wssl_engine eng; + // Unlike the OpenSSL engine, wolfSSL surfaces setup_error_ + // directly from init (its check_context() is a no-op). + BOOST_TEST(!!eng.init(ctx, tls_role::server, std::string())); } }; From 8a3218ab711cf8c703c98aa5514dd938533207fe Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Fri, 7 Aug 2026 23:06:01 +0200 Subject: [PATCH 04/10] fix(iocp): wait on in-flight packets instead of the work count at shutdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The shutdown drain waited for the run-loop work counter to reach zero, but that counter includes work-guard credits — and a coroutine frame abandoned with a parked operation at teardown (the documented shutdown contract) holds its run_async guard forever, so the destructor spun on a credit that could never be returned. The drain's actual precondition is narrower: no packet referencing overlapped-op memory may remain in flight to the completion port when the services free that memory. Count exactly that — a packet is owed after on_pending's publish (the kernel's completion will dispatch) or a successful stored-result post, and consumed at the single dispatch point per operation — and let shutdown reap until this count is zero. The run-loop counter keeps its original meaning untouched, and guards, posted handlers, and queued continuations are simply outside the drain's concern. --- .../native/detail/iocp/win_scheduler.hpp | 101 +++++++++++------- src/corosio/src/timer.cpp | 6 +- 2 files changed, 65 insertions(+), 42 deletions(-) diff --git a/include/boost/corosio/native/detail/iocp/win_scheduler.hpp b/include/boost/corosio/native/detail/iocp/win_scheduler.hpp index c717c755e..9faf555bb 100644 --- a/include/boost/corosio/native/detail/iocp/win_scheduler.hpp +++ b/include/boost/corosio/native/detail/iocp/win_scheduler.hpp @@ -119,6 +119,14 @@ class BOOST_COROSIO_DECL win_scheduler final timer_service* timer_svc_ = nullptr; void* iocp_; mutable long outstanding_work_; + + // Packets in flight to the completion port that reference + // overlapped-op memory: kernel completions owed after a pending + // submission, plus successful stored-result posts. Shutdown reaps + // until this is zero before the services free op storage. The + // run-loop counter cannot serve that role: frames abandoned at + // teardown never return their work-guard credits. + mutable long pending_io_ = 0; mutable long stopped_; long stop_event_posted_; mutable long dispatch_required_; @@ -334,13 +342,28 @@ win_scheduler::on_pending(overlapped_op* op) const // and stored the results — re-post so do_one() can dispatch. The acquire // on failure makes those payload writes visible, so the re-posted op // carries valid dwError / bytes_transferred. + // + // pending_io_ counts the packet that will dispatch this op: on CAS + // success the kernel's own completion will find ready_ == 1 and + // dispatch; on CAS failure the kernel's packet was consumed as a + // skip (uncounted) and the re-post is the dispatching packet. A + // failed re-post falls back to the deferred queue, which holds the + // op memory itself — no packet, no count. long expected = 0; - if (!op->ready_.compare_exchange_strong( + if (op->ready_.compare_exchange_strong( expected, 1, std::memory_order_acq_rel, std::memory_order_acquire)) { - if (!::PostQueuedCompletionStatus( + ::InterlockedIncrement(&pending_io_); + } + else + { + if (::PostQueuedCompletionStatus( iocp_, 0, key_result_stored, static_cast(op))) + { + ::InterlockedIncrement(&pending_io_); + } + else { std::lock_guard lock(dispatch_mutex_); completed_ops_.push(op); @@ -359,8 +382,12 @@ win_scheduler::on_completion(overlapped_op* op, DWORD error, DWORD bytes) const op->bytes_transferred = bytes; op->ready_.store(1, std::memory_order_release); - if (!::PostQueuedCompletionStatus( + if (::PostQueuedCompletionStatus( iocp_, 0, key_result_stored, static_cast(op))) + { + ::InterlockedIncrement(&pending_io_); + } + else { std::lock_guard lock(dispatch_mutex_); completed_ops_.push(op); @@ -573,6 +600,7 @@ win_scheduler::do_one(unsigned long timeout_ms) std::memory_order_acq_rel, std::memory_order_acquire)) { + ::InterlockedDecrement(&pending_io_); ov_op->complete( this, ov_op->bytes_transferred, ov_op->dwError); work_finished(); @@ -725,12 +753,15 @@ win_scheduler::shutdown() timer_svc_->shutdown(); // Same problem for the auxiliary wait reactor: ops parked in it - // hold work_started credit. Stop the reactor early so its loop - // drains them as cancelled and the work counter can reach zero. + // owe completion packets. Stop the reactor early so its loop + // posts them as cancelled and the pending count can reach zero. if (wait_reactor_ready_.load(std::memory_order_acquire)) wait_reactor_->stop(); - while (::InterlockedExchangeAdd(&outstanding_work_, 0) > 0) + // Reap every packet still owed to the port before the services + // free the op memory those packets reference. Work-guard credits, + // posted handlers, and queued continuations have no bearing here. + while (::InterlockedExchangeAdd(&pending_io_, 0) > 0) { op_queue ops; { @@ -738,42 +769,36 @@ win_scheduler::shutdown() ops.splice(completed_ops_); } - if (!ops.empty()) + // Deferred-queue entries are process-owned (failed-post + // fallbacks and posted handlers); no packet references them. + while (auto* h = ops.pop()) + h->destroy(); + + DWORD bytes; + ULONG_PTR key; + LPOVERLAPPED overlapped; + ::GetQueuedCompletionStatus( + iocp_, &bytes, &key, &overlapped, + iocp::shutdown_drain_timeout_ms); + if (overlapped) { - while (auto* h = ops.pop()) + if (key == key_posted) + { + auto* op = reinterpret_cast(overlapped); + op->destroy(); + } + else if (key == key_continuation) { - ::InterlockedDecrement(&outstanding_work_); - h->destroy(); + // Drain without resuming: destroy the parked frame. + auto* c = reinterpret_cast(overlapped); + if (c->h) + c->h.destroy(); } - } - else - { - DWORD bytes; - ULONG_PTR key; - LPOVERLAPPED overlapped; - ::GetQueuedCompletionStatus( - iocp_, &bytes, &key, &overlapped, - iocp::shutdown_drain_timeout_ms); - if (overlapped) + else { - ::InterlockedDecrement(&outstanding_work_); - if (key == key_posted) - { - auto* op = reinterpret_cast(overlapped); - op->destroy(); - } - else if (key == key_continuation) - { - // Drain without resuming: destroy the parked frame. - auto* c = reinterpret_cast(overlapped); - if (c->h) - c->h.destroy(); - } - else - { - auto* op = overlapped_to_op(overlapped); - op->destroy(); - } + ::InterlockedDecrement(&pending_io_); + auto* op = overlapped_to_op(overlapped); + op->destroy(); } } } diff --git a/src/corosio/src/timer.cpp b/src/corosio/src/timer.cpp index aecab9d61..a2dbc6faf 100644 --- a/src/corosio/src/timer.cpp +++ b/src/corosio/src/timer.cpp @@ -146,10 +146,8 @@ waiter_node::completion_op::destroy() // Called during scheduler shutdown drain when this completion_op is // in the scheduler's ready queue (posted by cancel_timer() or // process_expired()). Balances the work_started() from - // implementation::wait(). The scheduler drain loop separately - // balances the work_started() from post(). On IOCP both decrements - // are required for outstanding_work_ to reach zero; on other - // backends this is harmless. + // implementation::wait(), keeping the run-loop counter sane; no + // shutdown path waits on that counter. // // This override also prevents scheduler_op::destroy() from calling // do_complete(nullptr, ...). See also: timer_service::shutdown() From 26da19c5b1e9c062279242f2a3fc0336a0d78618 Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Sat, 8 Aug 2026 00:20:43 +0200 Subject: [PATCH 05/10] fix(iocp): sweep stranded completions after the shutdown drain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completions can reach the port or the deferred queue without a matching work credit — posted directly against the handle, or left behind once their credit was consumed. Destroy them after the counted drain so service teardown does not free state they still reference. --- .../native/detail/iocp/win_scheduler.hpp | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/include/boost/corosio/native/detail/iocp/win_scheduler.hpp b/include/boost/corosio/native/detail/iocp/win_scheduler.hpp index 9faf555bb..55e148417 100644 --- a/include/boost/corosio/native/detail/iocp/win_scheduler.hpp +++ b/include/boost/corosio/native/detail/iocp/win_scheduler.hpp @@ -802,6 +802,42 @@ win_scheduler::shutdown() } } } + + // Final sweep: packets can sit in the port or deferred queue + // without a pending_io_ count (posted directly against the + // handle). Destroy them so service teardown does not free state + // they still reference. + for (;;) + { + op_queue ops; + { + std::lock_guard lock(dispatch_mutex_); + ops.splice(completed_ops_); + } + while (auto* h = ops.pop()) + h->destroy(); + + DWORD bytes; + ULONG_PTR key; + LPOVERLAPPED overlapped; + ::GetQueuedCompletionStatus(iocp_, &bytes, &key, &overlapped, 0); + if (!overlapped) + break; + if (key == key_posted) + { + reinterpret_cast(overlapped)->destroy(); + } + else if (key == key_continuation) + { + auto* c = reinterpret_cast(overlapped); + if (c->h) + c->h.destroy(); + } + else + { + overlapped_to_op(overlapped)->destroy(); + } + } } inline win_scheduler::~win_scheduler() From 38ae3213f729ced0b78e49bba2b97fd45d920987 Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Thu, 6 Aug 2026 18:14:36 +0200 Subject: [PATCH 06/10] test: extend unit coverage across sockets, containers, and services Most local-stream socket and acceptor tests were gated behind BOOST_COROSIO_POSIX from before connect_pair and AF_UNIX support existed on Windows. Ungate everything that needs only the public API, keeping the raw-fd backlog test POSIX-only, and add tests for wait(error) cancellation, in-flight read cancellation through cancel() and stop tokens, zero-length transfers, socket options, and acceptor waits. The acceptor wait(write) test skips io_uring, whose poll never reports a listener writable. Also add direct unit tests for intrusive_list and intrusive_queue, option get/set round-trips with closed-object throws for the TCP and local acceptors, a delay test that re-issues waits so timer impls are drawn back out of the thread-local cache slot and the service free list, and coverage for both the WSA and Win32 forms of the IOCP error-code mapping. --- test/unit/Jamfile | 3 +- test/unit/delay.cpp | 28 ++ test/unit/intrusive.cpp | 227 +++++++++++++++ test/unit/local_stream_socket.cpp | 337 +++++++++++++++++++++-- test/unit/native/iocp/iocp_error_map.cpp | 80 ++++++ test/unit/tcp_acceptor.cpp | 21 ++ 6 files changed, 673 insertions(+), 23 deletions(-) create mode 100644 test/unit/intrusive.cpp create mode 100644 test/unit/native/iocp/iocp_error_map.cpp diff --git a/test/unit/Jamfile b/test/unit/Jamfile index 84bc9e01f..6bc31a5ea 100644 --- a/test/unit/Jamfile +++ b/test/unit/Jamfile @@ -34,7 +34,7 @@ project boost/corosio/test/unit ; # Non-TLS tests (recurses into test/, native/, etc.) -for local f in [ glob-tree-ex . : *.cpp : openssl_stream.cpp wolfssl_stream.cpp cross_ssl_stream.cpp tls_stream.cpp tls_stream_stress.cpp iocp_shutdown.cpp openssl_engine.cpp wolfssl_engine.cpp cross_engine.cpp ] +for local f in [ glob-tree-ex . : *.cpp : openssl_stream.cpp wolfssl_stream.cpp cross_ssl_stream.cpp tls_stream.cpp tls_stream_stress.cpp iocp_shutdown.cpp iocp_error_map.cpp openssl_engine.cpp wolfssl_engine.cpp cross_engine.cpp ] { run $(f) ; } @@ -44,6 +44,7 @@ for local f in [ glob-tree-ex . : *.cpp : openssl_stream.cpp wolfssl_stream.cpp if [ os.name ] = NT { run native/iocp/iocp_shutdown.cpp ; + run native/iocp/iocp_error_map.cpp ; } # OpenSSL tests - always link against boost_corosio_openssl diff --git a/test/unit/delay.cpp b/test/unit/delay.cpp index ef0176212..02b85fcf7 100644 --- a/test/unit/delay.cpp +++ b/test/unit/delay.cpp @@ -288,6 +288,33 @@ struct delay_test BOOST_TEST_EQ(count, 3); } + // Overlapping delays force multiple live impls; the batch after + // they complete draws from both the thread-local cache slot and + // the service free list instead of allocating. + void testImplRecycling() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + int count = 0; + + auto t = [](int& count_out) -> capy::task<> { + auto [ec] = co_await delay(std::chrono::milliseconds(1)); + if(!ec) + ++count_out; + }; + + for(int i = 0; i < 3; ++i) + capy::run_async(ex)(t(count)); + ioc.run(); + BOOST_TEST_EQ(count, 3); + + ioc.restart(); + for(int i = 0; i < 2; ++i) + capy::run_async(ex)(t(count)); + ioc.run(); + BOOST_TEST_EQ(count, 5); + } + // Issue: an executor whose context is not an io_context cannot // supply a timer service. await_suspend is normally reached only // through a noexcept coroutine-resumption path, where the @@ -918,6 +945,7 @@ struct delay_test testPastTimePointWithStopRequested(); testSingleThreadedHint(); testSequentialDelays(); + testImplRecycling(); testNonIoContextThrows(); testDelayActuallyWaits(); testConcurrentDelaysHeapRemoval(); diff --git a/test/unit/intrusive.cpp b/test/unit/intrusive.cpp new file mode 100644 index 000000000..fcb3ddd5b --- /dev/null +++ b/test/unit/intrusive.cpp @@ -0,0 +1,227 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// 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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include + +#include + +#include "test_suite.hpp" + +using namespace boost::corosio::detail; + +namespace { + +struct list_item : intrusive_list::node +{ + int id; + explicit list_item(int i) : id(i) {} +}; + +struct queue_item : intrusive_queue::node +{ + int id; + explicit queue_item(int i) : id(i) {} +}; + +} // namespace + +struct intrusive_test +{ + void testListPushPop() + { + intrusive_list l; + BOOST_TEST(l.empty()); + BOOST_TEST(l.front() == nullptr); + BOOST_TEST(l.pop_front() == nullptr); + + list_item a{1}, b{2}, c{3}; + l.push_back(&a); + l.push_back(&b); + l.push_back(&c); + BOOST_TEST(!l.empty()); + BOOST_TEST(l.front() == &a); + + BOOST_TEST(l.pop_front() == &a); + BOOST_TEST(l.pop_front() == &b); + BOOST_TEST(l.pop_front() == &c); + BOOST_TEST(l.empty()); + } + + void testListRemove() + { + // Remove from the middle, head, and tail. + intrusive_list l; + list_item a{1}, b{2}, c{3}; + l.push_back(&a); + l.push_back(&b); + l.push_back(&c); + + l.remove(&b); + BOOST_TEST(l.front() == &a); + l.remove(&a); + BOOST_TEST(l.front() == &c); + l.remove(&c); + BOOST_TEST(l.empty()); + + // Removing a node that was already popped is a no-op. + l.push_back(&a); + l.push_back(&b); + BOOST_TEST(l.pop_front() == &a); + l.remove(&a); + BOOST_TEST(l.front() == &b); + BOOST_TEST(l.pop_front() == &b); + } + + void testListForEach() + { + intrusive_list l; + list_item a{1}, b{2}, c{3}; + l.push_back(&a); + l.push_back(&b); + l.push_back(&c); + + std::vector seen; + l.for_each([&](list_item* p) { seen.push_back(p->id); }); + BOOST_TEST_EQ(seen.size(), 3u); + BOOST_TEST_EQ(seen[0], 1); + BOOST_TEST_EQ(seen[1], 2); + BOOST_TEST_EQ(seen[2], 3); + } + + void testListSpliceBack() + { + list_item a{1}, b{2}, c{3}, d{4}; + + // Splice from an empty list is a no-op. + { + intrusive_list dst, src; + dst.push_back(&a); + dst.splice_back(src); + BOOST_TEST(dst.front() == &a); + BOOST_TEST(dst.pop_front() == &a); + BOOST_TEST(dst.empty()); + } + + // Splice into an empty list adopts the source whole. + { + intrusive_list dst, src; + src.push_back(&a); + src.push_back(&b); + dst.splice_back(src); + BOOST_TEST(src.empty()); + BOOST_TEST(dst.pop_front() == &a); + BOOST_TEST(dst.pop_front() == &b); + } + + // Splice appends behind existing elements. + { + intrusive_list dst, src; + dst.push_back(&a); + dst.push_back(&b); + src.push_back(&c); + src.push_back(&d); + dst.splice_back(src); + BOOST_TEST(src.empty()); + BOOST_TEST(dst.pop_front() == &a); + BOOST_TEST(dst.pop_front() == &b); + BOOST_TEST(dst.pop_front() == &c); + BOOST_TEST(dst.pop_front() == &d); + BOOST_TEST(dst.empty()); + } + } + + void testListMove() + { + intrusive_list src; + list_item a{1}; + src.push_back(&a); + + intrusive_list dst(std::move(src)); + BOOST_TEST(src.empty()); + BOOST_TEST(dst.pop_front() == &a); + } + + void testQueuePushPop() + { + intrusive_queue q; + BOOST_TEST(q.empty()); + BOOST_TEST(q.pop() == nullptr); + + queue_item a{1}, b{2}; + q.push(&a); + q.push(&b); + BOOST_TEST(!q.empty()); + BOOST_TEST(q.pop() == &a); + BOOST_TEST(q.pop() == &b); + BOOST_TEST(q.empty()); + } + + void testQueueSplice() + { + queue_item a{1}, b{2}, c{3}; + + // Splice from an empty queue is a no-op. + { + intrusive_queue dst, src; + dst.push(&a); + dst.splice(src); + BOOST_TEST(dst.pop() == &a); + BOOST_TEST(dst.empty()); + } + + // Splice into an empty queue adopts the source whole. + { + intrusive_queue dst, src; + src.push(&a); + src.push(&b); + dst.splice(src); + BOOST_TEST(src.empty()); + BOOST_TEST(dst.pop() == &a); + BOOST_TEST(dst.pop() == &b); + } + + // Splice appends behind existing elements. + { + intrusive_queue dst, src; + dst.push(&a); + src.push(&b); + src.push(&c); + dst.splice(src); + BOOST_TEST(src.empty()); + BOOST_TEST(dst.pop() == &a); + BOOST_TEST(dst.pop() == &b); + BOOST_TEST(dst.pop() == &c); + } + } + + void testQueueMove() + { + intrusive_queue src; + queue_item a{1}; + src.push(&a); + + intrusive_queue dst(std::move(src)); + BOOST_TEST(src.empty()); + BOOST_TEST(dst.pop() == &a); + } + + void run() + { + testListPushPop(); + testListRemove(); + testListForEach(); + testListSpliceBack(); + testListMove(); + testQueuePushPop(); + testQueueSplice(); + testQueueMove(); + } +}; + +TEST_SUITE(intrusive_test, "boost.corosio.intrusive"); diff --git a/test/unit/local_stream_socket.cpp b/test/unit/local_stream_socket.cpp index 9421fa403..9b27387d6 100644 --- a/test/unit/local_stream_socket.cpp +++ b/test/unit/local_stream_socket.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,7 @@ #include #include #include +#include #include "context.hpp" #include "test_suite.hpp" @@ -186,7 +188,6 @@ struct local_stream_socket_test BOOST_TEST_EQ(!connect_ec, true); } -#if BOOST_COROSIO_POSIX void testReadWrite() { io_context ioc(Backend); @@ -246,9 +247,7 @@ struct local_stream_socket_test BOOST_TEST_EQ(s1.is_open(), true); BOOST_TEST_EQ(s2.is_open(), true); } -#endif // BOOST_COROSIO_POSIX -#if BOOST_COROSIO_POSIX void testUnlinkExisting() { io_context ioc(Backend); @@ -295,7 +294,6 @@ struct local_stream_socket_test local_endpoint(path), bind_option::unlink_existing); BOOST_TEST_EQ(!ec, true); } -#endif void testEndpointOrdering() { @@ -383,7 +381,6 @@ struct local_stream_socket_test BOOST_TEST_EQ(sock.remote_endpoint().empty(), true); } -#if BOOST_COROSIO_POSIX void testEndpointsConnected() { io_context ioc(Backend); @@ -464,7 +461,7 @@ struct local_stream_socket_test bool caught = false; try { - sock.assign(-1); + sock.assign(static_cast(-1)); } catch (std::logic_error const&) { @@ -529,6 +526,16 @@ struct local_stream_socket_test d = true; }(client, local_endpoint(path), result_ec, done)); + // Watchdog: if the platform parks the doomed connect instead + // of failing it, retract it so the test reports the miss + // instead of hanging the suite. + auto watchdog = [&]() -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(250)); + if (!done) + client.cancel(); + }; + capy::run_async(ex)(watchdog()); + ioc.run(); BOOST_TEST(done); @@ -613,9 +620,284 @@ struct local_stream_socket_test BOOST_TEST(accept_ec == capy::cond::canceled); } + // wait(wait_type::error) has no immediate-completion path; it + // parks until cancel() retracts it. On IOCP this routes through + // the auxiliary wait reactor rather than the completion port. + void testWaitErrorCancel() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + local_stream_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await s1.wait(wait_type::error); + wait_ec = ec; + wait_done = true; + }; + auto canceller = [&]() -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(20)); + s1.cancel(); + }; + + capy::run_async(ex)(waiter()); + capy::run_async(ex)(canceller()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(wait_ec == capy::cond::canceled); + } + + // Socket-wide cancel() of a parked read retracts the in-flight + // operation and completes it with capy::cond::canceled. + void testCancelPendingRead() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + local_stream_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + std::error_code read_ec; + bool read_done = false; + char buf[16]; + + auto reader = [&]() -> capy::task<> { + auto [ec, n] = co_await s1.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + (void)n; + read_ec = ec; + read_done = true; + }; + auto canceller = [&]() -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(20)); + s1.cancel(); + }; + + capy::run_async(ex)(reader()); + capy::run_async(ex)(canceller()); + ioc.run(); + + BOOST_TEST(read_done); + BOOST_TEST(read_ec == capy::cond::canceled); + } + + // Stop-token cancel of a parked read routes through the per-op + // stop callback rather than the socket-wide cancel. + void testStopTokenRead() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + local_stream_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + std::stop_source ss; + std::error_code read_ec; + bool read_done = false; + char buf[16]; + + auto reader = [&]() -> capy::task<> { + auto [ec, n] = co_await s1.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + (void)n; + read_ec = ec; + read_done = true; + }; + auto canceller = [&]() -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(20)); + ss.request_stop(); + }; + + capy::run_async(ex, ss.get_token())(reader()); + capy::run_async(ex)(canceller()); + ioc.run(); + + BOOST_TEST(read_done); + BOOST_TEST(read_ec == capy::cond::canceled); + } + + // Zero-length reads and writes complete immediately with zero + // bytes and no error; a zero-byte stream read is not EOF. + void testEmptyBufferOps() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + local_stream_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + std::error_code write_ec, read_ec; + std::size_t write_n = 1, read_n = 1; + + auto io = [&]() -> capy::task<> { + { + auto [ec, n] = + co_await s1.write_some(capy::const_buffer(nullptr, 0)); + write_ec = ec; + write_n = n; + } + { + auto [ec, n] = + co_await s1.read_some(capy::mutable_buffer(nullptr, 0)); + read_ec = ec; + read_n = n; + } + }; + + capy::run_async(ex)(io()); + ioc.run(); + + BOOST_TEST(!write_ec); + BOOST_TEST_EQ(write_n, 0u); + BOOST_TEST(!read_ec); + BOOST_TEST_EQ(read_n, 0u); + } + + void testOptions() + { + io_context ioc(Backend); + local_stream_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + // AF_UNIX option support varies by platform; the point is to + // drive the set/get paths, so accept a system error as a + // valid outcome. + try + { + s1.set_option(socket_option::send_buffer_size(16384)); + auto opt = s1.get_option(); + BOOST_TEST(opt.value() > 0); + } + catch (std::system_error const&) + { + BOOST_TEST_PASS(); + } + + local_stream_socket closed(ioc); + BOOST_TEST_THROWS( + closed.set_option(socket_option::send_buffer_size(4096)), + std::logic_error); + BOOST_TEST_THROWS( + (void)closed.get_option(), + std::logic_error); + } + + void testAcceptorOptions() + { + io_context ioc(Backend); + test::temp_socket_dir tmp; + + local_stream_acceptor acc(ioc); + acc.open(); + + // AF_UNIX option support varies by platform; the point is to + // drive the set/get paths, so accept a system error as a + // valid outcome. + try + { + acc.set_option(socket_option::reuse_address(true)); + (void)acc.get_option(); + } + catch (std::system_error const&) + { + BOOST_TEST_PASS(); + } + acc.close(); + + local_stream_acceptor closed(ioc); + BOOST_TEST_THROWS( + closed.set_option(socket_option::reuse_address(true)), + std::logic_error); + BOOST_TEST_THROWS( + (void)closed.get_option(), + std::logic_error); + } + + // Acceptor wait(wait_type::write) completes immediately: a listener + // is always writable by convention. + void testAcceptorWaitWrite() + { +#if BOOST_COROSIO_HAS_IO_URING + // The immediate-writable convention is a reactor/IOCP behavior; + // io_uring's poll never reports a listener writable, so the + // wait would park forever. + if constexpr (std::is_same_v< + std::remove_const_t, io_uring_t>) + return; +#endif + io_context ioc(Backend); + auto ex = ioc.get_executor(); + test::temp_socket_dir tmp; + + local_stream_acceptor acc(ioc); + acc.open(); + auto ec = acc.bind(local_endpoint(tmp.path())); + BOOST_TEST(!ec); + ec = acc.listen(); + BOOST_TEST(!ec); + + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [wec] = co_await acc.wait(wait_type::write); + wait_ec = wec; + wait_done = true; + }; + + capy::run_async(ex)(waiter()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(!wait_ec); + } + + // Acceptor wait(wait_type::read) parks until a cancel retracts it. + void testAcceptorWaitReadCancel() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + test::temp_socket_dir tmp; + + local_stream_acceptor acc(ioc); + acc.open(); + auto ec = acc.bind(local_endpoint(tmp.path())); + BOOST_TEST(!ec); + ec = acc.listen(); + BOOST_TEST(!ec); + + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [wec] = co_await acc.wait(wait_type::read); + wait_ec = wec; + wait_done = true; + }; + auto canceller = [&]() -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(20)); + acc.cancel(); + }; + + capy::run_async(ex)(waiter()); + capy::run_async(ex)(canceller()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(wait_ec == capy::cond::canceled); + } + +#if BOOST_COROSIO_POSIX // Accept a connection that is already queued in the listen backlog // before the io_context ever runs. The accept can then complete on - // the immediate path instead of parking a waiter. + // the immediate path instead of parking a waiter. Uses raw POSIX + // socket calls to connect without driving the io_context. void testAcceptPendingConnection() { io_context ioc(Backend); @@ -658,6 +940,7 @@ struct local_stream_socket_test BOOST_TEST(server.is_open()); ::close(cfd); } +#endif // BOOST_COROSIO_POSIX // accept() on an open, bound, but non-listening socket fails with // a system error instead of hanging. @@ -682,6 +965,17 @@ struct local_stream_socket_test accept_done = true; }; capy::run_async(ex)(acceptor_task()); + + // Watchdog: if the platform parks the accept instead of + // failing it, retract it so the test reports the miss + // instead of hanging the suite. + auto watchdog = [&]() -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(250)); + if (!accept_done) + acc.cancel(); + }; + capy::run_async(ex)(watchdog()); + ioc.run(); BOOST_TEST(accept_done); @@ -691,6 +985,7 @@ struct local_stream_socket_test BOOST_TEST(!server.is_open()); } +#if BOOST_COROSIO_POSIX // Destroy the io_context with an accept still parked; service // shutdown must release the waiter without resuming it. void testDestroyWithParkedAccept() @@ -899,7 +1194,6 @@ struct local_stream_socket_test testEndpointsClosed(); testConnectAccept(); testMoveAccept(); -#if BOOST_COROSIO_POSIX testReadWrite(); testSocketPair(); testEndpointsConnected(); @@ -910,12 +1204,21 @@ struct local_stream_socket_test testConnectToNonexistent(); testCancelPendingAccept(); testStopTokenAccept(); + testWaitErrorCancel(); + testCancelPendingRead(); + testStopTokenRead(); + testEmptyBufferOps(); + testOptions(); + testAcceptorOptions(); + testAcceptorWaitWrite(); + testAcceptorWaitReadCancel(); +#if BOOST_COROSIO_POSIX testAcceptPendingConnection(); +#endif testAcceptWithoutListen(); -#if !COROSIO_TEST_HAS_ASAN +#if BOOST_COROSIO_POSIX && !COROSIO_TEST_HAS_ASAN // Abandons a parked coroutine frame by design; see context.hpp. testDestroyWithParkedAccept(); -#endif #endif testAcceptorOnClosedNoOp(); testAcceptorBindClosedThrows(); @@ -930,19 +1233,14 @@ struct local_stream_socket_test #ifdef __linux__ testAbstractEndpoint(); #endif -#if BOOST_COROSIO_POSIX testUnlinkExisting(); testUnlinkNonexistent(); -#endif testEndpointOrdering(); testEndpointStreamOutput(); -#if BOOST_COROSIO_POSIX testAvailable(); testRelease(); -#endif } -#if BOOST_COROSIO_POSIX void testAvailable() { io_context ioc(Backend); @@ -971,13 +1269,9 @@ struct local_stream_socket_test BOOST_TEST_EQ(done, true); BOOST_TEST_EQ(s2.available(), std::strlen(msg)); } -#endif // BOOST_COROSIO_POSIX -#if BOOST_COROSIO_POSIX - // Exercises raw POSIX fd ops (::write, ::close) on the released - // descriptor. The released-handle semantics are tested via the - // platform helpers; skipped on Windows because the analogous - // path needs send/closesocket and isn't yet factored. + // Writes through the released handle with raw platform calls to + // prove ownership actually transferred. void testRelease() { io_context ioc(Backend); @@ -1003,7 +1297,6 @@ struct local_stream_socket_test ::close(handle); #endif } -#endif void testEndpointStreamOutput() { diff --git a/test/unit/native/iocp/iocp_error_map.cpp b/test/unit/native/iocp/iocp_error_map.cpp new file mode 100644 index 000000000..00c0da4c8 --- /dev/null +++ b/test/unit/native/iocp/iocp_error_map.cpp @@ -0,0 +1,80 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// 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) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include + +#if BOOST_COROSIO_HAS_IOCP + +#include +#include + +#include + +#include "test_suite.hpp" + +namespace boost::corosio { + +struct iocp_error_map_test +{ + // Both the WSA and Win32 form of each code must land on the same + // portable condition, and ERROR_NETNAME_DELETED must split by + // operation (reset for stream I/O, aborted for accept). + void run() + { + using detail::iocp_make_err; + + BOOST_TEST( + iocp_make_err(ERROR_NETNAME_DELETED, /*accept_path=*/false) == + std::errc::connection_reset); + BOOST_TEST( + iocp_make_err(ERROR_NETNAME_DELETED, /*accept_path=*/true) == + std::errc::connection_aborted); + + BOOST_TEST( + iocp_make_err(WSAECONNRESET, false) == + std::errc::connection_reset); + BOOST_TEST( + iocp_make_err(WSAECONNREFUSED, false) == + std::errc::connection_refused); + BOOST_TEST( + iocp_make_err(ERROR_CONNECTION_REFUSED, false) == + std::errc::connection_refused); + BOOST_TEST( + iocp_make_err(WSAECONNABORTED, false) == + std::errc::connection_aborted); + BOOST_TEST( + iocp_make_err(ERROR_CONNECTION_ABORTED, false) == + std::errc::connection_aborted); + BOOST_TEST( + iocp_make_err(WSAENETUNREACH, false) == + std::errc::network_unreachable); + BOOST_TEST( + iocp_make_err(ERROR_NETWORK_UNREACHABLE, false) == + std::errc::network_unreachable); + BOOST_TEST( + iocp_make_err(WSAEHOSTUNREACH, false) == + std::errc::host_unreachable); + BOOST_TEST( + iocp_make_err(ERROR_HOST_UNREACHABLE, false) == + std::errc::host_unreachable); + BOOST_TEST( + iocp_make_err(WSAETIMEDOUT, false) == std::errc::timed_out); + BOOST_TEST( + iocp_make_err(ERROR_SEM_TIMEOUT, false) == std::errc::timed_out); + + // Unmapped codes defer to make_err and stay non-empty. + BOOST_TEST(!!iocp_make_err(ERROR_ACCESS_DENIED, false)); + } +}; + +TEST_SUITE(iocp_error_map_test, "boost.corosio.iocp_error_map"); + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_HAS_IOCP diff --git a/test/unit/tcp_acceptor.cpp b/test/unit/tcp_acceptor.cpp index 60b10f7d8..07e712f21 100644 --- a/test/unit/tcp_acceptor.cpp +++ b/test/unit/tcp_acceptor.cpp @@ -74,6 +74,26 @@ struct tcp_acceptor_test BOOST_TEST_EQ(acc.is_open(), false); } + void testOptions() + { + io_context ioc(Backend); + tcp_acceptor acc(ioc); + + acc.open(); + acc.set_option(socket_option::reuse_address(true)); + auto opt = acc.get_option(); + BOOST_TEST(opt.value()); + acc.close(); + + tcp_acceptor closed(ioc); + BOOST_TEST_THROWS( + closed.set_option(socket_option::reuse_address(true)), + std::logic_error); + BOOST_TEST_THROWS( + (void)closed.get_option(), + std::logic_error); + } + void testMoveConstruct() { io_context ioc(Backend); @@ -880,6 +900,7 @@ struct tcp_acceptor_test { testConstruction(); testListen(); + testOptions(); testMoveConstruct(); testMoveAssign(); From 601a49830adcfe4d92509dd2432936661525fe21 Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Fri, 7 Aug 2026 15:42:15 +0200 Subject: [PATCH 07/10] test: destroy the io_context with parked operations on every platform Run the parked-accept and parked-read teardown tests for both local stream and TCP sockets on Windows, where they previously never ran. Destroying the context with operations still parked abandons their coroutine frames by design; these pin the IOCP shutdown drain to that contract. --- test/unit/local_stream_socket.cpp | 29 ++++++++++++--- test/unit/tcp_acceptor.cpp | 60 ++++++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/test/unit/local_stream_socket.cpp b/test/unit/local_stream_socket.cpp index 9b27387d6..bc53fac65 100644 --- a/test/unit/local_stream_socket.cpp +++ b/test/unit/local_stream_socket.cpp @@ -985,7 +985,6 @@ struct local_stream_socket_test BOOST_TEST(!server.is_open()); } -#if BOOST_COROSIO_POSIX // Destroy the io_context with an accept still parked; service // shutdown must release the waiter without resuming it. void testDestroyWithParkedAccept() @@ -1013,7 +1012,28 @@ struct local_stream_socket_test (void)ioc.run_one(); BOOST_TEST_PASS(); } -#endif // BOOST_COROSIO_POSIX + + // Destroy the io_context with a read still parked on a connected + // pair; the socket service's shutdown must drain the abandoned + // operation without resuming it. + void testDestroyWithParkedRead() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + local_stream_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + char buf[16]; + auto reader = [&]() -> capy::task<> { + (void)co_await s1.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + }; + capy::run_async(ex)(reader()); + + (void)ioc.run_one(); + BOOST_TEST_PASS(); + } void testAcceptorOnClosedNoOp() { @@ -1216,9 +1236,10 @@ struct local_stream_socket_test testAcceptPendingConnection(); #endif testAcceptWithoutListen(); -#if BOOST_COROSIO_POSIX && !COROSIO_TEST_HAS_ASAN - // Abandons a parked coroutine frame by design; see context.hpp. +#if !COROSIO_TEST_HAS_ASAN + // Abandon parked coroutine frames by design; see context.hpp. testDestroyWithParkedAccept(); + testDestroyWithParkedRead(); #endif testAcceptorOnClosedNoOp(); testAcceptorBindClosedThrows(); diff --git a/test/unit/tcp_acceptor.cpp b/test/unit/tcp_acceptor.cpp index 07e712f21..ace78bfba 100644 --- a/test/unit/tcp_acceptor.cpp +++ b/test/unit/tcp_acceptor.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -184,6 +185,54 @@ struct tcp_acceptor_test acc.close(); } + // Destroy the io_context with a read still parked on a connected + // socket; service shutdown must drain the abandoned operation + // without resuming it. + void testDestroyWithParkedRead() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + tcp_acceptor acc(ioc); + acc.open(); + acc.set_option(socket_option::reuse_address(true)); + // Bind to loopback explicitly: connecting to a wildcard-bound + // listener's 0.0.0.0 address only works on some platforms. + auto ec = acc.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + ec = acc.listen(); + BOOST_TEST(!ec); + auto ep = endpoint( + ipv4_address::loopback(), acc.local_endpoint().port()); + + tcp_socket server(ioc); + tcp_socket client(ioc); + + capy::run_async(ex)( + [](tcp_acceptor& a, tcp_socket& s) -> capy::task<> { + (void)co_await a.accept(s); + }(acc, server)); + capy::run_async(ex)( + [](tcp_socket& s, endpoint e) -> capy::task<> { + (void)co_await s.connect(e); + }(client, ep)); + ioc.run(); + BOOST_TEST(server.is_open()); + BOOST_TEST(client.is_open()); + + ioc.restart(); + + char buf[16]; + auto reader = [&]() -> capy::task<> { + (void)co_await server.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + }; + capy::run_async(ex)(reader()); + + (void)ioc.run_one(); + BOOST_TEST_PASS(); + } + void testCloseWhilePendingAccept() { // Tests that close() properly handles a pending accept operation. @@ -867,6 +916,7 @@ struct tcp_acceptor_test BOOST_TEST(accept_ec); BOOST_TEST(!peer.is_open()); } +#endif // !_WIN32 // Destroy the io_context with an accept still parked; service // shutdown must release the waiter without resuming it. @@ -894,7 +944,6 @@ struct tcp_acceptor_test (void)ioc.run_one(); BOOST_TEST_PASS(); } -#endif void run() { @@ -907,6 +956,11 @@ struct tcp_acceptor_test // Cancellation testCancelAccept(); testCloseWhilePendingAccept(); +#if !COROSIO_TEST_HAS_ASAN + // Abandon parked coroutine frames by design; see context.hpp. + testDestroyWithParkedAccept(); + testDestroyWithParkedRead(); +#endif // IPv6 testListenV6(); @@ -942,10 +996,6 @@ struct tcp_acceptor_test #ifndef _WIN32 testAcceptPendingConnection(); testAcceptWithoutListen(); -#if !COROSIO_TEST_HAS_ASAN - // Abandons a parked coroutine frame by design; see context.hpp. - testDestroyWithParkedAccept(); -#endif #endif } }; From ea75c36ac6bb79cb06572b0308b499c92348b5dc Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Tue, 11 Aug 2026 18:53:25 +0200 Subject: [PATCH 08/10] test(tcp): run the backlog and no-listen accept tests on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both were gated with the raw-socket includes even though only the backlog test touches raw sockets — and its calls are identical in Winsock apart from the handle type and closesocket. The no-listen accept gains the watchdog canceller used by its local-stream sibling, since a platform may park the accept instead of failing it. --- test/unit/tcp_acceptor.cpp | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/test/unit/tcp_acceptor.cpp b/test/unit/tcp_acceptor.cpp index ace78bfba..b0ad18671 100644 --- a/test/unit/tcp_acceptor.cpp +++ b/test/unit/tcp_acceptor.cpp @@ -33,6 +33,9 @@ #include #include #include +#else +// Raw-socket backlog setup in testAcceptPendingConnection. +#include #endif #include "context.hpp" @@ -839,7 +842,6 @@ struct tcp_acceptor_test BOOST_TEST(accept_ec == capy::cond::canceled); } -#ifndef _WIN32 // Accept a connection that is already queued in the listen backlog // before the io_context ever runs. The accept can then complete on // the immediate path instead of parking a waiter. @@ -857,9 +859,15 @@ struct tcp_acceptor_test auto port = acc.local_endpoint().port(); // Raw blocking connect: completes via the kernel's listen - // backlog without the io_context running. + // backlog without the io_context running. The io_context above + // has already initialized the socket layer. +#ifdef _WIN32 + SOCKET cfd = ::socket(AF_INET, SOCK_STREAM, 0); + BOOST_TEST(cfd != INVALID_SOCKET); +#else int cfd = ::socket(AF_INET, SOCK_STREAM, 0); BOOST_TEST(cfd >= 0); +#endif sockaddr_in sa{}; sa.sin_family = AF_INET; sa.sin_port = htons(port); @@ -883,7 +891,11 @@ struct tcp_acceptor_test BOOST_TEST(accept_done); BOOST_TEST(!accept_ec); BOOST_TEST(peer.is_open()); +#ifdef _WIN32 + ::closesocket(cfd); +#else ::close(cfd); +#endif } // accept() on an open, bound, but non-listening socket fails with @@ -908,6 +920,17 @@ struct tcp_acceptor_test accept_done = true; }; capy::run_async(ex)(acceptor_task()); + + // Watchdog: if the platform parks the accept instead of + // failing it, retract it so the test reports the miss + // instead of hanging the suite. + auto watchdog = [&]() -> capy::task<> { + (void)co_await corosio::delay(std::chrono::milliseconds(250)); + if (!accept_done) + acc.cancel(); + }; + capy::run_async(ex)(watchdog()); + ioc.run(); BOOST_TEST(accept_done); @@ -916,7 +939,6 @@ struct tcp_acceptor_test BOOST_TEST(accept_ec); BOOST_TEST(!peer.is_open()); } -#endif // !_WIN32 // Destroy the io_context with an accept still parked; service // shutdown must release the waiter without resuming it. @@ -993,10 +1015,8 @@ struct tcp_acceptor_test // Waiter lifecycle testStopTokenAccept(); -#ifndef _WIN32 testAcceptPendingConnection(); testAcceptWithoutListen(); -#endif } }; From 51c4eecb82f0ba81e645d536a8a8c49764c0cd2d Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Tue, 11 Aug 2026 19:31:27 +0200 Subject: [PATCH 09/10] test(local): exercise abstract endpoints as values on every platform local_endpoint is a value type, so constructing, classifying, and formatting an abstract address needs no kernel support; only binding one is Linux-specific. Ungate those checks, and verify on Windows that binding an abstract endpoint is refused with operation_not_supported rather than binding something else. --- test/unit/local_stream_socket.cpp | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/test/unit/local_stream_socket.cpp b/test/unit/local_stream_socket.cpp index bc53fac65..fb81f655f 100644 --- a/test/unit/local_stream_socket.cpp +++ b/test/unit/local_stream_socket.cpp @@ -1192,7 +1192,9 @@ struct local_stream_socket_test BOOST_TEST_EQ(ep.path().size(), local_endpoint::max_path_length); } -#ifdef __linux__ + // local_endpoint is a value type: constructing and classifying an + // abstract address needs no kernel support, so this runs on every + // platform even though only Linux can bind one. void testAbstractEndpoint() { std::string abs_path(1, '\0'); @@ -1201,6 +1203,21 @@ struct local_stream_socket_test BOOST_TEST(ep.is_abstract()); BOOST_TEST_EQ(ep.empty(), false); } + +#ifdef _WIN32 + // Windows AF_UNIX has no abstract namespace; bind must refuse the + // endpoint instead of silently binding something else. + void testAbstractBindRejected() + { + io_context ioc(Backend); + std::string abs_path(1, '\0'); + abs_path += "corosio_test_abstract_bind"; + + local_stream_acceptor acc(ioc); + acc.open(); + auto ec = acc.bind(local_endpoint(abs_path)); + BOOST_TEST(ec == std::errc::operation_not_supported); + } #endif void run() @@ -1251,8 +1268,9 @@ struct local_stream_socket_test testEndpointTooLongThrows(); testEndpointTooLongNoThrow(); testEndpointMaxPathLength(); -#ifdef __linux__ testAbstractEndpoint(); +#ifdef _WIN32 + testAbstractBindRejected(); #endif testUnlinkExisting(); testUnlinkNonexistent(); @@ -1335,8 +1353,8 @@ struct local_stream_socket_test BOOST_TEST_EQ(os.str(), std::string("")); } -#ifdef __linux__ - // Abstract socket + // Abstract socket: formatting is value-type behavior, so it + // is exercised on every platform. { std::string abs_path(1, '\0'); abs_path += "test_name"; @@ -1344,7 +1362,6 @@ struct local_stream_socket_test os << local_endpoint(abs_path); BOOST_TEST_EQ(os.str(), std::string("[abstract:test_name]")); } -#endif } }; From 2b22c8db94383ab452faa7ccb64fd1df9b8bea1e Mon Sep 17 00:00:00 2001 From: Steve Gerbino Date: Tue, 11 Aug 2026 21:00:27 +0200 Subject: [PATCH 10/10] test(delay): cancel far-future delays in the heap-removal test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The interleaved deadlines were all a few milliseconds out and the test asserted the middle three were canceled before any fired — a wall-clock race that fails under heavy slowdown (observed under valgrind, where all five expired first). Park the cancel targets hours out so no slowdown can expire them; only the two short delays ever fire, and removing the middle hour entry still exercises heap interior removal. --- test/unit/delay.cpp | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/test/unit/delay.cpp b/test/unit/delay.cpp index 02b85fcf7..7a8343667 100644 --- a/test/unit/delay.cpp +++ b/test/unit/delay.cpp @@ -351,10 +351,11 @@ struct delay_test void testConcurrentDelaysHeapRemoval() { - // Several concurrent delays at interleaved deadlines; the - // middle ones are canceled through their own stop tokens, - // exercising heap middle-removal. All must complete with the - // expected disposition. + // Several concurrent delays; the far-future ones are canceled + // through their own stop tokens, exercising heap interior + // removal. The cancel targets sit hours out so no slowdown + // (sanitizers, valgrind) can expire them before the stop + // requests land; only the two short delays ever fire. io_context ioc(Backend); auto ex = ioc.get_executor(); @@ -362,27 +363,28 @@ struct delay_test int ok = 0; int canceled = 0; - // Deadlines interleaved (5,4,3,2,1 ms); cancel the middle - // three (3,4,5 ms) so removals land in the heap interior. - auto d = [](int ms, int& ok_out, int& cancel_out) -> capy::task<> { - auto [ec] = co_await delay(std::chrono::milliseconds(ms)); + auto d = [](std::chrono::nanoseconds dur, int& ok_out, + int& cancel_out) -> capy::task<> { + auto [ec] = co_await delay(dur); if (ec == capy::cond::canceled) ++cancel_out; else if (!ec) ++ok_out; }; - capy::run_async(ex, s1.get_token())(d(1, ok, canceled)); - capy::run_async(ex, s2.get_token())(d(2, ok, canceled)); - capy::run_async(ex, s3.get_token())(d(3, ok, canceled)); - capy::run_async(ex, s4.get_token())(d(4, ok, canceled)); - capy::run_async(ex, s5.get_token())(d(5, ok, canceled)); + using namespace std::chrono_literals; + capy::run_async(ex, s1.get_token())(d(1ms, ok, canceled)); + capy::run_async(ex, s2.get_token())(d(2ms, ok, canceled)); + capy::run_async(ex, s3.get_token())(d(1h, ok, canceled)); + capy::run_async(ex, s4.get_token())(d(2h, ok, canceled)); + capy::run_async(ex, s5.get_token())(d(3h, ok, canceled)); - // Let all five suspend into the heap, then cancel the middle - // three before any fires. + // Let all five suspend into the heap, then cancel the hour + // delays; removing the 2h entry lands between its neighbors, + // in the heap interior. ioc.poll(); - s3.request_stop(); s4.request_stop(); + s3.request_stop(); s5.request_stop(); ioc.run();