Skip to content

Conversation

@vinniefalco
Copy link
Member

@vinniefalco vinniefalco commented Jan 20, 2026

Summary by CodeRabbit

  • New Features

    • Full TLS context configuration API (certificates, keys, trust anchors, protocol versions, ciphersuites, ALPN, revocation/OCSP).
    • Test utility to create connected socket pairs.
    • Accessor exposing underlying stream implementation.
  • Tests

    • New TLS test scaffolding and suites (OpenSSL, WolfSSL, cross-implementation).
    • Expanded socket and mocket unit tests and test autodiscovery.
  • Breaking Changes

    • TLS stream constructors now require an explicit TLS context.
  • Bug Fixes

    • Reduced unnecessary buffer copies; improved I/O passthrough, lifetime and cancellation behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

Adds an io_stream accessor, requires explicit tls::context for TLS streams, implements a concrete tls::context API, introduces socket-pair test utilities, converts many internal socket/acceptor backends to shared ownership, expands TLS tests (OpenSSL/WolfSSL/cross-impl), refines mock socket passthrough, and greatly expands unit tests and test discovery.

Changes

Cohort / File(s) Summary
IO Stream Accessor
include/boost/corosio/io_stream.hpp
Added io_stream_impl* get_impl() const noexcept exposing the underlying implementation pointer.
Socket Pair API & Tests
include/boost/corosio/test/socket_pair.hpp, src/corosio/src/test/socket_pair.cpp, test/unit/test/socket_pair.cpp
Added make_socket_pair(io_context&) header and implementation; new unit tests for creation and bidirectional transfer.
TLS Context Implementation
src/corosio/src/tls/context.cpp, include/.../tls/*.hpp
Added concrete tls::context implementation with certificate/key/chain loading, CA/trust, protocol versions, ciphersuites, ALPN, CRL/OCSP, verify settings, hostname and revocation policy; file-loads return ENOENT for missing files; PKCS#12 returns ENOTSUP.
TLS Stream Constructors & Impl Changes
include/boost/corosio/tls/openssl_stream.hpp, include/boost/corosio/tls/wolfssl_stream.hpp, src/openssl/src/openssl_stream.cpp, src/wolfssl/src/wolfssl_stream.cpp
Removed context-less constructors and construct() helpers; constructors now require tls::context; OpenSSL uses TLS_method() and applies ciphersuites; WolfSSL uses wolfSSLv23_method().
TLS Test Utilities & Test Suites
test/unit/tls/test_utils.hpp, test/unit/tls/openssl_stream.cpp, test/unit/tls/wolfssl_stream.cpp, test/unit/tls/cross_ssl_stream.cpp
Added inline test certificates, context factories, test runners (run_tls_test variants), and new test suites for OpenSSL, WolfSSL, and cross-implementation interoperability (some failure tests disabled/guarded).
I/O Backend Ownership & Lifetime
include/boost/corosio/acceptor.hpp, include/boost/corosio/socket.hpp, src/corosio/src/acceptor.cpp, src/corosio/src/socket.cpp, src/corosio/src/detail/iocp/sockets.{cpp,hpp}, src/corosio/src/detail/iocp/overlapped_op.hpp
Shifted socket/acceptor implementations to shared ownership: added impl_owner_ fields, changed create_impl/create_acceptor_impl to return std::shared_ptr, added unregister APIs, extended accept signatures with std::shared_ptr<void>* impl_owner_out, added impl_ptr/impl ownership in IOCP ops, and adjusted EOF/cancellation/empty-buffer semantics.
Mock Socket (mocket) Behavior & Test
src/corosio/src/test/mocket.cpp, test/unit/test/mocket.cpp
Conditional buffer extraction in read_some/write_some based on staged data/expectations; added testPassthrough() unit test.
I/O Read API
include/boost/corosio/read.hpp
Renamed template/parameter identifiers, adjusted consuming_buffers usage, simplified per-iteration error/consumption handling, and added final return path.
Extensive Unit Test Expansion & Discovery
test/unit/socket.cpp, test/unit/Jamfile, test/unit/CMakeLists.txt, many test/unit/*.cpp additions
Large expansion of socket tests (many new scenarios), switched test source discovery to glob in Jamfile, and added CMake conditional linking/defines for OpenSSL/WolfSSL.
Build Preset File
CMakePresets.json
Added minimal CMakePresets.json (empty presets skeleton).

Sequence Diagram(s)

sequenceDiagram
    participant TestApp as Test harness
    participant IO as io_context
    participant Acceptor as Acceptor
    participant SocketA as Socket A
    participant SocketB as Socket B

    TestApp->>Acceptor: bind(loopback:port) / listen
    TestApp->>IO: async_accept(SocketA)
    TestApp->>IO: async_connect(SocketB -> addr)
    IO->>Acceptor: accept incoming
    IO->>SocketB: perform connect
    Acceptor->>SocketA: accept => assign peer_impl + impl_owner
    IO->>TestApp: both complete -> return (SocketA, SocketB)
Loading
sequenceDiagram
    participant ClientApp as Client
    participant CtxC as Client tls::context
    participant ServerApp as Server
    participant CtxS as Server tls::context
    participant IO as io_context
    participant CStream as Client TLS stream
    participant SStream as Server TLS stream

    ClientApp->>CtxC: make_client_context()
    ServerApp->>CtxS: make_server_context()
    ClientApp->>CStream: construct(io_stream, CtxC)
    ServerApp->>SStream: construct(io_stream, CtxS)
    ClientApp->>IO: async_handshake(CStream)
    ServerApp->>IO: async_handshake(SStream)
    CStream->>SStream: ClientHello / ServerHello / Finished...
    IO->>ClientApp: handshake complete
    IO->>ServerApp: handshake complete
    ClientApp->>CStream: async_write("hello")
    CStream->>SStream: encrypted data
    ServerApp->>SStream: async_read -> "hello"
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 I tunneled sockets, stitched contexts tight,

Certs and handshakes dancing through the night,
OpenSSL and WolfSSL leap and play,
Tests hop, assert, and keep bugs at bay,
A rabbit nods — the build is bright.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'tests and more' is vague and generic, using non-descriptive terms that fail to convey meaningful information about the substantial changeset. Use a more descriptive title that summarizes the main changes, such as 'Add TLS stream tests, socket pair utilities, and context implementation' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@cppalliance-bot
Copy link

cppalliance-bot commented Jan 20, 2026

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

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

2026-01-20 12:47:54 UTC

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/wolfssl/src/wolfssl_stream.cpp (1)

929-938: Silent failure on init_ssl error may cause issues downstream.

When init_ssl() fails, the constructor silently returns with impl_ unset (nullptr). Callers have no way to detect this failure, which could lead to null pointer dereference or unexpected behavior when using the stream.

🔧 Consider throwing an exception or providing a validity check

Option 1: Throw on failure

     auto ec = impl->init_ssl();
     if( ec )
     {
         delete impl;
-        // For now, silently fail - could throw or store error
-        return;
+        throw system::system_error( ec );
     }

Option 2: Add is_valid() method and document the need to check:

bool wolfssl_stream::is_valid() const noexcept
{
    return impl_ != nullptr;
}
src/openssl/src/openssl_stream.cpp (1)

756-764: Same silent failure pattern as WolfSSL stream.

This has the same issue noted in wolfssl_stream.cpp - silent failure on init_ssl() error leaves impl_ unset with no way for callers to detect the failure.

🤖 Fix all issues with AI agents
In `@src/openssl/src/openssl_stream.cpp`:
- Around line 185-191: The call to SSL_CTX_set_cipher_list does not check its
return value so invalid cipher strings go unnoticed; update the block that
handles cd.ciphersuites (where SSL_CTX_set_security_level(ctx_, 0) and
SSL_CTX_set_cipher_list(ctx_, cd.ciphersuites.c_str()) are called) to verify the
return value of SSL_CTX_set_cipher_list, and on failure log an error (including
cd.ciphersuites and any OpenSSL error via ERR_get_error()/ERR_error_string) and
abort initialization or throw an exception so the caller knows cipher
configuration failed.

In `@test/unit/CMakeLists.txt`:
- Around line 27-35: The CMake if-statement is using the wrong OpenSSL variable
name; change the conditional from OpenSSL_FOUND to the standard OPENSSL_FOUND so
the block that links boost_corosio_openssl and sets BOOST_COROSIO_HAS_OPENSSL=1
actually runs when OpenSSL is found; update the if(...) check that surrounds
target_link_libraries(boost_corosio_tests PRIVATE boost_corosio_openssl) and
target_compile_definitions(boost_corosio_tests PRIVATE
BOOST_COROSIO_HAS_OPENSSL=1) to use OPENSSL_FOUND.

In `@test/unit/tls/test_utils.hpp`:
- Around line 96-114: The wrong_ca_cert_pem constant contains bogus placeholder
PEM data and must be replaced with a real self-signed CA certificate distinct
from server_cert_pem so parsing succeeds; update the inline constexpr char
const* wrong_ca_cert_pem to a valid PEM-encoded self-signed cert (different from
server_cert_pem) so add_certificate_authority() can load it and tests exercise
CA verification failure rather than failing during context setup.
🧹 Nitpick comments (7)
src/corosio/src/test/socket_pair.cpp (2)

26-36: Consider thread safety for parallel test execution.

The static next_test_port variable is accessed without synchronization. If tests run in parallel threads, this creates a data race. While the port cycling provides some mitigation against port conflicts, consider using std::atomic<std::uint16_t> if parallel test execution is expected.

♻️ Suggested fix for thread safety
-std::uint16_t next_test_port = 0;
+std::atomic<std::uint16_t> next_test_port{0};

 std::uint16_t
 get_test_port() noexcept
 {
-    auto port = test_port_base + (next_test_port % test_port_range);
-    ++next_test_port;
+    auto idx = next_test_port.fetch_add(1, std::memory_order_relaxed);
+    auto port = test_port_base + (idx % test_port_range);
     return static_cast<std::uint16_t>(port);
 }

80-91: Inconsistent resource cleanup in error paths.

The error paths close acc and conditionally s1, but s2 (which was opened at line 56) is not explicitly closed before throwing. While the socket destructor likely handles cleanup, explicit cleanup would be more consistent with the established pattern.

♻️ Suggested fix for consistent cleanup
     if (!accept_done || accept_ec)
     {
         acc.close();
+        s2.close();
         throw std::runtime_error("socket_pair accept failed");
     }

     if (!connect_done || connect_ec)
     {
         acc.close();
         s1.close();
+        s2.close();
         throw std::runtime_error("socket_pair connect failed");
     }
src/corosio/src/tls/context.cpp (2)

52-61: Consider using std::ios::ate to pre-allocate string size.

The current pattern reads the entire file through std::ostringstream, which may cause multiple reallocations for large certificate files. Consider pre-sizing for efficiency.

♻️ Optional: Pre-allocate string capacity
 system::result<void>
 context::
 use_certificate_file(
     std::string_view filename,
     file_format format )
 {
-    std::ifstream file( std::string( filename ), std::ios::binary );
+    std::ifstream file( std::string( filename ), std::ios::binary | std::ios::ate );
     if( !file )
         return system::error_code( ENOENT, system::generic_category() );
 
-    std::ostringstream ss;
-    ss << file.rdbuf();
-    impl_->entity_certificate = ss.str();
+    auto size = file.tellg();
+    file.seekg( 0 );
+    std::string data( static_cast<std::size_t>( size ), '\0' );
+    file.read( data.data(), size );
+    impl_->entity_certificate = std::move( data );
     impl_->entity_cert_format = format;
     return {};
 }

113-131: PKCS#12 stubs are acceptable with TODO markers.

The unimplemented PKCS#12 methods correctly return ENOTSUP and have clear TODO comments. Consider tracking these in an issue for future implementation.

Would you like me to open an issue to track PKCS#12 implementation?

test/unit/tls/cross_ssl_stream.cpp (1)

158-171: Disabled failure tests are well-documented.

The comment clearly explains the technical reason (socket cancellation doesn't propagate to TLS handshake state machines). Consider tracking the TLS-aware cancellation feature as a future enhancement.

Would you like me to open an issue to track implementing TLS-aware cancellation so these failure tests can be enabled?

test/unit/tls/test_utils.hpp (2)

121-169: Assert context configuration results to surface bad test assets early.
These helpers ignore boost::system::result<void> from context mutators; checking them makes failures explicit and avoids chasing handshake errors.

Suggested pattern (apply to the helpers below)
 inline context
 make_server_context()
 {
     context ctx;
-    ctx.use_certificate( server_cert_pem, file_format::pem );
-    ctx.use_private_key( server_key_pem, file_format::pem );
-    ctx.set_verify_mode( verify_mode::none );
+    BOOST_TEST( ctx.use_certificate( server_cert_pem, file_format::pem ) );
+    BOOST_TEST( ctx.use_private_key( server_key_pem, file_format::pem ) );
+    BOOST_TEST( ctx.set_verify_mode( verify_mode::none ) );
     return ctx;
 }

373-439: Cancel the timeout once both handshakes complete to avoid fixed 500ms waits.
As written, ioc.run() always waits for the timer even when both sides finish quickly; you already track client_done/server_done, so you can cancel the timer early.

One way to cancel early
-    // Concurrent handshakes (at least one should fail)
-    capy::run_async( ioc.get_executor() )(
-        [&client, &client_failed, &client_done]() -> capy::task<>
-        {
-            auto [ec] = co_await client.handshake( tls_stream::client );
-            if( ec )
-                client_failed = true;
-            client_done = true;
-        }() );
-
-    capy::run_async( ioc.get_executor() )(
-        [&server, &server_failed, &server_done]() -> capy::task<>
-        {
-            auto [ec] = co_await server.handshake( tls_stream::server );
-            if( ec )
-                server_failed = true;
-            server_done = true;
-        }() );
-
-    // Timer to unblock stuck handshakes - when one side fails, the other
-    // may block waiting for data. Timer cancels socket operations to unblock them.
-    timer timeout( ioc );
-    timeout.expires_after( std::chrono::milliseconds( 500 ) );
-    capy::run_async( ioc.get_executor() )(
-        [&timeout, &s1, &s2, &client_done, &server_done]() -> capy::task<>
-        {
-            auto [ec] = co_await timeout.wait();
-            if( !ec )
-            {
-                // Timer expired - cancel pending operations then close sockets
-                s1.cancel();
-                s2.cancel();
-                s1.close();
-                s2.close();
-            }
-        }() );
+    // Timer to unblock stuck handshakes - when one side fails, the other
+    // may block waiting for data. Timer cancels socket operations to unblock them.
+    timer timeout( ioc );
+    timeout.expires_after( std::chrono::milliseconds( 500 ) );
+    auto maybe_cancel = [&]()
+    {
+        if( client_done && server_done )
+            timeout.cancel();
+    };
+
+    // Concurrent handshakes (at least one should fail)
+    capy::run_async( ioc.get_executor() )(
+        [&client, &client_failed, &client_done, &maybe_cancel]() -> capy::task<>
+        {
+            auto [ec] = co_await client.handshake( tls_stream::client );
+            if( ec )
+                client_failed = true;
+            client_done = true;
+            maybe_cancel();
+        }() );
+
+    capy::run_async( ioc.get_executor() )(
+        [&server, &server_failed, &server_done, &maybe_cancel]() -> capy::task<>
+        {
+            auto [ec] = co_await server.handshake( tls_stream::server );
+            if( ec )
+                server_failed = true;
+            server_done = true;
+            maybe_cancel();
+        }() );
+
+    capy::run_async( ioc.get_executor() )(
+        [&timeout, &s1, &s2]() -> capy::task<>
+        {
+            auto [ec] = co_await timeout.wait();
+            if( !ec )
+            {
+                // Timer expired - cancel pending operations then close sockets
+                s1.cancel();
+                s2.cancel();
+                s1.close();
+                s2.close();
+            }
+        }() );

Comment on lines +185 to +191

// Apply cipher suites if provided
if( !cd.ciphersuites.empty() )
{
SSL_CTX_set_security_level( ctx_, 0 );
SSL_CTX_set_cipher_list( ctx_, cd.ciphersuites.c_str() );
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing error check on SSL_CTX_set_cipher_list.

SSL_CTX_set_cipher_list() returns 1 on success and 0 if no valid ciphers are found. The return value should be checked to detect invalid cipher strings.

🔧 Proposed fix
         // Apply cipher suites if provided
         if( !cd.ciphersuites.empty() )
         {
             SSL_CTX_set_security_level( ctx_, 0 );
-            SSL_CTX_set_cipher_list( ctx_, cd.ciphersuites.c_str() );
+            if( SSL_CTX_set_cipher_list( ctx_, cd.ciphersuites.c_str() ) != 1 )
+            {
+                SSL_CTX_free( ctx_ );
+                ctx_ = nullptr;
+                return;
+            }
         }
🤖 Prompt for AI Agents
In `@src/openssl/src/openssl_stream.cpp` around lines 185 - 191, The call to
SSL_CTX_set_cipher_list does not check its return value so invalid cipher
strings go unnoticed; update the block that handles cd.ciphersuites (where
SSL_CTX_set_security_level(ctx_, 0) and SSL_CTX_set_cipher_list(ctx_,
cd.ciphersuites.c_str()) are called) to verify the return value of
SSL_CTX_set_cipher_list, and on failure log an error (including cd.ciphersuites
and any OpenSSL error via ERR_get_error()/ERR_error_string) and abort
initialization or throw an exception so the caller knows cipher configuration
failed.

Comment on lines +27 to +35
if (WolfSSL_FOUND)
target_link_libraries(boost_corosio_tests PRIVATE boost_corosio_wolfssl)
target_compile_definitions(boost_corosio_tests PRIVATE BOOST_COROSIO_HAS_WOLFSSL=1)
endif()

if (OpenSSL_FOUND)
target_link_libraries(boost_corosio_tests PRIVATE boost_corosio_openssl)
target_compile_definitions(boost_corosio_tests PRIVATE BOOST_COROSIO_HAS_OPENSSL=1)
endif()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for CMake configuration related to OpenSSL and WolfSSL
rg -n "find_package.*OpenSSL|find_package.*WolfSSL|OpenSSL_FOUND|OPENSSL_FOUND|WolfSSL_FOUND|WOLFSSL_FOUND" --type cmake -A 2

Repository: cppalliance/corosio

Length of output: 1652


🏁 Script executed:

# Also check for target definitions
rg -n "boost_corosio_wolfssl|boost_corosio_openssl|add_library.*openssl|add_library.*wolfssl" --type cmake -A 2

Repository: cppalliance/corosio

Length of output: 1914


🏁 Script executed:

# Check the structure of CMake files in the project
git ls-files | grep -i cmake$ | head -20

Repository: cppalliance/corosio

Length of output: 86


🌐 Web query:

CMake FindOpenSSL module OpenSSL_FOUND variable name

💡 Result:

The FindOpenSSL module sets the variable named OPENSSL_FOUND (all uppercase). [1]

Sources:


🏁 Script executed:

# Search for any OpenSSL-related CMake files
find . -name "*OpenSSL*" -o -name "*openssl*" | grep -i cmake

Repository: cppalliance/corosio

Length of output: 45


🏁 Script executed:

# Check the find_package call for OpenSSL to see if it specifies CONFIG mode
rg -n "find_package.*OpenSSL" --type cmake -A 3

Repository: cppalliance/corosio

Length of output: 324


Fix CMake variable name for OpenSSL detection.

Change OpenSSL_FOUND to OPENSSL_FOUND in CMakeLists.txt:217 and test/unit/CMakeLists.txt:32. The standard CMake FindOpenSSL module sets OPENSSL_FOUND (all uppercase), not OpenSSL_FOUND. Using the incorrect variable name will cause the OpenSSL configuration and test linking to be silently skipped whenever the package is found.

🤖 Prompt for AI Agents
In `@test/unit/CMakeLists.txt` around lines 27 - 35, The CMake if-statement is
using the wrong OpenSSL variable name; change the conditional from OpenSSL_FOUND
to the standard OPENSSL_FOUND so the block that links boost_corosio_openssl and
sets BOOST_COROSIO_HAS_OPENSSL=1 actually runs when OpenSSL is found; update the
if(...) check that surrounds target_link_libraries(boost_corosio_tests PRIVATE
boost_corosio_openssl) and target_compile_definitions(boost_corosio_tests
PRIVATE BOOST_COROSIO_HAS_OPENSSL=1) to use OPENSSL_FOUND.

Comment on lines +96 to +114
// Different self-signed CA for "wrong CA" test scenarios
// (A different self-signed cert that won't verify server_cert_pem)
inline constexpr char const* wrong_ca_cert_pem =
"-----BEGIN CERTIFICATE-----\n"
"MIICpDCCAYwCCQDU+pQ4P0jwoDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls\n"
"b2NhbGhvc3QwHhcNMjMwMTAxMDAwMDAwWhcNMzMwMTAxMDAwMDAwWjAUMRIwEAYD\n"
"VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7\n"
"o5e7Xv5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z\n"
"5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z\n"
"5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z\n"
"5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z\n"
"5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z5Z\n"
"5Z5Z5Z5ZAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADummy0000000000000000000\n"
"0000000000000000000000000000000000000000000000000000000000000000000\n"
"0000000000000000000000000000000000000000000000000000000000000000000\n"
"0000000000000000000000000000000000000000000000000000000000000000000\n"
"0000000000000000000000000000000000000000000000000000000000000000000\n"
"0000000000000000000000000000000000000000000000=\n"
"-----END CERTIFICATE-----\n";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd test/unit/tls && head -150 test_utils.hpp | tail -70

Repository: cppalliance/corosio

Length of output: 3299


🏁 Script executed:

# Search for the wrong_ca_cert_pem usage in tests
rg "wrong_ca_cert_pem" --type cpp -B 2 -A 5

Repository: cppalliance/corosio

Length of output: 1260


🏁 Script executed:

# Check if there are other CA certificates defined in the same file for comparison
rg "cert_pem|key_pem" test/unit/tls/test_utils.hpp | head -20

Repository: cppalliance/corosio

Length of output: 624


Replace wrong_ca_cert_pem with a valid self-signed PEM certificate.
The certificate data contains placeholder content: the public key section has repeating Z characters, and the signature has a Dummy prefix with repeating 0s. This will fail to parse when add_certificate_authority() attempts to load it, causing CA verification tests to fail during context setup rather than during the intended verification logic. Use a real self-signed CA certificate distinct from server_cert_pem to ensure the "wrong CA" scenario actually tests the intended rejection pathway.

🤖 Prompt for AI Agents
In `@test/unit/tls/test_utils.hpp` around lines 96 - 114, The wrong_ca_cert_pem
constant contains bogus placeholder PEM data and must be replaced with a real
self-signed CA certificate distinct from server_cert_pem so parsing succeeds;
update the inline constexpr char const* wrong_ca_cert_pem to a valid PEM-encoded
self-signed cert (different from server_cert_pem) so add_certificate_authority()
can load it and tests exercise CA verification failure rather than failing
during context setup.

- Add socket_pair.hpp header declaring make_socket_pair function
- Add socket_pair.cpp implementation using loopback TCP connection
- Add unit tests for socket pair creation and bidirectional I/O
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@include/boost/corosio/read.hpp`:
- Around line 79-84: The docblock for the template function read(io_stream& ios,
MB const& bs) uses an outdated `@param` name "buffers"; update the `@param` entry to
use "bs" to match the parameter in the function signature
(template<capy::MutableBufferSequence MB> capy::task<io_result<std::size_t>>
read(io_stream& ios, MB const& bs)) so Doxygen stays in sync.

In `@test/unit/socket.cpp`:
- Around line 619-645: In testReadString() the socket `s1` (aliased to `a`
inside the coroutine) is closed inside the coroutine and then closed again after
ioc.run() causing a double-close; fix by removing the redundant post-run
`s1.close()` (or replace it with a conditional/is_open check) so only `s2` (or
sockets that weren't closed in the coroutine) are closed outside the coroutine;
update references in testReadString(), and keep the coroutine parameters
`socket& a, socket& b` and the existing close call inside the coroutine intact.
- Around line 647-674: The test testReadPartialEOF double-closes the same
socket: the coroutine closes 'a' (the s1 socket) via a.close(), and then the
test calls s1.close() again after ioc.run(); remove the redundant s1.close() (or
conditionally skip closing s1) so the socket is only closed once—update the
cleanup at the end of testReadPartialEOF to only close s2 (or neither if both
were closed in the coroutine).

In `@test/unit/tls/cross_ssl_stream.cpp`:
- Line 10: Remove the local Windows development path reference found in the
comment on cross_ssl_stream.cpp (the line containing
"c:\Users\Vinnie\.cursor\plans\tls_stream_tests_83c24f98.plan.md"); delete that
hard-coded path or replace it with a repository-relative or generic reference
(e.g., remove the .cursor path or replace with a neutral
"plans/tls_stream_tests.plan" or a short TODO) so no user-specific/local
filesystem paths remain in test/unit/tls/cross_ssl_stream.cpp.

In `@test/unit/tls/test_utils.hpp`:
- Around line 210-239: The test_stream helper assumes read_some returns the full
5 bytes which can be flaky; change the two reads that call b.read_some and
a.read_some to accumulate into buf in a loop until the expected number of bytes
(5) is received, checking the error (ec) on each iteration and advancing the
buffer pointer/count accordingly; keep the existing write_some calls and
assertions for their returned n values, but replace the single-read assertions
for ec and n with assertions after the loop that the total bytes read equals 5
and that the concatenated data equals the expected string ("hello" and "world");
this touches the test_stream function and the calls to io_stream::read_some.
- Around line 65-94: Add a .gitleaksignore allowlist entry to prevent false
positives for the embedded Boost.Beast test credentials: identify the constant
server_key_pem in test_utils.hpp and whitelist that file (or a pattern matching
this test file) in .gitleaksignore with a brief justification mentioning the key
is an intentionally embedded test certificate from Boost.Beast.
♻️ Duplicate comments (2)
test/unit/tls/test_utils.hpp (1)

96-114: Replace placeholder wrong_ca_cert_pem with a valid CA cert.
Line 99–113 still looks like placeholder data, so TLS backends may fail to parse or silently skip it, meaning the “wrong CA” path isn’t really exercised. Use a real self‑signed CA cert distinct from server_cert_pem.

src/openssl/src/openssl_stream.cpp (1)

186-191: Check SSL_CTX_set_cipher_list() return value.
Line 190 ignores the return code; invalid cipher strings will fail silently.

🔧 Suggested fix
         if( !cd.ciphersuites.empty() )
         {
             SSL_CTX_set_security_level( ctx_, 0 );
-            SSL_CTX_set_cipher_list( ctx_, cd.ciphersuites.c_str() );
+            if( SSL_CTX_set_cipher_list( ctx_, cd.ciphersuites.c_str() ) != 1 )
+            {
+                SSL_CTX_free( ctx_ );
+                ctx_ = nullptr;
+                return;
+            }
         }
🧹 Nitpick comments (4)
include/boost/corosio/io_stream.hpp (1)

220-231: Consider const/non-const overloads for get_impl().
Line 227 returns a mutable pointer from a const method, which permits mutation through a const io_stream. If this is meant for testing but you want to preserve const‑correctness, split into const and non‑const overloads.

♻️ Suggested adjustment
-    io_stream_impl*
-    get_impl() const noexcept
-    {
-        return static_cast<io_stream_impl*>(impl_);
-    }
+    io_stream_impl*
+    get_impl() noexcept
+    {
+        return static_cast<io_stream_impl*>(impl_);
+    }
+
+    io_stream_impl const*
+    get_impl() const noexcept
+    {
+        return static_cast<io_stream_impl const*>(impl_);
+    }
src/corosio/src/tls/context.cpp (1)

113-131: PKCS#12 TODOs: consider tracking or documenting ENOTSUP.
If these are intentionally deferred, consider a tracked issue or doc note so callers aren’t surprised. I can help wire this up if desired.

test/unit/tls/wolfssl_stream.cpp (1)

84-89: Consider tracking the disabled failure tests with an issue/TODO.
This helps ensure the cancellation behavior gets revisited.

test/unit/tls/cross_ssl_stream.cpp (1)

140-144: Clarify the intent of set_ciphersuites("").

Setting an empty cipher suite string on an anonymous context is unusual. If the intent is to force the server to have no valid ciphers (causing handshake failure), this works but could benefit from a brief inline comment explaining the intent. If the goal is something else, this may not behave as expected.

Suggested clarification
         {
             auto client_ctx = make_client_context();
             auto server_ctx = make_anon_context();
+            // Clear ciphersuites to force handshake failure - server has no usable ciphers
             server_ctx.set_ciphersuites( "" );
             run_tls_test_fail( ioc, client_ctx, server_ctx,
                 make_openssl, make_wolfssl );
             ioc.restart();
         }

Comment on lines +79 to +84
template<capy::MutableBufferSequence MB>
capy::task<io_result<std::size_t>>
read(io_stream& ios, MutableBufferSequence const& buffers)
read(io_stream& ios, MB const& bs)
{
consuming_buffers<MutableBufferSequence> consuming(buffers);
std::size_t const total_size = capy::buffer_size(buffers);
consuming_buffers<MB> consuming(bs);
std::size_t const total_size = capy::buffer_size(bs);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update the @param name to match bs.

The parameter was renamed to bs (Line 81), but the doc block still uses buffers, which can desync Doxygen-style docs.

📚 Suggested doc fix
-    `@param` buffers The buffer sequence to read data into.
+    `@param` bs The buffer sequence to read data into.
🤖 Prompt for AI Agents
In `@include/boost/corosio/read.hpp` around lines 79 - 84, The docblock for the
template function read(io_stream& ios, MB const& bs) uses an outdated `@param`
name "buffers"; update the `@param` entry to use "bs" to match the parameter in
the function signature (template<capy::MutableBufferSequence MB>
capy::task<io_result<std::size_t>> read(io_stream& ios, MB const& bs)) so
Doxygen stays in sync.

Comment on lines +619 to +651
void
testReadString()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);

capy::run_async(ioc.get_executor())(
[](socket& a, socket& b) -> capy::task<>
{
std::string send_data = "Hello, this is a test message!";
co_await write(a, capy::const_buffer(
send_data.data(), send_data.size()));
a.close();

// Read into string until EOF
std::string result;
auto [ec, n] = co_await read(b, result);
// EOF is expected
BOOST_TEST(ec == capy::error::eof);
BOOST_TEST_EQ(n, send_data.size());
BOOST_TEST_EQ(result, send_data);
}(s1, s2));

ioc.run();
s1.close();
s2.close();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Double-close on s1.

Similar to testReadAfterPeerClose, socket a (aliased as s1) is closed at line 631 inside the coroutine, then s1.close() is called again at line 643.

Suggested fix
         ioc.run();
-        s1.close();
+        // s1 already closed in coroutine
         s2.close();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void
testReadString()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);
capy::run_async(ioc.get_executor())(
[](socket& a, socket& b) -> capy::task<>
{
std::string send_data = "Hello, this is a test message!";
co_await write(a, capy::const_buffer(
send_data.data(), send_data.size()));
a.close();
// Read into string until EOF
std::string result;
auto [ec, n] = co_await read(b, result);
// EOF is expected
BOOST_TEST(ec == capy::error::eof);
BOOST_TEST_EQ(n, send_data.size());
BOOST_TEST_EQ(result, send_data);
}(s1, s2));
ioc.run();
s1.close();
s2.close();
}
void
testReadString()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);
capy::run_async(ioc.get_executor())(
[](socket& a, socket& b) -> capy::task<>
{
std::string send_data = "Hello, this is a test message!";
co_await write(a, capy::const_buffer(
send_data.data(), send_data.size()));
a.close();
// Read into string until EOF
std::string result;
auto [ec, n] = co_await read(b, result);
// EOF is expected
BOOST_TEST(ec == capy::error::eof);
BOOST_TEST_EQ(n, send_data.size());
BOOST_TEST_EQ(result, send_data);
}(s1, s2));
ioc.run();
// s1 already closed in coroutine
s2.close();
}
🤖 Prompt for AI Agents
In `@test/unit/socket.cpp` around lines 619 - 645, In testReadString() the socket
`s1` (aliased to `a` inside the coroutine) is closed inside the coroutine and
then closed again after ioc.run() causing a double-close; fix by removing the
redundant post-run `s1.close()` (or replace it with a conditional/is_open check)
so only `s2` (or sockets that weren't closed in the coroutine) are closed
outside the coroutine; update references in testReadString(), and keep the
coroutine parameters `socket& a, socket& b` and the existing close call inside
the coroutine intact.

Comment on lines +647 to +680
void
testReadPartialEOF()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);

capy::run_async(ioc.get_executor())(
[](socket& a, socket& b) -> capy::task<>
{
// Send 50 bytes but try to read 100
std::string send_data(50, 'Z');
co_await write(a, capy::const_buffer(
send_data.data(), send_data.size()));
a.close();

char buf[100] = {};
auto [ec, n] = co_await read(b, capy::mutable_buffer(
buf, sizeof(buf)));
// Should get EOF after reading available data
BOOST_TEST(ec == capy::error::eof);
BOOST_TEST_EQ(n, 50u);
BOOST_TEST_EQ(std::string_view(buf, n), send_data);
}(s1, s2));

ioc.run();
s1.close();
s2.close();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Double-close on s1.

Same pattern: a.close() at line 660, then s1.close() at line 672.

Suggested fix
         ioc.run();
-        s1.close();
+        // s1 already closed in coroutine
         s2.close();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void
testReadPartialEOF()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);
capy::run_async(ioc.get_executor())(
[](socket& a, socket& b) -> capy::task<>
{
// Send 50 bytes but try to read 100
std::string send_data(50, 'Z');
co_await write(a, capy::const_buffer(
send_data.data(), send_data.size()));
a.close();
char buf[100] = {};
auto [ec, n] = co_await read(b, capy::mutable_buffer(
buf, sizeof(buf)));
// Should get EOF after reading available data
BOOST_TEST(ec == capy::error::eof);
BOOST_TEST_EQ(n, 50u);
BOOST_TEST_EQ(std::string_view(buf, n), send_data);
}(s1, s2));
ioc.run();
s1.close();
s2.close();
}
void
testReadPartialEOF()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);
capy::run_async(ioc.get_executor())(
[](socket& a, socket& b) -> capy::task<>
{
// Send 50 bytes but try to read 100
std::string send_data(50, 'Z');
co_await write(a, capy::const_buffer(
send_data.data(), send_data.size()));
a.close();
char buf[100] = {};
auto [ec, n] = co_await read(b, capy::mutable_buffer(
buf, sizeof(buf)));
// Should get EOF after reading available data
BOOST_TEST(ec == capy::error::eof);
BOOST_TEST_EQ(n, 50u);
BOOST_TEST_EQ(std::string_view(buf, n), send_data);
}(s1, s2));
ioc.run();
// s1 already closed in coroutine
s2.close();
}
🤖 Prompt for AI Agents
In `@test/unit/socket.cpp` around lines 647 - 674, The test testReadPartialEOF
double-closes the same socket: the coroutine closes 'a' (the s1 socket) via
a.close(), and then the test calls s1.close() again after ioc.run(); remove the
redundant s1.close() (or conditionally skip closing s1) so the socket is only
closed once—update the cleanup at the end of testReadPartialEOF to only close s2
(or neither if both were closed in the coroutine).

// Official repository: https://github.com/cppalliance/corosio
//

// Plan: c:\Users\Vinnie\.cursor\plans\tls_stream_tests_83c24f98.plan.md
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove local development path reference.

This line contains a Windows-specific path to a local .cursor plans directory. It's a development artifact that shouldn't be committed to the repository.

Suggested fix
-// Plan: c:\Users\Vinnie\.cursor\plans\tls_stream_tests_83c24f98.plan.md
-
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Plan: c:\Users\Vinnie\.cursor\plans\tls_stream_tests_83c24f98.plan.md
🤖 Prompt for AI Agents
In `@test/unit/tls/cross_ssl_stream.cpp` at line 10, Remove the local Windows
development path reference found in the comment on cross_ssl_stream.cpp (the
line containing
"c:\Users\Vinnie\.cursor\plans\tls_stream_tests_83c24f98.plan.md"); delete that
hard-coded path or replace it with a repository-relative or generic reference
(e.g., remove the .cursor path or replace with a neutral
"plans/tls_stream_tests.plan" or a short TODO) so no user-specific/local
filesystem paths remain in test/unit/tls/cross_ssl_stream.cpp.

Comment on lines +65 to +94
// Server private key from Boost.Beast
inline constexpr char const* server_key_pem =
"-----BEGIN PRIVATE KEY-----\n"
"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCz0GwgnxSBhygx\n"
"BdhTHGx5LDLIJSuIDJ6nMwZFvAjdhLnB/vOTLppr5MKxqQHEpYdyDYGD1noBoz4T\n"
"iIRj5JapChMgx58NLq5QyXkHV/ONT7yi8x05P41c2F9pBEnUwUxIUG1Cb6AN0cZW\n"
"F/wSMOZ0w3DoBhnl1sdQfQiS25MTK6x4tATmWm9SJc2lsjWptbyIN6hFXLYPXTwn\n"
"YzCLvv1EK6Ft7tMPc/FcJpd/wYHgl8shDmY7rV+AiGTxUU35V0AzpJlmvct5aJV/\n"
"5vSRRLwT9qLZSddE9zy/0rovC5GML6S7BUC4lIzJ8yxzOzSStBPxvdrOobSSNlRZ\n"
"IlE7gnyNAgMBAAECggEAY0RorQmldGx9D7M+XYOPjsWLs1px0cXFwGA20kCgVEp1\n"
"kleBeHt93JqJsTKwOzN2tswl9/ZrnIPWPUpcbBlB40ggjzQk5k4jBY50Nk2jsxuV\n"
"9A9qzrP7AoqhAYTQjZe42SMtbkPZhEeOyvCqxBAi6csLhcv4eB4+In0kQo7dfvLs\n"
"Xu/3WhSsuAWqdD9EGnhD3n+hVTtgiasRe9318/3R9DzP+IokoQGOtXm+1dsfP0mV\n"
"8XGzQHBpUtJNn0yi6SC4kGEQuKkX33zORlSnZgT5VBLofNgra0THd7x3atOx1lbr\n"
"V0QizvCdBa6j6FwhOQwW8UwgOCnUbWXl/Xn4OaofMQKBgQDdRXSMyys7qUMe4SYM\n"
"Mdawj+rjv0Hg98/xORuXKEISh2snJGKEwV7L0vCn468n+sM19z62Axz+lvOUH8Qr\n"
"hLkBNqJvtIP+b0ljRjem78K4a4qIqUlpejpRLw6a/+44L76pMJXrYg3zdBfwzfwu\n"
"b9NXdwHzWoNuj4v36teGP6xOUwKBgQDQCT52XX96NseNC6HeK5BgWYYjjxmhksHi\n"
"stjzPJKySWXZqJpHfXI8qpOd0Sd1FHB+q1s3hand9c+Rxs762OXlqA9Q4i+4qEYZ\n"
"qhyRkTsl+2BhgzxmoqGd5gsVT7KV8XqtuHWLmetNEi+7+mGSFf2iNFnonKlvT1JX\n"
"4OQZC7ntnwKBgH/ORFmmaFxXkfteFLnqd5UYK5ZMvGKTALrWP4d5q2BEc7HyJC2F\n"
"+5lDR9nRezRedS7QlppPBgpPanXeO1LfoHSA+CYJYEwwP3Vl83Mq/Y/EHgp9rXeN\n"
"L+4AfjEtLo2pljjnZVDGHETIg6OFdunjkXDtvmSvnUbZBwG11bMnSAEdAoGBAKFw\n"
"qwJb6FNFM3JnNoQctnuuvYPWxwM1yjRMqkOIHCczAlD4oFEeLoqZrNhpuP8Ij4wd\n"
"GjpqBbpzyVLNP043B6FC3C/edz4Lh+resjDczVPaUZ8aosLbLiREoxE0udfWf2dU\n"
"oBNnrMwwcs6jrRga7Kr1iVgUSwBQRAxiP2CYUv7tAoGBAKdPdekPNP/rCnHkKIkj\n"
"o13pr+LJ8t+15vVzZNHwPHUWiYXFhG8Ivx7rqLQSPGcuPhNss3bg1RJiZAUvF6fd\n"
"e6QS4EZM9dhhlO2FmPQCJMrRVDXaV+9TcJZXCbclQnzzBus9pwZZyw4Anxo0vmir\n"
"nOMOU6XI4lO9Xge/QDEN4Y2R\n"
"-----END PRIVATE KEY-----\n";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the repository structure and locate the test file
find . -type f -name "*.hpp" | grep -i test | head -20

Repository: cppalliance/corosio

Length of output: 178


🏁 Script executed:

# Check if the file exists and examine its context
ls -la test/unit/tls/test_utils.hpp 2>/dev/null || echo "File not found in standard location"

Repository: cppalliance/corosio

Length of output: 141


🏁 Script executed:

# Search for secret scanning configuration files (gitleaks, truffleHog, etc.)
find . -type f \( -name ".gitleaksignore" -o -name ".gitleaks.toml" -o -name "gitleaks.toml" -o -name ".gitignore" -o -name ".secretsignore" -o -name ".pre-commit-config.yaml" \) | head -20

Repository: cppalliance/corosio

Length of output: 92


🏁 Script executed:

# Check CI configuration files for secret scanning setup
find . -type f \( -name ".github" -o -name ".gitlab-ci.yml" -o -name ".circleci" -o -name "*.yml" -o -name "*.yaml" \) | grep -E "(\.github|\.gitlab-ci|\.circleci)" | head -20

Repository: cppalliance/corosio

Length of output: 89


🏁 Script executed:

# Look for other embedded test credentials/keys in the repo
rg -i "(begin (private|public|rsa) key|begin certificate|-----BEGIN)" --type cpp --type hpp --type txt | head -30

Repository: cppalliance/corosio

Length of output: 90


🏁 Script executed:

# Check if there's documentation about test fixtures or test data handling
find . -type f -name "README*" -o -name "TESTING*" -o -name "*.md" | xargs grep -l "test.*key\|fixture\|credential" 2>/dev/null | head -10

Repository: cppalliance/corosio

Length of output: 94


🏁 Script executed:

# Let's examine the CI configuration
cat .github/workflows/ci.yml

Repository: cppalliance/corosio

Length of output: 21575


🏁 Script executed:

# Check .gitignore files
cat .gitignore
cat doc/.gitignore

Repository: cppalliance/corosio

Length of output: 175


🏁 Script executed:

# Read the test file to verify the embedded key and understand its purpose
head -100 test/unit/tls/test_utils.hpp

Repository: cppalliance/corosio

Length of output: 5259


🏁 Script executed:

# Search for other embedded credentials/keys using a broader approach
grep -r "BEGIN PRIVATE KEY\|BEGIN CERTIFICATE\|BEGIN RSA KEY" . --include="*.hpp" --include="*.cpp" --include="*.h" | head -20

Repository: cppalliance/corosio

Length of output: 265


🏁 Script executed:

# Check if there are any comments in the test file about the key being a test fixture
grep -B 5 -A 5 "BEGIN PRIVATE KEY" test/unit/tls/test_utils.hpp | head -30

Repository: cppalliance/corosio

Length of output: 671


Add .gitleaksignore allowlist entry for embedded test credentials. This file intentionally embeds test certificates and keys from Boost.Beast (documented in-code), but there is no secret scanning configuration (gitleaks, etc.) currently enabled in CI. While not an immediate risk, add a .gitleaksignore entry pointing to test/unit/tls/test_utils.hpp to prevent false positives if secret scanning tools are enabled in the future.

🧰 Tools
🪛 Gitleaks (8.30.0)

[high] 67-94: Identified a Private Key, which may compromise cryptographic security and sensitive data encryption.

(private-key)

🤖 Prompt for AI Agents
In `@test/unit/tls/test_utils.hpp` around lines 65 - 94, Add a .gitleaksignore
allowlist entry to prevent false positives for the embedded Boost.Beast test
credentials: identify the constant server_key_pem in test_utils.hpp and
whitelist that file (or a pattern matching this test file) in .gitleaksignore
with a brief justification mentioning the key is an intentionally embedded test
certificate from Boost.Beast.

Comment on lines +210 to +239
/** Test bidirectional data transfer on connected streams. */
inline capy::task<>
test_stream( io_stream& a, io_stream& b )
{
char buf[32] = {};

// Write from a, read from b
auto [ec1, n1] = co_await a.write_some(
capy::const_buffer( "hello", 5 ) );
BOOST_TEST( !ec1 );
BOOST_TEST_EQ( n1, 5u );

auto [ec2, n2] = co_await b.read_some(
capy::mutable_buffer( buf, sizeof( buf ) ) );
BOOST_TEST( !ec2 );
BOOST_TEST_EQ( n2, 5u );
BOOST_TEST_EQ( std::string_view( buf, n2 ), "hello" );

// Write from b, read from a
auto [ec3, n3] = co_await b.write_some(
capy::const_buffer( "world", 5 ) );
BOOST_TEST( !ec3 );
BOOST_TEST_EQ( n3, 5u );

auto [ec4, n4] = co_await a.read_some(
capy::mutable_buffer( buf, sizeof( buf ) ) );
BOOST_TEST( !ec4 );
BOOST_TEST_EQ( n4, 5u );
BOOST_TEST_EQ( std::string_view( buf, n4 ), "world" );
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

read_some can return partial data; test may be flaky.
Line 222 and Line 234 assume a full 5‑byte read. read_some permits shorter reads, so this can sporadically fail. Consider looping until the expected length is collected.

🐛 Suggested fix
-    auto [ec2, n2] = co_await b.read_some(
-        capy::mutable_buffer( buf, sizeof( buf ) ) );
-    BOOST_TEST( !ec2 );
-    BOOST_TEST_EQ( n2, 5u );
-    BOOST_TEST_EQ( std::string_view( buf, n2 ), "hello" );
+    std::size_t n2 = 0;
+    while( n2 < 5 )
+    {
+        auto [ec2, n] = co_await b.read_some(
+            capy::mutable_buffer( buf + n2, 5 - n2 ) );
+        BOOST_TEST( !ec2 );
+        n2 += n;
+    }
+    BOOST_TEST_EQ( n2, 5u );
+    BOOST_TEST_EQ( std::string_view( buf, n2 ), "hello" );
@@
-    auto [ec4, n4] = co_await a.read_some(
-        capy::mutable_buffer( buf, sizeof( buf ) ) );
-    BOOST_TEST( !ec4 );
-    BOOST_TEST_EQ( n4, 5u );
-    BOOST_TEST_EQ( std::string_view( buf, n4 ), "world" );
+    std::size_t n4 = 0;
+    while( n4 < 5 )
+    {
+        auto [ec4, n] = co_await a.read_some(
+            capy::mutable_buffer( buf + n4, 5 - n4 ) );
+        BOOST_TEST( !ec4 );
+        n4 += n;
+    }
+    BOOST_TEST_EQ( n4, 5u );
+    BOOST_TEST_EQ( std::string_view( buf, n4 ), "world" );
🤖 Prompt for AI Agents
In `@test/unit/tls/test_utils.hpp` around lines 210 - 239, The test_stream helper
assumes read_some returns the full 5 bytes which can be flaky; change the two
reads that call b.read_some and a.read_some to accumulate into buf in a loop
until the expected number of bytes (5) is received, checking the error (ec) on
each iteration and advancing the buffer pointer/count accordingly; keep the
existing write_some calls and assertions for their returned n values, but
replace the single-read assertions for ec and n with assertions after the loop
that the total bytes read equals 5 and that the concatenated data equals the
expected string ("hello" and "world"); this touches the test_stream function and
the calls to io_stream::read_some.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/corosio/src/detail/iocp/sockets.cpp (1)

193-215: Avoid dangling impl_out when impl_owner_out is null (possible UAF).

If impl_out is provided but impl_owner_out is null, peer_impl.reset() destroys the impl while the raw pointer is still returned. Also, on failure, impl_owner_out is left untouched. Consider enforcing ownership output or returning an error, and clear impl_owner_out on failure.

🔧 Suggested safeguard
     if (success && accepted_socket != INVALID_SOCKET && peer_impl)
     {
+        if (impl_out && !impl_owner_out)
+        {
+            if (ec_out)
+                *ec_out = make_error_code(system::errc::invalid_argument);
+            if (impl_out)
+                *impl_out = nullptr;
+            ::closesocket(accepted_socket);
+            accepted_socket = INVALID_SOCKET;
+            peer_impl.reset();
+            d.dispatch(h).resume();
+            return;
+        }
         // Pass impl and ownership to awaitable for assignment to peer socket
         if (impl_out)
             *impl_out = peer_impl.get();
         if (impl_owner_out)
             *impl_owner_out = std::move(peer_impl);
         else
             peer_impl.reset();
     }
     else
     {
         // Cleanup on failure
         if (accepted_socket != INVALID_SOCKET)
         {
             ::closesocket(accepted_socket);
             accepted_socket = INVALID_SOCKET;
         }

         // Release the peer_impl shared_ptr
         peer_impl.reset();

         if (impl_out)
             *impl_out = nullptr;
+        if (impl_owner_out)
+            *impl_owner_out = nullptr;
     }
🤖 Fix all issues with AI agents
In `@test/unit/socket.cpp`:
- Around line 434-468: In testWriteAfterPeerClose(), the socket s2 (aliased as b
inside the coroutine) is closed twice: once inside the coroutine (b.close()) and
again after ioc.run() (s2.close()); remove the redundant second close or guard
it with an is_open() check. Edit the testWriteAfterPeerClose function to either
delete the trailing s2.close() call or replace it with if (s2.is_open())
s2.close(), keeping s1.close() as-is and leaving the coroutine logic (b.close(),
write loop) unchanged.
- Around line 520-564: The testCloseWhileReading test double-closes socket s2
(aliased as b): b.close() is called inside the coroutine and then s2.close() is
invoked again after ioc.run(); remove the redundant second close (or guard it
with a check) so that s2 is not closed twice — update the teardown at the end of
testCloseWhileReading to avoid calling s2.close() if the coroutine already
closed it (identify the coroutine lambda nested_coro, the local socket alias b,
and the final s2.close() call to adjust).
- Around line 402-432: In testReadAfterPeerClose: remove the redundant close of
s1 after ioc.run() because socket a (aliased to s1) is already closed inside the
coroutine via a.close(); update or remove the trailing s1.close() call (leave
s2.close() if needed) so the socket isn't closed twice and do so by editing the
testReadAfterPeerClose function to eliminate the duplicate s1.close()
invocation.
♻️ Duplicate comments (6)
include/boost/corosio/read.hpp (1)

79-84: Sync Doxygen @param name with bs.

Doc still says buffers, but the signature uses bs. Please update to avoid drift.

📚 Suggested doc fix
-    `@param` buffers The buffer sequence to read data into.
+    `@param` bs The buffer sequence to read data into.
test/unit/socket.cpp (2)

625-651: Double-close on s1 (duplicate).

This issue was already flagged in a previous review. Socket a (aliased to s1) is closed inside the coroutine at line 637, then s1.close() is called again at line 649.


653-680: Double-close on s1 (duplicate).

This issue was already flagged in a previous review. Socket a (aliased to s1) is closed inside the coroutine at line 666, then s1.close() is called again at line 678.

test/unit/tls/test_utils.hpp (3)

65-94: Test credentials flagged by Gitleaks - already addressed in previous review.

The embedded private key is intentionally included for TLS testing (sourced from Boost.Beast). A previous review already recommended adding a .gitleaksignore entry to prevent false positives if secret scanning is enabled.


96-114: Placeholder certificate content - already addressed in previous review.

The wrong_ca_cert_pem contains invalid placeholder data (repeating Z characters in public key, Dummy prefix in signature). A previous review already flagged this as needing replacement with a real self-signed certificate to ensure the "wrong CA" scenario tests the intended verification failure rather than failing during context setup.


210-239: Partial read handling - already addressed in previous review.

A previous review noted that read_some may return fewer than 5 bytes, making the assertions on lines 225 and 237 potentially flaky. The suggested fix was to loop until the expected length is collected.

🧹 Nitpick comments (2)
test/unit/tls/test_utils.hpp (2)

188-202: Add default case to suppress compiler warnings.

The switch covers all context_mode values, but many compilers emit warnings about missing default cases. The fallback return after the switch (line 201) handles this at runtime, but adding an explicit default is cleaner.

💡 Suggested fix
     switch( mode )
     {
     case context_mode::anon:
         return { make_anon_context(), make_anon_context() };
     case context_mode::shared_cert:
     {
         auto ctx = make_server_context();
         ctx.add_certificate_authority( ca_cert_pem );
         return { ctx, ctx };
     }
     case context_mode::separate_cert:
         return { make_client_context(), make_server_context() };
+    default:
+        break;
     }
     return { make_anon_context(), make_anon_context() };

415-441: Potential double-close of sockets and unused captures.

Two concerns:

  1. Double-close: If the timer expires, sockets are closed in the callback (lines 426-427). Then lines 439-440 close them unconditionally again. While this is typically safe, it's redundant and could cause issues depending on the socket implementation.

  2. Unused captures: client_done and server_done are captured but only (void)-cast (lines 418-419). They serve no purpose in the callback and can be removed.

♻️ Suggested fix
     capy::run_async( ioc.get_executor() )(
-        [&timeout, &s1, &s2, &client_done, &server_done]() -> capy::task<>
+        [&timeout, &s1, &s2]() -> capy::task<>
         {
-            (void)client_done;
-            (void)server_done;
             auto [ec] = co_await timeout.wait();
             if( !ec )
             {
                 // Timer expired - cancel pending operations then close sockets
                 s1.cancel();
                 s2.cancel();
                 s1.close();
                 s2.close();
             }
         }() );

     ioc.run();

     // Cancel timer if handshakes completed before timeout
     timeout.cancel();

     // At least one side should have failed
     BOOST_TEST( client_failed || server_failed );

-    s1.close();
-    s2.close();
+    // Note: sockets may already be closed by timer callback
 }

