Fix crash when HTTP pipeline data arrives during async response writing#814
Fix crash when HTTP pipeline data arrives during async response writing#814
Conversation
…equest Instead of resetting the existing HttpRequest/HttpResponse objects in HttpHandler::Reset(), create new ones. This prevents a race condition where async handlers (holding shared_ptr references to the old req/resp) could crash when FeedRecvData calls Reset() upon receiving HTTP pipeline data or an attack while the async response is still being written. The old req/resp objects remain alive through the shared_ptr references held by the async handler, while the handler gets fresh objects for the new incoming request. Co-authored-by: ithewei <26049660+ithewei@users.noreply.github.com>
Co-authored-by: ithewei <26049660+ithewei@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR aims to prevent a crash/data race in the HTTP server when new request data arrives on a keep-alive connection while an async handler is still writing the previous response, by avoiding in-place resets of shared request/response objects.
Changes:
- Updates
HttpHandler::Reset()to allocate newHttpRequest/HttpResponseinstances instead of callingReset()in-place. - Recreates
HttpResponseWriterduringReset(), reinitializes the parser with the new request, and re-hookshttp_cb. - Preserves HTTP/2 version fields on newly created messages and sets
writerto null wheniois null.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
http/server/HttpHandler.cpp
Outdated
| // Re-hook http_cb for the new request object | ||
| req->http_cb = [this](HttpMessage* msg, http_parser_state state, const char* data, size_t size) { | ||
| if (this->state == WANT_CLOSE) return; | ||
| switch (state) { | ||
| case HP_HEADERS_COMPLETE: | ||
| if (this->error != 0) return; | ||
| onHeadersComplete(); | ||
| break; | ||
| case HP_BODY: | ||
| if (this->error != 0) return; | ||
| onBody(data, size); | ||
| break; | ||
| case HP_MESSAGE_COMPLETE: | ||
| onMessageComplete(); | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| }; |
There was a problem hiding this comment.
The http_cb lambda is duplicated in both Init() and Reset(). Since this callback wiring is now required whenever a new HttpRequest is allocated, consider extracting it into a small helper (e.g., a private method) to reduce duplication and the risk of future divergence between the two copies.
| if (io) { | ||
| writer = std::make_shared<HttpResponseWriter>(io, resp); | ||
| writer->status = hv::SocketChannel::CONNECTED; | ||
| } else { | ||
| writer = NULL; |
There was a problem hiding this comment.
Reset() now replaces writer with a new HttpResponseWriter bound to the same hio_t. The old writer will be destroyed as soon as its last shared_ptr is released, and hv::Channel::~Channel() closes the underlying hio_t when isOpened() is true—this will break keep-alive (old writer destroyed immediately on normal Reset) and can also close an active connection while a subsequent request is being processed. Additionally, constructing a new Channel/SocketChannel on the same hio_t overwrites hio_context, so on_write/on_close callbacks will no longer be routed to the in-flight writer instance. Consider keeping a single Channel wrapper per connection and making per-request writers non-owning (no hio_context mutation / no close-on-destroy), or otherwise ensure retiring writers cannot close or steal callbacks from the connection while still allowing the active writer to finish sending.
|
@copilot apply changes based on the comments in this thread |
…okHttpCb helper Before creating a new HttpResponseWriter in Reset(), mark the old writer as DISCONNECTED and clear its io_ pointer. This ensures: - ~Channel() won't close the hio_t when the async handler's shared_ptr reference is eventually released (isOpened() returns false) - Any further write attempts through the retired writer fail gracefully (write() returns -1) instead of interfering with the new request Also extract the duplicated http_cb lambda from Init() and Reset() into a private hookHttpCb() helper method. Co-authored-by: ithewei <26049660+ithewei@users.noreply.github.com>
Applied both review suggestions in commit
|
HttpHandler::Reset()callsreq->Reset()andresp->Reset()in-place, but async handlers holdshared_ptrreferences to these same objects from a threadpool. When pipeline data (or malicious requests) arrive on a keep-alive connection while the async handler is still writing,FeedRecvDatatriggersReset(), causing a data race and segfault.Fix: Allocate new
HttpRequest/HttpResponse/HttpResponseWriterobjects inReset()instead of resetting in-place. The old objects stay alive via theshared_ptrrefs held by the async handler — no lock contention, no race.HttpResponseWriterbefore creating a new one by settingstatus = DISCONNECTEDandio_ = NULL, preventing~Channel()from closing thehio_twhen the async handler'sshared_ptris released, and ensuring stale write attempts fail gracefully (write()returns -1) instead of interfering with the new request or closing the connectionhttp_cblambda into a privatehookHttpCb()helper method, called by bothInit()andReset()protocol == HTTP_V2writerwheniois nullOriginal prompt
🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. Learn more about Advanced Security.