Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tls: fix bugs of double TLS #48796

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 51 additions & 18 deletions lib/_tls_wrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -545,25 +545,36 @@ function TLSSocket(socket, opts) {
this[kPendingSession] = null;

let wrap;
if ((socket instanceof net.Socket && socket._handle) || !socket) {
// 1. connected socket
// 2. no socket, one will be created with net.Socket().connect
wrap = socket;
let handle;
let wrapHasActiveWriteFromPrevOwner;

if (socket) {
if (socket instanceof net.Socket && socket._handle) {
// 1. connected socket
wrap = socket;
} else {
// 2. socket has no handle so it is js not c++
// 3. unconnected sockets are wrapped
// TLS expects to interact from C++ with a net.Socket that has a C++ stream
// handle, but a JS stream doesn't have one. Wrap it up to make it look like
// a socket.
wrap = new JSStreamSocket(socket);
}

handle = wrap._handle;
wrapHasActiveWriteFromPrevOwner = wrap.writableLength > 0;
} else {
// 3. socket has no handle so it is js not c++
// 4. unconnected sockets are wrapped
// TLS expects to interact from C++ with a net.Socket that has a C++ stream
// handle, but a JS stream doesn't have one. Wrap it up to make it look like
// a socket.
wrap = new JSStreamSocket(socket);
// 4. no socket, one will be created with net.Socket().connect
wrap = null;
wrapHasActiveWriteFromPrevOwner = false;
}

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

ReflectApply(net.Socket, this, [{
handle: this._wrapHandle(wrap),
handle: this._wrapHandle(wrap, handle, wrapHasActiveWriteFromPrevOwner),
allowHalfOpen: socket ? socket.allowHalfOpen : tlsOptions.allowHalfOpen,
pauseOnCreate: tlsOptions.pauseOnConnect,
manualStart: true,
Expand All @@ -582,6 +593,22 @@ function TLSSocket(socket, opts) {
if (enableTrace && this._handle)
this._handle.enableTrace();

if (wrapHasActiveWriteFromPrevOwner) {
// `wrap` is a streams.Writable in JS. This empty write will be queued
// and hence finish after all existing writes, which is the timing
// we want to start to send any tls data to `wrap`.
const that = this;
ywave620 marked this conversation as resolved.
Show resolved Hide resolved
wrap.write('', (err) => {
if (err) {
debug('error got before writing any tls data to the underlying stream');
that.destroy(err);
return;
}

that._handle.writesIssuedByPrevListenerDone();
});
}

// Read on next tick so the caller has a chance to setup listeners
process.nextTick(initRead, this, socket);
}
Expand Down Expand Up @@ -642,11 +669,14 @@ TLSSocket.prototype.disableRenegotiation = function disableRenegotiation() {
this[kDisableRenegotiation] = true;
};

TLSSocket.prototype._wrapHandle = function(wrap, handle) {
if (!handle && wrap) {
handle = wrap._handle;
}

/**
*
* @param {null|net.Socket} wrap
* @param {null|object} handle
* @param {boolean} wrapHasActiveWriteFromPrevOwner
* @returns {object}
*/
TLSSocket.prototype._wrapHandle = function(wrap, handle, wrapHasActiveWriteFromPrevOwner) {
const options = this._tlsOptions;
if (!handle) {
handle = options.pipe ?
Expand All @@ -663,7 +693,10 @@ TLSSocket.prototype._wrapHandle = function(wrap, handle) {
if (!(context.context instanceof NativeSecureContext)) {
throw new ERR_TLS_INVALID_CONTEXT('context');
}
const res = tls_wrap.wrap(handle, context.context, !!options.isServer);

const res = tls_wrap.wrap(handle, context.context,
!!options.isServer,
wrapHasActiveWriteFromPrevOwner);
res._parent = handle; // C++ "wrap" object: TCPWrap, JSStream, ...
res._parentWrap = wrap; // JS object: net.Socket, JSStreamSocket, ...
res._secureContext = context;
Expand All @@ -680,7 +713,7 @@ TLSSocket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) {
const originalServername = this.ssl ? this._handle.getServername() : null;
const originalSession = this.ssl ? this._handle.getSession() : null;

this.handle = this._wrapHandle(null, handle);
this.handle = this._wrapHandle(null, handle, false);
this.ssl = this._handle;

net.Socket.prototype[kReinitializeHandle].call(this, this.handle);
Expand Down
41 changes: 37 additions & 4 deletions src/crypto/crypto_tls.cc
Original file line number Diff line number Diff line change
Expand Up @@ -357,12 +357,15 @@ TLSWrap::TLSWrap(Environment* env,
Local<Object> obj,
Kind kind,
StreamBase* stream,
SecureContext* sc)
SecureContext* sc,
bool stream_has_active_write)
: AsyncWrap(env, obj, AsyncWrap::PROVIDER_TLSWRAP),
StreamBase(env),
env_(env),
kind_(kind),
sc_(sc) {
sc_(sc),
has_active_write_issued_by_prev_listener_(
stream_has_active_write) {
MakeWeak();
CHECK(sc_);
ssl_ = sc_->CreateSSL();
Expand Down Expand Up @@ -472,10 +475,11 @@ void TLSWrap::InitSSL() {
void TLSWrap::Wrap(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

CHECK_EQ(args.Length(), 3);
CHECK_EQ(args.Length(), 4);
CHECK(args[0]->IsObject());
CHECK(args[1]->IsObject());
CHECK(args[2]->IsBoolean());
CHECK(args[3]->IsBoolean());

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

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

args.GetReturnValue().Set(res->object());
}
Expand Down Expand Up @@ -596,6 +601,12 @@ void TLSWrap::EncOut() {
return;
}

if (UNLIKELY(has_active_write_issued_by_prev_listener_)) {
Debug(this, "Returning from EncOut(), "
"has_active_write_issued_by_prev_listener_ is true");
return;
}

// Split-off queue
if (established_ && current_write_) {
Debug(this, "EncOut() write is scheduled");
Expand Down Expand Up @@ -666,6 +677,15 @@ void TLSWrap::EncOut() {

void TLSWrap::OnStreamAfterWrite(WriteWrap* req_wrap, int status) {
Debug(this, "OnStreamAfterWrite(status = %d)", status);

if (UNLIKELY(has_active_write_issued_by_prev_listener_)) {
Debug(this, "Notify write finish to the previous_listener_");
CHECK_EQ(write_size_, 0); // we must have restrained writes

previous_listener_->OnStreamAfterWrite(req_wrap, status);
return;
}

if (current_empty_write_) {
Debug(this, "Had empty write");
BaseObjectPtr<AsyncWrap> current_empty_write =
Expand Down Expand Up @@ -2021,6 +2041,16 @@ void TLSWrap::GetALPNNegotiatedProto(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(result);
}

void TLSWrap::WritesIssuedByPrevListenerDone(
const FunctionCallbackInfo<Value>& args) {
TLSWrap* w;
ASSIGN_OR_RETURN_UNWRAP(&w, args.Holder());

Debug(w, "WritesIssuedByPrevListenerDone is called");
w->has_active_write_issued_by_prev_listener_ = false;
w->EncOut(); // resume all of our restrained writes
}

void TLSWrap::Cycle() {
// Prevent recursion
if (++cycle_depth_ > 1)
Expand Down Expand Up @@ -2098,6 +2128,8 @@ void TLSWrap::Initialize(
SetProtoMethod(isolate, t, "setSession", SetSession);
SetProtoMethod(isolate, t, "setVerifyMode", SetVerifyMode);
SetProtoMethod(isolate, t, "start", Start);
SetProtoMethod(isolate, t, "writesIssuedByPrevListenerDone",
WritesIssuedByPrevListenerDone);

SetProtoMethodNoSideEffect(
isolate, t, "exportKeyingMaterial", ExportKeyingMaterial);
Expand Down Expand Up @@ -2180,6 +2212,7 @@ void TLSWrap::RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(GetSharedSigalgs);
registry->Register(GetTLSTicket);
registry->Register(VerifyError);
registry->Register(WritesIssuedByPrevListenerDone);

#ifdef SSL_set_max_send_fragment
registry->Register(SetMaxSendFragment);
Expand Down
7 changes: 6 additions & 1 deletion src/crypto/crypto_tls.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ class TLSWrap : public AsyncWrap,
v8::Local<v8::Object> obj,
Kind kind,
StreamBase* stream,
SecureContext* sc);
SecureContext* sc,
bool stream_has_active_write);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking nit: I'm generally not a fan of bool arguments to methods, largely because just seeing the true or false a the callsite tends not to be descriptive enough to let folks know what the impact is. I certainly won't block this change because of it, but I'd prefer a enum arg here instead.


static void SSLInfoCallback(const SSL* ssl_, int where, int ret);
void InitSSL();
Expand Down Expand Up @@ -217,6 +218,8 @@ class TLSWrap : public AsyncWrap,
static void Start(const v8::FunctionCallbackInfo<v8::Value>& args);
static void VerifyError(const v8::FunctionCallbackInfo<v8::Value>& args);
static void Wrap(const v8::FunctionCallbackInfo<v8::Value>& args);
static void WritesIssuedByPrevListenerDone(
const v8::FunctionCallbackInfo<v8::Value>& args);

#ifdef SSL_set_max_send_fragment
static void SetMaxSendFragment(
Expand Down Expand Up @@ -284,6 +287,8 @@ class TLSWrap : public AsyncWrap,

BIOPointer bio_trace_;

bool has_active_write_issued_by_prev_listener_ = false;

public:
std::vector<unsigned char> alpn_protos_; // Accessed by SelectALPNCallback.
bool alpn_callback_enabled_ = false; // Accessed by SelectALPNCallback.
Expand Down
58 changes: 58 additions & 0 deletions test/parallel/test-double-tls-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
'use strict';
const common = require('../common');
const assert = require('assert');
if (!common.hasCrypto) common.skip('missing crypto');
const fixtures = require('../common/fixtures');
const tls = require('tls');

// In reality, this can be a HTTP CONNECT message, signaling the incoming
// data is TLS encrypted
const HEAD = 'XXXX';

const subserver = tls.createServer({
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem'),
})
.on('secureConnection', common.mustCall(() => {
process.exit(0);
}));

const server = tls.createServer({
key: fixtures.readKey('agent1-key.pem'),
cert: fixtures.readKey('agent1-cert.pem'),
})
.listen(client)
.on('secureConnection', (serverTlsSock) => {
serverTlsSock.on('data', (chunk) => {
assert.strictEqual(chunk.toString(), HEAD);
subserver.emit('connection', serverTlsSock);
});
});

function client() {
const down = tls.connect({
host: '127.0.0.1',
port: server.address().port,
rejectUnauthorized: false
}).on('secureConnect', () => {
down.write(HEAD, common.mustSucceed());

// Sending tls data on a client TLSSocket with an active write led to a crash:
//
// node[16862]: ../src/crypto/crypto_tls.cc:963:virtual int node::crypto::TLSWrap::DoWrite(node::WriteWrap*,
// uv_buf_t*, size_t, uv_stream_t*): Assertion `!current_write_' failed.
// 1: 0xb090e0 node::Abort() [node]
// 2: 0xb0915e [node]
// 3: 0xca8413 node::crypto::TLSWrap::DoWrite(node::WriteWrap*, uv_buf_t*, unsigned long, uv_stream_s*) [node]
// 4: 0xcaa549 node::StreamBase::Write(uv_buf_t*, unsigned long, uv_stream_s*, v8::Local<v8::Object>) [node]
// 5: 0xca88d7 node::crypto::TLSWrap::EncOut() [node]
// 6: 0xd3df3e [node]
// 7: 0xd3f35f v8::internal::Builtin_HandleApiCall(int, unsigned long*, v8::internal::Isolate*) [node]
// 8: 0x15d9ef9 [node]
// Aborted
tls.connect({
socket: down,
rejectUnauthorized: false
});
});
}
Loading