Skip to content

Commit 6d687f7

Browse files
addaleaxBethGriggs
authored andcommittedAug 15, 2019
http2: pause input processing if sending output
If we are waiting for the ability to send more output, we should not process more input. This commit a) makes us send output earlier, during processing of input, if we accumulate a lot and b) allows interrupting the call into nghttp2 that processes input data and resuming it at a later time, if we do find ourselves in a position where we are waiting to be able to send more output. This is part of mitigating CVE-2019-9511/CVE-2019-9517. Backport-PR-URL: #29124 PR-URL: #29122 Reviewed-By: Rich Trott <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 854dba6 commit 6d687f7

File tree

3 files changed

+141
-45
lines changed

3 files changed

+141
-45
lines changed
 

‎src/node_http2.cc

+90-43
Original file line numberDiff line numberDiff line change
@@ -918,31 +918,51 @@ inline ssize_t Http2Session::OnCallbackPadding(size_t frameLen,
918918
// various callback functions. Each of these will typically result in a call
919919
// out to JavaScript so this particular function is rather hot and can be
920920
// quite expensive. This is a potential performance optimization target later.
921-
inline ssize_t Http2Session::Write(const uv_buf_t* bufs, size_t nbufs) {
922-
size_t total = 0;
923-
// Note that nghttp2_session_mem_recv is a synchronous operation that
924-
// will trigger a number of other callbacks. Those will, in turn have
925-
// multiple side effects.
926-
for (size_t n = 0; n < nbufs; n++) {
927-
DEBUG_HTTP2SESSION2(this, "receiving %d bytes [wants data? %d]",
928-
bufs[n].len,
929-
nghttp2_session_want_read(session_));
930-
ssize_t ret =
931-
nghttp2_session_mem_recv(session_,
932-
reinterpret_cast<uint8_t*>(bufs[n].base),
933-
bufs[n].len);
934-
CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
935-
936-
if (ret < 0)
937-
return ret;
921+
ssize_t Http2Session::ConsumeHTTP2Data() {
922+
CHECK_NE(stream_buf_.base, nullptr);
923+
CHECK_LT(stream_buf_offset_, stream_buf_.len);
924+
size_t read_len = stream_buf_.len - stream_buf_offset_;
925+
926+
DEBUG_HTTP2SESSION2(this, "receiving %d bytes [wants data? %d]",
927+
read_len,
928+
nghttp2_session_want_read(session_));
929+
flags_ &= ~SESSION_STATE_NGHTTP2_RECV_PAUSED;
930+
ssize_t ret =
931+
nghttp2_session_mem_recv(session_,
932+
reinterpret_cast<uint8_t*>(stream_buf_.base) +
933+
stream_buf_offset_,
934+
read_len);
935+
CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
936+
937+
if (flags_ & SESSION_STATE_NGHTTP2_RECV_PAUSED) {
938+
CHECK_NE(flags_ & SESSION_STATE_READING_STOPPED, 0);
939+
940+
CHECK_GT(ret, 0);
941+
CHECK_LE(static_cast<size_t>(ret), read_len);
938942

939-
total += ret;
943+
if (static_cast<size_t>(ret) < read_len) {
944+
// Mark the remainder of the data as available for later consumption.
945+
stream_buf_offset_ += ret;
946+
return ret;
947+
}
940948
}
949+
950+
// We are done processing the current input chunk.
951+
DecrementCurrentSessionMemory(stream_buf_.len);
952+
stream_buf_offset_ = 0;
953+
stream_buf_ab_.Reset();
954+
free(stream_buf_allocation_.base);
955+
stream_buf_allocation_ = uv_buf_init(nullptr, 0);
956+
stream_buf_ = uv_buf_init(nullptr, 0);
957+
958+
if (ret < 0)
959+
return ret;
960+
941961
// Send any data that was queued up while processing the received data.
942962
if (!IsDestroyed()) {
943963
SendPendingData();
944964
}
945-
return total;
965+
return ret;
946966
}
947967

948968

@@ -1238,6 +1258,7 @@ inline int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
12381258
size_t offset = data - reinterpret_cast<uint8_t*>(session->stream_buf_.base);
12391259

12401260
// Verify that the data offset is inside the current read buffer.
1261+
CHECK_GE(offset, session->stream_buf_offset_);
12411262
CHECK_LE(offset, session->stream_buf_.len);
12421263
CHECK_LE(offset + len, session->stream_buf_.len);
12431264

@@ -1250,6 +1271,16 @@ inline int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
12501271
else
12511272
nghttp2_session_consume_stream(handle, id, len);
12521273

1274+
// If we have a gathered a lot of data for output, try sending it now.
1275+
if (session->outgoing_length_ > 4096) session->SendPendingData();
1276+
1277+
// If we are currently waiting for a write operation to finish, we should
1278+
// tell nghttp2 that we want to wait before we process more input data.
1279+
if (session->flags_ & SESSION_STATE_WRITE_IN_PROGRESS) {
1280+
session->flags_ |= SESSION_STATE_NGHTTP2_RECV_PAUSED;
1281+
return NGHTTP2_ERR_PAUSE;
1282+
}
1283+
12531284
return 0;
12541285
}
12551286

@@ -1597,6 +1628,11 @@ void Http2Session::OnStreamAfterWriteImpl(WriteWrap* w, int status, void* ctx) {
15971628
session->stream_->ReadStart();
15981629
}
15991630

1631+
// If there is more incoming data queued up, consume it.
1632+
if (session->stream_buf_offset_ > 0) {
1633+
session->ConsumeHTTP2Data();
1634+
}
1635+
16001636
if (!(session->flags_ & SESSION_STATE_WRITE_SCHEDULED)) {
16011637
// Schedule a new write if nghttp2 wants to send data.
16021638
session->MaybeScheduleWrite();
@@ -1654,6 +1690,7 @@ void Http2Session::ClearOutgoing(int status) {
16541690

16551691
if (outgoing_buffers_.size() > 0) {
16561692
outgoing_storage_.clear();
1693+
outgoing_length_ = 0;
16571694

16581695
std::vector<nghttp2_stream_write> current_outgoing_buffers_;
16591696
current_outgoing_buffers_.swap(outgoing_buffers_);
@@ -1680,6 +1717,11 @@ void Http2Session::ClearOutgoing(int status) {
16801717
}
16811718
}
16821719

1720+
void Http2Session::PushOutgoingBuffer(nghttp2_stream_write&& write) {
1721+
outgoing_length_ += write.buf.len;
1722+
outgoing_buffers_.emplace_back(std::move(write));
1723+
}
1724+
16831725
// Queue a given block of data for sending. This always creates a copy,
16841726
// so it is used for the cases in which nghttp2 requests sending of a
16851727
// small chunk of data.
@@ -1692,7 +1734,7 @@ void Http2Session::CopyDataIntoOutgoing(const uint8_t* src, size_t src_length) {
16921734
// of the outgoing_buffers_ vector may invalidate the pointer.
16931735
// The correct base pointers will be set later, before writing to the
16941736
// underlying socket.
1695-
outgoing_buffers_.emplace_back(nghttp2_stream_write {
1737+
PushOutgoingBuffer(nghttp2_stream_write {
16961738
uv_buf_init(nullptr, src_length)
16971739
});
16981740
}
@@ -1826,13 +1868,13 @@ int Http2Session::OnSendData(
18261868
if (write.buf.len <= length) {
18271869
// This write does not suffice by itself, so we can consume it completely.
18281870
length -= write.buf.len;
1829-
session->outgoing_buffers_.emplace_back(std::move(write));
1871+
session->PushOutgoingBuffer(std::move(write));
18301872
stream->queue_.pop();
18311873
continue;
18321874
}
18331875

18341876
// Slice off `length` bytes of the first write in the queue.
1835-
session->outgoing_buffers_.emplace_back(nghttp2_stream_write {
1877+
session->PushOutgoingBuffer(nghttp2_stream_write {
18361878
uv_buf_init(write.buf.base, length)
18371879
});
18381880
write.buf.base += length;
@@ -1842,7 +1884,7 @@ int Http2Session::OnSendData(
18421884

18431885
if (frame->data.padlen > 0) {
18441886
// Send padding if that was requested.
1845-
session->outgoing_buffers_.emplace_back(nghttp2_stream_write {
1887+
session->PushOutgoingBuffer(nghttp2_stream_write {
18461888
uv_buf_init(const_cast<char*>(zero_bytes_256), frame->data.padlen - 1)
18471889
});
18481890
}
@@ -1896,8 +1938,6 @@ void Http2Session::OnStreamReadImpl(ssize_t nread,
18961938
Http2Scope h2scope(session);
18971939
CHECK_NE(session->stream_, nullptr);
18981940
DEBUG_HTTP2SESSION2(session, "receiving %d bytes", nread);
1899-
CHECK_EQ(session->stream_buf_allocation_.base, nullptr);
1900-
CHECK(session->stream_buf_ab_.IsEmpty());
19011941

19021942
// Only pass data on if nread > 0
19031943
if (nread <= 0) {
@@ -1913,26 +1953,34 @@ void Http2Session::OnStreamReadImpl(ssize_t nread,
19131953
return;
19141954
}
19151955

1916-
// Shrink to the actual amount of used data.
19171956
uv_buf_t buf = *buf_;
1918-
buf.base = Realloc(buf.base, nread);
19191957

1920-
session->IncrementCurrentSessionMemory(nread);
1921-
OnScopeLeave on_scope_leave([&]() {
1922-
// Once finished handling this write, reset the stream buffer.
1923-
// The memory has either been free()d or was handed over to V8.
1924-
// We use `nread` instead of `buf.size()` here, because the buffer is
1925-
// cleared as part of the `.ToArrayBuffer()` call below.
1926-
session->DecrementCurrentSessionMemory(nread);
1958+
session->statistics_.data_received += nread;
1959+
1960+
if (UNLIKELY(session->stream_buf_offset_ > 0)) {
1961+
// This is a very unlikely case, and should only happen if the ReadStart()
1962+
// call in OnStreamAfterWrite() immediately provides data. If that does
1963+
// happen, we concatenate the data we received with the already-stored
1964+
// pending input data, slicing off the already processed part.
1965+
char* new_buf = Malloc(
1966+
session->stream_buf_.len - session->stream_buf_offset_ + nread);
1967+
memcpy(new_buf,
1968+
session->stream_buf_.base + session->stream_buf_offset_,
1969+
session->stream_buf_.len - session->stream_buf_offset_);
1970+
memcpy(new_buf + session->stream_buf_.len - session->stream_buf_offset_,
1971+
buf.base,
1972+
nread);
1973+
free(buf.base);
1974+
nread = session->stream_buf_.len - session->stream_buf_offset_ + nread;
1975+
buf = uv_buf_init(new_buf, nread);
1976+
session->stream_buf_offset_ = 0;
19271977
session->stream_buf_ab_.Reset();
1928-
free(session->stream_buf_allocation_.base);
1929-
session->stream_buf_allocation_ = uv_buf_init(nullptr, 0);
1930-
session->stream_buf_ = uv_buf_init(nullptr, 0);
1931-
});
1978+
session->DecrementCurrentSessionMemory(session->stream_buf_offset_);
1979+
}
19321980

1933-
// Make sure that there was no read previously active.
1934-
CHECK_EQ(session->stream_buf_.base, nullptr);
1935-
CHECK_EQ(session->stream_buf_.len, 0);
1981+
// Shrink to the actual amount of used data.
1982+
buf.base = Realloc(buf.base, nread);
1983+
session->IncrementCurrentSessionMemory(nread);
19361984

19371985
// Remember the current buffer, so that OnDataChunkReceived knows the
19381986
// offset of a DATA frame's data into the socket read buffer.
@@ -1949,8 +1997,7 @@ void Http2Session::OnStreamReadImpl(ssize_t nread,
19491997
// to copy memory.
19501998
session->stream_buf_allocation_ = buf;
19511999

1952-
session->statistics_.data_received += nread;
1953-
ssize_t ret = session->Write(&session->stream_buf_, 1);
2000+
ssize_t ret = session->ConsumeHTTP2Data();
19542001

19552002
if (UNLIKELY(ret < 0)) {
19562003
DEBUG_HTTP2SESSION2(session, "fatal error receiving data: %d", ret);

‎src/node_http2.h

+11-2
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ enum session_state_flags {
386386
SESSION_STATE_SENDING = 0x10,
387387
SESSION_STATE_WRITE_IN_PROGRESS = 0x20,
388388
SESSION_STATE_READING_STOPPED = 0x40,
389+
SESSION_STATE_NGHTTP2_RECV_PAUSED = 0x80
389390
};
390391

391392
// This allows for 4 default-sized frames with their frame headers
@@ -831,8 +832,8 @@ class Http2Session : public AsyncWrap {
831832
// Indicates whether there currently exist outgoing buffers for this stream.
832833
bool HasWritesOnSocketForStream(Http2Stream* stream);
833834

834-
// Write data to the session
835-
inline ssize_t Write(const uv_buf_t* bufs, size_t nbufs);
835+
// Write data from stream_buf_ to the session
836+
ssize_t ConsumeHTTP2Data();
836837

837838
size_t self_size() const override { return sizeof(*this); }
838839

@@ -896,6 +897,9 @@ class Http2Session : public AsyncWrap {
896897
}
897898

898899
void DecrementCurrentSessionMemory(uint64_t amount) {
900+
#ifdef DEBUG
901+
CHECK_LE(amount, current_session_memory_);
902+
#endif
899903
current_session_memory_ -= amount;
900904
}
901905

@@ -1061,8 +1065,11 @@ class Http2Session : public AsyncWrap {
10611065
uint32_t chunks_sent_since_last_write_ = 0;
10621066

10631067
uv_buf_t stream_buf_ = uv_buf_init(nullptr, 0);
1068+
// When processing input data, either stream_buf_ab_ or stream_buf_allocation_
1069+
// will be set. stream_buf_ab_ is lazily created from stream_buf_allocation_.
10641070
v8::Global<v8::ArrayBuffer> stream_buf_ab_;
10651071
uv_buf_t stream_buf_allocation_ = uv_buf_init(nullptr, 0);
1072+
size_t stream_buf_offset_ = 0;
10661073

10671074
size_t max_outstanding_pings_ = DEFAULT_MAX_PINGS;
10681075
std::queue<Http2Ping*> outstanding_pings_;
@@ -1072,6 +1079,7 @@ class Http2Session : public AsyncWrap {
10721079

10731080
std::vector<nghttp2_stream_write> outgoing_buffers_;
10741081
std::vector<uint8_t> outgoing_storage_;
1082+
size_t outgoing_length_ = 0;
10751083
std::vector<int32_t> pending_rst_streams_;
10761084
// Count streams that have been rejected while being opened. Exceeding a fixed
10771085
// limit will result in the session being destroyed, as an indication of a
@@ -1081,6 +1089,7 @@ class Http2Session : public AsyncWrap {
10811089
// Also use the invalid frame count as a measure for rejecting input frames.
10821090
int32_t invalid_frame_count_ = 0;
10831091

1092+
void PushOutgoingBuffer(nghttp2_stream_write&& write);
10841093
void CopyDataIntoOutgoing(const uint8_t* src, size_t src_length);
10851094
void ClearOutgoing(int status);
10861095

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
'use strict';
2+
const common = require('../common');
3+
if (!common.hasCrypto)
4+
common.skip('missing crypto');
5+
6+
const fixtures = require('../common/fixtures');
7+
const assert = require('assert');
8+
const http2 = require('http2');
9+
10+
const content = fixtures.readSync('person-large.jpg');
11+
12+
const server = http2.createServer({
13+
maxSessionMemory: 1000
14+
});
15+
server.on('stream', (stream, headers) => {
16+
stream.respond({
17+
'content-type': 'image/jpeg',
18+
':status': 200
19+
});
20+
stream.end(content);
21+
});
22+
server.unref();
23+
24+
server.listen(0, common.mustCall(() => {
25+
const client = http2.connect(`http://localhost:${server.address().port}/`);
26+
27+
let finished = 0;
28+
for (let i = 0; i < 100; i++) {
29+
const req = client.request({ ':path': '/' });
30+
req.end();
31+
const chunks = [];
32+
req.on('data', (chunk) => {
33+
chunks.push(chunk);
34+
});
35+
req.on('end', common.mustCall(() => {
36+
assert.deepStrictEqual(Buffer.concat(chunks), content);
37+
if (++finished === 100) client.close();
38+
}));
39+
}
40+
}));

0 commit comments

Comments
 (0)
Please sign in to comment.