From d940e09d472a47c8fea9dd4969c3fe9947a1c84f Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Thu, 24 Sep 2026 13:39:40 +0900 Subject: [PATCH] ssl: clear error queue before raising SystemCallError Clear the OpenSSL error queue when an OpenSSL function fails with SSL_ERROR_SYSCALL. ruby/openssl methods are expected not to leave stale entries in the error queue. While this no longer appears to happen with OpenSSL >= 3.0, LibreSSL and older versions of OpenSSL may generate an ERR_LIB_SYS entry in the error queue when the underlying socket fails with an errno. This entry must be cleared before another OpenSSL function is called, otherwise SSL_get_error() for that call may return an incorrect result. Fixes https://github.com/ruby/openssl/pull/1114 --- ext/openssl/ossl_ssl.c | 56 +++++++++++++++++++++------------------- test/openssl/test_ssl.rb | 1 + 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 90d316004..f551032b2 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1874,13 +1874,15 @@ ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname, VALUE opts) io_wait_readable(io); continue; case SSL_ERROR_SYSCALL: + if (saved_errno) { + ossl_clear_error(); #ifdef __APPLE__ - /* See ossl_ssl_write_internal() */ - if (saved_errno == EPROTOTYPE) - continue; + /* See ossl_ssl_write_internal() */ + if (saved_errno == EPROTOTYPE) + continue; #endif - if (saved_errno) rb_exc_raise(rb_syserr_new(saved_errno, funcname)); + } /* fallthrough */ default: { VALUE error_append = Qnil; @@ -2078,20 +2080,20 @@ ossl_ssl_read_internal(int argc, VALUE *argv, VALUE self, int nonblock) io_wait_readable(io); break; case SSL_ERROR_SYSCALL: + if (saved_errno) { + ossl_clear_error(); + rb_exc_raise(rb_syserr_new(saved_errno, "SSL_read")); + } if (!ERR_peek_error()) { - if (saved_errno) - rb_exc_raise(rb_syserr_new(saved_errno, "SSL_read")); - else { - /* - * The underlying BIO returned 0. This is actually a - * protocol error. But unfortunately, not all - * implementations cleanly shutdown the TLS connection - * but just shutdown/close the TCP connection. So report - * EOF for now... - */ - if (no_exception_p(opts)) { return Qnil; } - rb_eof_error(); - } + /* + * The underlying BIO returned 0. This is actually a + * protocol error. But unfortunately, not all + * implementations cleanly shutdown the TLS connection + * but just shutdown/close the TCP connection. So report + * EOF for now... + */ + if (no_exception_p(opts)) { return Qnil; } + rb_eof_error(); } /* fall through */ default: @@ -2188,18 +2190,20 @@ ossl_ssl_write_internal_safe(VALUE _args) io_wait_readable(io); continue; case SSL_ERROR_SYSCALL: + if (saved_errno) { + ossl_clear_error(); #ifdef __APPLE__ - /* - * It appears that send syscall can return EPROTOTYPE if the - * socket is being torn down. Retry to get a proper errno to - * make the error handling in line with the socket library. - * [Bug #14713] https://bugs.ruby-lang.org/issues/14713 - */ - if (saved_errno == EPROTOTYPE) - continue; + /* + * It appears that send syscall can return EPROTOTYPE if the + * socket is being torn down. Retry to get a proper errno to + * make the error handling in line with the socket library. + * [Bug #14713] https://bugs.ruby-lang.org/issues/14713 + */ + if (saved_errno == EPROTOTYPE) + continue; #endif - if (saved_errno) rb_exc_raise(rb_syserr_new(saved_errno, "SSL_write")); + } /* fallthrough */ default: ossl_raise(eSSLError, "SSL_write"); diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index ae3c4502e..90f464141 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -1255,6 +1255,7 @@ def test_connect_systemcallerror assert_raise(Errno::ECONNRESET, Errno::EPIPE) { ssl.connect } + assert_empty(OpenSSL.errors) ensure sock&.close end