Skip to content

Commit 9c85ada

Browse files
jasnellMylesBorins
authored andcommitted
http2: implement maxSessionMemory
The maxSessionMemory is a cap for the amount of memory an Http2Session is permitted to consume. If exceeded, new `Http2Stream` sessions will be rejected with an `ENHANCE_YOUR_CALM` error and existing `Http2Stream` instances that are still receiving headers will be terminated with an `ENHANCE_YOUR_CALM` error. Backport-PR-URL: #18050 Backport-PR-URL: #20456 PR-URL: #17967 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent 3766e04 commit 9c85ada

File tree

7 files changed

+172
-15
lines changed

7 files changed

+172
-15
lines changed

doc/api/http2.md

+27
Original file line numberDiff line numberDiff line change
@@ -1652,6 +1652,15 @@ changes:
16521652
* `options` {Object}
16531653
* `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
16541654
for deflating header fields. **Default:** `4Kib`
1655+
* `maxSessionMemory`{number} Sets the maximum memory that the `Http2Session`
1656+
is permitted to use. The value is expressed in terms of number of megabytes,
1657+
e.g. `1` equal 1 megabyte. The minimum value allowed is `1`. **Default:**
1658+
`10`. This is a credit based limit, existing `Http2Stream`s may cause this
1659+
limit to be exceeded, but new `Http2Stream` instances will be rejected
1660+
while this limit is exceeded. The current number of `Http2Stream` sessions,
1661+
the current memory use of the header compression tables, current data
1662+
queued to be sent, and unacknowledged PING and SETTINGS frames are all
1663+
counted towards the current limit.
16551664
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
16561665
**Default:** `128`. The minimum value is `4`.
16571666
* `maxOutstandingPings` {number} Sets the maximum number of outstanding,
@@ -1730,6 +1739,15 @@ changes:
17301739
`false`. See the [`'unknownProtocol'`][] event. See [ALPN negotiation][].
17311740
* `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
17321741
for deflating header fields. **Default:** `4Kib`
1742+
* `maxSessionMemory`{number} Sets the maximum memory that the `Http2Session`
1743+
is permitted to use. The value is expressed in terms of number of megabytes,
1744+
e.g. `1` equal 1 megabyte. The minimum value allowed is `1`. **Default:**
1745+
`10`. This is a credit based limit, existing `Http2Stream`s may cause this
1746+
limit to be exceeded, but new `Http2Stream` instances will be rejected
1747+
while this limit is exceeded. The current number of `Http2Stream` sessions,
1748+
the current memory use of the header compression tables, current data
1749+
queued to be sent, and unacknowledged PING and SETTINGS frames are all
1750+
counted towards the current limit.
17331751
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
17341752
**Default:** `128`. The minimum value is `4`.
17351753
* `maxOutstandingPings` {number} Sets the maximum number of outstanding,
@@ -1813,6 +1831,15 @@ changes:
18131831
* `options` {Object}
18141832
* `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
18151833
for deflating header fields. **Default:** `4Kib`
1834+
* `maxSessionMemory`{number} Sets the maximum memory that the `Http2Session`
1835+
is permitted to use. The value is expressed in terms of number of megabytes,
1836+
e.g. `1` equal 1 megabyte. The minimum value allowed is `1`. **Default:**
1837+
`10`. This is a credit based limit, existing `Http2Stream`s may cause this
1838+
limit to be exceeded, but new `Http2Stream` instances will be rejected
1839+
while this limit is exceeded. The current number of `Http2Stream` sessions,
1840+
the current memory use of the header compression tables, current data
1841+
queued to be sent, and unacknowledged PING and SETTINGS frames are all
1842+
counted towards the current limit.
18161843
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
18171844
**Default:** `128`. The minimum value is `1`.
18181845
* `maxOutstandingPings` {number} Sets the maximum number of outstanding,

lib/internal/http2/util.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ const IDX_OPTIONS_PADDING_STRATEGY = 4;
175175
const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5;
176176
const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6;
177177
const IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS = 7;
178-
const IDX_OPTIONS_FLAGS = 8;
178+
const IDX_OPTIONS_MAX_SESSION_MEMORY = 8;
179+
const IDX_OPTIONS_FLAGS = 9;
179180

180181
function updateOptionsBuffer(options) {
181182
var flags = 0;
@@ -219,6 +220,11 @@ function updateOptionsBuffer(options) {
219220
optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS] =
220221
Math.max(1, options.maxOutstandingSettings);
221222
}
223+
if (typeof options.maxSessionMemory === 'number') {
224+
flags |= (1 << IDX_OPTIONS_MAX_SESSION_MEMORY);
225+
optionsBuffer[IDX_OPTIONS_MAX_SESSION_MEMORY] =
226+
Math.max(1, options.maxSessionMemory);
227+
}
222228
optionsBuffer[IDX_OPTIONS_FLAGS] = flags;
223229
}
224230

src/node_http2.cc

+44-11
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,18 @@ Http2Options::Http2Options(Environment* env) {
174174
if (flags & (1 << IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS)) {
175175
SetMaxOutstandingSettings(buffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS]);
176176
}
177+
178+
// The HTTP2 specification places no limits on the amount of memory
179+
// that a session can consume. In order to prevent abuse, we place a
180+
// cap on the amount of memory a session can consume at any given time.
181+
// this is a credit based system. Existing streams may cause the limit
182+
// to be temporarily exceeded but once over the limit, new streams cannot
183+
// created.
184+
// Important: The maxSessionMemory option in javascript is expressed in
185+
// terms of MB increments (i.e. the value 1 == 1 MB)
186+
if (flags & (1 << IDX_OPTIONS_MAX_SESSION_MEMORY)) {
187+
SetMaxSessionMemory(buffer[IDX_OPTIONS_MAX_SESSION_MEMORY] * 1e6);
188+
}
177189
}
178190

179191
void Http2Session::Http2Settings::Init() {
@@ -482,11 +494,13 @@ Http2Session::Http2Session(Environment* env,
482494
// Capture the configuration options for this session
483495
Http2Options opts(env);
484496

485-
int32_t maxHeaderPairs = opts.GetMaxHeaderPairs();
497+
max_session_memory_ = opts.GetMaxSessionMemory();
498+
499+
uint32_t maxHeaderPairs = opts.GetMaxHeaderPairs();
486500
max_header_pairs_ =
487501
type == NGHTTP2_SESSION_SERVER
488-
? std::max(maxHeaderPairs, 4) // minimum # of request headers
489-
: std::max(maxHeaderPairs, 1); // minimum # of response headers
502+
? std::max(maxHeaderPairs, 4U) // minimum # of request headers
503+
: std::max(maxHeaderPairs, 1U); // minimum # of response headers
490504

491505
max_outstanding_pings_ = opts.GetMaxOutstandingPings();
492506
max_outstanding_settings_ = opts.GetMaxOutstandingSettings();
@@ -673,18 +687,21 @@ inline bool Http2Session::CanAddStream() {
673687
size_t maxSize =
674688
std::min(streams_.max_size(), static_cast<size_t>(maxConcurrentStreams));
675689
// We can add a new stream so long as we are less than the current
676-
// maximum on concurrent streams
677-
return streams_.size() < maxSize;
690+
// maximum on concurrent streams and there's enough available memory
691+
return streams_.size() < maxSize &&
692+
IsAvailableSessionMemory(sizeof(Http2Stream));
678693
}
679694

680695
inline void Http2Session::AddStream(Http2Stream* stream) {
681696
CHECK_GE(++statistics_.stream_count, 0);
682697
streams_[stream->id()] = stream;
698+
IncrementCurrentSessionMemory(stream->self_size());
683699
}
684700

685701

686-
inline void Http2Session::RemoveStream(int32_t id) {
687-
streams_.erase(id);
702+
inline void Http2Session::RemoveStream(Http2Stream* stream) {
703+
streams_.erase(stream->id());
704+
DecrementCurrentSessionMemory(stream->self_size());
688705
}
689706

690707
// Used as one of the Padding Strategy functions. Will attempt to ensure
@@ -1678,7 +1695,7 @@ Http2Stream::Http2Stream(
16781695

16791696
Http2Stream::~Http2Stream() {
16801697
if (session_ != nullptr) {
1681-
session_->RemoveStream(id_);
1698+
session_->RemoveStream(this);
16821699
session_ = nullptr;
16831700
}
16841701

@@ -2008,7 +2025,7 @@ inline int Http2Stream::DoWrite(WriteWrap* req_wrap,
20082025
i == nbufs - 1 ? req_wrap : nullptr,
20092026
bufs[i]
20102027
});
2011-
available_outbound_length_ += bufs[i].len;
2028+
IncrementAvailableOutboundLength(bufs[i].len);
20122029
}
20132030
CHECK_NE(nghttp2_session_resume_data(**session_, id_), NGHTTP2_ERR_NOMEM);
20142031
return 0;
@@ -2030,7 +2047,10 @@ inline bool Http2Stream::AddHeader(nghttp2_rcbuf* name,
20302047
if (this->statistics_.first_header == 0)
20312048
this->statistics_.first_header = uv_hrtime();
20322049
size_t length = GetBufferLength(name) + GetBufferLength(value) + 32;
2033-
if (current_headers_.size() == max_header_pairs_ ||
2050+
// A header can only be added if we have not exceeded the maximum number
2051+
// of headers and the session has memory available for it.
2052+
if (!session_->IsAvailableSessionMemory(length) ||
2053+
current_headers_.size() == max_header_pairs_ ||
20342054
current_headers_length_ + length > max_header_length_) {
20352055
return false;
20362056
}
@@ -2174,7 +2194,7 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
21742194
// Just return the length, let Http2Session::OnSendData take care of
21752195
// actually taking the buffers out of the queue.
21762196
*flags |= NGHTTP2_DATA_FLAG_NO_COPY;
2177-
stream->available_outbound_length_ -= amount;
2197+
stream->DecrementAvailableOutboundLength(amount);
21782198
}
21792199
}
21802200

@@ -2197,6 +2217,15 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
21972217
return amount;
21982218
}
21992219

2220+
inline void Http2Stream::IncrementAvailableOutboundLength(size_t amount) {
2221+
available_outbound_length_ += amount;
2222+
session_->IncrementCurrentSessionMemory(amount);
2223+
}
2224+
2225+
inline void Http2Stream::DecrementAvailableOutboundLength(size_t amount) {
2226+
available_outbound_length_ -= amount;
2227+
session_->DecrementCurrentSessionMemory(amount);
2228+
}
22002229

22012230

22022231
// Implementation of the JavaScript API
@@ -2690,6 +2719,7 @@ Http2Session::Http2Ping* Http2Session::PopPing() {
26902719
if (!outstanding_pings_.empty()) {
26912720
ping = outstanding_pings_.front();
26922721
outstanding_pings_.pop();
2722+
DecrementCurrentSessionMemory(ping->self_size());
26932723
}
26942724
return ping;
26952725
}
@@ -2698,6 +2728,7 @@ bool Http2Session::AddPing(Http2Session::Http2Ping* ping) {
26982728
if (outstanding_pings_.size() == max_outstanding_pings_)
26992729
return false;
27002730
outstanding_pings_.push(ping);
2731+
IncrementCurrentSessionMemory(ping->self_size());
27012732
return true;
27022733
}
27032734

@@ -2706,6 +2737,7 @@ Http2Session::Http2Settings* Http2Session::PopSettings() {
27062737
if (!outstanding_settings_.empty()) {
27072738
settings = outstanding_settings_.front();
27082739
outstanding_settings_.pop();
2740+
DecrementCurrentSessionMemory(settings->self_size());
27092741
}
27102742
return settings;
27112743
}
@@ -2714,6 +2746,7 @@ bool Http2Session::AddSettings(Http2Session::Http2Settings* settings) {
27142746
if (outstanding_settings_.size() == max_outstanding_settings_)
27152747
return false;
27162748
outstanding_settings_.push(settings);
2749+
IncrementCurrentSessionMemory(settings->self_size());
27172750
return true;
27182751
}
27192752

src/node_http2.h

+44-1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ void inline debug_vfprintf(const char* format, ...) {
8282
// Also strictly limit the number of outstanding SETTINGS frames a user sends
8383
#define DEFAULT_MAX_SETTINGS 10
8484

85+
// Default maximum total memory cap for Http2Session.
86+
#define DEFAULT_MAX_SESSION_MEMORY 1e7;
87+
8588
// These are the standard HTTP/2 defaults as specified by the RFC
8689
#define DEFAULT_SETTINGS_HEADER_TABLE_SIZE 4096
8790
#define DEFAULT_SETTINGS_ENABLE_PUSH 1
@@ -500,8 +503,17 @@ class Http2Options {
500503
return max_outstanding_settings_;
501504
}
502505

506+
void SetMaxSessionMemory(uint64_t max) {
507+
max_session_memory_ = max;
508+
}
509+
510+
uint64_t GetMaxSessionMemory() {
511+
return max_session_memory_;
512+
}
513+
503514
private:
504515
nghttp2_option* options_;
516+
uint64_t max_session_memory_ = DEFAULT_MAX_SESSION_MEMORY;
505517
uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
506518
padding_strategy_type padding_strategy_ = PADDING_STRATEGY_NONE;
507519
size_t max_outstanding_pings_ = DEFAULT_MAX_PINGS;
@@ -628,6 +640,9 @@ class Http2Stream : public AsyncWrap,
628640
// Returns the stream identifier for this stream
629641
inline int32_t id() const { return id_; }
630642

643+
inline void IncrementAvailableOutboundLength(size_t amount);
644+
inline void DecrementAvailableOutboundLength(size_t amount);
645+
631646
inline bool AddHeader(nghttp2_rcbuf* name,
632647
nghttp2_rcbuf* value,
633648
uint8_t flags);
@@ -848,7 +863,7 @@ class Http2Session : public AsyncWrap {
848863
inline void AddStream(Http2Stream* stream);
849864

850865
// Removes a stream instance from this session
851-
inline void RemoveStream(int32_t id);
866+
inline void RemoveStream(Http2Stream* stream);
852867

853868
// Write data to the session
854869
inline ssize_t Write(const uv_buf_t* bufs, size_t nbufs);
@@ -906,6 +921,30 @@ class Http2Session : public AsyncWrap {
906921
Http2Settings* PopSettings();
907922
bool AddSettings(Http2Settings* settings);
908923

924+
void IncrementCurrentSessionMemory(uint64_t amount) {
925+
current_session_memory_ += amount;
926+
}
927+
928+
void DecrementCurrentSessionMemory(uint64_t amount) {
929+
current_session_memory_ -= amount;
930+
}
931+
932+
// Returns the current session memory including the current size of both
933+
// the inflate and deflate hpack headers, the current outbound storage
934+
// queue, and pending writes.
935+
uint64_t GetCurrentSessionMemory() {
936+
uint64_t total = current_session_memory_ + sizeof(Http2Session);
937+
total += nghttp2_session_get_hd_deflate_dynamic_table_size(session_);
938+
total += nghttp2_session_get_hd_inflate_dynamic_table_size(session_);
939+
total += outgoing_storage_.size();
940+
return total;
941+
}
942+
943+
// Return true if current_session_memory + amount is less than the max
944+
bool IsAvailableSessionMemory(uint64_t amount) {
945+
return GetCurrentSessionMemory() + amount <= max_session_memory_;
946+
}
947+
909948
struct Statistics {
910949
uint64_t start_time;
911950
uint64_t end_time;
@@ -1035,6 +1074,10 @@ class Http2Session : public AsyncWrap {
10351074
// The maximum number of header pairs permitted for streams on this session
10361075
uint32_t max_header_pairs_ = DEFAULT_MAX_HEADER_LIST_PAIRS;
10371076

1077+
// The maximum amount of memory allocated for this session
1078+
uint64_t max_session_memory_ = DEFAULT_MAX_SESSION_MEMORY;
1079+
uint64_t current_session_memory_ = 0;
1080+
10381081
// The collection of active Http2Streams associated with this session
10391082
std::unordered_map<int32_t, Http2Stream*> streams_;
10401083

src/node_http2_state.h

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ namespace http2 {
5050
IDX_OPTIONS_MAX_HEADER_LIST_PAIRS,
5151
IDX_OPTIONS_MAX_OUTSTANDING_PINGS,
5252
IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS,
53+
IDX_OPTIONS_MAX_SESSION_MEMORY,
5354
IDX_OPTIONS_FLAGS
5455
};
5556

test/parallel/test-http2-util-update-options-buffer.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ const IDX_OPTIONS_PADDING_STRATEGY = 4;
2020
const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5;
2121
const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6;
2222
const IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS = 7;
23-
const IDX_OPTIONS_FLAGS = 8;
23+
const IDX_OPTIONS_MAX_SESSION_MEMORY = 8;
24+
const IDX_OPTIONS_FLAGS = 9;
2425

2526
{
2627
updateOptionsBuffer({
@@ -31,7 +32,8 @@ const IDX_OPTIONS_FLAGS = 8;
3132
paddingStrategy: 5,
3233
maxHeaderListPairs: 6,
3334
maxOutstandingPings: 7,
34-
maxOutstandingSettings: 8
35+
maxOutstandingSettings: 8,
36+
maxSessionMemory: 9
3537
});
3638

3739
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE], 1);
@@ -42,6 +44,7 @@ const IDX_OPTIONS_FLAGS = 8;
4244
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS], 6);
4345
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS], 7);
4446
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS], 8);
47+
strictEqual(optionsBuffer[IDX_OPTIONS_MAX_SESSION_MEMORY], 9);
4548

4649
const flags = optionsBuffer[IDX_OPTIONS_FLAGS];
4750

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
if (!common.hasCrypto)
5+
common.skip('missing crypto');
6+
7+
const http2 = require('http2');
8+
9+
// Test that maxSessionMemory Caps work
10+
11+
const largeBuffer = Buffer.alloc(1e6);
12+
13+
const server = http2.createServer({ maxSessionMemory: 1 });
14+
15+
server.on('stream', common.mustCall((stream) => {
16+
stream.respond();
17+
stream.end(largeBuffer);
18+
}));
19+
20+
server.listen(0, common.mustCall(() => {
21+
const client = http2.connect(`http://localhost:${server.address().port}`);
22+
23+
{
24+
const req = client.request();
25+
26+
req.on('response', () => {
27+
// This one should be rejected because the server is over budget
28+
// on the current memory allocation
29+
const req = client.request();
30+
req.on('error', common.expectsError({
31+
code: 'ERR_HTTP2_STREAM_ERROR',
32+
type: Error,
33+
message: 'Stream closed with error code 11'
34+
}));
35+
req.on('close', common.mustCall(() => {
36+
server.close();
37+
client.destroy();
38+
}));
39+
});
40+
41+
req.resume();
42+
req.on('close', common.mustCall());
43+
}
44+
}));

0 commit comments

Comments
 (0)