基于ASIO的TLS客户端关闭连接时SSL流崩溃的原因与解决
我有一个基于ASIO的TLS客户端,简化代码如下(注:所有套接字操作均由互斥锁保护,所有ASIO异步回调在同一独立线程执行):
std::mutex socket_op_lock; ssl::stream<tcp::socket> ssl_socket; Connect() { ssl_socket.next_layer().async_connect(on_connect); } on_connect(const asio::error_code& error) { std::lock_guard<std::mutex> socket_op_lock; if (error) { CloseSocket(); } else { ssl_socket.async_handshake(on_handshake); } } on_handshake(const asio::error_code& error) { std::lock_guard<std::mutex> socket_op_lock; if (error) { CloseSocket(); } else { Receive(); } } Receive() { // called internal, already locked async_read(ssl_socket, ..., on_read); } on_read() { std::lock_guard<std::mutex> socket_op_lock; if (error) { CloseSocket(); } else { Receive(); } } Disconnect() { std::lock_guard<std::mutex> socket_op_lock; CloseSocket(); } CloseSocket() { // called internal, already locked ssl_socket.next_layer().shutdown(); ssl_socket.next_layer().close(); }
问题出现在连接关闭阶段:因担心ssl::stream的shutdown会阻塞,我直接关闭底层TCP连接,但在Android/iOS上偶现崩溃,堆栈信息如下:
0 MyApp asio::detail::kqueue_reactor::start_op(int, int, asio::detail::kqueue_reactor::descriptor_state*&, asio::detail::reactor_op*, bool, bool) + 108 1 MyApp asio::detail::kqueue_reactor::start_op(int, int, asio::detail::kqueue_reactor::descriptor_state*&, asio::detail::reactor_op*, bool, bool) + 100 2 MyApp void asio::detail::reactive_socket_service_base::async_receive<asio::mutable_buffers_1, asio::ssl::detail::io_op<asio::basic_stream_socket<asio::ip::tcp>, asio::ssl::detail::handshake_op, std::__1::__bind<void (MyApp::MyTcpClient::*)(std::__1::error_code const&), std::__1::shared_ptr<MyApp::MyTcpClient>, std::__1::placeholders::__ph<1> const&> > >(asio::detail::reactive_socket_service_base::base_implementation_type&, asio::mutable_buffers_1 const&, int, asio::ssl::detail::io_op<asio::basic_stream_socket<asio::ip::tcp>, asio::ssl::detail::handshake_op, std::__1::__bind<void (MyApp::MyTcpClient::*)(std::__1::error_code const&), std::__1::shared_ptr<MyApp::MyTcpClient>, std::__1::placeholders::__ph<1> const&> >&) + 384 3 MyApp asio::ssl::detail::io_op<asio::basic_stream_socket<asio::ip::tcp>, asio::ssl::detail::handshake_op, std::__1::__bind<void (MyApp::MyTcpClient::*)(std::__1::error_code const&), std::__1::shared_ptr<MyApp::MyTcpClient>, std::__1::placeholders::__ph<1> const&> >::operator()(std::__1::error_code, unsigned long, int) + 900 4 MyApp asio::detail::reactive_socket_send_op<asio::const_buffers_1, asio::detail::write_op<asio::basic_stream_socket<asio::ip::tcp>, asio::mutable_buffer, asio::mutable_buffer const*, asio::detail::transfer_all_t, asio::ssl::detail::io_op<asio::basic_stream_socket<asio::ip::tcp>, asio::ssl::detail::handshake_op, std::__1::__bind<void (MyApp::MyTcpClient::*)(std::__1::error_code const&), std::__1::shared_ptr<MyApp::MyTcpClient>, std::__1::placeholders::__ph<1> const&> > > >::do_complete(void*, asio::detail::scheduler_operation*, std::__1::error_code const&, unsigned long) + 228 5 MyApp asio::detail::scheduler::do_run_one(asio::detail::conditionally_enabled_mutex::scoped_lock&, asio::detail::scheduler_thread_info&, std::__1::error_code const&) + 692 6 MyApp asio::detail::scheduler::run(std::__1::error_code&) + 188 7 MyApp MyApp::NetworkIoThread::CustomLoop() + 44 8 MyApp MyApp::OMMsgThread::Run() + 96 9 libsystem_pthread.dylib __pthread_start + 136
我猜测未显式调用ssl::stream的shutdown,导致关闭TCP后仍有SSL流操作触发崩溃,于是添加SSL层关闭流程,修改后的代码如下:
Disconnect() { std::lock_guard<std::mutex> socket_op_lock; ssl_socket.async_shutdown(on_shutdown); } on_shutdown() { std::lock_guard<std::mutex> socket_op_lock; CloseSocket(); }
修改后iOS平台崩溃次数增多,堆栈信息如下:
0 MyApp CRYPTO_memcmp + 76 1 MyApp aes_gcm_tls_cipher + 624 2 MyApp aes_gcm_cipher + 96 3 MyApp EVP_Cipher + 64 4 MyApp tls1_enc + 2748 5 MyApp ssl3_get_record + 3220 6 MyApp ssl3_read_bytes + 744 7 MyApp ssl3_read_internal + 128 8 MyApp ssl3_read + 52 9 MyApp ssl_read_internal + 340 10 MyApp SSL_read + 96 11 MyApp asio::ssl::detail::engine::perform(int (asio::ssl::detail::engine::*)(void*, unsigned long), void*, unsigned long, std::__1::error_code&, unsigned long*) + 96 12 MyApp asio::ssl::detail::io_op<asio::basic_stream_socket<asio::ip::tcp>, asio::ssl::detail::read_op<asio::mutable_buffers_1>, asio::detail::read_dynbuf_op<asio::ssl::stream<asio::basic_stream_socket<asio::ip::tcp> >, asio::basic_streambuf_ref<std::__1::allocator<char> >, asio::detail::transfer_at_least_t, std::__1::__bind<void (MyApp::MyTcpClient::*)(std::__1::error_code const&, unsigned long), std::__1::shared_ptr<MyApp::MyTcpClient>, std::__1::placeholders::__ph<1> const&, std::__1::placeholders::__ph<2> const&> > >::operator()(std::__1::error_code, unsigned long, int) + 92 13 MyApp asio::detail::reactive_socket_recv_op<asio::mutable_buffers_1, asio::ssl::detail::io_op<asio::basic_stream_socket<asio::ip::tcp>, asio::ssl::detail::read_op<asio::mutable_buffers_1>, asio::detail::read_dynbuf_op<asio::ssl::stream<asio::basic_stream_socket<asio::ip::tcp> >, asio::basic_streambuf_ref<std::__1::allocator<char> >, asio::detail::transfer_at_least_t, std::__1::__bind<void (MyApp::MyTcpClient::*)(std::__1::error_code const&, unsigned long), std::__1::shared_ptr<MyApp::MyTcpClient>, std::__1::placeholders::__ph<1> const&, std::__1::placeholders::__ph<2> const&> > >::do_complete(void*, asio::detail::scheduler_operation*, std::__1::error_code const&, unsigned long) + 196 14 MyApp asio::detail::scheduler::do_run_one(asio::detail::conditionally_enabled_mutex::scoped_lock&, asio::detail::scheduler_thread_info&, std::__1::error_code const&) + 692 15 MyApp asio::detail::scheduler::run(std::__1::error_code&) + 188 16 MyApp MyApp::NetworkIoThread::CustomLoop() + 44 17 MyApp MyApp::OMMsgThread::Run() + 96 18 libsystem_pthread.dylib __pthread_start + 148
但奇怪的是Android平台无异常。请问该问题的原因是什么,如何解决?
直接关闭底层TCP的问题:直接关闭TCP套接字时,ASIO的SSL流可能存在未完成的异步操作(如握手、读操作),这些操作后续会访问已关闭的套接字或失效的SSL上下文,触发崩溃。iOS的kqueue reactor对文件描述符状态变更更敏感,因此更容易出现这类问题。
添加
async_shutdown后的问题:崩溃增多的核心原因是异步SSL shutdown与正在进行的异步读操作发生竞态。调用async_shutdown时,当前可能仍有async_read在执行,SSL引擎在shutdown过程中会尝试处理剩余SSL记录,而读操作也在访问SSL引擎内部状态,导致线程安全冲突。虽然代码用了互斥锁,但ASIO的SSL操作内部可能存在未被覆盖的共享状态访问。此外,iOS上的OpenSSL版本或ASIO实现对这类并发操作的兼容性更差,而Android的实现可能做了额外保护。
步骤1:取消所有未完成的异步操作
发起shutdown前,必须先取消所有挂起的异步读/写操作,避免与shutdown操作冲突。在持有锁的情况下调用ssl_socket.cancel():
Disconnect() { std::lock_guard<std::mutex> socket_op_lock; // 取消所有未完成的异步操作 ssl_socket.cancel(); ssl_socket.async_shutdown(on_shutdown); }
步骤2:正确处理shutdown错误
async_shutdown可能返回正常错误(如对方已关闭连接),无需额外处理,直接关闭套接字即可:
on_shutdown(const asio::error_code& error) { std::lock_guard<std::mutex> socket_op_lock; // 忽略shutdown的正常错误,比如对方提前关闭连接 CloseSocket(); }
步骤3:完善CloseSocket逻辑
关闭底层套接字前,先清理SSL引擎状态,同时处理可能的错误:
CloseSocket() { // called internal, already locked asio::error_code ec; // 先关闭SSL引擎的读写 ssl_socket.shutdown(ec); // 关闭底层TCP套接字 ssl_socket.next_layer().shutdown(tcp::socket::shutdown_both, ec); ssl_socket.next_layer().close(ec); }
步骤4:标记连接状态,避免重复操作
添加is_disconnecting标志,防止重复发起shutdown或其他操作:
std::mutex socket_op_lock; ssl::stream<tcp::socket> ssl_socket; bool is_disconnecting = false; Disconnect() { std::lock_guard<std::mutex> socket_op_lock; if (is_disconnecting) return; is_disconnecting = true; ssl_socket.cancel(); ssl_socket.async_shutdown(on_shutdown); } // 在所有回调中先检查状态,再执行后续操作 on_read(const asio::error_code& error, std::size_t bytes_transferred) { std::lock_guard<std::mutex> socket_op_lock; if (is_disconnecting) return; if (error) { CloseSocket(); } else { Receive(); } }
关键说明
- 取消异步操作是核心:必须确保shutdown前没有其他异步操作访问SSL流,否则会引发状态竞争。
- 忽略shutdown错误:SSL shutdown失败是常见情况,无需视为异常,直接关闭底层套接字即可。
- 状态标记防止重复操作:避免多次调用Disconnect导致的重复shutdown或cancel操作。
内容的提问来源于stack exchange,提问作者0luan

