Skip to content

Commit 882e7ef

Browse files
committed
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. PR-URL: #17967 Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent feaf6ac commit 882e7ef

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
@@ -1633,6 +1633,15 @@ changes:
16331633
* `options` {Object}
16341634
* `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
16351635
for deflating header fields. **Default:** `4Kib`
1636+
* `maxSessionMemory`{number} Sets the maximum memory that the `Http2Session`
1637+
is permitted to use. The value is expressed in terms of number of megabytes,
1638+
e.g. `1` equal 1 megabyte. The minimum value allowed is `1`. **Default:**
1639+
`10`. This is a credit based limit, existing `Http2Stream`s may cause this
1640+
limit to be exceeded, but new `Http2Stream` instances will be rejected
1641+
while this limit is exceeded. The current number of `Http2Stream` sessions,
1642+
the current memory use of the header compression tables, current data
1643+
queued to be sent, and unacknowledged PING and SETTINGS frames are all
1644+
counted towards the current limit.
16361645
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
16371646
**Default:** `128`. The minimum value is `4`.
16381647
* `maxOutstandingPings` {number} Sets the maximum number of outstanding,
@@ -1711,6 +1720,15 @@ changes:
17111720
`false`. See the [`'unknownProtocol'`][] event. See [ALPN negotiation][].
17121721
* `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
17131722
for deflating header fields. **Default:** `4Kib`
1723+
* `maxSessionMemory`{number} Sets the maximum memory that the `Http2Session`
1724+
is permitted to use. The value is expressed in terms of number of megabytes,
1725+
e.g. `1` equal 1 megabyte. The minimum value allowed is `1`. **Default:**
1726+
`10`. This is a credit based limit, existing `Http2Stream`s may cause this
1727+
limit to be exceeded, but new `Http2Stream` instances will be rejected
1728+
while this limit is exceeded. The current number of `Http2Stream` sessions,
1729+
the current memory use of the header compression tables, current data
1730+
queued to be sent, and unacknowledged PING and SETTINGS frames are all
1731+
counted towards the current limit.
17141732
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
17151733
**Default:** `128`. The minimum value is `4`.
17161734
* `maxOutstandingPings` {number} Sets the maximum number of outstanding,
@@ -1794,6 +1812,15 @@ changes:
17941812
* `options` {Object}
17951813
* `maxDeflateDynamicTableSize` {number} Sets the maximum dynamic table size
17961814
for deflating header fields. **Default:** `4Kib`
1815+
* `maxSessionMemory`{number} Sets the maximum memory that the `Http2Session`
1816+
is permitted to use. The value is expressed in terms of number of megabytes,
1817+
e.g. `1` equal 1 megabyte. The minimum value allowed is `1`. **Default:**
1818+
`10`. This is a credit based limit, existing `Http2Stream`s may cause this
1819+
limit to be exceeded, but new `Http2Stream` instances will be rejected
1820+
while this limit is exceeded. The current number of `Http2Stream` sessions,
1821+
the current memory use of the header compression tables, current data
1822+
queued to be sent, and unacknowledged PING and SETTINGS frames are all
1823+
counted towards the current limit.
17971824
* `maxHeaderListPairs` {number} Sets the maximum number of header entries.
17981825
**Default:** `128`. The minimum value is `1`.
17991826
* `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();
@@ -672,18 +686,21 @@ inline bool Http2Session::CanAddStream() {
672686
size_t maxSize =
673687
std::min(streams_.max_size(), static_cast<size_t>(maxConcurrentStreams));
674688
// We can add a new stream so long as we are less than the current
675-
// maximum on concurrent streams
676-
return streams_.size() < maxSize;
689+
// maximum on concurrent streams and there's enough available memory
690+
return streams_.size() < maxSize &&
691+
IsAvailableSessionMemory(sizeof(Http2Stream));
677692
}
678693

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

684700

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

689706
// Used as one of the Padding Strategy functions. Will attempt to ensure
@@ -1677,7 +1694,7 @@ Http2Stream::Http2Stream(
16771694

16781695
Http2Stream::~Http2Stream() {
16791696
if (session_ != nullptr) {
1680-
session_->RemoveStream(id_);
1697+
session_->RemoveStream(this);
16811698
session_ = nullptr;
16821699
}
16831700

@@ -2007,7 +2024,7 @@ inline int Http2Stream::DoWrite(WriteWrap* req_wrap,
20072024
i == nbufs - 1 ? req_wrap : nullptr,
20082025
bufs[i]
20092026
});
2010-
available_outbound_length_ += bufs[i].len;
2027+
IncrementAvailableOutboundLength(bufs[i].len);
20112028
}
20122029
CHECK_NE(nghttp2_session_resume_data(**session_, id_), NGHTTP2_ERR_NOMEM);
20132030
return 0;
@@ -2029,7 +2046,10 @@ inline bool Http2Stream::AddHeader(nghttp2_rcbuf* name,
20292046
if (this->statistics_.first_header == 0)
20302047
this->statistics_.first_header = uv_hrtime();
20312048
size_t length = GetBufferLength(name) + GetBufferLength(value) + 32;
2032-
if (current_headers_.size() == max_header_pairs_ ||
2049+
// A header can only be added if we have not exceeded the maximum number
2050+
// of headers and the session has memory available for it.
2051+
if (!session_->IsAvailableSessionMemory(length) ||
2052+
current_headers_.size() == max_header_pairs_ ||
20332053
current_headers_length_ + length > max_header_length_) {
20342054
return false;
20352055
}
@@ -2173,7 +2193,7 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
21732193
// Just return the length, let Http2Session::OnSendData take care of
21742194
// actually taking the buffers out of the queue.
21752195
*flags |= NGHTTP2_DATA_FLAG_NO_COPY;
2176-
stream->available_outbound_length_ -= amount;
2196+
stream->DecrementAvailableOutboundLength(amount);
21772197
}
21782198
}
21792199

@@ -2196,6 +2216,15 @@ ssize_t Http2Stream::Provider::Stream::OnRead(nghttp2_session* handle,
21962216
return amount;
21972217
}
21982218

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

22002229

22012230
// Implementation of the JavaScript API
@@ -2689,6 +2718,7 @@ Http2Session::Http2Ping* Http2Session::PopPing() {
26892718
if (!outstanding_pings_.empty()) {
26902719
ping = outstanding_pings_.front();
26912720
outstanding_pings_.pop();
2721+
DecrementCurrentSessionMemory(ping->self_size());
26922722
}
26932723
return ping;
26942724
}
@@ -2697,6 +2727,7 @@ bool Http2Session::AddPing(Http2Session::Http2Ping* ping) {
26972727
if (outstanding_pings_.size() == max_outstanding_pings_)
26982728
return false;
26992729
outstanding_pings_.push(ping);
2730+
IncrementCurrentSessionMemory(ping->self_size());
27002731
return true;
27012732
}
27022733

@@ -2705,6 +2736,7 @@ Http2Session::Http2Settings* Http2Session::PopSettings() {
27052736
if (!outstanding_settings_.empty()) {
27062737
settings = outstanding_settings_.front();
27072738
outstanding_settings_.pop();
2739+
DecrementCurrentSessionMemory(settings->self_size());
27082740
}
27092741
return settings;
27102742
}
@@ -2713,6 +2745,7 @@ bool Http2Session::AddSettings(Http2Session::Http2Settings* settings) {
27132745
if (outstanding_settings_.size() == max_outstanding_settings_)
27142746
return false;
27152747
outstanding_settings_.push(settings);
2748+
IncrementCurrentSessionMemory(settings->self_size());
27162749
return true;
27172750
}
27182751

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
@@ -501,8 +504,17 @@ class Http2Options {
501504
return max_outstanding_settings_;
502505
}
503506

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

644+
inline void IncrementAvailableOutboundLength(size_t amount);
645+
inline void DecrementAvailableOutboundLength(size_t amount);
646+
632647
inline bool AddHeader(nghttp2_rcbuf* name,
633648
nghttp2_rcbuf* value,
634649
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)