Skip to content

Commit 8669486

Browse files
committed
tls: defer re-entrant calls to SSL state machine from JS
Signed-off-by: Tim Perry <pimterry@gmail.com>
1 parent fe4a42b commit 8669486

5 files changed

Lines changed: 294 additions & 6 deletions

File tree

src/crypto/crypto_tls.cc

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -849,7 +849,10 @@ void TLSWrap::ClearOut() {
849849
char out[kClearOutChunkSize];
850850
int read;
851851
for (;;) {
852-
read = SSL_read(ssl_.get(), out, sizeof(out));
852+
{
853+
SSLLibraryCallScope ssl_library_call_scope(this);
854+
read = SSL_read(ssl_.get(), out, sizeof(out));
855+
}
853856
Debug(this, "Read %d bytes of cleartext output", read);
854857

855858
if (read <= 0)
@@ -970,7 +973,11 @@ void TLSWrap::ClearIn() {
970973
MarkPopErrorOnReturn mark_pop_error_on_return;
971974

972975
NodeBIO::FromBIO(enc_out_)->set_allocate_tls_hint(bs->ByteLength());
973-
int written = SSL_write(ssl_.get(), bs->Data(), bs->ByteLength());
976+
int written;
977+
{
978+
SSLLibraryCallScope ssl_library_call_scope(this);
979+
written = SSL_write(ssl_.get(), bs->Data(), bs->ByteLength());
980+
}
974981
Debug(this, "Writing %zu bytes, written = %d", bs->ByteLength(), written);
975982
CHECK(written == -1 || written == static_cast<int>(bs->ByteLength()));
976983

@@ -1073,6 +1080,33 @@ int TLSWrap::DoWrite(WriteWrap* w,
10731080
}
10741081
}
10751082

1083+
// If we got here from a call inside the OpenSSL/BoringSSL stack, we need to
1084+
// defer reentrant write calls:
1085+
if (in_ssl_library_call()) {
1086+
Debug(this, "Deferring write issued from the SSL library's stack");
1087+
CHECK(!current_write_);
1088+
current_write_.reset(w->GetAsyncWrap());
1089+
1090+
if (length > 0) {
1091+
CHECK(!pending_cleartext_input_ ||
1092+
pending_cleartext_input_->ByteLength() == 0);
1093+
std::unique_ptr<BackingStore> bs = ArrayBuffer::NewBackingStore(
1094+
env()->isolate(),
1095+
length,
1096+
BackingStoreInitializationMode::kUninitialized);
1097+
size_t offset = 0;
1098+
for (i = 0; i < count; i++) {
1099+
memcpy(
1100+
static_cast<char*>(bs->Data()) + offset, bufs[i].base, bufs[i].len);
1101+
offset += bufs[i].len;
1102+
}
1103+
pending_cleartext_input_ = std::move(bs);
1104+
}
1105+
1106+
ScheduleDeferredCycle();
1107+
return 0;
1108+
}
1109+
10761110
// We want to trigger a Write() on the underlying stream to drive the stream
10771111
// system, but don't want to encrypt empty buffers into a TLS frame, so see
10781112
// if we can find something to Write().
@@ -1138,12 +1172,18 @@ int TLSWrap::DoWrite(WriteWrap* w,
11381172
}
11391173

11401174
NodeBIO::FromBIO(enc_out_)->set_allocate_tls_hint(length);
1141-
written = SSL_write(ssl_.get(), bs->Data(), length);
1175+
{
1176+
SSLLibraryCallScope ssl_library_call_scope(this);
1177+
written = SSL_write(ssl_.get(), bs->Data(), length);
1178+
}
11421179
} else {
11431180
// Only one buffer: try to write directly, only store if it fails
11441181
uv_buf_t* buf = &bufs[nonempty_i];
11451182
NodeBIO::FromBIO(enc_out_)->set_allocate_tls_hint(buf->len);
1146-
written = SSL_write(ssl_.get(), buf->base, buf->len);
1183+
{
1184+
SSLLibraryCallScope ssl_library_call_scope(this);
1185+
written = SSL_write(ssl_.get(), buf->base, buf->len);
1186+
}
11471187

11481188
if (written == -1) {
11491189
bs = ArrayBuffer::NewBackingStore(
@@ -1229,16 +1269,55 @@ ShutdownWrap* TLSWrap::CreateShutdownWrap(Local<Object> req_wrap_object) {
12291269

12301270
int TLSWrap::DoShutdown(ShutdownWrap* req_wrap) {
12311271
Debug(this, "DoShutdown()");
1272+
1273+
// We must not call SSL_shutdown from inside the TLS library stack, so
1274+
// defer if required:
1275+
if (in_ssl_library_call()) {
1276+
Debug(this, "Deferring shutdown issued from the SSL library's stack");
1277+
CHECK(!pending_shutdown_);
1278+
pending_shutdown_.reset(req_wrap->GetAsyncWrap());
1279+
flags_.shutdown = true;
1280+
ScheduleDeferredCycle();
1281+
return 0;
1282+
}
1283+
12321284
MarkPopErrorOnReturn mark_pop_error_on_return;
12331285

1234-
if (ssl_ && SSL_shutdown(ssl_.get()) == 0)
1235-
SSL_shutdown(ssl_.get());
1286+
if (ssl_) {
1287+
SSLLibraryCallScope ssl_library_call_scope(this);
1288+
if (SSL_shutdown(ssl_.get()) == 0) SSL_shutdown(ssl_.get());
1289+
}
12361290

12371291
flags_.shutdown = true;
12381292
EncOut();
12391293
return underlying_stream()->DoShutdown(req_wrap);
12401294
}
12411295

1296+
void TLSWrap::ScheduleDeferredCycle() {
1297+
if (flags_.deferred_cycle_scheduled) return;
1298+
flags_.deferred_cycle_scheduled = true;
1299+
1300+
BaseObjectPtr<TLSWrap> strong_ref{this};
1301+
env()->SetImmediate([this, strong_ref](Environment* env) {
1302+
flags_.deferred_cycle_scheduled = false;
1303+
if (ssl_) Cycle();
1304+
});
1305+
}
1306+
1307+
void TLSWrap::FlushPendingShutdown() {
1308+
if (!pending_shutdown_ || !ssl_) return;
1309+
1310+
if (!SSL_is_init_finished(ssl_.get())) {
1311+
Debug(this, "Holding deferred shutdown, handshake still in progress");
1312+
return;
1313+
}
1314+
1315+
BaseObjectPtr<AsyncWrap> pending = std::move(pending_shutdown_);
1316+
ShutdownWrap* req_wrap = ShutdownWrap::FromObject(pending);
1317+
int err = DoShutdown(req_wrap);
1318+
if (err != 0) req_wrap->Done(err);
1319+
}
1320+
12421321
void TLSWrap::SetVerifyMode(const FunctionCallbackInfo<Value>& args) {
12431322
TLSWrap* wrap;
12441323
ASSIGN_OR_RETURN_UNWRAP(&wrap, args.This());
@@ -1335,6 +1414,13 @@ void TLSWrap::Destroy() {
13351414
// And destroy
13361415
InvokeQueued(UV_ECANCELED, "Canceled because of SSL destruction");
13371416

1417+
// A shutdown held back off the SSL library's stack will never be replayed
1418+
// now, so complete it here rather than leaving the stream waiting on it.
1419+
if (pending_shutdown_) {
1420+
BaseObjectPtr<AsyncWrap> pending = std::move(pending_shutdown_);
1421+
ShutdownWrap::FromObject(pending)->Done(UV_ECANCELED);
1422+
}
1423+
13381424
env()->external_memory_accounter()->Decrease(env()->isolate(), kExternalSize);
13391425
ssl_.reset();
13401426

@@ -2177,13 +2263,24 @@ void TLSWrap::WritesIssuedByPrevListenerDone(
21772263
}
21782264

21792265
void TLSWrap::Cycle() {
2266+
// With no loop to extend, cycling now would re-enter the SSL library.
2267+
if (cycle_depth_ == 0 && in_ssl_library_call()) {
2268+
Debug(this, "Deferring cycle requested from the SSL library's stack");
2269+
ScheduleDeferredCycle();
2270+
return;
2271+
}
2272+
21802273
// Prevent recursion
21812274
if (++cycle_depth_ > 1)
21822275
return;
21832276

21842277
for (; cycle_depth_ > 0; cycle_depth_--) {
21852278
ClearIn();
21862279
ClearOut();
2280+
// ClearOut() could defer a write/shutdown, so we ClearIn() again now
2281+
// to avoid needing a second pass:
2282+
ClearIn();
2283+
FlushPendingShutdown();
21872284
// EncIn() doesn't exist, it happens via stream listener callbacks.
21882285
EncOut();
21892286
}

src/crypto/crypto_tls.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,23 @@ class TLSWrap : public AsyncWrap,
5454

5555
enum class UnderlyingStreamWriteStatus { kHasActive, kVacancy };
5656

57+
// The SSL library's state machine is not reentrant. Node holds this scope
58+
// across every call into it, so that JS the SSL library invokes on its own
59+
// stack is recognised and kept from re-entering.
60+
class SSLLibraryCallScope {
61+
public:
62+
explicit SSLLibraryCallScope(TLSWrap* wrap) : wrap_(wrap) {
63+
wrap_->ssl_library_call_depth_++;
64+
}
65+
~SSLLibraryCallScope() { wrap_->ssl_library_call_depth_--; }
66+
67+
SSLLibraryCallScope(const SSLLibraryCallScope&) = delete;
68+
SSLLibraryCallScope& operator=(const SSLLibraryCallScope&) = delete;
69+
70+
private:
71+
TLSWrap* wrap_;
72+
};
73+
5774
static void Initialize(v8::Local<v8::Object> target,
5875
v8::Local<v8::Value> unused,
5976
v8::Local<v8::Context> context,
@@ -194,6 +211,16 @@ class TLSWrap : public AsyncWrap,
194211
// underlying stream even if there is no clear text to read or write.
195212
void Cycle();
196213

214+
inline bool in_ssl_library_call() const {
215+
return ssl_library_call_depth_ > 0;
216+
}
217+
218+
// Setup Cycle() after a library call, to flush anything DoWrite() held back
219+
void ScheduleDeferredCycle();
220+
221+
// Flush a shutdown held back by DoShutdown(), if there is one.
222+
void FlushPendingShutdown();
223+
197224
// Implement StreamListener:
198225
// Returns buf that points into enc_in_.
199226
uv_buf_t OnStreamAlloc(size_t size) override;
@@ -296,6 +323,9 @@ class TLSWrap : public AsyncWrap,
296323
size_t write_size_ = 0;
297324
BaseObjectPtr<AsyncWrap> current_write_;
298325
BaseObjectPtr<AsyncWrap> current_empty_write_;
326+
// Set when DoShutdown() was called while the SSL library was on the stack,
327+
// and so has yet to send close_notify.
328+
BaseObjectPtr<AsyncWrap> pending_shutdown_;
299329
std::string error_;
300330

301331
// TODO(@jasnell): These state flags should be revisited.
@@ -316,6 +346,7 @@ class TLSWrap : public AsyncWrap,
316346
bool shutdown : 1;
317347
bool cert_cb_running : 1;
318348
bool eof : 1;
349+
bool deferred_cycle_scheduled : 1;
319350
bool established : 1;
320351
bool write_callback_scheduled : 1;
321352
bool has_active_write_issued_by_prev_listener : 1;
@@ -325,6 +356,9 @@ class TLSWrap : public AsyncWrap,
325356

326357
int cycle_depth_ = 0;
327358

359+
// Nesting depth of calls into the SSL library. See SSLLibraryCallScope.
360+
int ssl_library_call_depth_ = 0;
361+
328362
// SSL_set_cert_cb
329363
CertCb cert_cb_ = nullptr;
330364
void* cert_cb_arg_ = nullptr;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use strict';
2+
3+
// Ending a server TLSSocket synchronously from inside an ALPNCallback must
4+
// finish the handshake and then shut the connection down cleanly, rather than
5+
// dropping the underlying socket part way through it.
6+
7+
const common = require('../common');
8+
9+
if (!common.hasCrypto)
10+
common.skip('missing crypto');
11+
12+
const assert = require('assert');
13+
const fixtures = require('../common/fixtures');
14+
const tls = require('tls');
15+
16+
function test(maxVersion) {
17+
const server = tls.createServer({
18+
key: fixtures.readKey('agent1-key.pem'),
19+
cert: fixtures.readKey('agent1-cert.pem'),
20+
maxVersion,
21+
ALPNCallback: common.mustCall(function({ protocols }) {
22+
this.end();
23+
return protocols[0];
24+
}),
25+
});
26+
27+
server.on('tlsClientError', common.mustNotCall());
28+
server.on('secureConnection', common.mustCall((socket) => {
29+
socket.on('error', common.mustNotCall());
30+
}));
31+
32+
server.listen(0, common.mustCall(() => {
33+
const client = tls.connect({
34+
port: server.address().port,
35+
ALPNProtocols: ['a'],
36+
rejectUnauthorized: false,
37+
maxVersion,
38+
}, common.mustCall(() => {
39+
assert.strictEqual(client.alpnProtocol, 'a');
40+
}));
41+
42+
// A clean close_notify, not a truncated connection.
43+
client.on('end', common.mustCall());
44+
client.on('close', common.mustCall((hadError) => {
45+
assert.strictEqual(hadError, false);
46+
server.close();
47+
}));
48+
client.on('error', common.mustNotCall());
49+
}));
50+
}
51+
52+
test('TLSv1.2');
53+
test('TLSv1.3');
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
'use strict';
2+
3+
// Writing to a server TLSSocket synchronously from inside an ALPNCallback,
4+
// which the TLS library invokes on its own stack mid-handshake, must not break
5+
// the connection; the data must be delivered once the handshake ends.
6+
7+
const common = require('../common');
8+
9+
if (!common.hasCrypto)
10+
common.skip('missing crypto');
11+
12+
const assert = require('assert');
13+
const fixtures = require('../common/fixtures');
14+
const tls = require('tls');
15+
16+
const server = tls.createServer({
17+
key: fixtures.readKey('agent1-key.pem'),
18+
cert: fixtures.readKey('agent1-cert.pem'),
19+
ALPNCallback: common.mustCall(function({ protocols }) {
20+
// The write cannot complete until the handshake does, but it must be
21+
// accepted and eventually flushed rather than dropped or encrypted into
22+
// the middle of the handshake.
23+
this.write('from-mid-handshake', common.mustCall());
24+
return protocols[0];
25+
}),
26+
});
27+
28+
server.on('tlsClientError', common.mustNotCall());
29+
server.on('secureConnection', common.mustCall((socket) => {
30+
assert.strictEqual(socket.alpnProtocol, 'a');
31+
socket.on('error', common.mustNotCall());
32+
}));
33+
34+
server.listen(0, common.mustCall(() => {
35+
const client = tls.connect({
36+
port: server.address().port,
37+
ALPNProtocols: ['a', 'b'],
38+
rejectUnauthorized: false,
39+
}, common.mustCall(() => {
40+
assert.strictEqual(client.alpnProtocol, 'a');
41+
42+
client.on('data', common.mustCall((data) => {
43+
assert.strictEqual(data.toString(), 'from-mid-handshake');
44+
client.end();
45+
server.close();
46+
}));
47+
}));
48+
client.on('error', common.mustNotCall());
49+
}));

0 commit comments

Comments
 (0)