mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-18 11:09:39 +00:00
Fix WebSocket::close() racing a concurrent read() on the same stream
close() drained the peer's Close reply with its own frame read. If an application reader thread was inside read() at that moment, two threads parsed frames off one stream: read_websocket_frame()'s payload loop keeps reading until it has the declared length, so bytes stolen by the drain were silently replaced with bytes from further along the stream. The in-flight message kept its correct length but got the wrong content. Add a read_mutex_ that marks which thread owns the stream's read side. read() holds it for the whole call. close() sends the Close frame, then drains the peer's reply (RFC 6455 7.1.1) only if it can try_lock the mutex; otherwise it returns immediately, leaving the stream entirely to the thread already reading it. This also fixes close() blocking for the full close timeout when a reader thread was parked waiting on a peer that never replies. Add WebSocketTest.CloseDoesNotStealBytesFromConcurrentRead, which drives a raw TCP peer that stalls mid-payload to force the race; it fails reliably against the old code and passes against the fix. Update README-websocket.md: close() during a concurrent read() is now supported.
This commit is contained in:
1 parent
f82d2d90b6
commit
00d1f54267
3 files changed
+182
-4
No files matched your search
@@ -4331,6 +4331,11 @@ private:
|
||||
int unacked_pings_ = 0;
|
||||
std::atomic<bool> closed_{false};
|
||||
std::mutex write_mutex_;
|
||||
// Owned by whichever thread is parsing frames off strm_. Only one thread
|
||||
// may do so: read_websocket_frame() reads a payload until it has the whole
|
||||
// declared length, so a second parser stealing bytes silently corrupts the
|
||||
// message the first one is assembling.
|
||||
std::mutex read_mutex_;
|
||||
std::thread ping_thread_;
|
||||
std::mutex ping_mutex_;
|
||||
std::condition_variable ping_cv_;
|
||||
@@ -21649,6 +21654,7 @@ inline bool WebSocket::send_frame(Opcode op, const char *data, size_t len,
|
||||
}
|
||||
|
||||
inline ReadResult WebSocket::read(std::string &msg) {
|
||||
std::unique_lock<std::mutex> read_lock(read_mutex_);
|
||||
while (!closed_) {
|
||||
Opcode opcode;
|
||||
std::string payload;
|
||||
@@ -21734,6 +21740,9 @@ inline ReadResult WebSocket::read(std::string &msg) {
|
||||
}
|
||||
// RFC 6455 Section 5.6: text frames must contain valid UTF-8
|
||||
if (result == Text && !impl::is_valid_utf8(msg)) {
|
||||
// close() takes the read lock to wait for the peer's Close reply, so
|
||||
// it must not run while this thread still holds it.
|
||||
read_lock.unlock();
|
||||
close(CloseStatus::InvalidPayload, "invalid UTF-8");
|
||||
return Fail;
|
||||
}
|
||||
@@ -21770,9 +21779,18 @@ inline void WebSocket::close(CloseStatus status, const std::string &reason) {
|
||||
}
|
||||
|
||||
// RFC 6455 Section 7.1.1: after sending a Close frame, wait for the peer's
|
||||
// Close response before closing the TCP connection. Use a short timeout to
|
||||
// avoid hanging if the peer doesn't respond.
|
||||
// Close response before closing the TCP connection.
|
||||
//
|
||||
// Wait only when no other thread is parsing frames. When one is, it is the
|
||||
// thread positioned to see the peer's reply, and reading here would take
|
||||
// bytes out of the message it is assembling. Bailing out also leaves the
|
||||
// stream, including its read timeout, entirely to that thread.
|
||||
std::unique_lock<std::mutex> read_lock(read_mutex_, std::try_to_lock);
|
||||
if (!read_lock.owns_lock()) { return; }
|
||||
|
||||
// Use a short timeout to avoid hanging if the peer doesn't respond.
|
||||
strm_.set_read_timeout(CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND, 0);
|
||||
|
||||
Opcode op;
|
||||
std::string resp;
|
||||
bool fin;
|
||||
|
||||
Reference in new issue
Block a user