mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-09-12 21:52:53 +02:00
http: prevent race condition between worker thread and I/O thread
This prevents a losing race condition that could prevent the server from reading requests from an HTTP client. A connected socket can either be written to or read from based on the result of GenerateWaitSockets(). That method checks the HTTPRemoteClient flag m_send_ready. If it's `true` the implication is that there is data in the client's send buffer ready to go. Once that data is sent and the buffer is empty, MaybeSendBytesFromBuffer() sets it `false` again. The sad case was when a worker thread calling WriteReply() adds data to the send buffer, but before it sets m_send_ready to `true`, the I/O thread sends that data and empties the buffer. With the buffer unexpectedly empty, WriteReply() sets m_send_ready to `true`. The effect of this is that the socket will stay in "write" mode with nothing to write. With nothing to write, MaybeSendBytesFromBuffer() never sets it back to `false` and the socket is stuck forever.
This commit is contained in:
@@ -595,6 +595,16 @@ void HTTPRequest::WriteReply(HTTPStatusCode status, std::span<const std::byte> r
|
||||
// data. The original data will go out of scope when WriteReply() returns.
|
||||
// This is analogous to the memcpy() in libevent's evbuffer_add()
|
||||
m_client->m_send_buffer.insert(m_client->m_send_buffer.end(), reply_body.begin(), reply_body.end());
|
||||
|
||||
// If the buffer already held data, the I/O thread is (or soon will be)
|
||||
// draining it, so flag that there is more data to send. This must happen
|
||||
// while holding m_send_mutex and while the buffer is known non-empty:
|
||||
// setting m_send_ready after releasing the lock would race with the I/O
|
||||
// thread draining the buffer to empty and clearing m_send_ready in
|
||||
// between, leaving m_send_ready set on an empty buffer. The I/O loop would
|
||||
// then only ever poll the socket for writeability, never read the client's
|
||||
// next request, and wedge the connection.
|
||||
if (!send_buffer_was_empty) m_client->m_send_ready = true;
|
||||
}
|
||||
|
||||
LogDebug(
|
||||
@@ -611,10 +621,6 @@ void HTTPRequest::WriteReply(HTTPStatusCode status, std::span<const std::byte> r
|
||||
// of waiting for the next iteration of the I/O loop.
|
||||
if (send_buffer_was_empty) {
|
||||
m_client->MaybeSendBytesFromBuffer();
|
||||
} else {
|
||||
// Inform HTTPServer I/O that data is ready to be sent to this client
|
||||
// in the next loop iteration.
|
||||
m_client->m_send_ready = true;
|
||||
}
|
||||
|
||||
// Signal to the I/O loop that we are ready to handle the next request.
|
||||
@@ -935,7 +941,12 @@ HTTPServer::IOReadiness HTTPServer::GenerateWaitSockets() const
|
||||
|
||||
// Check if client is ready to send data. Don't try to receive again
|
||||
// until the send buffer is cleared (all data sent to client).
|
||||
Sock::Event event = (http_client->m_send_ready ? Sock::SendEvent : Sock::RecvEvent);
|
||||
// Keep this as a separate critical section from the m_sock_mutex one above:
|
||||
// never hold m_sock_mutex and m_send_mutex at the same time here.
|
||||
// MaybeSendBytesFromBuffer() locks m_send_mutex then m_sock_mutex, so nesting
|
||||
// them in the opposite order here would risk a lock-order inversion deadlock.
|
||||
const bool send_ready{WITH_LOCK(http_client->m_send_mutex, return http_client->m_send_ready;)};
|
||||
Sock::Event event = (send_ready ? Sock::SendEvent : Sock::RecvEvent);
|
||||
io_readiness.events_per_sock.emplace(sock, Sock::Events{event});
|
||||
io_readiness.httpclients_per_sock.emplace(sock, http_client);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user