From 05b85fc1abdefd81a5561ac7d7aed433b7a01400 Mon Sep 17 00:00:00 2001 From: Sharon Rosner Date: Mon, 19 Jan 2026 09:15:41 +0100 Subject: [PATCH 1/5] [ruby/openssl] Add `sync_close` kwarg to `SSLSocket.new` (fixes https://github.com/ruby/openssl/pull/955) https://github.com/ruby/openssl/commit/8d9a676dfa --- ext/openssl/ossl_ssl.c | 23 +++++++++++++++++++++-- test/openssl/test_ssl.rb | 16 ++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 630d46e43f256e..d622f175854344 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -47,7 +47,7 @@ static ID id_i_cert_store, id_i_ca_file, id_i_ca_path, id_i_verify_mode, id_i_session_remove_cb, id_i_npn_select_cb, id_i_npn_protocols, id_i_alpn_select_cb, id_i_alpn_protocols, id_i_servername_cb, id_i_verify_hostname, id_i_keylog_cb, id_i_tmp_dh_callback; -static ID id_i_io, id_i_context, id_i_hostname; +static ID id_i_io, id_i_context, id_i_hostname, id_i_sync_close; static int ossl_ssl_ex_ptr_idx; static int ossl_sslctx_ex_ptr_idx; @@ -1616,6 +1616,7 @@ peeraddr_ip_str(VALUE self) * call-seq: * SSLSocket.new(io) => aSSLSocket * SSLSocket.new(io, ctx) => aSSLSocket + * SSLSocket.new(io, ctx, sync_close:) => aSSLSocket * * Creates a new SSL socket from _io_ which must be a real IO object (not an * IO-like object that responds to read/write). @@ -1623,6 +1624,10 @@ peeraddr_ip_str(VALUE self) * If _ctx_ is provided the SSL Sockets initial params will be taken from * the context. * + * The optional _sync_close_ keyword parameter sets the _sync_close_ instance + * variable. Setting this to +true+ will cause the underlying socket to be + * closed when the SSL/TLS connection is shut down. + * * The OpenSSL::Buffering module provides additional IO methods. * * This method will freeze the SSLContext if one is provided; @@ -1631,6 +1636,10 @@ peeraddr_ip_str(VALUE self) static VALUE ossl_ssl_initialize(int argc, VALUE *argv, VALUE self) { + static ID kw_ids[1]; + VALUE kw_args[1]; + VALUE opts; + VALUE io, v_ctx; SSL *ssl; SSL_CTX *ctx; @@ -1639,9 +1648,18 @@ ossl_ssl_initialize(int argc, VALUE *argv, VALUE self) if (ssl) ossl_raise(eSSLError, "SSL already initialized"); - if (rb_scan_args(argc, argv, "11", &io, &v_ctx) == 1) + if (rb_scan_args(argc, argv, "11:", &io, &v_ctx, &opts) == 1) v_ctx = rb_funcall(cSSLContext, rb_intern("new"), 0); + if (!kw_ids[0]) { + kw_ids[0] = rb_intern_const("sync_close"); + } + + rb_get_kwargs(opts, kw_ids, 0, 1, kw_args); + if (kw_args[0] != Qundef) { + rb_ivar_set(self, id_i_sync_close, kw_args[0]); + } + GetSSLCTX(v_ctx, ctx); rb_ivar_set(self, id_i_context, v_ctx); ossl_sslctx_setup(v_ctx); @@ -3300,5 +3318,6 @@ Init_ossl_ssl(void) DefIVarID(io); DefIVarID(context); DefIVarID(hostname); + DefIVarID(sync_close); #endif /* !defined(OPENSSL_NO_SOCK) */ } diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index 5d20ccd1f4b2e4..62728065852914 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -355,6 +355,22 @@ def test_sync_close end end + def test_sync_close_initialize_opt + start_server do |port| + begin + sock = TCPSocket.new("127.0.0.1", port) + ssl = OpenSSL::SSL::SSLSocket.new(sock, sync_close: true) + assert_equal true, ssl.sync_close + ssl.connect + ssl.puts "abc"; assert_equal "abc\n", ssl.gets + ssl.close + assert_predicate sock, :closed? + ensure + sock&.close + end + end + end + def test_copy_stream start_server do |port| server_connect(port) do |ssl| From 0379aab6c0f345908e03868ced03936c07b7651a Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Wed, 3 Dec 2025 02:57:18 +0900 Subject: [PATCH 2/5] [ruby/openssl] ssl: fix errno display in exception messages The errno reported in an OpenSSL::SSL::SSLError raised by SSLSocket#accept and #connect sometimes does not match what SSL_accept() or SSL_connect() actually encountered. Depending on the evaluation order of arguments passed to ossl_raise(), errno may be overwritten by peeraddr_ip_str(). While we could just fix peeraddr_ip_str(), we should avoid passing around errno since it is error-prone. Replace rb_sys_fail() and rb_io_{maybe_,}wait_{read,writ}able() with equivalents that do not read errno. https://github.com/ruby/openssl/commit/bfc7df860f --- ext/openssl/ossl_ssl.c | 49 +++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index d622f175854344..f7292520e303fe 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1714,11 +1714,15 @@ ossl_ssl_setup(VALUE self) return Qtrue; } +static int +errno_mapped(void) +{ #ifdef _WIN32 -#define ssl_get_error(ssl, ret) (errno = rb_w32_map_errno(WSAGetLastError()), SSL_get_error((ssl), (ret))) + return rb_w32_map_errno(WSAGetLastError()); #else -#define ssl_get_error(ssl, ret) SSL_get_error((ssl), (ret)) + return errno; #endif +} static void write_would_block(int nonblock) @@ -1759,13 +1763,13 @@ static void io_wait_writable(VALUE io) { #ifdef HAVE_RB_IO_MAYBE_WAIT - if (!rb_io_maybe_wait_writable(errno, io, RUBY_IO_TIMEOUT_DEFAULT)) { + if (!rb_io_wait(io, INT2NUM(RUBY_IO_WRITABLE), RUBY_IO_TIMEOUT_DEFAULT)) { rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become writable!"); } #else rb_io_t *fptr; GetOpenFile(io, fptr); - rb_io_wait_writable(fptr->fd); + rb_thread_fd_writable(fptr->fd); #endif } @@ -1773,13 +1777,13 @@ static void io_wait_readable(VALUE io) { #ifdef HAVE_RB_IO_MAYBE_WAIT - if (!rb_io_maybe_wait_readable(errno, io, RUBY_IO_TIMEOUT_DEFAULT)) { + if (!rb_io_wait(io, INT2NUM(RUBY_IO_READABLE), RUBY_IO_TIMEOUT_DEFAULT)) { rb_raise(IO_TIMEOUT_ERROR, "Timed out while waiting to become readable!"); } #else rb_io_t *fptr; GetOpenFile(io, fptr); - rb_io_wait_readable(fptr->fd); + rb_thread_wait_fd(fptr->fd); #endif } @@ -1787,7 +1791,6 @@ static VALUE ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname, VALUE opts) { SSL *ssl; - int ret, ret2; VALUE cb_state; int nonblock = opts != Qfalse; @@ -1797,7 +1800,8 @@ ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname, VALUE opts) VALUE io = rb_attr_get(self, id_i_io); for (;;) { - ret = func(ssl); + int ret = func(ssl); + int saved_errno = errno_mapped(); cb_state = rb_attr_get(self, ID_callback_state); if (!NIL_P(cb_state)) { @@ -1809,7 +1813,8 @@ ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname, VALUE opts) if (ret > 0) break; - switch ((ret2 = ssl_get_error(ssl, ret))) { + int code = SSL_get_error(ssl, ret); + switch (code) { case SSL_ERROR_WANT_WRITE: if (no_exception_p(opts)) { return sym_wait_writable; } write_would_block(nonblock); @@ -1823,10 +1828,11 @@ ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname, VALUE opts) case SSL_ERROR_SYSCALL: #ifdef __APPLE__ /* See ossl_ssl_write_internal() */ - if (errno == EPROTOTYPE) + if (saved_errno == EPROTOTYPE) continue; #endif - if (errno) rb_sys_fail(funcname); + if (saved_errno) + rb_exc_raise(rb_syserr_new(saved_errno, funcname)); /* fallthrough */ default: { VALUE error_append = Qnil; @@ -1847,9 +1853,9 @@ ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname, VALUE opts) ossl_raise(eSSLError, "%s%s returned=%d errno=%d peeraddr=%"PRIsVALUE" state=%s%"PRIsVALUE, funcname, - ret2 == SSL_ERROR_SYSCALL ? " SYSCALL" : "", - ret2, - errno, + code == SSL_ERROR_SYSCALL ? " SYSCALL" : "", + code, + saved_errno, peeraddr_ip_str(self), SSL_state_string_long(ssl), error_append); @@ -1992,6 +1998,7 @@ ossl_ssl_read_internal(int argc, VALUE *argv, VALUE self, int nonblock) for (;;) { rb_str_locktmp(str); int nread = SSL_read(ssl, RSTRING_PTR(str), ilen); + int saved_errno = errno_mapped(); rb_str_unlocktmp(str); cb_state = rb_attr_get(self, ID_callback_state); @@ -2001,7 +2008,7 @@ ossl_ssl_read_internal(int argc, VALUE *argv, VALUE self, int nonblock) rb_jump_tag(NUM2INT(cb_state)); } - switch (ssl_get_error(ssl, nread)) { + switch (SSL_get_error(ssl, nread)) { case SSL_ERROR_NONE: rb_str_set_len(str, nread); return str; @@ -2024,8 +2031,8 @@ ossl_ssl_read_internal(int argc, VALUE *argv, VALUE self, int nonblock) break; case SSL_ERROR_SYSCALL: if (!ERR_peek_error()) { - if (errno) - rb_sys_fail(0); + if (saved_errno) + rb_exc_raise(rb_syserr_new(saved_errno, "SSL_read")); else { /* * The underlying BIO returned 0. This is actually a @@ -2110,6 +2117,7 @@ ossl_ssl_write_internal_safe(VALUE _args) for (;;) { int nwritten = SSL_write(ssl, RSTRING_PTR(str), num); + int saved_errno = errno_mapped(); cb_state = rb_attr_get(self, ID_callback_state); if (!NIL_P(cb_state)) { @@ -2118,7 +2126,7 @@ ossl_ssl_write_internal_safe(VALUE _args) rb_jump_tag(NUM2INT(cb_state)); } - switch (ssl_get_error(ssl, nwritten)) { + switch (SSL_get_error(ssl, nwritten)) { case SSL_ERROR_NONE: return INT2NUM(nwritten); case SSL_ERROR_WANT_WRITE: @@ -2139,10 +2147,11 @@ ossl_ssl_write_internal_safe(VALUE _args) * make the error handling in line with the socket library. * [Bug #14713] https://bugs.ruby-lang.org/issues/14713 */ - if (errno == EPROTOTYPE) + if (saved_errno == EPROTOTYPE) continue; #endif - if (errno) rb_sys_fail(0); + if (saved_errno) + rb_exc_raise(rb_syserr_new(saved_errno, "SSL_write")); /* fallthrough */ default: ossl_raise(eSSLError, "SSL_write"); From 0fddb9afcac8d68da07a24a2db3763e237f59a22 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Mon, 8 Dec 2025 00:55:15 +0900 Subject: [PATCH 3/5] [ruby/openssl] ssl: refactor peeraddr_ip_str() Remove an unnecessary instance variable lookup and constant lookup. Ruby's public headers provide rb_eSystemCallError. https://github.com/ruby/openssl/commit/46c92233fb --- ext/openssl/ossl_ssl.c | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index f7292520e303fe..c6dec32a9e5e82 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1590,26 +1590,24 @@ ossl_ssl_s_alloc(VALUE klass) } static VALUE -peer_ip_address(VALUE self) +peer_ip_address(VALUE io) { - VALUE remote_address = rb_funcall(rb_attr_get(self, id_i_io), rb_intern("remote_address"), 0); + VALUE remote_address = rb_funcall(io, rb_intern("remote_address"), 0); return rb_funcall(remote_address, rb_intern("inspect_sockaddr"), 0); } static VALUE -fallback_peer_ip_address(VALUE self, VALUE args) +fallback_peer_ip_address(VALUE self, VALUE exc) { return rb_str_new_cstr("(null)"); } static VALUE -peeraddr_ip_str(VALUE self) +peeraddr_ip_str(VALUE io) { - VALUE rb_mErrno = rb_const_get(rb_cObject, rb_intern("Errno")); - VALUE rb_eSystemCallError = rb_const_get(rb_mErrno, rb_intern("SystemCallError")); - - return rb_rescue2(peer_ip_address, self, fallback_peer_ip_address, (VALUE)0, rb_eSystemCallError, NULL); + return rb_rescue2(peer_ip_address, io, fallback_peer_ip_address, Qnil, + rb_eSystemCallError, (VALUE)0); } /* @@ -1856,7 +1854,7 @@ ossl_start_ssl(VALUE self, int (*func)(SSL *), const char *funcname, VALUE opts) code == SSL_ERROR_SYSCALL ? " SYSCALL" : "", code, saved_errno, - peeraddr_ip_str(self), + peeraddr_ip_str(io), SSL_state_string_long(ssl), error_append); } From 48848e8da43a1caa6169db68b1a826a185d2549c Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Tue, 4 Mar 2025 01:56:07 +0900 Subject: [PATCH 4/5] [ruby/openssl] ssl: update tests for SSLContext#servername_cb callback If an exception is raised by the SSLContext#servername_cb proc, the handshake should be canceled by sending an "unrecognized_name" alert to the client, and the exception should be re-raised from SSLSocket#accept. Add more direct assertions to confirm these behaviors. https://github.com/ruby/openssl/commit/ac8df7f30f --- test/openssl/test_ssl.rb | 50 ++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index 62728065852914..ce1b2c1e966fd0 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -1080,36 +1080,46 @@ def test_tlsext_hostname end end - def test_servername_cb_raises_an_exception_on_unknown_objects - hostname = 'example.org' - - ctx2 = OpenSSL::SSL::SSLContext.new - ctx2.cert = @svr_cert - ctx2.key = @svr_key - ctx2.servername_cb = lambda { |args| Object.new } - + def test_servername_cb_exception sock1, sock2 = socketpair + t = Thread.new { + s1 = OpenSSL::SSL::SSLSocket.new(sock1) + s1.hostname = "localhost" + assert_raise_with_message(OpenSSL::SSL::SSLError, /unrecognized.name/i) { + s1.connect + } + } + + ctx2 = OpenSSL::SSL::SSLContext.new + ctx2.servername_cb = lambda { |args| raise RuntimeError, "foo" } s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) + assert_raise_with_message(RuntimeError, "foo") { s2.accept } + assert t.join + ensure + sock1.close + sock2.close + t.kill.join + end - ctx1 = OpenSSL::SSL::SSLContext.new + def test_servername_cb_raises_an_exception_on_unknown_objects + sock1, sock2 = socketpair - s1 = OpenSSL::SSL::SSLSocket.new(sock1, ctx1) - s1.hostname = hostname t = Thread.new { - assert_raise(OpenSSL::SSL::SSLError) do - s1.connect - end + s1 = OpenSSL::SSL::SSLSocket.new(sock1) + s1.hostname = "localhost" + assert_raise(OpenSSL::SSL::SSLError) { s1.connect } } - assert_raise(ArgumentError) do - s2.accept - end - + ctx2 = OpenSSL::SSL::SSLContext.new + ctx2.servername_cb = lambda { |args| Object.new } + s2 = OpenSSL::SSL::SSLSocket.new(sock2, ctx2) + assert_raise(ArgumentError) { s2.accept } assert t.join ensure - sock1.close if sock1 - sock2.close if sock2 + sock1.close + sock2.close + t.kill.join end def test_accept_errors_include_peeraddr From e7e930323660c6b0b277a998e178f18b074c3d5c Mon Sep 17 00:00:00 2001 From: Luke Gruber Date: Fri, 23 Jan 2026 11:54:44 -0500 Subject: [PATCH 5/5] Fix kqueue timeout for 0-valued timespec (#15940) Timeout with 0-valued timespec means try to get an event, but return immediately if there is none. Apparently timespec can have other members, so best to 0 it out in that case. --- thread_pthread_mn.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/thread_pthread_mn.c b/thread_pthread_mn.c index 5c21f212e4ab37..72a5d8fce2fe09 100644 --- a/thread_pthread_mn.c +++ b/thread_pthread_mn.c @@ -617,11 +617,17 @@ kqueue_wait(rb_vm_t *vm) struct timespec *timeout = NULL; int timeout_ms = timer_thread_set_timeout(vm); - if (timeout_ms >= 0) { + if (timeout_ms > 0) { calculated_timeout.tv_sec = timeout_ms / 1000; calculated_timeout.tv_nsec = (timeout_ms % 1000) * 1000000; timeout = &calculated_timeout; } + else if (timeout_ms == 0) { + // Relying on the absence of other members of struct timespec is not strictly portable, + // and kevent needs a 0-valued timespec to mean immediate timeout. + memset(&calculated_timeout, 0, sizeof(struct timespec)); + timeout = &calculated_timeout; + } return kevent(timer_th.event_fd, NULL, 0, timer_th.finished_events, KQUEUE_EVENTS_MAX, timeout); }