Comment on lines +402 to +432
void
testReadAfterPeerClose()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);

capy::run_async(ioc.get_executor())(
[](socket& a, socket& b) -> capy::task<>
{
// Write data then close
co_await a.write_some(capy::const_buffer("final", 5));
a.close();

// Read the data
char buf[32] = {};
auto [ec1, n1] = co_await b.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
BOOST_TEST(!ec1);
BOOST_TEST_EQ(std::string_view(buf, n1), "final");

// Next read should get EOF (0 bytes or error)
auto [ec2, n2] = co_await b.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
// EOF indicated by error or zero bytes
BOOST_TEST(ec2 || n2 == 0);
}(s1, s2));

ioc.run();
s1.close();
s2.close();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Double-close on s1.

Socket a (aliased to s1) is closed inside the coroutine at line 413, then s1.close() is called again at line 430 after ioc.run().

Suggested fix
         ioc.run();
-        s1.close();
+        // s1 already closed in coroutine
         s2.close();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void
testReadAfterPeerClose()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);
capy::run_async(ioc.get_executor())(
[](socket& a, socket& b) -> capy::task<>
{
// Write data then close
co_await a.write_some(capy::const_buffer("final", 5));
a.close();
// Read the data
char buf[32] = {};
auto [ec1, n1] = co_await b.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
BOOST_TEST(!ec1);
BOOST_TEST_EQ(std::string_view(buf, n1), "final");
// Next read should get EOF (0 bytes or error)
auto [ec2, n2] = co_await b.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
// EOF indicated by error or zero bytes
BOOST_TEST(ec2 || n2 == 0);
}(s1, s2));
ioc.run();
s1.close();
s2.close();
}
void
testReadAfterPeerClose()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);
capy::run_async(ioc.get_executor())(
[](socket& a, socket& b) -> capy::task<>
{
// Write data then close
co_await a.write_some(capy::const_buffer("final", 5));
a.close();
// Read the data
char buf[32] = {};
auto [ec1, n1] = co_await b.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
BOOST_TEST(!ec1);
BOOST_TEST_EQ(std::string_view(buf, n1), "final");
// Next read should get EOF (0 bytes or error)
auto [ec2, n2] = co_await b.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
// EOF indicated by error or zero bytes
BOOST_TEST(ec2 || n2 == 0);
}(s1, s2));
ioc.run();
// s1 already closed in coroutine
s2.close();
}
🤖 Prompt for AI Agents
In `@test/unit/socket.cpp` around lines 402 - 432, In testReadAfterPeerClose:
remove the redundant close of s1 after ioc.run() because socket a (aliased to
s1) is already closed inside the coroutine via a.close(); update or remove the
trailing s1.close() call (leave s2.close() if needed) so the socket isn't closed
twice and do so by editing the testReadAfterPeerClose function to eliminate the
duplicate s1.close() invocation.

