Skip to content

Commit 5579c49

Browse files
committed
http2: defer send/close/destroy while session is receiving
nghttp2 forbids calling nghttp2_session_mem_send() from mem_recv callbacks. Signed-off-by: Evgeniy Gorbanev <gorbanev.es@gmail.com>
1 parent dec48cc commit 5579c49

2 files changed

Lines changed: 104 additions & 6 deletions

File tree

src/node_http2.cc

Lines changed: 88 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) {
@@ -978,6 +999,10 @@ void Http2Session::ConsumeHTTP2Data() {
978999
// Even if all bytes were received, a paused stream may delay the
9791000
// nghttp2_on_frame_recv_callback which may have an END_STREAM flag.
9801001
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();
9811006
goto done;
9821007
}
9831008

@@ -988,12 +1013,23 @@ void Http2Session::ConsumeHTTP2Data() {
9881013
stream_buf_allocation_.reset();
9891014
stream_buf_ = uv_buf_init(nullptr, 0);
9901015

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+
9911028
// Send any data that was queued up while processing the received data.
9921029
if (ret >= 0 && !is_destroyed()) {
9931030
SendPendingData();
9941031
}
9951032

996-
done:
9971033
if (ret < 0) [[unlikely]] {
9981034
Isolate* isolate = env()->isolate();
9991035
Debug(this,
@@ -1420,6 +1456,9 @@ int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
14201456
len -= avail;
14211457
stream->EmitRead(avail, buf);
14221458

1459+
// JS may have destroyed the stream from inside onread; stop delivering.
1460+
if (stream->is_destroyed()) break;
1461+
14231462
// If the stream owner (e.g. the JS Http2Stream) wants more data, just
14241463
// tell nghttp2 that all data has been consumed. Otherwise, defer until
14251464
// more data is being requested.
@@ -1977,6 +2016,12 @@ uint8_t Http2Session::SendPendingData() {
19772016
// SendPendingData should not be called recursively.
19782017
if (is_sending())
19792018
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+
19802025
// This is cleared by ClearOutgoing().
19812026
set_sending();
19822027

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

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

23962479
// Wait until the start of the next loop to delete because there
@@ -2427,7 +2510,6 @@ void Http2Stream::Destroy() {
24272510
EmitStatistics();
24282511
}
24292512

2430-
24312513
// Initiates a response on the Http2Stream using data provided via the
24322514
// StreamBase Streams API.
24332515
int Http2Stream::SubmitResponse(const Http2Headers& headers, int options) {
@@ -2575,8 +2657,8 @@ void Http2Stream::SubmitRstStream(const uint32_t code) {
25752657
}
25762658

25772659
void Http2Stream::FlushRstStream() {
2578-
if (is_destroyed())
2579-
return;
2660+
if (!session_) return;
2661+
session_->RemovePendingRstStream(id_);
25802662
Http2Scope h2scope(this);
25812663
CHECK_EQ(nghttp2_submit_rst_stream(
25822664
session_->session(),

src/node_http2.h

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ constexpr int kSessionStateWriteInProgress = 0x20;
7878
constexpr int kSessionStateReadingStopped = 0x40;
7979
constexpr int kSessionStateReceivePaused = 0x80;
8080
constexpr int kSessionStateReceiving = 0x100;
81+
constexpr int kSessionStateClosePending = 0x200;
8182

8283
// The Padding Strategy determines the method by which extra padding is
8384
// selected for HEADERS and DATA frames. These are configurable via the
@@ -333,6 +334,10 @@ class Http2Stream : public AsyncWrap,
333334
// Destroy this stream instance and free all held memory.
334335
void Destroy();
335336

337+
// Completes Destroy() after set_destroyed(); may run deferred until after
338+
// nghttp2_session_mem_recv() returns.
339+
void CompleteDestroyCleanup();
340+
336341
bool is_destroyed() const {
337342
return flags_ & kStreamStateDestroyed;
338343
}
@@ -671,6 +676,7 @@ class Http2Session : public AsyncWrap,
671676
IS_FLAG(reading_stopped, kSessionStateReadingStopped)
672677
IS_FLAG(receive_paused, kSessionStateReceivePaused)
673678
IS_FLAG(receiving, kSessionStateReceiving)
679+
IS_FLAG(close_pending, kSessionStateClosePending)
674680

675681
#undef IS_FLAG
676682

@@ -714,6 +720,10 @@ class Http2Session : public AsyncWrap,
714720
std::ranges::find(pending_rst_streams_, stream_id);
715721
}
716722

723+
void RemovePendingRstStream(int32_t stream_id) {
724+
std::erase(pending_rst_streams_, stream_id);
725+
}
726+
717727
// Handle reads/writes from the underlying network transport.
718728
uv_buf_t OnStreamAlloc(size_t suggested_size) override;
719729
void OnStreamRead(ssize_t nread, const uv_buf_t& buf) override;
@@ -963,6 +973,10 @@ class Http2Session : public AsyncWrap,
963973
std::vector<uint8_t> outgoing_storage_;
964974
size_t outgoing_length_ = 0;
965975
std::vector<int32_t> pending_rst_streams_;
976+
// Saved arguments for Close() deferred while nghttp2_session_mem_recv()
977+
// callbacks are active.
978+
uint32_t pending_close_code_ = NGHTTP2_NO_ERROR;
979+
bool pending_close_socket_closed_ = false;
966980
// Count streams that have been rejected while being opened. Exceeding a fixed
967981
// limit will result in the session being destroyed, as an indication of a
968982
// misbehaving peer. This counter is reset once new streams are being
@@ -977,6 +991,8 @@ class Http2Session : public AsyncWrap,
977991

978992
void CopyDataIntoOutgoing(const uint8_t* src, size_t src_length);
979993
void ClearOutgoing(int status);
994+
void FinishClose(uint32_t code, bool socket_closed);
995+
void MaybeFinishPendingClose();
980996

981997
void MaybeNotifyGracefulCloseComplete();
982998

0 commit comments

Comments
 (0)