test_websocket_thread_safety.cc 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. // Standalone test for TLS-session thread safety on wss:// connections.
  2. //
  3. // A wss:// WebSocket enters one TLS session from multiple threads: the read
  4. // path, the application's send()/close(), and the heartbeat ping thread. A
  5. // TLS session must never be entered concurrently, so httplib routes wss://
  6. // through WebSocketSSLStream, which serializes every TLS call. These tests
  7. // drive that concurrency directly. Built with ASan in CI, so a regression
  8. // surfaces as a heap-buffer-overflow, not just a flaky assertion.
  9. // Fire the heartbeat every second so the ping-vs-read case actually crosses a
  10. // ping while the reader is idle.
  11. #define CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND 1
  12. #include <httplib.h>
  13. #include "gtest/gtest.h"
  14. #include <atomic>
  15. #include <chrono>
  16. #include <string>
  17. #include <thread>
  18. #ifdef CPPHTTPLIB_SSL_ENABLED
  19. using namespace httplib;
  20. namespace {
  21. const size_t kPayloadBytes = 2048;
  22. const size_t kSendCount = 2000;
  23. const size_t kBurstPerFrame = 100;
  24. const int kCloseCycles = 20;
  25. } // namespace
  26. class WebSocketTlsThreadSafetyTest : public ::testing::Test {
  27. protected:
  28. WebSocketTlsThreadSafetyTest() : svr_("cert.pem", "key.pem") {}
  29. void TearDown() override {
  30. if (thread_.joinable()) {
  31. svr_.stop();
  32. thread_.join();
  33. }
  34. }
  35. // Registers the handler and starts the TLS server. Called by each test
  36. // after its own server configuration, since the heartbeat test needs the
  37. // pings the others switch off.
  38. bool start(Server::WebSocketHandler handler) {
  39. if (!svr_.is_valid()) { return false; }
  40. svr_.WebSocket("/ws", std::move(handler));
  41. port_ = svr_.bind_to_any_port("localhost");
  42. if (port_ <= 0) { return false; }
  43. thread_ = std::thread([this]() { svr_.listen_after_bind(); });
  44. svr_.wait_until_ready();
  45. return true;
  46. }
  47. std::string url() const {
  48. return "wss://localhost:" + std::to_string(port_) + "/ws";
  49. }
  50. SSLServer svr_;
  51. int port_ = 0;
  52. std::thread thread_;
  53. };
  54. // A sender thread hammers send() while another thread loops read(). Both
  55. // enter the same TLS session, and every echoed frame must arrive intact.
  56. TEST_F(WebSocketTlsThreadSafetyTest, SendWhileAnotherThreadReads) {
  57. svr_.set_websocket_ping_interval(0);
  58. ASSERT_TRUE(start([](const Request &, ws::WebSocket &sock) {
  59. std::string msg;
  60. while (sock.read(msg) != ws::ReadResult::Fail) {
  61. if (!sock.send(msg.data(), msg.size())) { break; }
  62. }
  63. }));
  64. ws::WebSocketClient cli(url());
  65. cli.enable_server_certificate_verification(false);
  66. ASSERT_TRUE(cli.connect());
  67. const std::string payload(kPayloadBytes, 'x');
  68. // close() drains the peer's Close reply with its own frame reader, so once
  69. // it starts, two threads parse frames from one stream and can split a
  70. // payload between them. That is frame-level, not TLS-level, and happens on
  71. // ws:// too, so only frames completed before close() are checked here.
  72. std::atomic<bool> closing(false);
  73. std::atomic<size_t> frames_read(0);
  74. std::atomic<size_t> corrupt_frames(0);
  75. std::thread reader([&]() {
  76. std::string msg;
  77. while (cli.read(msg) != ws::ReadResult::Fail) {
  78. if (msg != payload && !closing.load()) { corrupt_frames++; }
  79. frames_read++;
  80. }
  81. });
  82. size_t sent = 0;
  83. for (size_t i = 0; i < kSendCount; i++) {
  84. if (!cli.send(payload.data(), payload.size())) { break; }
  85. sent++;
  86. }
  87. closing.store(true);
  88. cli.close();
  89. reader.join();
  90. EXPECT_EQ(kSendCount, sent);
  91. EXPECT_EQ(static_cast<size_t>(0), corrupt_frames.load());
  92. EXPECT_GT(frames_read.load(), static_cast<size_t>(0));
  93. }
  94. // close() sends a Close frame and drains the peer's reply while a second
  95. // thread is inside read(). Repeated to shake out the race.
  96. TEST_F(WebSocketTlsThreadSafetyTest, CloseWhileAnotherThreadReads) {
  97. svr_.set_websocket_ping_interval(0);
  98. ASSERT_TRUE(start([](const Request &, ws::WebSocket &sock) {
  99. const std::string burst(64, 'p');
  100. std::string msg;
  101. while (sock.read(msg) != ws::ReadResult::Fail) {
  102. for (size_t i = 0; i < kBurstPerFrame; i++) {
  103. if (!sock.send(burst.data(), burst.size())) { return; }
  104. }
  105. }
  106. }));
  107. for (int cycle = 0; cycle < kCloseCycles; cycle++) {
  108. ws::WebSocketClient cli(url());
  109. cli.enable_server_certificate_verification(false);
  110. ASSERT_TRUE(cli.connect()) << "cycle " << cycle;
  111. std::thread reader([&]() {
  112. std::string msg;
  113. while (cli.read(msg) != ws::ReadResult::Fail) {}
  114. });
  115. const std::string trigger(64, 't');
  116. ASSERT_TRUE(cli.send(trigger.data(), trigger.size()));
  117. cli.close();
  118. reader.join();
  119. }
  120. }
  121. // The heartbeat ping thread writes to the TLS session on its own timer while
  122. // the application blocks in read() with no traffic. The ping's write must not
  123. // collide with the reader. The 1-second interval above means several pings
  124. // fire on both sides during this idle window.
  125. TEST_F(WebSocketTlsThreadSafetyTest, HeartbeatPingWhileReaderIsIdle) {
  126. ASSERT_TRUE(start([](const Request &, ws::WebSocket &sock) {
  127. std::string msg;
  128. while (sock.read(msg) != ws::ReadResult::Fail) {}
  129. }));
  130. ws::WebSocketClient cli(url());
  131. cli.enable_server_certificate_verification(false);
  132. ASSERT_TRUE(cli.connect());
  133. // No data frames are sent, so the reader stays parked inside read() while
  134. // both sides exchange pings and pongs on the heartbeat timer. read() only
  135. // returns once close() below tears the connection down.
  136. std::thread reader([&]() {
  137. std::string msg;
  138. while (cli.read(msg) != ws::ReadResult::Fail) {}
  139. });
  140. std::this_thread::sleep_for(std::chrono::seconds(4));
  141. // The connection survived the heartbeat exchange without a TLS-session race.
  142. EXPECT_TRUE(cli.is_open());
  143. cli.close();
  144. reader.join();
  145. }
  146. #endif // CPPHTTPLIB_SSL_ENABLED