Skip to content

Fix crash when HTTP pipeline data arrives during async response writing#814

Open
Copilot wants to merge 4 commits intomasterfrom
copilot/fix-http-server-crash-issue
Open

Fix crash when HTTP pipeline data arrives during async response writing#814
Copilot wants to merge 4 commits intomasterfrom
copilot/fix-http-server-crash-issue

Conversation

Copy link
Contributor

Copilot AI commented Mar 10, 2026

HttpHandler::Reset() calls req->Reset() and resp->Reset() in-place, but async handlers hold shared_ptr references 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, FeedRecvData triggers Reset(), causing a data race and segfault.

Fix: Allocate new HttpRequest/HttpResponse/HttpResponseWriter objects in Reset() instead of resetting in-place. The old objects stay alive via the shared_ptr refs held by the async handler — no lock contention, no race.

// Before: resets shared objects in-place (race with async handler)
req->Reset();
resp->Reset();

// After: new objects per request, old ones kept alive by async handler's shared_ptr
req  = std::make_shared<HttpRequest>();
resp = std::make_shared<HttpResponse>();
writer = std::make_shared<HttpResponseWriter>(io, resp);
  • Retires the old HttpResponseWriter before creating a new one by setting status = DISCONNECTED and io_ = NULL, preventing ~Channel() from closing the hio_t when the async handler's shared_ptr is released, and ensuring stale write attempts fail gracefully (write() returns -1) instead of interfering with the new request or closing the connection
  • Extracts the duplicated http_cb lambda into a private hookHttpCb() helper method, called by both Init() and Reset()
  • Preserves HTTP/2 version fields on new objects when protocol == HTTP_V2
  • Defensively nulls writer when io is null
Original prompt

This section details on the original issue you should resolve

<issue_title>[http server]Receiving http pipeline while asynchronously writing resp, there is a potential crash risk</issue_title>
<issue_description>Hello, libhv is a very good open source library, recently I am learning the source code of libhv. While reading the http server code, I found a thread safety issue that may cause coredump, so I want to confirm it with you. The problem is described as follows:
(1) Pre-scenario: libhv's http server supports asynchronous response, that is, its own thread in the upper layer to use HttpResponseWriterPtr to write data to HttpResponsePtr.
At the same time, libhv support keep-alive, namely in the connection I received a new request, will call an HttpHandler: : FeedRecvData function, then the request to reset the old data.
(2) Reproduction method: When the upper-layer asynchronously calls HttpResponseWriterPtr to write the response data, if the customer sends the data again (can be http-pipeline or attack), the state of HttpHandler is not equal to WANT_RECV, Then call HttpHandler::Reset, and then call HttpMessage::Reset(), which will empty some object data.These data is not done thread-safe, so in the upper layer with HttpResponseWriterPtr modification, may cause segment errors.
Hope to get your reply:
(1) Whether the scene of this problem exists.
(2) If so, can it be optimized?

很抱歉我的英文表达能力不是很好,以下是issue的中文描述:
您好,libhv是一个非常优秀的开源库,最近我在学习libhv的源码。在阅读http服务器代码的时候,发现了一个可能会引发coredump的线程安全问题,因此想跟您确认一下。问题描述如下:
(1)前置场景:libhv的http server是支持异步响应的,也就是在上层自己的线程去使用HttpResponseWriterPtr写入数据到HttpResponsePtr。
同时,libhv也支持keep-alive,也就是在连接收到新的请求时,会进入HttpHandler::FeedRecvData函数,然后去reset旧的请求的数据。
(2)复现方式:在上层异步调用HttpResponseWriterPtr写入响应数据时,如果这时候客户再次发送数据过来(可以是http-pipeline,也可以是攻击),这时候会HttpHandler的state不等于WANT_RECV,然后会调用HttpHandler::Reset,进而调用HttpMessage::Reset(),这里会清空一些对象的数据,因为这些数据是没有做线程安全的,因此在上层用HttpResponseWriterPtr修改的时候,可能会导致段错误。

希望能得到您的答复:
(1)这个问题的场景是否存在。
(2)如果存在,能否优化呢?</issue_description>

<agent_instructions>Please analyze and respond to this issue.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@ithewei 我也直接使用中文回复了,首先表扬你看源码看的很仔细,思考的很深入。 (1)这里确实没有考虑http pipeline场景或者恶意攻击的情况,假设的是常规的一应一答的请求-响应场景,在你描述的场景里使用http异步回调确实有线程安全问题,可能导致coredump。 (2)上面问题主要是因为一个HTTP连接里只使用了一个HttpRequest、HttpResponse请求上下文导致的,如果要优化的话,就得每接收一个新的请求就是New一个HttpRequest、HttpResponse请求上下文,不要去复用之前的来避免竞争,缺点就是多个请求上下文的生命周期管理有点麻烦,连接断开后,才能释放掉这条连接上所有的请求上下文。

如果不考虑支持http pipeline场景,作为服务端确实也应该考虑这种情况可能导致的coredump,后续我想下怎么规避下,加锁可能会影响效率。</comment_new>


🔒 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.

Copilot AI and others added 2 commits March 10, 2026 13:59
…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>
Copilot AI changed the title [WIP] Fix potential crash risk in HTTP server during asynchronous response Fix crash when HTTP pipeline data arrives during async response writing Mar 10, 2026
@ithewei ithewei marked this pull request as ready for review March 10, 2026 14:05
Copilot AI review requested due to automatic review settings March 10, 2026 14:05
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 new HttpRequest/HttpResponse instances instead of calling Reset() in-place.
  • Recreates HttpResponseWriter during Reset(), reinitializes the parser with the new request, and re-hooks http_cb.
  • Preserves HTTP/2 version fields on newly created messages and sets writer to null when io is null.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +134 to +152
// 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;
}
};
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +127 to +131
if (io) {
writer = std::make_shared<HttpResponseWriter>(io, resp);
writer->status = hv::SocketChannel::CONNECTED;
} else {
writer = NULL;
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 uses AI. Check for mistakes.
@ithewei
Copy link
Owner

ithewei commented Mar 11, 2026

@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>
Copy link
Contributor Author

Copilot AI commented Mar 11, 2026

@copilot apply changes based on the comments in this thread

Applied both review suggestions in commit 5084c45:

  1. Writer lifecycle fix: Before creating a new HttpResponseWriter in Reset(), the old writer is now "retired" by setting status = DISCONNECTED and io_ = NULL. This prevents ~Channel() from closing the hio_t when the async handler's shared_ptr reference is released, and ensures any write attempts through the retired writer fail gracefully (write() returns -1) instead of interfering with the new request or closing the connection.

  2. Extracted hookHttpCb(): The duplicated http_cb lambda is now in a private hookHttpCb() method called by both Init() and Reset().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[http server]Receiving http pipeline while asynchronously writing resp, there is a potential crash risk

3 participants