Skip to content

Commit 46de80d

Browse files
authored
http2: avoid uaf while receiving and sending rst_stream
Mark the session as receiving around nghttp2_session_mem_recv() and defer RST_STREAM handling while receive is in progress. This prevents closing a stream while nghttp2 still processes it and avoids heap-use-after-free in nghttp2_session_mem_recv2(). Fixes: #64113 Signed-off-by: Evgeniy Gorbanev <gorbanev.es@gmail.com> PR-URL: #64166 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Tim Perry <pimterry@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
1 parent 5520388 commit 46de80d

2 files changed

Lines changed: 120 additions & 6 deletions

File tree

src/node_http2.cc

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,21 @@ void Http2Session::Close(uint32_t code, bool socket_closed) {
815815
return;
816816
set_closing();
817817

818+
// Do not flush GOAWAY from inside nghttp2_session_mem_recv() callbacks.
819+
// ConsumeHTTP2Data() finishes the close once mem_recv returns.
820+
if (is_receiving()) {
821+
set_close_pending();
822+
pending_close_code_ = code;
823+
pending_close_socket_closed_ = socket_closed;
824+
return;
825+
}
826+
827+
FinishClose(code, socket_closed);
828+
}
829+
830+
void Http2Session::FinishClose(uint32_t code, bool socket_closed) {
831+
CHECK(is_closing());
832+
818833
// Stop reading on the i/o stream
819834
if (stream_ != nullptr) {
820835
set_reading_stopped();
@@ -864,6 +879,12 @@ void Http2Session::Close(uint32_t code, bool socket_closed) {
864879
EmitStatistics();
865880
}
866881

882+
void Http2Session::MaybeFinishPendingClose() {
883+
if (!is_close_pending() || is_destroyed()) return;
884+
set_close_pending(false);
885+
FinishClose(pending_close_code_, pending_close_socket_closed_);
886+
}
887+
867888
// Locates an existing known stream by ID. nghttp2 has a similar method
868889
// but this is faster and does not fail if the stream is not found.
869890
BaseObjectPtr<Http2Stream> Http2Session::FindStream(int32_t id) {
@@ -958,11 +979,13 @@ void Http2Session::ConsumeHTTP2Data() {
958979
nghttp2_session_want_read(session_.get()));
959980
set_receive_paused(false);
960981
custom_recv_error_code_ = nullptr;
982+
set_receiving();
961983
ssize_t ret =
962984
nghttp2_session_mem_recv(session_.get(),
963985
reinterpret_cast<uint8_t*>(stream_buf_.base) +
964986
stream_buf_offset_,
965987
read_len);
988+
set_receiving(false);
966989
CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
967990
CHECK_IMPLIES(custom_recv_error_code_ != nullptr, ret < 0);
968991

@@ -976,6 +999,10 @@ void Http2Session::ConsumeHTTP2Data() {
976999
// Even if all bytes were received, a paused stream may delay the
9771000
// nghttp2_on_frame_recv_callback which may have an END_STREAM flag.
9781001
stream_buf_offset_ += ret;
1002+
// Still complete a Close() deferred during mem_recv; do not fall through
1003+
// to SendPendingData() here (paused receives historically skip that flush
1004+
// because a write may already be in progress).
1005+
MaybeFinishPendingClose();
9791006
goto done;
9801007
}
9811008

@@ -986,12 +1013,23 @@ void Http2Session::ConsumeHTTP2Data() {
9861013
stream_buf_allocation_.reset();
9871014
stream_buf_ = uv_buf_init(nullptr, 0);
9881015

1016+
// Finish a Close() deferred during mem_recv before flushing, so GOAWAY is
1017+
// not written after pending RST_STREAM frames.
1018+
MaybeFinishPendingClose();
1019+
1020+
done:
1021+
// Finish a Close() deferred above before flushing, so GOAWAY is not written
1022+
// after pending RST_STREAM frames.
1023+
if (is_close_pending() && !is_destroyed()) {
1024+
set_close_pending(false);
1025+
FinishClose(pending_close_code_, pending_close_socket_closed_);
1026+
}
1027+
9891028
// Send any data that was queued up while processing the received data.
9901029
if (ret >= 0 && !is_destroyed()) {
9911030
SendPendingData();
9921031
}
9931032

994-
done:
9951033
if (ret < 0) [[unlikely]] {
9961034
Isolate* isolate = env()->isolate();
9971035
Debug(this,
@@ -1418,6 +1456,9 @@ int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
14181456
len -= avail;
14191457
stream->EmitRead(avail, buf);
14201458

1459+
// JS may have destroyed the stream from inside onread; stop delivering.
1460+
if (stream->is_destroyed()) break;
1461+
14211462
// If the stream owner (e.g. the JS Http2Stream) wants more data, just
14221463
// tell nghttp2 that all data has been consumed. Otherwise, defer until
14231464
// more data is being requested.
@@ -1975,6 +2016,12 @@ uint8_t Http2Session::SendPendingData() {
19752016
// SendPendingData should not be called recursively.
19762017
if (is_sending())
19772018
return 1;
2019+
2020+
// Do not call `nghttp2_session_mem_send()` while nghttp2 is processing
2021+
// incoming data. Sending may close the stream and free nghttp2 state
2022+
// that is still in use by `nghttp2_session_mem_recv()`.
2023+
if (is_receiving()) return 1;
2024+
19782025
// This is cleared by ClearOutgoing().
19792026
set_sending();
19802027

@@ -2387,10 +2434,48 @@ void Http2Stream::Destroy() {
23872434
// Do nothing if this stream instance is already destroyed
23882435
if (is_destroyed())
23892436
return;
2390-
if (session_->has_pending_rststream(id_))
2391-
FlushRstStream();
2437+
2438+
// Session may already be gone if destroy was deferred across a session
2439+
// teardown.
2440+
if (!session_) {
2441+
set_destroyed();
2442+
Detach();
2443+
return;
2444+
}
2445+
2446+
// Mark destroyed immediately so OnDataChunkReceived stops EmitRead into an
2447+
// already-destroyed JS stream (which would treat the byte count as errno).
23922448
set_destroyed();
23932449

2450+
// While mem_recv is active, do not FlushRstStream or RemoveStream yet:
2451+
// - FlushRstStream would close the nghttp2 stream before queued response
2452+
// DATA can be mem_send'd after receive returns.
2453+
// - RemoveStream would make OnSendData/Provider::OnRead fail to FindStream.
2454+
// Pending RSTs stay in pending_rst_streams_ and are flushed from
2455+
// ClearOutgoing after the post-receive SendPendingData.
2456+
if (session_->is_receiving()) {
2457+
BaseObjectPtr<Http2Stream> strong_ref{this};
2458+
env()->SetImmediate(
2459+
[this, strong_ref](Environment*) { CompleteDestroyCleanup(); });
2460+
return;
2461+
}
2462+
2463+
if (session_->has_pending_rststream(id_)) FlushRstStream();
2464+
2465+
CompleteDestroyCleanup();
2466+
}
2467+
2468+
void Http2Stream::CompleteDestroyCleanup() {
2469+
if (!session_) {
2470+
Detach();
2471+
return;
2472+
}
2473+
2474+
// Destroy() always set_destroyed() before scheduling or calling this.
2475+
CHECK(is_destroyed());
2476+
2477+
if (session_->has_pending_rststream(id_)) FlushRstStream();
2478+
23942479
Debug(this, "destroying stream");
23952480

23962481
// Wait until the start of the next loop to delete because there
@@ -2427,7 +2512,6 @@ void Http2Stream::Destroy() {
24272512
EmitStatistics();
24282513
}
24292514

2430-
24312515
// Initiates a response on the Http2Stream using data provided via the
24322516
// StreamBase Streams API.
24332517
int Http2Stream::SubmitResponse(const Http2Headers& headers, int options) {
@@ -2559,6 +2643,18 @@ void Http2Stream::SubmitRstStream(const uint32_t code) {
25592643
return code == NGHTTP2_CANCEL;
25602644
};
25612645

2646+
// Do not call `nghttp2_session_mem_send()` while nghttp2 is processing
2647+
// incoming data. Sending may close the stream and free nghttp2 state
2648+
// that is still in use by `nghttp2_session_mem_recv()`.
2649+
if (session_->is_receiving() && available_outbound_length_ == 0) {
2650+
if (is_stream_cancel(code)) {
2651+
session_->AddPendingRstStream(id_);
2652+
return;
2653+
}
2654+
FlushRstStream();
2655+
return;
2656+
}
2657+
25622658
// If RST_STREAM frame is received with error code NGHTTP2_CANCEL,
25632659
// add it to the pending list and don't force purge the data. It is
25642660
// to avoids the double free error due to unwanted behavior of nghttp2.
@@ -2586,8 +2682,8 @@ void Http2Stream::SubmitRstStream(const uint32_t code) {
25862682
}
25872683

25882684
void Http2Stream::FlushRstStream() {
2589-
if (is_destroyed())
2590-
return;
2685+
if (!session_) return;
2686+
session_->RemovePendingRstStream(id_);
25912687
Http2Scope h2scope(this);
25922688
CHECK_EQ(nghttp2_submit_rst_stream(
25932689
session_->session(),

src/node_http2.h

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ constexpr int kSessionStateSending = 0x10;
8282
constexpr int kSessionStateWriteInProgress = 0x20;
8383
constexpr int kSessionStateReadingStopped = 0x40;
8484
constexpr int kSessionStateReceivePaused = 0x80;
85+
constexpr int kSessionStateReceiving = 0x100;
86+
constexpr int kSessionStateClosePending = 0x200;
8587

8688
// The Padding Strategy determines the method by which extra padding is
8789
// selected for HEADERS and DATA frames. These are configurable via the
@@ -339,6 +341,10 @@ class Http2Stream : public AsyncWrap,
339341
// Destroy this stream instance and free all held memory.
340342
void Destroy();
341343

344+
// Completes Destroy() after set_destroyed(); may run deferred until after
345+
// nghttp2_session_mem_recv() returns.
346+
void CompleteDestroyCleanup();
347+
342348
bool is_destroyed() const {
343349
return flags_ & kStreamStateDestroyed;
344350
}
@@ -689,6 +695,8 @@ class Http2Session : public AsyncWrap,
689695
IS_FLAG(write_in_progress, kSessionStateWriteInProgress)
690696
IS_FLAG(reading_stopped, kSessionStateReadingStopped)
691697
IS_FLAG(receive_paused, kSessionStateReceivePaused)
698+
IS_FLAG(receiving, kSessionStateReceiving)
699+
IS_FLAG(close_pending, kSessionStateClosePending)
692700

693701
#undef IS_FLAG
694702

@@ -732,6 +740,10 @@ class Http2Session : public AsyncWrap,
732740
std::ranges::find(pending_rst_streams_, stream_id);
733741
}
734742

743+
void RemovePendingRstStream(int32_t stream_id) {
744+
std::erase(pending_rst_streams_, stream_id);
745+
}
746+
735747
// Handle reads/writes from the underlying network transport.
736748
uv_buf_t OnStreamAlloc(size_t suggested_size) override;
737749
void OnStreamRead(ssize_t nread, const uv_buf_t& buf) override;
@@ -981,6 +993,10 @@ class Http2Session : public AsyncWrap,
981993
std::vector<uint8_t> outgoing_storage_;
982994
size_t outgoing_length_ = 0;
983995
std::vector<int32_t> pending_rst_streams_;
996+
// Saved arguments for Close() deferred while nghttp2_session_mem_recv()
997+
// callbacks are active.
998+
uint32_t pending_close_code_ = NGHTTP2_NO_ERROR;
999+
bool pending_close_socket_closed_ = false;
9841000
// Count streams that have been rejected while being opened. Exceeding a fixed
9851001
// limit will result in the session being destroyed, as an indication of a
9861002
// misbehaving peer. This counter is reset once new streams are being
@@ -995,6 +1011,8 @@ class Http2Session : public AsyncWrap,
9951011

9961012
void CopyDataIntoOutgoing(const uint8_t* src, size_t src_length);
9971013
void ClearOutgoing(int status);
1014+
void FinishClose(uint32_t code, bool socket_closed);
1015+
void MaybeFinishPendingClose();
9981016

9991017
void MaybeNotifyGracefulCloseComplete();
10001018

0 commit comments

Comments
 (0)