Comment on lines +434 to +468
void
testWriteAfterPeerClose()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);

capy::run_async(ioc.get_executor())(
[](socket& a, socket& b) -> capy::task<>
{
// Close the receiving end
b.close();

// Give OS time to process the close
timer t(a.context());
t.expires_after(std::chrono::milliseconds(50));
co_await t.wait();

// Writing to closed peer should eventually fail
system::error_code last_ec;
for (int i = 0; i < 10; ++i)
{
auto [ec, n] = co_await a.write_some(
capy::const_buffer("data", 4));
last_ec = ec;
if (ec)
break;
}
// Should get an error (broken pipe or similar)
BOOST_TEST(last_ec);
}(s1, s2));

ioc.run();
s1.close();
s2.close();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Double-close on s2.

Socket b (aliased to s2) is closed inside the coroutine at line 444, then s2.close() is called again at line 467 after ioc.run().

Suggested fix
         ioc.run();
         s1.close();
-        s2.close();
+        // s2 already closed in coroutine
🤖 Prompt for AI Agents
In `@test/unit/socket.cpp` around lines 434 - 468, In testWriteAfterPeerClose(),
the socket s2 (aliased as b inside the coroutine) is closed twice: once inside
the coroutine (b.close()) and again after ioc.run() (s2.close()); remove the
redundant second close or guard it with an is_open() check. Edit the
testWriteAfterPeerClose function to either delete the trailing s2.close() call
or replace it with if (s2.is_open()) s2.close(), keeping s1.close() as-is and
leaving the coroutine logic (b.close(), write loop) unchanged.

