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
Like errno, OpenSSL's API requires SSL_get_error and error queue be
checked immediately after the failing operation, otherwise the error
queue or SSL object may have changed state and no longer report
information about the operation the caller wanted.

TLSWrap almost heeds this rule, except in TLSWrap::ClearOut. If SSL_read
picks up a closing alert (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, which may change the error.

In particular, I've observed that, on close_notify, 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.
Back in #1661, Node needed to account for GetSSLError being called after
ssl_ was destroyed. This was the real cause. With this fixed, there's no
need to account for this. (Any case where ssl_ may be destroyed before
SSL_get_error is a case where ssl_ or the error queue could 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.

PR-URL: #44563
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
  • Loading branch information
davidben authored and pull[bot] committed Dec 6, 2023
1 parent 2382278 commit 5909434
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 @@ -668,11 +668,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 @@ -726,19 +721,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 @@ -749,11 +750,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 @@ -829,7 +827,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 @@ -1005,7 +1003,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 @@ -167,8 +167,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 5909434

Please sign in to comment.