Skip to content

Commit 530004e

Browse files
addaleaxtargos
authored andcommitted
http2: only call into JS when necessary for session events
For some JS events, it only makes sense to call into JS when there are listeners for the event in question. The overhead is noticeable if a lot of these events are emitted during the lifetime of a session. To reduce this overhead, keep track of whether any/how many JS listeners are present, and if there are none, skip calls into JS altogether. This is part of performance improvements to mitigate CVE-2019-9513. PR-URL: #29122 Reviewed-By: Rich Trott <rtrott@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 58d8c9e commit 530004e

File tree

4 files changed

+161
-8
lines changed

4 files changed

+161
-8
lines changed

lib/internal/http2/core.js

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ const kID = Symbol('id');
178178
const kInit = Symbol('init');
179179
const kInfoHeaders = Symbol('sent-info-headers');
180180
const kLocalSettings = Symbol('local-settings');
181+
const kNativeFields = Symbol('kNativeFields');
181182
const kOptions = Symbol('options');
182183
const kOwner = owner_symbol;
183184
const kOrigin = Symbol('origin');
@@ -200,7 +201,15 @@ const {
200201
paddingBuffer,
201202
PADDING_BUF_FRAME_LENGTH,
202203
PADDING_BUF_MAX_PAYLOAD_LENGTH,
203-
PADDING_BUF_RETURN_VALUE
204+
PADDING_BUF_RETURN_VALUE,
205+
kBitfield,
206+
kSessionPriorityListenerCount,
207+
kSessionFrameErrorListenerCount,
208+
kSessionUint8FieldCount,
209+
kSessionHasRemoteSettingsListeners,
210+
kSessionRemoteSettingsIsUpToDate,
211+
kSessionHasPingListeners,
212+
kSessionHasAltsvcListeners,
204213
} = binding;
205214

206215
const {
@@ -376,6 +385,76 @@ function submitRstStream(code) {
376385
}
377386
}
378387

