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:
Matthew Zipkin
2026-06-26 16:33:14 -04:00
parent 57b3bf8496
commit 73da2a8a52
2 changed files with 24 additions and 10 deletions

View File

@@ -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);
}