Skip to content

Commit 0f76c89

Browse files
committed
TLS shutdown tests and fixes
1 parent 9d462b9 commit 0f76c89

5 files changed

Lines changed: 297 additions & 3 deletions

File tree

src/openssl/src/openssl_stream.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,11 @@ struct openssl_stream_impl_
357357
{
358358
if(ec == make_error_code(capy::error::eof))
359359
{
360-
// Check if we got a proper shutdown
360+
// Check if we got a proper TLS shutdown
361361
if(SSL_get_shutdown(ssl_) & SSL_RECEIVED_SHUTDOWN)
362362
ec = make_error_code(capy::error::eof);
363+
else
364+
ec = make_error_code(capy::error::stream_truncated);
363365
}
364366
goto done;
365367
}
@@ -373,7 +375,7 @@ struct openssl_stream_impl_
373375
{
374376
unsigned long ssl_err = ERR_get_error();
375377
if(ssl_err == 0)
376-
ec = make_error_code(capy::error::eof);
378+
ec = make_error_code(capy::error::stream_truncated);
377379
else
378380
ec = system::error_code(
379381
static_cast<int>(ssl_err), system::system_category());

src/wolfssl/src/wolfssl_stream.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,22 @@ struct wolfssl_stream_impl_
421421
if(read_in_pos_ == read_in_len_) { read_in_pos_ = 0; read_in_len_ = 0; }
422422
capy::mutable_buffer buf(read_in_buf_.data() + read_in_len_, read_in_buf_.size() - read_in_len_);
423423
auto [rec, rn] = co_await do_underlying_read(buf);
424-
if(rec) { ec = rec; goto done; }
424+
if(rec)
425+
{
426+
if(rec == make_error_code(capy::error::eof))
427+
{
428+
// Check if we got a proper TLS shutdown
429+
if(wolfSSL_get_shutdown(ssl_) & SSL_RECEIVED_SHUTDOWN)
430+
ec = make_error_code(capy::error::eof);
431+
else
432+
ec = make_error_code(capy::error::stream_truncated);
433+
}
434+
else
435+
{
436+
ec = rec;
437+
}
438+
goto done;
439+
}
425440
read_in_len_ += rn;
426441
}
427442
else if(err == WOLFSSL_ERROR_WANT_WRITE)

test/unit/tls/openssl_stream.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,45 @@ struct openssl_stream_test
7373
ioc.restart();
7474
}
7575
}
76+
77+
void
78+
testTlsShutdown()
79+
{
80+
using namespace tls::test;
81+
82+
for( auto mode : { context_mode::shared_cert,
83+
context_mode::separate_cert } )
84+
{
85+
io_context ioc;
86+
auto [client_ctx, server_ctx] = make_contexts( mode );
87+
run_tls_shutdown_test( ioc, client_ctx, server_ctx,
88+
make_stream, make_stream );
89+
}
90+
}
91+
92+
void
93+
testStreamTruncated()
94+
{
95+
using namespace tls::test;
96+
97+
for( auto mode : { context_mode::shared_cert,
98+
context_mode::separate_cert } )
99+
{
100+
io_context ioc;
101+
auto [client_ctx, server_ctx] = make_contexts( mode );
102+
run_tls_truncation_test( ioc, client_ctx, server_ctx,
103+
make_stream, make_stream );
104+
}
105+
}
76106
#endif
77107

