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/include/boost/corosio/native/detail/iocp/win_scheduler.hpp b/include/boost/corosio/native/detail/iocp/win_scheduler.hpp index c717c755e..55e148417 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,43 +769,73 @@ 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) + { + // Drain without resuming: destroy the parked frame. + auto* c = reinterpret_cast(overlapped); + if (c->h) + c->h.destroy(); + } + else { - ::InterlockedDecrement(&outstanding_work_); - h->destroy(); + ::InterlockedDecrement(&pending_io_); + auto* op = overlapped_to_op(overlapped); + op->destroy(); } } + } + + // 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 { - DWORD bytes; - ULONG_PTR key; - LPOVERLAPPED overlapped; - ::GetQueuedCompletionStatus( - iocp_, &bytes, &key, &overlapped, - iocp::shutdown_drain_timeout_ms); - if (overlapped) - { - ::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(); - } - } + overlapped_to_op(overlapped)->destroy(); } } } 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 diff --git a/src/corosio/src/timer.cpp b/src/corosio/src/timer.cpp index 669284d8e..a2dbc6faf 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 @@ -161,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() 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/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..7a8343667 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 @@ -324,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(); @@ -335,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(); @@ -918,6 +947,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..fb81f655f 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); @@ -718,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() { @@ -877,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'); @@ -886,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() @@ -899,7 +1231,6 @@ struct local_stream_socket_test testEndpointsClosed(); testConnectAccept(); testMoveAccept(); -#if BOOST_COROSIO_POSIX testReadWrite(); testSocketPair(); testEndpointsConnected(); @@ -910,12 +1241,22 @@ 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 - // Abandons a parked coroutine frame by design; see context.hpp. + // Abandon parked coroutine frames by design; see context.hpp. testDestroyWithParkedAccept(); -#endif + testDestroyWithParkedRead(); #endif testAcceptorOnClosedNoOp(); testAcceptorBindClosedThrows(); @@ -927,22 +1268,18 @@ struct local_stream_socket_test testEndpointTooLongThrows(); testEndpointTooLongNoThrow(); testEndpointMaxPathLength(); -#ifdef __linux__ testAbstractEndpoint(); +#ifdef _WIN32 + testAbstractBindRejected(); #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 +1308,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 +1336,6 @@ struct local_stream_socket_test ::close(handle); #endif } -#endif void testEndpointStreamOutput() { @@ -1021,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"; @@ -1030,7 +1362,6 @@ struct local_stream_socket_test os << local_endpoint(abs_path); BOOST_TEST_EQ(os.str(), std::string("[abstract:test_name]")); } -#endif } }; 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/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/tcp_acceptor.cpp b/test/unit/tcp_acceptor.cpp index 60b10f7d8..b0ad18671 100644 --- a/test/unit/tcp_acceptor.cpp +++ b/test/unit/tcp_acceptor.cpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -32,6 +33,9 @@ #include #include #include +#else +// Raw-socket backlog setup in testAcceptPendingConnection. +#include #endif #include "context.hpp" @@ -74,6 +78,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); @@ -164,6 +188,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. @@ -770,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. @@ -788,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); @@ -814,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 @@ -839,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); @@ -874,18 +966,23 @@ struct tcp_acceptor_test (void)ioc.run_one(); BOOST_TEST_PASS(); } -#endif void run() { testConstruction(); testListen(); + testOptions(); testMoveConstruct(); testMoveAssign(); // Cancellation testCancelAccept(); testCloseWhilePendingAccept(); +#if !COROSIO_TEST_HAS_ASAN + // Abandon parked coroutine frames by design; see context.hpp. + testDestroyWithParkedAccept(); + testDestroyWithParkedRead(); +#endif // IPv6 testListenV6(); @@ -918,14 +1015,8 @@ struct tcp_acceptor_test // Waiter lifecycle testStopTokenAccept(); -#ifndef _WIN32 testAcceptPendingConnection(); testAcceptWithoutListen(); -#if !COROSIO_TEST_HAS_ASAN - // Abandons a parked coroutine frame by design; see context.hpp. - testDestroyWithParkedAccept(); -#endif -#endif } }; 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())); } };