Comment on lines +520 to +564
void
testCloseWhileReading()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);

capy::run_async(ioc.get_executor())(
[&](socket& a, socket& b) -> capy::task<>
{
timer t(a.context());
t.expires_after(std::chrono::milliseconds(50));

bool read_done = false;
system::error_code read_ec;

// Store lambda in variable to ensure it outlives the coroutine.
// Lambda coroutines capture 'this' by reference, so the lambda
// must remain alive while the coroutine is suspended.
auto nested_coro = [&b, &read_done, &read_ec]() -> capy::task<>
{
char buf[32];
auto [ec, n] = co_await b.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
read_ec = ec;
read_done = true;
};
capy::run_async(ioc.get_executor())(nested_coro());

// Wait then close the socket
co_await t.wait();
b.close();

timer t2(a.context());
t2.expires_after(std::chrono::milliseconds(50));
co_await t2.wait();

BOOST_TEST(read_done);
// Close should cancel pending operations
BOOST_TEST(read_ec == capy::cond::canceled);
}(s1, s2));

ioc.run();
s1.close();
s2.close();
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Double-close on s2.

Socket b (aliased to s2) is closed inside the coroutine at line 550, then s2.close() is called again at line 563 after ioc.run().

Suggested fix
         ioc.run();
         s1.close();