388+
// Keep track of the number/presence of JS event listeners. Knowing that there
389+
// are no listeners allows the C++ code to skip calling into JS for an event.
390+
function sessionListenerAdded(name) {
391+
switch (name) {
392+
case 'ping':
393+
this[kNativeFields][kBitfield] |= 1 << kSessionHasPingListeners;
394+
break;
395+
case 'altsvc':
396+
this[kNativeFields][kBitfield] |= 1 << kSessionHasAltsvcListeners;
397+
break;
398+
case 'remoteSettings':
399+
this[kNativeFields][kBitfield] |= 1 << kSessionHasRemoteSettingsListeners;
400+
break;
401+
case 'priority':
402+
this[kNativeFields][kSessionPriorityListenerCount]++;
403+
break;
404+
case 'frameError':
405+
this[kNativeFields][kSessionFrameErrorListenerCount]++;
406+
break;
407+
}
408+
}
409+
410+
function sessionListenerRemoved(name) {
411+
switch (name) {
412+
case 'ping':
413+
if (this.listenerCount(name) > 0) return;
414+
this[kNativeFields][kBitfield] &= ~(1 << kSessionHasPingListeners);
415+
break;
416+
case 'altsvc':
417+
if (this.listenerCount(name) > 0) return;
418+
this[kNativeFields][kBitfield] &= ~(1 << kSessionHasAltsvcListeners);
419+
break;
420+
case 'remoteSettings':
421+
if (this.listenerCount(name) > 0) return;
422+
this[kNativeFields][kBitfield] &=
423+
~(1 << kSessionHasRemoteSettingsListeners);
424+
break;
425+
case 'priority':
426+
this[kNativeFields][kSessionPriorityListenerCount]--;
427+
break;
428+
case 'frameError':
429+
this[kNativeFields][kSessionFrameErrorListenerCount]--;
430+
break;
431+
}
432+
}
433+
434+
// Also keep track of listeners for the Http2Stream instances, as some events
435+
// are emitted on those objects.
436+
function streamListenerAdded(name) {
437+
switch (name) {
438+
case 'priority':
439+
this[kSession][kNativeFields][kSessionPriorityListenerCount]++;
440+
break;
441+
case 'frameError':
442+
this[kSession][kNativeFields][kSessionFrameErrorListenerCount]++;
443+
break;
444+
}
445+
}
446+
447+
function streamListenerRemoved(name) {
448+
switch (name) {
449+
case 'priority':
450+
this[kSession][kNativeFields][kSessionPriorityListenerCount]--;
451+
break;
452+
case 'frameError':
453+
this[kSession][kNativeFields][kSessionFrameErrorListenerCount]--;
454+
break;
455+
}
456+
}
457+
379458
function onPing(payload) {
380459
const session = this[kOwner];
381460
if (session.destroyed)
@@ -434,7 +513,6 @@ function onSettings() {
434513
return;
435514
session[kUpdateTimer]();
436515
debugSessionObj(session, 'new settings received');
437-
session[kRemoteSettings] = undefined;
438516
session.emit('remoteSettings', session.remoteSettings);
439517
}
440518

@@ -858,6 +936,10 @@ function setupHandle(socket, type, options) {
858936
handle.consume(socket._handle);
859937

860938
this[kHandle] = handle;
939+
if (this[kNativeFields])
940+
handle.fields.set(this[kNativeFields]);
941+
else
942+
this[kNativeFields] = handle.fields;
861943

862944
if (socket.encrypted) {
863945
this[kAlpnProtocol] = socket.alpnProtocol;
@@ -899,6 +981,7 @@ function finishSessionDestroy(session, error) {
899981
session[kProxySocket] = undefined;
900982
session[kSocket] = undefined;
901983
session[kHandle] = undefined;
984+
session[kNativeFields] = new Uint8Array(kSessionUint8FieldCount);
902985
socket[kSession] = undefined;
903986
socket[kServer] = undefined;
904987

@@ -978,6 +1061,7 @@ class Http2Session extends EventEmitter {
9781061
this[kProxySocket] = null;
9791062
this[kSocket] = socket;
9801063
this[kTimeout] = null;
1064+
this[kHandle] = undefined;
9811065

9821066
// Do not use nagle's algorithm
9831067
if (typeof socket.setNoDelay === 'function')
@@ -1002,6 +1086,11 @@ class Http2Session extends EventEmitter {
10021086
setupFn();
10031087
}
10041088

1089+
if (!this[kNativeFields])
1090+
this[kNativeFields] = new Uint8Array(kSessionUint8FieldCount);
1091+
this.on('newListener', sessionListenerAdded);
1092+
this.on('removeListener', sessionListenerRemoved);
1093+
10051094
debugSession(type, 'created');
10061095
}
10071096

@@ -1159,13 +1248,18 @@ class Http2Session extends EventEmitter {
11591248

11601249
// The settings currently in effect for the remote peer.
11611250
get remoteSettings() {
1162-
const settings = this[kRemoteSettings];
1163-
if (settings !== undefined)
1164-
return settings;
1251+
if (this[kNativeFields][kBitfield] &
1252+
(1 << kSessionRemoteSettingsIsUpToDate)) {
1253+
const settings = this[kRemoteSettings];
1254+
if (settings !== undefined) {
1255+
return settings;
1256+
}
1257+
}
11651258

11661259
if (this.destroyed || this.connecting)
11671260
return {};
11681261

1262+
this[kNativeFields][kBitfield] |= (1 << kSessionRemoteSettingsIsUpToDate);
11691263
return this[kRemoteSettings] = getSettings(this[kHandle], true); // Remote
11701264
}
11711265

@@ -1344,6 +1438,12 @@ class ServerHttp2Session extends Http2Session {
13441438
constructor(options, socket, server) {
13451439
super(NGHTTP2_SESSION_SERVER, options, socket);
13461440
this[kServer] = server;
1441+
// This is a bit inaccurate because it does not reflect changes to
1442+
// number of listeners made after the session was created. This should
1443+
// not be an issue in practice. Additionally, the 'priority' event on
1444+
// server instances (or any other object) is fully undocumented.
1445+
this[kNativeFields][kSessionPriorityListenerCount] =
1446+
server.listenerCount('priority');
13471447
}
13481448

13491449
get server() {
@@ -1656,6 +1756,9 @@ class Http2Stream extends Duplex {
16561756
this[kProxySocket] = null;
16571757

16581758
this.on('pause', streamOnPause);
1759+
1760+
this.on('newListener', streamListenerAdded);
1761+
this.on('removeListener', streamListenerRemoved);
16591762
}
16601763

16611764
[kUpdateTimer]() {
@@ -2612,7 +2715,7 @@ function sessionOnPriority(stream, parent, weight, exclusive) {
26122715
}
26132716

26142717
function sessionOnError(error) {
2615-
if (this[kServer])
2718+
if (this[kServer] !== undefined)
26162719
this[kServer].emit('sessionError', error, this);
26172720
}
26182721

@@ -2661,8 +2764,10 @@ function connectionListener(socket) {
26612764
const session = new ServerHttp2Session(options, socket, this);
26622765

26632766
session.on('stream', sessionOnStream);
2664-
session.on('priority', sessionOnPriority);
26652767
session.on('error', sessionOnError);
2768+
// Don't count our own internal listener.
2769+
session.on('priority', sessionOnPriority);
2770+
session[kNativeFields][kSessionPriorityListenerCount]--;
26662771

26672772
if (this.timeout)
26682773
session.setTimeout(this.timeout, sessionOnTimeout);

src/env.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ constexpr size_t kFsStatsBufferLength = kFsStatsFieldsNumber * 2;
208208
V(family_string, "family") \
209209
V(fatal_exception_string, "_fatalException") \
210210
V(fd_string, "fd") \
211+
V(fields_string, "fields") \
211212
V(file_string, "file") \
212213
V(fingerprint256_string, "fingerprint256") \
213214
V(fingerprint_string, "fingerprint") \

src/node_http2.cc

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ using v8::ObjectTemplate;
2525
using v8::String;
2626
using v8::Uint32;
2727
using v8::Uint32Array;
28+
using v8::Uint8Array;
2829
using v8::Undefined;
2930

3031
using node::performance::PerformanceEntry;
@@ -639,6 +640,15 @@ Http2Session::Http2Session(Environment* env,
639640

640641
outgoing_storage_.reserve(4096);
641642
outgoing_buffers_.reserve(32);
643+
644+
{
645+
// Make the js_fields_ property accessible to JS land.
646+
Local<ArrayBuffer> ab =
647+
ArrayBuffer::New(env->isolate(), js_fields_, kSessionUint8FieldCount);
648+
Local<Uint8Array> uint8_arr =
649+
Uint8Array::New(ab, 0, kSessionUint8FieldCount);
650+
USE(wrap->Set(env->context(), env->fields_string(), uint8_arr));
651+
}
642652
}
643653

644654
Http2Session::~Http2Session() {
@@ -1033,7 +1043,8 @@ int Http2Session::OnFrameNotSent(nghttp2_session* handle,
10331043
// Do not report if the frame was not sent due to the session closing
10341044
if (error_code == NGHTTP2_ERR_SESSION_CLOSING ||
10351045
error_code == NGHTTP2_ERR_STREAM_CLOSED ||
1036-
error_code == NGHTTP2_ERR_STREAM_CLOSING) {
1046+
error_code == NGHTTP2_ERR_STREAM_CLOSING ||
1047+
session->js_fields_[kSessionFrameErrorListenerCount] == 0) {
10371048
return 0;
10381049
}
10391050

@@ -1316,6 +1327,7 @@ void Http2Session::HandleHeadersFrame(const nghttp2_frame* frame) {
13161327
// are considered advisory only, so this has no real effect other than to
13171328
// simply let user code know that the priority has changed.
13181329
void Http2Session::HandlePriorityFrame(const nghttp2_frame* frame) {
1330+
if (js_fields_[kSessionPriorityListenerCount] == 0) return;
13191331
Isolate* isolate = env()->isolate();
13201332
HandleScope scope(isolate);
13211333
Local<Context> context = env()->context();
@@ -1380,6 +1392,7 @@ void Http2Session::HandleGoawayFrame(const nghttp2_frame* frame) {
13801392

13811393
// Called by OnFrameReceived when a complete ALTSVC frame has been received.
13821394
void Http2Session::HandleAltSvcFrame(const nghttp2_frame* frame) {
1395+
if (!(js_fields_[kBitfield] & (1 << kSessionHasAltsvcListeners))) return;
13831396
Isolate* isolate = env()->isolate();
13841397
HandleScope scope(isolate);
13851398
Local<Context> context = env()->context();
@@ -1458,6 +1471,7 @@ void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
14581471
return;
14591472
}
14601473

1474+
if (!(js_fields_[kBitfield] & (1 << kSessionHasPingListeners))) return;
14611475
// Notify the session that a ping occurred
14621476
arg = Buffer::Copy(env(),
14631477
reinterpret_cast<const char*>(frame->ping.opaque_data),
@@ -1469,6 +1483,9 @@ void Http2Session::HandlePingFrame(const nghttp2_frame* frame) {
14691483
void Http2Session::HandleSettingsFrame(const nghttp2_frame* frame) {
14701484
bool ack = frame->hd.flags & NGHTTP2_FLAG_ACK;
14711485
if (!ack) {
1486+
js_fields_[kBitfield] &= ~(1 << kSessionRemoteSettingsIsUpToDate);
1487+
if (!(js_fields_[kBitfield] & (1 << kSessionHasRemoteSettingsListeners)))
1488+
return;
14721489
// This is not a SETTINGS acknowledgement, notify and return
14731490
MakeCallback(env()->http2session_on_settings_function(), 0, nullptr);
14741491
return;
@@ -2981,6 +2998,16 @@ void Initialize(Local<Object> target,
29812998
NODE_DEFINE_CONSTANT(target, PADDING_BUF_MAX_PAYLOAD_LENGTH);
29822999
NODE_DEFINE_CONSTANT(target, PADDING_BUF_RETURN_VALUE);
29833000

3001+
NODE_DEFINE_CONSTANT(target, kBitfield);
3002+
NODE_DEFINE_CONSTANT(target, kSessionPriorityListenerCount);
3003+
NODE_DEFINE_CONSTANT(target, kSessionFrameErrorListenerCount);
3004+
NODE_DEFINE_CONSTANT(target, kSessionUint8FieldCount);
3005+
3006+
NODE_DEFINE_CONSTANT(target, kSessionHasRemoteSettingsListeners);
3007+
NODE_DEFINE_CONSTANT(target, kSessionRemoteSettingsIsUpToDate);
3008+
NODE_DEFINE_CONSTANT(target, kSessionHasPingListeners);
3009+
NODE_DEFINE_CONSTANT(target, kSessionHasAltsvcListeners);
3010+
29843011
// Method to fetch the nghttp2 string description of an nghttp2 error code
29853012
env->SetMethod(target, "nghttp2ErrorString", HttpErrorString);
29863013

src/node_http2.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,23 @@ class Http2Stream::Provider::Stream : public Http2Stream::Provider {
672672
void* user_data);
673673
};
674674

675+
// Indices for js_fields_, which serves as a way to communicate data with JS
676+
// land fast. In particular, we store information about the number/presence
677+
// of certain event listeners in JS, and skip calls from C++ into JS if they
678+
// are missing.
679+
enum SessionUint8Fields {
680+
kBitfield, // See below
681+
kSessionPriorityListenerCount,
682+
kSessionFrameErrorListenerCount,
683+
kSessionUint8FieldCount
684+
};
685+
686+
enum SessionBitfieldFlags {
687+
kSessionHasRemoteSettingsListeners,
688+
kSessionRemoteSettingsIsUpToDate,
689+
kSessionHasPingListeners,
690+
kSessionHasAltsvcListeners
691+
};
675692

676693
class Http2Session : public AsyncWrap, public StreamListener {
677694
public:
@@ -949,6 +966,9 @@ class Http2Session : public AsyncWrap, public StreamListener {
949966
// The underlying nghttp2_session handle
950967
nghttp2_session* session_;
951968

969+
// JS-accessible numeric fields, as indexed by SessionUint8Fields.
970+
uint8_t js_fields_[kSessionUint8FieldCount] = {};
971+
952972
// The session type: client or server
953973
nghttp2_session_type session_type_;
954974

0 commit comments

Comments
 (0)