Skip to content

Commit

Permalink
tls: fix re-entrancy issue with TLS close_notify
Browse files Browse the repository at this point in the history
OpenSSL's API requires SSL_get_error be called immediately after the
failing operation, otherwise the SSL object may have changed state and
no longer report information about the failing error.

TLSWrap almost heeds this rule, except in TLSWrap::ClearOut. If SSL_read
picks up a close_notify (detected by checking SSL_get_shutdown), Node
calls out to JS with EmitRead(UV_EOF) and only afterwards proceeds to
dispatch on the error. But, by this point, Node has already re-entered
JS and indeed JS seems to sometimes call back into TLSWrap::DoShutdown,
calling SSL_shutdown. (I think this comes from onStreamRead in
stream_base_commons.js?)

Instead, SSL_get_error and the error queue should be sampled earlier.
This avoids the issue worked around by nodejs#1661, where GetSSLError needed
to check if ssl_ was destroyed before calling SSL_get_error. We can now
remove that wrapper and just call SSL_get_error directly. (Any case
where ssl_ may be destroyed first is a case where ssl_ may change state,
so it's a bug either way.)

This is the first of two fixes in error-handling here. The
EmitRead(UV_EOF) seems to additionally swallow fatal alerts from the
peer. Some of the ECONNRESET expectations in the tests aren't actually
correct. The next commit will fix this as well.
  • Loading branch information
davidben committed Sep 6, 2022
1 parent 0917626 commit 4c60bfb
Show file tree
Hide file tree
Showing 2 changed files with 16 additions and 20 deletions.
34 changes: 16 additions & 18 deletions src/crypto/crypto_tls.cc
Original file line number Diff line number Diff line change
Expand Up @@ -677,11 +677,6 @@ void TLSWrap::OnStreamAfterWrite(WriteWrap* req_wrap, int status) {
EncOut();
}

int TLSWrap::GetSSLError(int status) const {
// ssl_ might already be destroyed for reading EOF from a close notify alert.
return ssl_ != nullptr ? SSL_get_error(ssl_.get(), status) : 0;
}

void TLSWrap::ClearOut() {
Debug(this, "Trying to read cleartext output");
// Ignore cycling data if ClientHello wasn't yet parsed
Expand Down Expand Up @@ -735,19 +730,25 @@ void TLSWrap::ClearOut() {
}
}

int flags = SSL_get_shutdown(ssl_.get());
if (!eof_ && flags & SSL_RECEIVED_SHUTDOWN) {
eof_ = true;
EmitRead(UV_EOF);
}

// We need to check whether an error occurred or the connection was
// shutdown cleanly (SSL_ERROR_ZERO_RETURN) even when read == 0.
// See node#1642 and SSL_read(3SSL) for details.
// See node#1642 and SSL_read(3SSL) for details. SSL_get_error must be
// called immediately after SSL_read, without calling into JS, which may
// change OpenSSL's error queue, modify ssl_, or even destroy ssl_
// altogether.
if (read <= 0) {
int err = SSL_get_error(ssl_.get(), read);
unsigned long ssl_err = ERR_peek_error(); // NOLINT(runtime/int)
const std::string error_str = GetBIOError();

int flags = SSL_get_shutdown(ssl_.get());
if (!eof_ && flags & SSL_RECEIVED_SHUTDOWN) {
eof_ = true;
EmitRead(UV_EOF);
}

HandleScope handle_scope(env()->isolate());
Local<Value> error;
int err = GetSSLError(read);
switch (err) {
case SSL_ERROR_ZERO_RETURN:
// Ignore ZERO_RETURN after EOF, it is basically not an error.
Expand All @@ -758,11 +759,8 @@ void TLSWrap::ClearOut() {
case SSL_ERROR_SSL:
case SSL_ERROR_SYSCALL:
{
unsigned long ssl_err = ERR_peek_error(); // NOLINT(runtime/int)

Local<Context> context = env()->isolate()->GetCurrentContext();
if (UNLIKELY(context.IsEmpty())) return;
const std::string error_str = GetBIOError();
Local<String> message = OneByteString(
env()->isolate(), error_str.c_str(), error_str.size());
if (UNLIKELY(message.IsEmpty())) return;
Expand Down Expand Up @@ -838,7 +836,7 @@ void TLSWrap::ClearIn() {
}

// Error or partial write
int err = GetSSLError(written);
int err = SSL_get_error(ssl_.get(), written);
if (err == SSL_ERROR_SSL || err == SSL_ERROR_SYSCALL) {
Debug(this, "Got SSL error (%d)", err);
write_callback_scheduled_ = true;
Expand Down Expand Up @@ -1014,7 +1012,7 @@ int TLSWrap::DoWrite(WriteWrap* w,

if (written == -1) {
// If we stopped writing because of an error, it's fatal, discard the data.
int err = GetSSLError(written);
int err = SSL_get_error(ssl_.get(), written);
if (err == SSL_ERROR_SSL || err == SSL_ERROR_SYSCALL) {
// TODO(@jasnell): What are we doing with the error?
Debug(this, "Got SSL error (%d), returning UV_EPROTO", err);
Expand Down
2 changes: 0 additions & 2 deletions src/crypto/crypto_tls.h
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,6 @@ class TLSWrap : public AsyncWrap,

int SetCACerts(SecureContext* sc);

int GetSSLError(int status) const;

static int SelectSNIContextCallback(SSL* s, int* ad, void* arg);

static void CertCbDone(const v8::FunctionCallbackInfo<v8::Value>& args);
Expand Down

0 comments on commit 4c60bfb

Please sign in to comment.