-        s2.close();
+        // s2 already closed in coroutine
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void
testCloseWhileReading()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);
capy::run_async(ioc.get_executor())(
[&](socket& a, socket& b) -> capy::task<>
{
timer t(a.context());
t.expires_after(std::chrono::milliseconds(50));
bool read_done = false;
system::error_code read_ec;
// Store lambda in variable to ensure it outlives the coroutine.
// Lambda coroutines capture 'this' by reference, so the lambda
// must remain alive while the coroutine is suspended.
auto nested_coro = [&b, &read_done, &read_ec]() -> capy::task<>
{
char buf[32];
auto [ec, n] = co_await b.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
read_ec = ec;
read_done = true;
};
capy::run_async(ioc.get_executor())(nested_coro());
// Wait then close the socket
co_await t.wait();
b.close();
timer t2(a.context());
t2.expires_after(std::chrono::milliseconds(50));
co_await t2.wait();
BOOST_TEST(read_done);
// Close should cancel pending operations
BOOST_TEST(read_ec == capy::cond::canceled);
}(s1, s2));
ioc.run();
s1.close();
s2.close();
}
void
testCloseWhileReading()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);
capy::run_async(ioc.get_executor())(
[&](socket& a, socket& b) -> capy::task<>
{
timer t(a.context());
t.expires_after(std::chrono::milliseconds(50));
bool read_done = false;
system::error_code read_ec;
// Store lambda in variable to ensure it outlives the coroutine.
// Lambda coroutines capture 'this' by reference, so the lambda
// must remain alive while the coroutine is suspended.
auto nested_coro = [&b, &read_done, &read_ec]() -> capy::task<>
{
char buf[32];
auto [ec, n] = co_await b.read_some(
capy::mutable_buffer(buf, sizeof(buf)));
read_ec = ec;
read_done = true;
};
capy::run_async(ioc.get_executor())(nested_coro());
// Wait then close the socket
co_await t.wait();
b.close();
timer t2(a.context());
t2.expires_after(std::chrono::milliseconds(50));
co_await t2.wait();
BOOST_TEST(read_done);
// Close should cancel pending operations
BOOST_TEST(read_ec == capy::cond::canceled);
}(s1, s2));
ioc.run();
s1.close();
// s2 already closed in coroutine
}
🤖 Prompt for AI Agents
In `@test/unit/socket.cpp` around lines 520 - 564, The testCloseWhileReading test
double-closes socket s2 (aliased as b): b.close() is called inside the coroutine
and then s2.close() is invoked again after ioc.run(); remove the redundant
second close (or guard it with a check) so that s2 is not closed twice — update
the teardown at the end of testCloseWhileReading to avoid calling s2.close() if
the coroutine already closed it (identify the coroutine lambda nested_coro, the
local socket alias b, and the final s2.close() call to adjust).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
include/boost/corosio/acceptor.hpp (1)