78108
void
79109
run()
80110
{
81111
#ifdef BOOST_COROSIO_HAS_OPENSSL
82112
testSuccessCases();
113+
testTlsShutdown();
114+
testStreamTruncated();
83115
// Failure tests disabled: socket cancellation doesn't propagate to
84116
// TLS handshake operations, causing hangs when one side fails.
85117
// testFailureCases();

test/unit/tls/test_utils.hpp

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include <boost/corosio/tls/tls_stream.hpp>
1818
#include <boost/corosio/test/socket_pair.hpp>
1919
#include <boost/capy/buffers.hpp>
20+
#include <boost/capy/cond.hpp>
2021
#include <boost/capy/ex/run_async.hpp>
2122
#include <boost/capy/task.hpp>
2223

@@ -446,6 +447,218 @@ run_tls_test_fail(
446447
s2.close();
447448
}
448449

450+
/** Run a TLS shutdown test with graceful close_notify.
451+
452+
Tests that one side can initiate TLS shutdown (sends close_notify)
453+
and the other side receives EOF. Uses unidirectional shutdown to
454+
avoid deadlock in single-threaded io_context.
455+
456+
Note: TLS shutdown in a single-threaded context can deadlock when both
457+
sides wait for each other. We use a timeout to detect and recover from
458+
potential deadlocks.
459+
460+
@param ioc The io_context to use
461+
@param client_ctx TLS context for the client
462+
@param server_ctx TLS context for the server
463+
@param make_client Factory: (io_stream&, context) -> TLS stream
464+
@param make_server Factory: (io_stream&, context) -> TLS stream
465+
*/
466+
template<typename ClientStreamFactory, typename ServerStreamFactory>
467+
void
468+
run_tls_shutdown_test(
469+
io_context& ioc,
470+
context client_ctx,
471+
context server_ctx,
472+
ClientStreamFactory make_client,
473+
ServerStreamFactory make_server )
474+
{
475+
auto [s1, s2] = corosio::test::make_socket_pair( ioc );
476+
477+
auto client = make_client( s1, client_ctx );
478+
auto server = make_server( s2, server_ctx );
479+
480+
// Handshake phase
481+
auto client_hs = [&client]() -> capy::task<>
482+
{
483+
auto [ec] = co_await client.handshake( tls_stream::client );
484+
BOOST_TEST( !ec );
485+
};
486+
487+
auto server_hs = [&server]() -> capy::task<>
488+
{
489+
auto [ec] = co_await server.handshake( tls_stream::server );
490+
BOOST_TEST( !ec );
491+
};
492+
493+
capy::run_async( ioc.get_executor() )( client_hs() );
494+
capy::run_async( ioc.get_executor() )( server_hs() );
495+
496+
ioc.run();
497+
ioc.restart();
498+
499+
// Data transfer phase
500+
auto transfer_task = [&client, &server]() -> capy::task<>
501+
{
502+
co_await test_stream( client, server );
503+
};
504+
capy::run_async( ioc.get_executor() )( transfer_task() );
505+
506+
ioc.run();
507+
ioc.restart();
508+
509+
// Shutdown phase with timeout protection
510+
bool shutdown_done = false;
511+
bool read_done = false;
512+
513+
auto client_shutdown = [&client, &shutdown_done]() -> capy::task<>
514+
{
515+
auto [ec] = co_await client.shutdown();
516+
shutdown_done = true;
517+
// Shutdown may return success, canceled, or stream_truncated
518+
BOOST_TEST( !ec || ec == capy::cond::stream_truncated ||
519+
ec == capy::cond::canceled );
520+
};
521+
522+
auto server_read_eof = [&server, &read_done]() -> capy::task<>
523+
{
524+
char buf[32];
525+
auto [ec, n] = co_await server.read_some(
526+
capy::mutable_buffer( buf, sizeof( buf ) ) );
527+
read_done = true;
528+
// Should get EOF, stream_truncated, or canceled
529+
BOOST_TEST( ec == capy::cond::eof || ec == capy::cond::stream_truncated ||
530+
ec == capy::cond::canceled );
531+
};
532+
533+
// Timeout to prevent deadlock
534+
timer timeout( ioc );
535+
timeout.expires_after( std::chrono::milliseconds( 500 ) );
536+
auto timeout_task = [&timeout, &s1, &s2, &shutdown_done, &read_done]() -> capy::task<>
537+
{
538+
(void)shutdown_done;
539+
(void)read_done;
540+
auto [ec] = co_await timeout.wait();
541+
if( !ec )
542+
{
543+
// Timer expired - cancel pending operations (check if still open)
544+
if( s1.is_open() ) { s1.cancel(); s1.close(); }
545+
if( s2.is_open() ) { s2.cancel(); s2.close(); }
546+
}
547+
};
548+
549+
capy::run_async( ioc.get_executor() )( client_shutdown() );
550+
capy::run_async( ioc.get_executor() )( server_read_eof() );
551+
capy::run_async( ioc.get_executor() )( timeout_task() );
552+
553+
ioc.run();
554+
555+
timeout.cancel();
556+
if( s1.is_open() ) s1.close();
557+
if( s2.is_open() ) s2.close();
558+
}
559+
560+
/** Run a test for stream truncation (socket close without TLS shutdown).
561+
562+
Tests that when one side closes the underlying socket without
563+
performing TLS shutdown, the other side receives stream_truncated.
564+
565+
@param ioc The io_context to use
566+
@param client_ctx TLS context for the client
567+
@param server_ctx TLS context for the server
568+
@param make_client Factory: (io_stream&, context) -> TLS stream
569+
@param make_server Factory: (io_stream&, context) -> TLS stream
570+
*/
571+
template<typename ClientStreamFactory, typename ServerStreamFactory>
572+
void
573+
run_tls_truncation_test(
574+
io_context& ioc,
575+
context client_ctx,
576+
context server_ctx,
577+
ClientStreamFactory make_client,
578+
ServerStreamFactory make_server )
579+
{
580+
auto [s1, s2] = corosio::test::make_socket_pair( ioc );
581+
582+
auto client = make_client( s1, client_ctx );
583+
auto server = make_server( s2, server_ctx );
584+
585+
// Handshake phase
586+
auto client_hs = [&client]() -> capy::task<>
587+
{
588+
auto [ec] = co_await client.handshake( tls_stream::client );
589+
BOOST_TEST( !ec );
590+
};
591+
592+
auto server_hs = [&server]() -> capy::task<>
593+
{
594+
auto [ec] = co_await server.handshake( tls_stream::server );
595+
BOOST_TEST( !ec );
596+
};
597+
598+
capy::run_async( ioc.get_executor() )( client_hs() );
599+
capy::run_async( ioc.get_executor() )( server_hs() );
600+
601+
ioc.run();
602+
ioc.restart();
603+
604+
// Data transfer phase
605+
auto transfer_task = [&client, &server]() -> capy::task<>
606+
{
607+
co_await test_stream( client, server );
608+
};
609+
capy::run_async( ioc.get_executor() )( transfer_task() );
610+
611+
ioc.run();
612+
ioc.restart();
613+
614+
// Truncation test with timeout protection
615+
bool read_done = false;
616+
617+
auto client_close = [&s1]() -> capy::task<>
618+
{
619+
// Close underlying socket without TLS shutdown
620+
s1.close();
621+
co_return;
622+
};
623+
624+
auto server_read_truncated = [&server, &read_done]() -> capy::task<>
625+
{
626+
char buf[32];
627+
auto [ec, n] = co_await server.read_some(
628+
capy::mutable_buffer( buf, sizeof( buf ) ) );
629+
read_done = true;
630+
// Should get stream_truncated, eof, or canceled
631+
BOOST_TEST( ec == capy::cond::stream_truncated ||
632+
ec == capy::cond::eof ||
633+
ec == capy::cond::canceled );
634+
};
635+
636+
// Timeout to prevent deadlock
637+
timer timeout( ioc );
638+
timeout.expires_after( std::chrono::milliseconds( 500 ) );
639+
auto timeout_task = [&timeout, &s1, &s2, &read_done]() -> capy::task<>
640+
{
641+
(void)read_done;
642+
auto [ec] = co_await timeout.wait();
643+
if( !ec )
644+
{
645+
// Timer expired - cancel pending operations (check if still open)
646+
if( s1.is_open() ) { s1.cancel(); s1.close(); }
647+
if( s2.is_open() ) { s2.cancel(); s2.close(); }
648+
}
649+
};
650+
651+
capy::run_async( ioc.get_executor() )( client_close() );
652+
capy::run_async( ioc.get_executor() )( server_read_truncated() );
653+
capy::run_async( ioc.get_executor() )( timeout_task() );
654+
655+
ioc.run();
656+
657+
timeout.cancel();
658+
if( s1.is_open() ) s1.close();
659+
if( s2.is_open() ) s2.close();
660+
}
661+
449662
} // namespace test
450663
} // namespace tls
451664
} // namespace corosio

