Skip to content

Commit 8a4a193

Browse files
addaleaxtargos
authored andcommitted
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. PR-URL: nodejs#29122 Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent ba624b6 commit 8a4a193

File tree

3 files changed

+136
-43
lines changed

3 files changed

+136
-43
lines changed

src/node_http2.cc

+87-41
Original file line numberDiff line numberDiff line change
@@ -868,31 +868,51 @@ ssize_t Http2Session::OnCallbackPadding(size_t frameLen,
868868
// various callback functions. Each of these will typically result in a call
869869
// out to JavaScript so this particular function is rather hot and can be
870870
// quite expensive. This is a potential performance optimization target later.
871-
ssize_t Http2Session::Write(const uv_buf_t* bufs, size_t nbufs) {
872-
size_t total = 0;
873-
// Note that nghttp2_session_mem_recv is a synchronous operation that
874-
// will trigger a number of other callbacks. Those will, in turn have
871+
ssize_t Http2Session::ConsumeHTTP2Data() {
872+
CHECK_NOT_NULL(stream_buf_.base);
873+
CHECK_LT(stream_buf_offset_, stream_buf_.len);
874+
size_t read_len = stream_buf_.len - stream_buf_offset_;
875+
875876
// multiple side effects.
876-
for (size_t n = 0; n < nbufs; n++) {
877-
Debug(this, "receiving %d bytes [wants data? %d]",
878-
bufs[n].len,
879-
nghttp2_session_want_read(session_));
880-
ssize_t ret =
881-
nghttp2_session_mem_recv(session_,
882-
reinterpret_cast<uint8_t*>(bufs[n].base),
883-
bufs[n].len);
884-
CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
885-
886-
if (ret < 0)
887-
return ret;
877+
Debug(this, "receiving %d bytes [wants data? %d]",
878+
read_len,
879+
nghttp2_session_want_read(session_));
880+
flags_ &= ~SESSION_STATE_NGHTTP2_RECV_PAUSED;
881+
ssize_t ret =
882+
nghttp2_session_mem_recv(session_,
883+
reinterpret_cast<uint8_t*>(stream_buf_.base) +
884+
stream_buf_offset_,
885+
read_len);
886+
CHECK_NE(ret, NGHTTP2_ERR_NOMEM);
888887

889-
total += ret;
888+
if (flags_ & SESSION_STATE_NGHTTP2_RECV_PAUSED) {
889+
CHECK_NE(flags_ & SESSION_STATE_READING_STOPPED, 0);
890+
891+
CHECK_GT(ret, 0);
892+
CHECK_LE(static_cast<size_t>(ret), read_len);
893+
894+
if (static_cast<size_t>(ret) < read_len) {
895+
// Mark the remainder of the data as available for later consumption.
896+
stream_buf_offset_ += ret;
897+
return ret;
898+
}
890899
}
900+
901+
// We are done processing the current input chunk.
902+
DecrementCurrentSessionMemory(stream_buf_.len);
903+
stream_buf_offset_ = 0;
904+
stream_buf_ab_.Reset();
905+
stream_buf_allocation_.clear();
906+
stream_buf_ = uv_buf_init(nullptr, 0);
907+
908+
if (ret < 0)
909+
return ret;
910+
891911
// Send any data that was queued up while processing the received data.
892912
if (!IsDestroyed()) {
893913
SendPendingData();
894914
}
895-
return total;
915+
return ret;
896916
}
897917

898918

@@ -1194,8 +1214,18 @@ int Http2Session::OnDataChunkReceived(nghttp2_session* handle,
11941214
nghttp2_session_consume_stream(handle, id, avail);
11951215
else
11961216
stream->inbound_consumed_data_while_paused_ += avail;
1217+
1218+
// If we have a gathered a lot of data for output, try sending it now.
1219+
if (session->outgoing_length_ > 4096) session->SendPendingData();
11971220
} while (len != 0);
11981221

1222+
// If we are currently waiting for a write operation to finish, we should
1223+
// tell nghttp2 that we want to wait before we process more input data.
1224+
if (session->flags_ & SESSION_STATE_WRITE_IN_PROGRESS) {
1225+
session->flags_ |= SESSION_STATE_NGHTTP2_RECV_PAUSED;
1226+
return NGHTTP2_ERR_PAUSE;
1227+
}
1228+
11991229
return 0;
12001230
}
12011231

@@ -1283,6 +1313,7 @@ void Http2StreamListener::OnStreamRead(ssize_t nread, const uv_buf_t& buf) {
12831313
size_t offset = buf.base - session->stream_buf_.base;
12841314

12851315
// Verify that the data offset is inside the current read buffer.
1316+
CHECK_GE(offset, session->stream_buf_offset_);
12861317
CHECK_LE(offset, session->stream_buf_.len);
12871318
CHECK_LE(offset + buf.len, session->stream_buf_.len);
12881319

@@ -1554,6 +1585,11 @@ void Http2Session::OnStreamAfterWrite(WriteWrap* w, int status) {
15541585
stream_->ReadStart();
15551586
}
15561587

1588+
// If there is more incoming data queued up, consume it.
1589+
if (stream_buf_offset_ > 0) {
1590+
ConsumeHTTP2Data();
1591+
}
1592+
15571593
if (!(flags_ & SESSION_STATE_WRITE_SCHEDULED)) {
15581594
// Schedule a new write if nghttp2 wants to send data.
15591595
MaybeScheduleWrite();
@@ -1609,6 +1645,7 @@ void Http2Session::ClearOutgoing(int status) {
16091645

16101646
if (outgoing_buffers_.size() > 0) {
16111647
outgoing_storage_.clear();
1648+
outgoing_length_ = 0;
16121649

16131650
std::vector<nghttp2_stream_write> current_outgoing_buffers_;
16141651
current_outgoing_buffers_.swap(outgoing_buffers_);
@@ -1639,6 +1676,11 @@ void Http2Session::ClearOutgoing(int status) {
16391676
}
16401677
}
16411678

1679+
void Http2Session::PushOutgoingBuffer(nghttp2_stream_write&& write) {
1680+
outgoing_length_ += write.buf.len;
1681+
outgoing_buffers_.emplace_back(std::move(write));
1682+
}
1683+
16421684
// Queue a given block of data for sending. This always creates a copy,
16431685
// so it is used for the cases in which nghttp2 requests sending of a
16441686
// small chunk of data.
@@ -1651,7 +1693,7 @@ void Http2Session::CopyDataIntoOutgoing(const uint8_t* src, size_t src_length) {
16511693
// of the outgoing_buffers_ vector may invalidate the pointer.
16521694
// The correct base pointers will be set later, before writing to the
16531695
// underlying socket.
1654-
outgoing_buffers_.emplace_back(nghttp2_stream_write {
1696+
PushOutgoingBuffer(nghttp2_stream_write {
16551697
uv_buf_init(nullptr, src_length)
16561698
});
16571699
}
@@ -1774,13 +1816,13 @@ int Http2Session::OnSendData(
17741816
if (write.buf.len <= length) {
17751817
// This write does not suffice by itself, so we can consume it completely.
17761818
length -= write.buf.len;
1777-
session->outgoing_buffers_.emplace_back(std::move(write));
1819+
session->PushOutgoingBuffer(std::move(write));
17781820
stream->queue_.pop();
17791821
continue;
17801822
}
17811823

17821824
// Slice off `length` bytes of the first write in the queue.
1783-
session->outgoing_buffers_.emplace_back(nghttp2_stream_write {
1825+
session->PushOutgoingBuffer(nghttp2_stream_write {
17841826
uv_buf_init(write.buf.base, length)
17851827
});
17861828
write.buf.base += length;
@@ -1790,7 +1832,7 @@ int Http2Session::OnSendData(
17901832

17911833
if (frame->data.padlen > 0) {
17921834
// Send padding if that was requested.
1793-
session->outgoing_buffers_.emplace_back(nghttp2_stream_write {
1835+
session->PushOutgoingBuffer(nghttp2_stream_write {
17941836
uv_buf_init(const_cast<char*>(zero_bytes_256), frame->data.padlen - 1)
17951837
});
17961838
}
@@ -1827,8 +1869,6 @@ void Http2Session::OnStreamRead(ssize_t nread, const uv_buf_t& buf_) {
18271869
Http2Scope h2scope(this);
18281870
CHECK_NOT_NULL(stream_);
18291871
Debug(this, "receiving %d bytes", nread);
1830-
CHECK_EQ(stream_buf_allocation_.size(), 0);
1831-
CHECK(stream_buf_ab_.IsEmpty());
18321872
AllocatedBuffer buf(env(), buf_);
18331873

18341874
// Only pass data on if nread > 0
@@ -1839,24 +1879,31 @@ void Http2Session::OnStreamRead(ssize_t nread, const uv_buf_t& buf_) {
18391879
return;
18401880
}
18411881

1842-
// Shrink to the actual amount of used data.
1843-
buf.Resize(nread);
1882+
statistics_.data_received += nread;
18441883

1845-
IncrementCurrentSessionMemory(nread);
1846-
OnScopeLeave on_scope_leave([&]() {
1847-
// Once finished handling this write, reset the stream buffer.
1848-
// The memory has either been free()d or was handed over to V8.
1849-
// We use `nread` instead of `buf.size()` here, because the buffer is
1850-
// cleared as part of the `.ToArrayBuffer()` call below.
1851-
DecrementCurrentSessionMemory(nread);
1884+
if (UNLIKELY(stream_buf_offset_ > 0)) {
1885+
// This is a very unlikely case, and should only happen if the ReadStart()
1886+
// call in OnStreamAfterWrite() immediately provides data. If that does
1887+
// happen, we concatenate the data we received with the already-stored
1888+
// pending input data, slicing off the already processed part.
1889+
AllocatedBuffer new_buf = env()->AllocateManaged(
1890+
stream_buf_.len - stream_buf_offset_ + nread);
1891+
memcpy(new_buf.data(),
1892+
stream_buf_.base + stream_buf_offset_,
1893+
stream_buf_.len - stream_buf_offset_);
1894+
memcpy(new_buf.data() + stream_buf_.len - stream_buf_offset_,
1895+
buf.data(),
1896+
nread);
1897+
buf = std::move(new_buf);
1898+
nread = buf.size();
1899+
stream_buf_offset_ = 0;
18521900
stream_buf_ab_.Reset();
1853-
stream_buf_allocation_.clear();
1854-
stream_buf_ = uv_buf_init(nullptr, 0);
1855-
});
1901+
DecrementCurrentSessionMemory(stream_buf_offset_);
1902+
}
18561903

1857-
// Make sure that there was no read previously active.
1858-
CHECK_NULL(stream_buf_.base);
1859-
CHECK_EQ(stream_buf_.len, 0);
1904+
// Shrink to the actual amount of used data.
1905+
buf.Resize(nread);
1906+
IncrementCurrentSessionMemory(nread);
18601907

18611908
// Remember the current buffer, so that OnDataChunkReceived knows the
18621909
// offset of a DATA frame's data into the socket read buffer.
@@ -1869,8 +1916,7 @@ void Http2Session::OnStreamRead(ssize_t nread, const uv_buf_t& buf_) {
18691916
// to copy memory.
18701917
stream_buf_allocation_ = std::move(buf);
18711918

1872-
statistics_.data_received += nread;
1873-
ssize_t ret = Write(&stream_buf_, 1);
1919+
ssize_t ret = ConsumeHTTP2Data();
18741920

18751921
if (UNLIKELY(ret < 0)) {
18761922
Debug(this, "fatal error receiving data: %d", ret);

src/node_http2.h

+10-2
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ enum session_state_flags {
337337
SESSION_STATE_SENDING = 0x10,
338338
SESSION_STATE_WRITE_IN_PROGRESS = 0x20,
339339
SESSION_STATE_READING_STOPPED = 0x40,
340+
SESSION_STATE_NGHTTP2_RECV_PAUSED = 0x80
340341
};
341342

342343
typedef uint32_t(*get_setting)(nghttp2_session* session,
@@ -767,14 +768,15 @@ class Http2Session : public AsyncWrap, public StreamListener {
767768
// Indicates whether there currently exist outgoing buffers for this stream.
768769
bool HasWritesOnSocketForStream(Http2Stream* stream);
769770

770-
// Write data to the session
771-
ssize_t Write(const uv_buf_t* bufs, size_t nbufs);
771+
// Write data from stream_buf_ to the session
772+
ssize_t ConsumeHTTP2Data();
772773

773774
void MemoryInfo(MemoryTracker* tracker) const override {
774775
tracker->TrackField("streams", streams_);
775776
tracker->TrackField("outstanding_pings", outstanding_pings_);
776777
tracker->TrackField("outstanding_settings", outstanding_settings_);
777778
tracker->TrackField("outgoing_buffers", outgoing_buffers_);
779+
tracker->TrackFieldWithSize("stream_buf", stream_buf_.len);
778780
tracker->TrackFieldWithSize("outgoing_storage", outgoing_storage_.size());
779781
tracker->TrackFieldWithSize("pending_rst_streams",
780782
pending_rst_streams_.size() * sizeof(int32_t));
@@ -833,6 +835,7 @@ class Http2Session : public AsyncWrap, public StreamListener {
833835
}
834836

835837
void DecrementCurrentSessionMemory(uint64_t amount) {
838+
DCHECK_LE(amount, current_session_memory_);
836839
current_session_memory_ -= amount;
837840
}
838841

@@ -995,8 +998,11 @@ class Http2Session : public AsyncWrap, public StreamListener {
995998
uint32_t chunks_sent_since_last_write_ = 0;
996999

9971000
uv_buf_t stream_buf_ = uv_buf_init(nullptr, 0);
1001+
// When processing input data, either stream_buf_ab_ or stream_buf_allocation_
1002+
// will be set. stream_buf_ab_ is lazily created from stream_buf_allocation_.
9981003
v8::Global<v8::ArrayBuffer> stream_buf_ab_;
9991004
AllocatedBuffer stream_buf_allocation_;
1005+
size_t stream_buf_offset_ = 0;
10001006

10011007
size_t max_outstanding_pings_ = DEFAULT_MAX_PINGS;
10021008
std::queue<std::unique_ptr<Http2Ping>> outstanding_pings_;
@@ -1006,6 +1012,7 @@ class Http2Session : public AsyncWrap, public StreamListener {
10061012

10071013
std::vector<nghttp2_stream_write> outgoing_buffers_;
10081014
std::vector<uint8_t> outgoing_storage_;
1015+
size_t outgoing_length_ = 0;
10091016
std::vector<int32_t> pending_rst_streams_;
10101017
// Count streams that have been rejected while being opened. Exceeding a fixed
10111018
// limit will result in the session being destroyed, as an indication of a
@@ -1015,6 +1022,7 @@ class Http2Session : public AsyncWrap, public StreamListener {
10151022
// Also use the invalid frame count as a measure for rejecting input frames.
10161023
int32_t invalid_frame_count_ = 0;
10171024

1025+
void PushOutgoingBuffer(nghttp2_stream_write&& write);
10181026
void CopyDataIntoOutgoing(const uint8_t* src, size_t src_length);
10191027
void ClearOutgoing(int status);
10201028

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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': '/' }).end();
30+
const chunks = [];
31+
req.on('data', (chunk) => {
32+
chunks.push(chunk);
33+
});
34+
req.on('end', common.mustCall(() => {
35+
assert.deepStrictEqual(Buffer.concat(chunks), content);
36+
if (++finished === 100) client.close();
37+
}));
38+
}
39+
}));

0 commit comments

Comments
 (0)