272-290: epoll_acceptor_impl::accept signature is out of sync with the interface.

The abstract base class acceptor_impl::accept now requires 6 parameters including the new std::shared_ptr<void>* parameter, but the epoll implementation in src/corosio/src/detail/epoll/sockets.hpp line 120-125 still has only 5 parameters. The Windows implementation (win_acceptor_impl) is correctly updated with all 6 parameters. Update the epoll signature to match:

void accept(
    std::coroutine_handle<>,
    capy::any_executor_ref,
    std::stop_token,
    system::error_code*,
    io_object::io_object_impl**,
    std::shared_ptr<void>*) override;

Then update the implementation in the corresponding .cpp file to handle the new parameter.

🤖 Fix all issues with AI agents
In `@test/unit/socket.cpp`:
- Around line 349-393: The loops in testLargeBuffer calling write_some and
read_some can spin forever if an I/O error occurs because BOOST_TEST(!ec)
doesn't abort and total_sent/total_recv won't advance; update both loops (the
write loop around a.write_some and the read loop around b.read_some) to check
for error or zero bytes (if ec || n == 0) immediately after the co_await, and on
that condition fail the test and exit the coroutine (e.g., call
BOOST_TEST(false) or equivalent and co_return/return) so the test stops instead
of looping indefinitely.
♻️ Duplicate comments (5)
test/unit/socket.cpp (5)

