Skip to content

Commit 63c8fb0

Browse files
committed
tls: fix bugs of double TLS
Fixs two issues in `TLSWrap`, one of them is reported in nodejs#30896. 1. `TLSWrap` has exactly one `StreamListener`, however, that `StreamListener` can be replaced. We have not been rigorous enough here: if an active write has not been finished before the transition, the finish callback of it will be wrongly fired the successor `StreamListener`. 2. A `TLSWrap` does not allow more than one active write, as checked in the assertion about current_write in `TLSWrap::DoWrite()`. However, when users make use of an existing `tls.TLSSocket` to establish double TLS, by either tls.connect({socket: tlssock}) or tlsServer.emit('connection', tlssock) we have both of the user provided `tls.TLSSocket`, tlssock and a brand new created `TLSWrap` writing to the `TLSWrap` bound to tlssock, which easily violates the constranint because two writers have no idea of each other. The design of the fix is: when a `TLSWrap` is created on top of a user provided socket, do not send any data to the socket until all existing writes of the socket are done and ensure registered callbacks of those writes can be fired.
1 parent f9737b1 commit 63c8fb0

File tree

7 files changed

+296
-24
lines changed

7 files changed

+296
-24
lines changed

lib/_tls_wrap.js

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -545,25 +545,36 @@ function TLSSocket(socket, opts) {
545545
this[kPendingSession] = null;
546546

547547
let wrap;
548-
if ((socket instanceof net.Socket && socket._handle) || !socket) {
549-
// 1. connected socket
550-
// 2. no socket, one will be created with net.Socket().connect
551-
wrap = socket;
548+
let handle;
549+
let wrapHasActiveWriteFromPrevOwner;
550+
551+
if (socket) {
552+
if (socket instanceof net.Socket && socket._handle) {
553+
// 1. connected socket
554+
wrap = socket;
555+
} else {
556+
// 2. socket has no handle so it is js not c++
557+
// 3. unconnected sockets are wrapped
558+
// TLS expects to interact from C++ with a net.Socket that has a C++ stream
559+
// handle, but a JS stream doesn't have one. Wrap it up to make it look like
560+
// a socket.
561+
wrap = new JSStreamSocket(socket);
562+
}
563+
564+
handle = wrap._handle;
565+
wrapHasActiveWriteFromPrevOwner = wrap.writableLength > 0;
552566
} else {
553-
// 3. socket has no handle so it is js not c++
554-
// 4. unconnected sockets are wrapped
555-
// TLS expects to interact from C++ with a net.Socket that has a C++ stream
556-
// handle, but a JS stream doesn't have one. Wrap it up to make it look like
557-
// a socket.
558-
wrap = new JSStreamSocket(socket);
567+
// 4. no socket, one will be created with net.Socket().connect
568+
wrap = null;
569+
wrapHasActiveWriteFromPrevOwner = false;
559570
}
560571

561572
// Just a documented property to make secure sockets
562573
// distinguishable from regular ones.
563574
this.encrypted = true;
564575

565576
ReflectApply(net.Socket, this, [{
566-
handle: this._wrapHandle(wrap),
577+
handle: this._wrapHandle(wrap, handle, wrapHasActiveWriteFromPrevOwner),
567578
allowHalfOpen: socket ? socket.allowHalfOpen : tlsOptions.allowHalfOpen,
568579
pauseOnCreate: tlsOptions.pauseOnConnect,
569580
manualStart: true,
@@ -582,6 +593,22 @@ function TLSSocket(socket, opts) {
582593
if (enableTrace && this._handle)
583594
this._handle.enableTrace();
584595

596+
if (wrapHasActiveWriteFromPrevOwner) {
597+
// `wrap` is a streams.Writable in JS. This empty write will be queued
598+
// and hence finish after all existing writes, which is the timing
599+
// we want to start to send any tls data to `wrap`.
600+
const that = this;
601+
wrap.write('', (err) => {
602+
if (err) {
603+
debug('error got before writing any tls data to the underlying stream');
604+
that.destroy(err);
605+
return;
606+
}
607+
608+
that._handle.writesIssuedByPrevListenerDone();
609+
});
610+
}
611+
585612
// Read on next tick so the caller has a chance to setup listeners
586613
process.nextTick(initRead, this, socket);
587614
}
@@ -642,11 +669,14 @@ TLSSocket.prototype.disableRenegotiation = function disableRenegotiation() {
642669
this[kDisableRenegotiation] = true;
643670
};
644671

645-
TLSSocket.prototype._wrapHandle = function(wrap, handle) {
646-
if (!handle && wrap) {
647-
handle = wrap._handle;
648-
}
649-
672+
/**
673+
*
674+
* @param {null|net.Socket} wrap
675+
* @param {null|object} handle
676+
* @param {boolean} wrapHasActiveWriteFromPrevOwner
677+
* @returns {object}
678+
*/
679+
TLSSocket.prototype._wrapHandle = function(wrap, handle, wrapHasActiveWriteFromPrevOwner) {
650680
const options = this._tlsOptions;
651681
if (!handle) {
652682
handle = options.pipe ?
@@ -663,7 +693,10 @@ TLSSocket.prototype._wrapHandle = function(wrap, handle) {
663693
if (!(context.context instanceof NativeSecureContext)) {
664694
throw new ERR_TLS_INVALID_CONTEXT('context');
665695
}
666-
const res = tls_wrap.wrap(handle, context.context, !!options.isServer);
696+
697+
const res = tls_wrap.wrap(handle, context.context,
698+
!!options.isServer,
699+
wrapHasActiveWriteFromPrevOwner);
667700
res._parent = handle; // C++ "wrap" object: TCPWrap, JSStream, ...
668701
res._parentWrap = wrap; // JS object: net.Socket, JSStreamSocket, ...
669702
res._secureContext = context;
@@ -680,7 +713,7 @@ TLSSocket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) {
680713
const originalServername = this.ssl ? this._handle.getServername() : null;
681714
const originalSession = this.ssl ? this._handle.getSession() : null;
682715

683-
this.handle = this._wrapHandle(null, handle);
716+
this.handle = this._wrapHandle(null, handle, false);
684717
this.ssl = this._handle;
685718

686719
net.Socket.prototype[kReinitializeHandle].call(this, this.handle);

src/crypto/crypto_tls.cc

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -357,12 +357,15 @@ TLSWrap::TLSWrap(Environment* env,
357357
Local<Object> obj,
358358
Kind kind,
359359
StreamBase* stream,
360-
SecureContext* sc)
360+
SecureContext* sc,
361+
bool stream_has_active_write)
361362
: AsyncWrap(env, obj, AsyncWrap::PROVIDER_TLSWRAP),
362363
StreamBase(env),
363364
env_(env),
364365
kind_(kind),
365-
sc_(sc) {
366+
sc_(sc),
367+
has_active_write_issued_by_prev_listener_(
368+
stream_has_active_write) {
366369
MakeWeak();
367370
CHECK(sc_);
368371
ssl_ = sc_->CreateSSL();
@@ -472,10 +475,11 @@ void TLSWrap::InitSSL() {
472475
void TLSWrap::Wrap(const FunctionCallbackInfo<Value>& args) {
473476
Environment* env = Environment::GetCurrent(args);
474477

475-
CHECK_EQ(args.Length(), 3);
478+
CHECK_EQ(args.Length(), 4);
476479
CHECK(args[0]->IsObject());
477480
CHECK(args[1]->IsObject());
478481
CHECK(args[2]->IsBoolean());
482+
CHECK(args[3]->IsBoolean());
479483

480484
Local<Object> sc = args[1].As<Object>();
481485
Kind kind = args[2]->IsTrue() ? Kind::kServer : Kind::kClient;
@@ -490,7 +494,8 @@ void TLSWrap::Wrap(const FunctionCallbackInfo<Value>& args) {
490494
return;
491495
}
492496

493-
TLSWrap* res = new TLSWrap(env, obj, kind, stream, Unwrap<SecureContext>(sc));
497+
TLSWrap* res = new TLSWrap(env, obj, kind, stream, Unwrap<SecureContext>(sc),
498+
args[3]->IsTrue() /* stream_has_active_write */);
494499

495500
args.GetReturnValue().Set(res->object());
496501
}
@@ -596,6 +601,12 @@ void TLSWrap::EncOut() {
596601
return;
597602
}
598603

604+
if (UNLIKELY(has_active_write_issued_by_prev_listener_)) {
605+
Debug(this, "Returning from EncOut(), "
606+
"has_active_write_issued_by_prev_listener_ is true");
607+
return;
608+
}
609+
599610
// Split-off queue
600611
if (established_ && current_write_) {
601612
Debug(this, "EncOut() write is scheduled");
@@ -666,6 +677,15 @@ void TLSWrap::EncOut() {
666677

667678
void TLSWrap::OnStreamAfterWrite(WriteWrap* req_wrap, int status) {
668679
Debug(this, "OnStreamAfterWrite(status = %d)", status);
680+
681+
if (UNLIKELY(has_active_write_issued_by_prev_listener_)) {
682+
Debug(this, "Notify write finish to the previous_listener_");
683+
CHECK_EQ(write_size_, 0); // we must have restrained writes
684+
685+
previous_listener_->OnStreamAfterWrite(req_wrap, status);
686+
return;
687+
}
688+
669689
if (current_empty_write_) {
670690
Debug(this, "Had empty write");
671691
BaseObjectPtr<AsyncWrap> current_empty_write =
@@ -2021,6 +2041,16 @@ void TLSWrap::GetALPNNegotiatedProto(const FunctionCallbackInfo<Value>& args) {
20212041
args.GetReturnValue().Set(result);
20222042
}
20232043

2044+
void TLSWrap::WritesIssuedByPrevListenerDone(
2045+
const FunctionCallbackInfo<Value>& args) {
2046+
TLSWrap* w;
2047+
ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder());
2048+
2049+
Debug(w, "WritesIssuedByPrevListenerDone is called");
2050+
w->has_active_write_issued_by_prev_listener_ = false;
2051+
w->EncOut(); // resume all of our restrained writes
2052+
}
2053+
20242054
void TLSWrap::Cycle() {
20252055
// Prevent recursion
20262056
if (++cycle_depth_ > 1)
@@ -2098,6 +2128,8 @@ void TLSWrap::Initialize(
20982128
SetProtoMethod(isolate, t, "setSession", SetSession);
20992129
SetProtoMethod(isolate, t, "setVerifyMode", SetVerifyMode);
21002130
SetProtoMethod(isolate, t, "start", Start);
2131+
SetProtoMethod(isolate, t, "writesIssuedByPrevListenerDone",
2132+
WritesIssuedByPrevListenerDone);
21012133

21022134
SetProtoMethodNoSideEffect(
21032135
isolate, t, "exportKeyingMaterial", ExportKeyingMaterial);
@@ -2180,6 +2212,7 @@ void TLSWrap::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
21802212
registry->Register(GetSharedSigalgs);
21812213
registry->Register(GetTLSTicket);
21822214
registry->Register(VerifyError);
2215+
registry->Register(WritesIssuedByPrevListenerDone);
21832216

21842217
#ifdef SSL_set_max_send_fragment
21852218
registry->Register(SetMaxSendFragment);

src/crypto/crypto_tls.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,8 @@ class TLSWrap : public AsyncWrap,
136136
v8::Local<v8::Object> obj,
137137
Kind kind,
138138
StreamBase* stream,
139-
SecureContext* sc);
139+
SecureContext* sc,
140+
bool stream_has_active_write);
140141

141142
static void SSLInfoCallback(const SSL* ssl_, int where, int ret);
142143
void InitSSL();
@@ -217,6 +218,8 @@ class TLSWrap : public AsyncWrap,
217218
static void Start(const v8::FunctionCallbackInfo<v8::Value>& args);
218219
static void VerifyError(const v8::FunctionCallbackInfo<v8::Value>& args);
219220
static void Wrap(const v8::FunctionCallbackInfo<v8::Value>& args);
221+
static void WritesIssuedByPrevListenerDone(
222+
const v8::FunctionCallbackInfo<v8::Value>& args);
220223

221224
#ifdef SSL_set_max_send_fragment
222225
static void SetMaxSendFragment(
@@ -284,6 +287,8 @@ class TLSWrap : public AsyncWrap,
284287

285288
BIOPointer bio_trace_;
286289

290+
bool has_active_write_issued_by_prev_listener_ = false;
291+
287292
public:
288293
std::vector<unsigned char> alpn_protos_; // Accessed by SelectALPNCallback.
289294
bool alpn_callback_enabled_ = false; // Accessed by SelectALPNCallback.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
if (!common.hasCrypto) common.skip('missing crypto');
5+
const fixtures = require('../common/fixtures');
6+
const tls = require('tls');
7+
8+
// In reality, this can be a HTTP CONNECT message, signaling the incoming
9+
// data is TLS encrypted
10+
const HEAD = 'XXXX';
11+
12+
const subserver = tls.createServer({
13+
key: fixtures.readKey('agent1-key.pem'),
14+
cert: fixtures.readKey('agent1-cert.pem'),
15+
})
16+
.on('secureConnection', common.mustCall(() => {
17+
process.exit(0);
18+
}));
19+
20+
const server = tls.createServer({
21+
key: fixtures.readKey('agent1-key.pem'),
22+
cert: fixtures.readKey('agent1-cert.pem'),
23+
})
24+
.listen(client)
25+
.on('secureConnection', (serverTlsSock) => {
26+
serverTlsSock.on('data', (chunk) => {
27+
assert.strictEqual(chunk.toString(), HEAD);
28+
subserver.emit('connection', serverTlsSock);
29+
});
30+
});
31+
32+
function client() {
33+
const down = tls.connect({
34+
host: '127.0.0.1',
35+
port: server.address().port,
36+
rejectUnauthorized: false
37+
}).on('secureConnect', () => {
38+
down.write(HEAD, common.mustSucceed());
39+
40+
// Sending tls data on a client TLSSocket with an active write led to a crash:
41+
//
42+
// node[16862]: ../src/crypto/crypto_tls.cc:963:virtual int node::crypto::TLSWrap::DoWrite(node::WriteWrap*,
43+
// uv_buf_t*, size_t, uv_stream_t*): Assertion `!current_write_' failed.
44+
// 1: 0xb090e0 node::Abort() [node]
45+
// 2: 0xb0915e [node]
46+
// 3: 0xca8413 node::crypto::TLSWrap::DoWrite(node::WriteWrap*, uv_buf_t*, unsigned long, uv_stream_s*) [node]
47+
// 4: 0xcaa549 node::StreamBase::Write(uv_buf_t*, unsigned long, uv_stream_s*, v8::Local<v8::Object>) [node]
48+
// 5: 0xca88d7 node::crypto::TLSWrap::EncOut() [node]
49+
// 6: 0xd3df3e [node]
50+
// 7: 0xd3f35f v8::internal::Builtin_HandleApiCall(int, unsigned long*, v8::internal::Isolate*) [node]
51+
// 8: 0x15d9ef9 [node]
52+
// Aborted
53+
tls.connect({
54+
socket: down,
55+
rejectUnauthorized: false
56+
});
57+
});
58+
}

0 commit comments

Comments
 (0)