Skip to content

Commit 7f11465

Browse files
addaleaxBethGriggs
authored andcommitted
http2: do not create ArrayBuffers when no DATA received
Lazily allocate `ArrayBuffer`s for the contents of DATA frames. Creating `ArrayBuffer`s is, sadly, not a cheap operation with V8. This is part of performance improvements to mitigate CVE-2019-9513. Together with the previous commit, these changes improve throughput in the adversarial case by about 100 %, and there is little more that we can do besides artificially limiting the rate of incoming metadata frames (i.e. after this patch, CPU usage is virtually exclusively in libnghttp2). [This backport also applies changes from 83e1b97 and required some manual work due to the lack of `AllocatedBuffer` on v10.x.] Refs: #26201 Backport-PR-URL: #29123 PR-URL: #29122 Reviewed-By: Rich Trott <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 2eb914f commit 7f11465

File tree

3 files changed

+40
-22
lines changed

3 files changed

+40
-22
lines changed

src/node_http2.cc

+37-20
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,7 @@ Http2Session::~Http2Session() {
650650
stream.second->session_ = nullptr;
651651
nghttp2_session_del(session_);
652652
CHECK_EQ(current_nghttp2_memory_, 0);
653+
free(stream_buf_allocation_.base);
653654
}
654655

655656
std::string Http2Session::diagnostic_name() const {
@@ -1259,7 +1260,17 @@ void Http2StreamListener::OnStreamRead(ssize_t nread, const uv_buf_t& buf) {
12591260
return;
12601261
}
12611262

1262-
CHECK(!session->stream_buf_ab_.IsEmpty());
1263+
Local<ArrayBuffer> ab;
1264+
if (session->stream_buf_ab_.IsEmpty()) {
1265+
ab = ArrayBuffer::New(env->isolate(),
1266+
session->stream_buf_allocation_.base,
1267+
session->stream_buf_allocation_.len,
1268+
v8::ArrayBufferCreationMode::kInternalized);
1269+
session->stream_buf_allocation_ = uv_buf_init(nullptr, 0);
1270+
session->stream_buf_ab_.Reset(env->isolate(), ab);
1271+
} else {
1272+
ab = session->stream_buf_ab_.Get(env->isolate());
1273+
}
12631274

12641275
// There is a single large array buffer for the entire data read from the
12651276
// network; create a slice of that array buffer and emit it as the
@@ -1271,7 +1282,7 @@ void Http2StreamListener::OnStreamRead(ssize_t nread, const uv_buf_t& buf) {
12711282
CHECK_LE(offset + buf.len, session->stream_buf_.len);
12721283

12731284
Local<Object> buffer =
1274-
Buffer::New(env, session->stream_buf_ab_, offset, nread).ToLocalChecked();
1285+
Buffer::New(env, ab, offset, nread).ToLocalChecked();
12751286

12761287
stream->CallJSOnreadMethod(nread, buffer);
12771288
}
@@ -1803,32 +1814,41 @@ Http2Stream* Http2Session::SubmitRequest(
18031814
}
18041815

18051816
// Callback used to receive inbound data from the i/o stream
1806-
void Http2Session::OnStreamRead(ssize_t nread, const uv_buf_t& buf) {
1817+
void Http2Session::OnStreamRead(ssize_t nread, const uv_buf_t& buf_) {
18071818
HandleScope handle_scope(env()->isolate());
18081819
Context::Scope context_scope(env()->context());
18091820
Http2Scope h2scope(this);
18101821
CHECK_NOT_NULL(stream_);
18111822
Debug(this, "receiving %d bytes", nread);
1812-
IncrementCurrentSessionMemory(buf.len);
1823+
CHECK_EQ(stream_buf_allocation_.base, nullptr);
18131824
CHECK(stream_buf_ab_.IsEmpty());
18141825

1815-
OnScopeLeave on_scope_leave([&]() {
1816-
// Once finished handling this write, reset the stream buffer.
1817-
// The memory has either been free()d or was handed over to V8.
1818-
DecrementCurrentSessionMemory(buf.len);
1819-
stream_buf_ab_ = Local<ArrayBuffer>();
1820-
stream_buf_ = uv_buf_init(nullptr, 0);
1821-
});
1822-
18231826
// Only pass data on if nread > 0
18241827
if (nread <= 0) {
1825-
free(buf.base);
1828+
free(buf_.base);
18261829
if (nread < 0) {
18271830
PassReadErrorToPreviousListener(nread);
18281831
}
18291832
return;
18301833
}
18311834

1835+
// Shrink to the actual amount of used data.
1836+
uv_buf_t buf = buf_;
1837+
buf.base = Realloc(buf.base, nread);
1838+
1839+
IncrementCurrentSessionMemory(nread);
1840+
OnScopeLeave on_scope_leave([&]() {
1841+
// Once finished handling this write, reset the stream buffer.
1842+
// The memory has either been free()d or was handed over to V8.
1843+
// We use `nread` instead of `buf.size()` here, because the buffer is
1844+
// cleared as part of the `.ToArrayBuffer()` call below.
1845+
DecrementCurrentSessionMemory(nread);
1846+
stream_buf_ab_.Reset();
1847+
free(stream_buf_allocation_.base);
1848+
stream_buf_allocation_ = uv_buf_init(nullptr, 0);
1849+
stream_buf_ = uv_buf_init(nullptr, 0);
1850+
});
1851+
18321852
// Make sure that there was no read previously active.
18331853
CHECK_NULL(stream_buf_.base);
18341854
CHECK_EQ(stream_buf_.len, 0);
@@ -1845,13 +1865,10 @@ void Http2Session::OnStreamRead(ssize_t nread, const uv_buf_t& buf) {
18451865

18461866
Isolate* isolate = env()->isolate();
18471867

1848-
// Create an array buffer for the read data. DATA frames will be emitted
1849-
// as slices of this array buffer to avoid having to copy memory.
1850-
stream_buf_ab_ =
1851-
ArrayBuffer::New(isolate,
1852-
buf.base,
1853-
nread,
1854-
v8::ArrayBufferCreationMode::kInternalized);
1868+
// Store this so we can create an ArrayBuffer for read data from it.
1869+
// DATA frames will be emitted as slices of that ArrayBuffer to avoid having
1870+
// to copy memory.
1871+
stream_buf_allocation_ = buf;
18551872

18561873
statistics_.data_received += nread;
18571874
ssize_t ret = Write(&stream_buf_, 1);

src/node_http2.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -1005,7 +1005,8 @@ class Http2Session : public AsyncWrap, public StreamListener {
10051005
uint32_t chunks_sent_since_last_write_ = 0;
10061006

10071007
uv_buf_t stream_buf_ = uv_buf_init(nullptr, 0);
1008-
v8::Local<v8::ArrayBuffer> stream_buf_ab_;
1008+
v8::Global<v8::ArrayBuffer> stream_buf_ab_;
1009+
uv_buf_t stream_buf_allocation_ = uv_buf_init(nullptr, 0);
10091010

10101011
size_t max_outstanding_pings_ = DEFAULT_MAX_PINGS;
10111012
std::queue<Http2Ping*> outstanding_pings_;

test/sequential/test-http2-max-session-memory.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const http2 = require('http2');
88

99
// Test that maxSessionMemory Caps work
1010

11-
const largeBuffer = Buffer.alloc(1e6);
11+
const largeBuffer = Buffer.alloc(2e6);
1212

1313
const server = http2.createServer({ maxSessionMemory: 1 });
1414

0 commit comments

Comments
 (0)