402-432: Avoid double-close on s1.
a.close() happens in the coroutine, then s1.close() runs after ioc.run().


434-468: Avoid double-close on s2.
b.close() happens in the coroutine, then s2.close() runs after ioc.run().


520-564: Avoid double-close on s2.
b.close() happens in the coroutine, then s2.close() runs after ioc.run().


625-651: Avoid double-close on s1.
a.close() happens in the coroutine, then s1.close() runs after ioc.run().


653-680: Avoid double-close on s1.
a.close() happens in the coroutine, then s1.close() runs after ioc.run().

🧹 Nitpick comments (2)
src/corosio/src/detail/iocp/sockets.hpp (1)

55-55: Consider documenting why operations override operator().

The operations connect_op, read_op, and write_op now override operator() (declared on lines 55, 71, 87). From the .cpp file, these overrides call the base class then reset impl_ptr. A brief doc comment would clarify this pattern for maintainers.

Also applies to: 71-71, 87-87

src/corosio/src/detail/iocp/sockets.cpp (1)

495-502: Missing empty_buffer = true for write_some empty buffer case.

For consistency with read_some, consider setting op.empty_buffer = true here. While it doesn't affect EOF detection (writes don't trigger EOF), maintaining symmetry improves code clarity and future-proofs against potential uses of the flag.