test/unit/tls/wolfssl_stream.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,45 @@ struct wolfssl_stream_test
7676
ioc.restart();
7777
}
7878
}
79+
80+
void
81+
testTlsShutdown()
82+
{
83+
using namespace tls::test;
84+
85+
for( auto mode : { context_mode::shared_cert,
86+
context_mode::separate_cert } )
87+
{
88+
io_context ioc;
89+
auto [client_ctx, server_ctx] = make_contexts( mode );
90+
run_tls_shutdown_test( ioc, client_ctx, server_ctx,
91+
make_stream, make_stream );
92+
}
93+
}
94+
95+
void
96+
testStreamTruncated()
97+
{
98+
using namespace tls::test;
99+
100+
for( auto mode : { context_mode::shared_cert,
101+
context_mode::separate_cert } )
102+
{
103+
io_context ioc;
104+
auto [client_ctx, server_ctx] = make_contexts( mode );
105+
run_tls_truncation_test( ioc, client_ctx, server_ctx,
106+
make_stream, make_stream );
107+
}
108+
}
79109
#endif
80110

81111
void
82112
run()
83113
{
84114
#ifdef BOOST_COROSIO_HAS_WOLFSSL
85115
testSuccessCases();
116+
testTlsShutdown();
117+
testStreamTruncated();
86118
// Failure tests disabled: socket cancellation doesn't propagate to
87119
// TLS handshake operations, causing hangs when one side fails.
88120
// testFailureCases();

0 commit comments

Comments
 (0)