[nginx] SSL: clarified clean shutdown condition.

Maxim Dounin mdounin at mdounin.ru
Sun Mar 22 13:38:10 UTC 2026


details:   http://freenginx.org/hg/nginx/rev/4c0b66b5046f
branches:  
changeset: 9485:4c0b66b5046f
user:      Maxim Dounin <mdounin at mdounin.ru>
date:      Sun Mar 22 16:26:54 2026 +0300
description:
SSL: clarified clean shutdown condition.

OpenSSL up to 3.0, when a TCP connection is closed by the peer, returns
SSL_ERROR_SYSCALL without an error queue, and with errno set to 0.  Starting
with OpenSSL 3.0 and with SSL_OP_IGNORE_UNEXPECTED_EOF, the
SSL_ERROR_ZERO_RETURN is reported.

Closing the connection without close_notify alert is incorrect, yet quite
common in the real world, and therefore this is handled as a non-error
condition.  Potential truncation attacks are expected to be handled at
the protocol level (notably, truncation attacks are not at all possible
for HTTP/1.x requests, and only possible for HTTP/1.x responses if the
server uses neither Content-Length nor chunked transfer encoding).

Still, previously "ERR_peek_error() == 0" was checked to catch this, which
seems too broad.  This condition also catches TCP-level errors, such as
ECONNRESET, which are better to be explicitly reported as errors.

With this change, only clean TCP close is reported as clean connection
close.  Most notably, connection resets now result in "SSL_read() failed"
errors.

diffstat:

 src/event/ngx_event_openssl.c |  52 +++++++++++++++++++++++++++++++++++++++---
 1 files changed, 48 insertions(+), 4 deletions(-)

diffs (104 lines):

diff --git a/src/event/ngx_event_openssl.c b/src/event/ngx_event_openssl.c
--- a/src/event/ngx_event_openssl.c
+++ b/src/event/ngx_event_openssl.c
@@ -2430,6 +2430,17 @@ ngx_ssl_handshake(ngx_connection_t *c)
         return NGX_AGAIN;
     }
 
+    if (sslerr == SSL_ERROR_SYSCALL && ERR_peek_error() == 0 && err == 0) {
+
+        /*
+         * OpenSSL up to 3.0 returns SSL_ERROR_SYSCALL
+         * without an error queue and with errno set to 0
+         * if connection is closed cleanly
+         */
+
+        sslerr = SSL_ERROR_ZERO_RETURN;
+    }
+
     if (sslerr != SSL_ERROR_SYSCALL) {
         err = 0;
     }
@@ -2438,7 +2449,7 @@ ngx_ssl_handshake(ngx_connection_t *c)
     c->ssl->no_send_shutdown = 1;
     c->read->eof = 1;
 
-    if (sslerr == SSL_ERROR_ZERO_RETURN || ERR_peek_error() == 0) {
+    if (sslerr == SSL_ERROR_ZERO_RETURN) {
         ngx_connection_error(c, err,
                              "peer closed connection in SSL handshake");
 
@@ -2581,6 +2592,17 @@ ngx_ssl_try_early_data(ngx_connection_t 
         return NGX_AGAIN;
     }
 
+    if (sslerr == SSL_ERROR_SYSCALL && ERR_peek_error() == 0 && err == 0) {
+
+        /*
+         * OpenSSL up to 3.0 returns SSL_ERROR_SYSCALL
+         * without an error queue and with errno set to 0
+         * if connection is closed cleanly
+         */
+
+        sslerr = SSL_ERROR_ZERO_RETURN;
+    }
+
     if (sslerr != SSL_ERROR_SYSCALL) {
         err = 0;
     }
@@ -2589,7 +2611,7 @@ ngx_ssl_try_early_data(ngx_connection_t 
     c->ssl->no_send_shutdown = 1;
     c->read->eof = 1;
 
-    if (sslerr == SSL_ERROR_ZERO_RETURN || ERR_peek_error() == 0) {
+    if (sslerr == SSL_ERROR_ZERO_RETURN) {
         ngx_connection_error(c, err,
                              "peer closed connection in SSL handshake");
 
@@ -3101,6 +3123,17 @@ ngx_ssl_handle_recv(ngx_connection_t *c,
         return NGX_AGAIN;
     }
 
+    if (sslerr == SSL_ERROR_SYSCALL && ERR_peek_error() == 0 && err == 0) {
+
+        /*
+         * OpenSSL up to 3.0 returns SSL_ERROR_SYSCALL
+         * without an error queue and with errno set to 0
+         * if connection is closed cleanly
+         */
+
+        sslerr = SSL_ERROR_ZERO_RETURN;
+    }
+
     if (sslerr != SSL_ERROR_SYSCALL) {
         err = 0;
     }
@@ -3108,7 +3141,7 @@ ngx_ssl_handle_recv(ngx_connection_t *c,
     c->ssl->no_wait_shutdown = 1;
     c->ssl->no_send_shutdown = 1;
 
-    if (sslerr == SSL_ERROR_ZERO_RETURN || ERR_peek_error() == 0) {
+    if (sslerr == SSL_ERROR_ZERO_RETURN) {
         ngx_log_debug0(NGX_LOG_DEBUG_EVENT, c->log, 0,
                        "peer shutdown SSL cleanly");
         return NGX_DONE;
@@ -3892,7 +3925,18 @@ ngx_ssl_shutdown(ngx_connection_t *c)
             return NGX_AGAIN;
         }
 
-        if (sslerr == SSL_ERROR_ZERO_RETURN || ERR_peek_error() == 0) {
+        if (sslerr == SSL_ERROR_SYSCALL && ERR_peek_error() == 0 && err == 0) {
+
+            /*
+             * OpenSSL up to 3.0 returns SSL_ERROR_SYSCALL
+             * without an error queue and with errno set to 0
+             * if connection is closed cleanly
+             */
+
+            sslerr = SSL_ERROR_ZERO_RETURN;
+        }
+
+        if (sslerr == SSL_ERROR_ZERO_RETURN) {
             goto done;
         }
 


More information about the nginx-devel mailing list