Skip to content

Commit 426b4a6

Browse files
committed
tls: fix re-entrancy issue with TLS close_notify
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 nodejs#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.
1 parent 0917626 commit 426b4a6

File tree

2 files changed

+16
-20
lines changed

2 files changed

+16
-20
lines changed

src/crypto/crypto_tls.cc

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -677,11 +677,6 @@ void TLSWrap::OnStreamAfterWrite(WriteWrap* req_wrap, int status) {
677677
EncOut();
678678
}
679679

680-
int TLSWrap::GetSSLError(int status) const {
681-
// ssl_ might already be destroyed for reading EOF from a close notify alert.
682-
return ssl_ != nullptr ? SSL_get_error(ssl_.get(), status) : 0;
683-
}
684-
685680
void TLSWrap::ClearOut() {
686681
Debug(this, "Trying to read cleartext output");
687682
// Ignore cycling data if ClientHello wasn't yet parsed
@@ -735,19 +730,25 @@ void TLSWrap::ClearOut() {
735730
}
736731
}
737732

738-
int flags = SSL_get_shutdown(ssl_.get());
739-
if (!eof_ && flags & SSL_RECEIVED_SHUTDOWN) {
740-
eof_ = true;
741-
EmitRead(UV_EOF);
742-
}
743-
744733
// We need to check whether an error occurred or the connection was
745734
// shutdown cleanly (SSL_ERROR_ZERO_RETURN) even when read == 0.
746-
// See node#1642 and SSL_read(3SSL) for details.
735+
// See node#1642 and SSL_read(3SSL) for details. SSL_get_error must be
736+
// called immediately after SSL_read, without calling into JS, which may
737+
// change OpenSSL's error queue, modify ssl_, or even destroy ssl_
738+
// altogether.
747739
if (read <= 0) {
740+
int err = SSL_get_error(ssl_.get(), read);
741+
unsigned long ssl_err = ERR_peek_error(); // NOLINT(runtime/int)
742+
const std::string error_str = GetBIOError();
743+
744+
int flags = SSL_get_shutdown(ssl_.get());
745+
if (!eof_ && flags & SSL_RECEIVED_SHUTDOWN) {
746+
eof_ = true;
747+
EmitRead(UV_EOF);
748+
}
749+
748750
HandleScope handle_scope(env()->isolate());
749751
Local<Value> error;
750-
int err = GetSSLError(read);
751752
switch (err) {
752753
case SSL_ERROR_ZERO_RETURN:
753754
// Ignore ZERO_RETURN after EOF, it is basically not an error.
@@ -758,11 +759,8 @@ void TLSWrap::ClearOut() {
758759
case SSL_ERROR_SSL:
759760
case SSL_ERROR_SYSCALL:
760761
{
761-
unsigned long ssl_err = ERR_peek_error(); // NOLINT(runtime/int)
762-
763762
Local<Context> context = env()->isolate()->GetCurrentContext();
764763
if (UNLIKELY(context.IsEmpty())) return;
765-
const std::string error_str = GetBIOError();
766764
Local<String> message = OneByteString(
767765
env()->isolate(), error_str.c_str(), error_str.size());
768766
if (UNLIKELY(message.IsEmpty())) return;
@@ -838,7 +836,7 @@ void TLSWrap::ClearIn() {
838836
}
839837

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

10151013
if (written == -1) {
10161014
// If we stopped writing because of an error, it's fatal, discard the data.
1017-
int err = GetSSLError(written);
1015+
int err = SSL_get_error(ssl_.get(), written);
10181016
if (err == SSL_ERROR_SSL || err == SSL_ERROR_SYSCALL) {
10191017
// TODO(@jasnell): What are we doing with the error?
10201018
Debug(this, "Got SSL error (%d), returning UV_EPROTO", err);

src/crypto/crypto_tls.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,6 @@ class TLSWrap : public AsyncWrap,
166166

167167
int SetCACerts(SecureContext* sc);
168168

169-
int GetSSLError(int status) const;
170-
171169
static int SelectSNIContextCallback(SSL* s, int* ad, void* arg);
172170

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

0 commit comments

Comments
 (0)