Suggested change
     // Handle empty buffer: complete immediately with 0 bytes
     if (op.wsabuf_count == 0)
     {
         op.bytes_transferred = 0;
         op.error = 0;
+        op.empty_buffer = true;
         svc_.post(&op);
         return;
     }

Comment on lines +349 to +393
void
testLargeBuffer()
{
io_context ioc;
auto [s1, s2] = test::make_socket_pair(ioc);

capy::run_async(ioc.get_executor())(
[](socket& a, socket& b) -> capy::task<>
{
// 64KB data - larger than typical TCP segment
constexpr std::size_t size = 64 * 1024;
std::vector<char> send_data(size);
for (std::size_t i = 0; i < size; ++i)
send_data[i] = static_cast<char>(i & 0xFF);

std::vector<char> recv_data(size);
std::size_t total_sent = 0;
std::size_t total_recv = 0;

// Send all data (may take multiple write_some calls)
while (total_sent < size)
{
auto [ec, n] = co_await a.write_some(
capy::const_buffer(
send_data.data() + total_sent,
size - total_sent));
BOOST_TEST(!ec);
total_sent += n;
}

// Receive all data (may take multiple read_some calls)
while (total_recv < size)
{
auto [ec, n] = co_await b.read_some(
capy::mutable_buffer(
recv_data.data() + total_recv,
size - total_recv));
BOOST_TEST(!ec);
total_recv += n;
}

BOOST_TEST_EQ(total_sent, size);
BOOST_TEST_EQ(total_recv, size);
BOOST_TEST(send_data == recv_data);
}(s1, s2));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for read_some and write_some definitions in headers
find . -type f -name "*.hpp" -o -name "*.h" | head -20

Repository: cppalliance/corosio

Length of output: 872


🏁 Script executed:

#!/bin/bash
# Search for read_some/write_some contract details
rg -n "read_some|write_some" --type-list | head -10

Repository: cppalliance/corosio

Length of output: 452


🏁 Script executed:

#!/bin/bash
# Find socket implementations and their documentation
fd -e hpp -e h | xargs grep -l "read_some\|write_some" | head -10

Repository: cppalliance/corosio

Length of output: 434


🏁 Script executed:

#!/bin/bash
# Look for capy socket implementation
find . -path "*/capy*" -name "*.hpp" -o -path "*/capy*" -name "*.h" | head -20

Repository: cppalliance/corosio

Length of output: 45


🌐 Web query:

BOOST_TEST framework behavior continue on failure abort

💡 Result:

Short answer:

  • BOOST_CHECK, BOOST_CHECK_EQUAL, BOOST_CHECK_MESSAGE, BOOST_ERROR — report a failure and continue executing the current test case (non‑fatal). [1]
  • BOOST_REQUIRE, BOOST_FAIL (and throwing an exception) — abort the current test case immediately (fatal for that test case). [1]
  • Fatal system errors (signals / access violations) are handled separately by the framework (caught by default); you can disable catching to get a core dump or to let the process abort. Use --catch_system_errors=no to let such failures abort the process. [2][3]

Sources:
[1] Boost.Test tutorial / examples (behavior of BOOST_CHECK vs BOOST_REQUIRE).
[2] Boost.Test command-line usage recommendations (catch_system_errors).
[3] Boost.Test reference: catch_system_errors option.


🌐 Web query:

read_some write_some contract zero bytes return asio

💡 Result:

Short answer: read_some/write_some are allowed to return 0 only in these cases:

  • You supplied an empty buffer sequence (total buffer size == 0) — then 0 bytes are read/written. [1][2]
  • An error occurred (including EOF for reads). With the throwing overloads read_some throws (and read signals EOF via boost::asio::error::eof); with the ec overloads the error is reported in ec and write_some's ec-overload returns 0 on error. [1][3]
    Otherwise the documented contract is that the call blocks (or completes) only after one or more bytes are transferred or an error occurs — so a successful non-error call should return >0. [1][3][4]

Sources:

  • Boost.Asio basic_stream_socket::read_some (remarks/return/exceptions). [1]
  • Boost.Asio basic_stream_socket::write_some (return-value notes; ec overload returns 0 on error). [3]
  • Common practical cause: empty buffer -> read_some/read_some returns 0. (explained in StackOverflow answer). [2]

🏁 Script executed:

#!/bin/bash
# Read socket.hpp to understand read_some/write_some implementation
wc -l include/boost/corosio/socket.hpp

Repository: cppalliance/corosio

Length of output: 99


🏁 Script executed:

#!/bin/bash
# Search for read_some and write_some in socket.hpp
rg -n "read_some|write_some" include/boost/corosio/socket.hpp -A 5

Repository: cppalliance/corosio

Length of output: 393


🏁 Script executed:

#!/bin/bash
# Look at io_result to understand the return type
cat include/boost/corosio/io_result.hpp

Repository: cppalliance/corosio

Length of output: 5124


🏁 Script executed:

#!/bin/bash
# Read socket.hpp to find method implementations
head -100 include/boost/corosio/socket.hpp

Repository: cppalliance/corosio

Length of output: 2915


🏁 Script executed:

#!/bin/bash
# Search for read_some method in socket.hpp
rg -n "read_some" include/boost/corosio/socket.hpp -B 2 -A 10

Repository: cppalliance/corosio

Length of output: 584


🏁 Script executed:

#!/bin/bash
# Search for write_some method in socket.hpp
rg -n "write_some" include/boost/corosio/socket.hpp -B 2 -A 10

Repository: cppalliance/corosio

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Search for read_some and write_some across all headers
rg -n "read_some|write_some" include/ -A 5

Repository: cppalliance/corosio

Length of output: 13377


🏁 Script executed:

#!/bin/bash
# Check the io_stream base class
cat include/boost/corosio/io_stream.hpp

Repository: cppalliance/corosio

Length of output: 7903


Add guards to prevent infinite loops on write/read errors.

The test loops calling write_some/read_some without checking for errors that stop progress. Per the documented contract, these functions return n=0 when an error occurs (ec != 0). Since BOOST_TEST(!ec) doesn't abort the test, a failed I/O operation leaves total_sent/total_recv unchanged, causing the loop to spin indefinitely.

Add early exits on ec || n == 0:

Suggested guards
                 while (total_sent < size)
                 {
                     auto [ec, n] = co_await a.write_some(
                         capy::const_buffer(
                             send_data.data() + total_sent,
                             size - total_sent));
                     BOOST_TEST(!ec);
+                    if (ec || n == 0)
+                        co_return;
                     total_sent += n;
                 }
@@
                 while (total_recv < size)
                 {
                     auto [ec, n] = co_await b.read_some(
                         capy::mutable_buffer(
                             recv_data.data() + total_recv,
                             size - total_recv));
                     BOOST_TEST(!ec);
+                    if (ec || n == 0)
+                        co_return;
                     total_recv += n;
                 }
🤖 Prompt for AI Agents
In `@test/unit/socket.cpp` around lines 349 - 393, The loops in testLargeBuffer
calling write_some and read_some can spin forever if an I/O error occurs because
BOOST_TEST(!ec) doesn't abort and total_sent/total_recv won't advance; update
both loops (the write loop around a.write_some and the read loop around
b.read_some) to check for error or zero bytes (if ec || n == 0) immediately
after the co_await, and on that condition fail the test and exit the coroutine
(e.g., call BOOST_TEST(false) or equivalent and co_return/return) so the test
stops instead of looping indefinitely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants