From b8f8d646a64f883652b44780c4174a85f98d1c82 Mon Sep 17 00:00:00 2001 From: Earlopain <14981592+Earlopain@users.noreply.github.com> Date: Wed, 8 Oct 2025 14:54:14 +0200 Subject: [PATCH 01/11] [ruby/prism] For these special cases, there exists no optional argument type. Since a endless method is started with `=`, there was ambiguity here. We have to simply reject these in all cases. This adds a new error for the following reason: * `def foo arg = nil` is interpreted as a normal method call with optional `arg` without matching `end` * `def foo *arg = nil; end` is interpreted as a endless method call that has body `nil` with extraneous `end` `def foo *arg = nil` is somewhere inbetween and I don't know how to otherwise indicate the error. Now the second case above also shows the newly added error message. Fixes [Bug #21623] https://github.com/ruby/prism/commit/e1910d4492 --- prism/config.yml | 1 + prism/prism.c | 32 +++++++++++++++++++ prism/templates/src/diagnostic.c.erb | 1 + test/prism/errors/def_with_optional_splat.txt | 6 ++++ ...endless_method_command_call_parameters.txt | 24 ++++++++++++++ 5 files changed, 64 insertions(+) create mode 100644 test/prism/errors/def_with_optional_splat.txt create mode 100644 test/prism/errors/endless_method_command_call_parameters.txt diff --git a/prism/config.yml b/prism/config.yml index a8a8e70aace78e..ed5c9d8b9cde98 100644 --- a/prism/config.yml +++ b/prism/config.yml @@ -60,6 +60,7 @@ errors: - CONDITIONAL_WHILE_PREDICATE - CONSTANT_PATH_COLON_COLON_CONSTANT - DEF_ENDLESS + - DEF_ENDLESS_PARAMETERS - DEF_ENDLESS_SETTER - DEF_NAME - DEF_PARAMS_TERM diff --git a/prism/prism.c b/prism/prism.c index f85a69332ed95c..cc1896ee3486ed 100644 --- a/prism/prism.c +++ b/prism/prism.c @@ -14612,6 +14612,18 @@ update_parameter_state(pm_parser_t *parser, pm_token_t *token, pm_parameters_ord return true; } +/** + * Ensures that after parsing a parameter, the next token is not `=`. + * Some parameters like `def(* = 1)` cannot become optional. When no parens + * are present like in `def * = 1`, this creates ambiguity with endless method definitions. + */ +static inline void +refute_optional_parameter(pm_parser_t *parser) { + if (match1(parser, PM_TOKEN_EQUAL)) { + pm_parser_err_previous(parser, PM_ERR_DEF_ENDLESS_PARAMETERS); + } +} + /** * Parse a list of parameters on a method definition. */ @@ -14664,6 +14676,10 @@ parse_parameters( parser->current_scope->parameters |= PM_SCOPE_PARAMETERS_FORWARDING_BLOCK; } + if (!uses_parentheses) { + refute_optional_parameter(parser); + } + pm_block_parameter_node_t *param = pm_block_parameter_node_create(parser, &name, &operator); if (repeated) { pm_node_flag_set_repeated_parameter((pm_node_t *)param); @@ -14685,6 +14701,10 @@ parse_parameters( bool succeeded = update_parameter_state(parser, &parser->current, &order); parser_lex(parser); + if (!uses_parentheses) { + refute_optional_parameter(parser); + } + parser->current_scope->parameters |= PM_SCOPE_PARAMETERS_FORWARDING_ALL; pm_forwarding_parameter_node_t *param = pm_forwarding_parameter_node_create(parser, &parser->previous); @@ -14866,6 +14886,10 @@ parse_parameters( context_pop(parser); pm_parameters_node_keywords_append(params, param); + if (!uses_parentheses) { + refute_optional_parameter(parser); + } + // If parsing the value of the parameter resulted in error recovery, // then we can put a missing node in its place and stop parsing the // parameters entirely now. @@ -14897,6 +14921,10 @@ parse_parameters( parser->current_scope->parameters |= PM_SCOPE_PARAMETERS_FORWARDING_POSITIONALS; } + if (!uses_parentheses) { + refute_optional_parameter(parser); + } + pm_node_t *param = (pm_node_t *) pm_rest_parameter_node_create(parser, &operator, &name); if (repeated) { pm_node_flag_set_repeated_parameter(param); @@ -14945,6 +14973,10 @@ parse_parameters( } } + if (!uses_parentheses) { + refute_optional_parameter(parser); + } + if (params->keyword_rest == NULL) { pm_parameters_node_keyword_rest_set(params, param); } else { diff --git a/prism/templates/src/diagnostic.c.erb b/prism/templates/src/diagnostic.c.erb index 9a30a57e3b0eca..23732530854e50 100644 --- a/prism/templates/src/diagnostic.c.erb +++ b/prism/templates/src/diagnostic.c.erb @@ -144,6 +144,7 @@ static const pm_diagnostic_data_t diagnostic_messages[PM_DIAGNOSTIC_ID_MAX] = { [PM_ERR_CONDITIONAL_WHILE_PREDICATE] = { "expected a predicate expression for the `while` statement", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_CONSTANT_PATH_COLON_COLON_CONSTANT] = { "expected a constant after the `::` operator", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_DEF_ENDLESS] = { "could not parse the endless method body", PM_ERROR_LEVEL_SYNTAX }, + [PM_ERR_DEF_ENDLESS_PARAMETERS] = { "could not parse the endless method parameters", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_DEF_ENDLESS_SETTER] = { "invalid method name; a setter method cannot be defined in an endless method definition", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_DEF_NAME] = { "unexpected %s; expected a method name", PM_ERROR_LEVEL_SYNTAX }, [PM_ERR_DEF_PARAMS_TERM] = { "expected a delimiter to close the parameters", PM_ERROR_LEVEL_SYNTAX }, diff --git a/test/prism/errors/def_with_optional_splat.txt b/test/prism/errors/def_with_optional_splat.txt new file mode 100644 index 00000000000000..74a833ceec0dac --- /dev/null +++ b/test/prism/errors/def_with_optional_splat.txt @@ -0,0 +1,6 @@ +def foo(*bar = nil); end + ^ unexpected '='; expected a `)` to close the parameters + ^ unexpected ')', expecting end-of-input + ^ unexpected ')', ignoring it + ^~~ unexpected 'end', ignoring it + diff --git a/test/prism/errors/endless_method_command_call_parameters.txt b/test/prism/errors/endless_method_command_call_parameters.txt new file mode 100644 index 00000000000000..94c4f88fc835df --- /dev/null +++ b/test/prism/errors/endless_method_command_call_parameters.txt @@ -0,0 +1,24 @@ +def f x: = 1 + ^~ could not parse the endless method parameters + +def f ... = 1 + ^~~ could not parse the endless method parameters + +def f * = 1 + ^ could not parse the endless method parameters + +def f ** = 1 + ^~ could not parse the endless method parameters + +def f & = 1 + ^ could not parse the endless method parameters + +def f *a = 1 + ^ could not parse the endless method parameters + +def f **a = 1 + ^ could not parse the endless method parameters + +def f &a = 1 + ^ could not parse the endless method parameters + From 810b3a405bf7431c852778580d44c1421edfcad9 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Fri, 1 Aug 2025 18:39:41 +0900 Subject: [PATCH 02/11] [ruby/openssl] provider: load "default" provider in test_openssl_legacy_provider Update the test case to explicitly load both the "default" and the "legacy" providers. Currently, the "default" provider as a side effect by the OpenSSL::PKey::DH.new call in lib/openssl/ssl.rb. It will be cleaned up in a following patch. https://github.com/ruby/openssl/commit/013db02fb2 --- test/openssl/test_provider.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/test/openssl/test_provider.rb b/test/openssl/test_provider.rb index 6f85c00c988204..10081e208ced1b 100644 --- a/test/openssl/test_provider.rb +++ b/test/openssl/test_provider.rb @@ -46,6 +46,7 @@ def test_openssl_legacy_provider with_openssl(<<-'end;') begin + OpenSSL::Provider.load("default") OpenSSL::Provider.load("legacy") rescue OpenSSL::Provider::ProviderError omit "Only for OpenSSL with legacy provider" From 8dfe5403415fc1bd0c6ce56e5edd8749d081e33d Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Sun, 20 Apr 2025 19:24:27 +0900 Subject: [PATCH 03/11] [ruby/openssl] ssl: fix extconf.rb check for SSL_CTX_set0_tmp_dh_pkey() Check for the function we actually use. Both SSL_set0_tmp_dh_pkey() and SSL_CTX_set0_tmp_dh_pkey() were added in OpenSSL 3.0. https://github.com/ruby/openssl/commit/a9b6a64e5f --- ext/openssl/extconf.rb | 2 +- ext/openssl/ossl_ssl.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/openssl/extconf.rb b/ext/openssl/extconf.rb index 8aac52ef47e546..6c178c12f2b4f4 100644 --- a/ext/openssl/extconf.rb +++ b/ext/openssl/extconf.rb @@ -147,7 +147,7 @@ def find_openssl_library have_func("EVP_PKEY_check(NULL)", evp_h) # added in 3.0.0 -have_func("SSL_set0_tmp_dh_pkey(NULL, NULL)", ssl_h) +have_func("SSL_CTX_set0_tmp_dh_pkey(NULL, NULL)", ssl_h) have_func("ERR_get_error_all(NULL, NULL, NULL, NULL, NULL)", "openssl/err.h") have_func("SSL_CTX_load_verify_file(NULL, \"\")", ssl_h) have_func("BN_check_prime(NULL, NULL, NULL)", "openssl/bn.h") diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 29564a8139ac2e..9e34bd2520e535 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -1148,7 +1148,7 @@ ossl_sslctx_set_client_sigalgs(VALUE self, VALUE v) * contained in the key object, if any, are ignored. The server will always * generate a new key pair for each handshake. * - * Added in version 3.0. See also the man page SSL_set0_tmp_dh_pkey(3). + * Added in version 3.0. See also the man page SSL_CTX_set0_tmp_dh_pkey(3). * * Example: * ctx = OpenSSL::SSL::SSLContext.new @@ -1169,7 +1169,7 @@ ossl_sslctx_set_tmp_dh(VALUE self, VALUE arg) if (EVP_PKEY_base_id(pkey) != EVP_PKEY_DH) rb_raise(eSSLError, "invalid pkey type %s (expected DH)", OBJ_nid2sn(EVP_PKEY_base_id(pkey))); -#ifdef HAVE_SSL_SET0_TMP_DH_PKEY +#ifdef HAVE_SSL_CTX_SET0_TMP_DH_PKEY if (!SSL_CTX_set0_tmp_dh_pkey(ctx, pkey)) ossl_raise(eSSLError, "SSL_CTX_set0_tmp_dh_pkey"); EVP_PKEY_up_ref(pkey); From ea79fe225cc28960595b53cf20e698ec5bbddb0e Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Sun, 20 Apr 2025 20:26:00 +0900 Subject: [PATCH 04/11] [ruby/openssl] ssl: use SSL_CTX_set_dh_auto() by default Rely on OpenSSL's builtin DH parameters for TLS 1.2 and earlier instead of providing a default SSLContext#tmp_dh_callback proc. SSL_CTX_set_dh_auto() has been available since OpenSSL 1.1.0. The parameters can still be overridden by specifying SSLContext#tmp_dh_callback or #tmp_dh, as confirmed by existing tests. SSLContext#tmp_dh_callback depends on a deprecated OpenSSL feature. We also prefer not to hard-code parameters, which is a maintenance burden. This change also improves Ractor compatibility by removing the unshareable proc. https://github.com/ruby/openssl/commit/9cfec9bf5e --- ext/openssl/lib/openssl/ssl.rb | 21 +-------------------- ext/openssl/ossl_ssl.c | 12 ++++++++++-- test/openssl/test_ssl.rb | 6 +----- 3 files changed, 12 insertions(+), 27 deletions(-) diff --git a/ext/openssl/lib/openssl/ssl.rb b/ext/openssl/lib/openssl/ssl.rb index 46509c333e0857..59e379635d15e0 100644 --- a/ext/openssl/lib/openssl/ssl.rb +++ b/ext/openssl/lib/openssl/ssl.rb @@ -32,25 +32,6 @@ class SSLContext }.call } - if defined?(OpenSSL::PKey::DH) - DH_ffdhe2048 = OpenSSL::PKey::DH.new <<-_end_of_pem_ ------BEGIN DH PARAMETERS----- -MIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz -+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a -87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7 -YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi -7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD -ssbzSibBsu/6iGtCOGEoXJf//////////wIBAg== ------END DH PARAMETERS----- - _end_of_pem_ - private_constant :DH_ffdhe2048 - - DEFAULT_TMP_DH_CALLBACK = lambda { |ctx, is_export, keylen| # :nodoc: - warn "using default DH parameters." if $VERBOSE - DH_ffdhe2048 - } - end - if !OpenSSL::OPENSSL_VERSION.start_with?("OpenSSL") DEFAULT_PARAMS.merge!( min_version: OpenSSL::SSL::TLS1_VERSION, @@ -457,7 +438,7 @@ def client_cert_cb end def tmp_dh_callback - @context.tmp_dh_callback || OpenSSL::SSL::SSLContext::DEFAULT_TMP_DH_CALLBACK + @context.tmp_dh_callback end def session_new_cb diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index 9e34bd2520e535..c7b9cb74a9dcae 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_id_context, id_i_session_get_cb, id_i_session_new_cb, 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_verify_hostname, id_i_keylog_cb, id_i_tmp_dh_callback; static ID id_i_io, id_i_context, id_i_hostname; static int ossl_ssl_ex_ptr_idx; @@ -90,6 +90,7 @@ ossl_sslctx_s_alloc(VALUE klass) ossl_raise(eSSLError, "SSL_CTX_new"); } SSL_CTX_set_mode(ctx, mode); + SSL_CTX_set_dh_auto(ctx, 1); RTYPEDDATA_DATA(obj) = ctx; SSL_CTX_set_ex_data(ctx, ossl_sslctx_ex_ptr_idx, (void *)obj); @@ -703,7 +704,10 @@ ossl_sslctx_setup(VALUE self) GetSSLCTX(self, ctx); #if !defined(OPENSSL_NO_DH) - SSL_CTX_set_tmp_dh_callback(ctx, ossl_tmp_dh_callback); + if (!NIL_P(rb_attr_get(self, id_i_tmp_dh_callback))) { + SSL_CTX_set_tmp_dh_callback(ctx, ossl_tmp_dh_callback); + SSL_CTX_set_dh_auto(ctx, 0); + } #endif #if !defined(OPENSSL_IS_AWSLC) /* AWS-LC has no support for TLS 1.3 PHA. */ @@ -1178,6 +1182,9 @@ ossl_sslctx_set_tmp_dh(VALUE self, VALUE arg) ossl_raise(eSSLError, "SSL_CTX_set_tmp_dh"); #endif + // Turn off the "auto" DH parameters set by ossl_sslctx_s_alloc() + SSL_CTX_set_dh_auto(ctx, 0); + return arg; } #endif @@ -3289,6 +3296,7 @@ Init_ossl_ssl(void) DefIVarID(servername_cb); DefIVarID(verify_hostname); DefIVarID(keylog_cb); + DefIVarID(tmp_dh_callback); DefIVarID(io); DefIVarID(context); diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index 7abe2c6df5d330..d843b00f639319 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -2129,11 +2129,7 @@ def test_connect_works_when_setting_dh_callback_to_nil ctx.tmp_dh_callback = nil } start_server(ctx_proc: ctx_proc) do |port| - EnvUtil.suppress_warning { # uses default callback - assert_nothing_raised { - server_connect(port) { } - } - } + assert_nothing_raised { server_connect(port) { } } end end From e4f12808318d743642e6c0a579b35df2eededd3c Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Sun, 20 Apr 2025 22:28:04 +0900 Subject: [PATCH 05/11] [ruby/openssl] ssl: refactor tmp_dh_callback handling tmp_dh_callback no longer has a default value. It also no longer has to share code with tmp_ecdh_callback, which has been removed in v3.0.0. https://github.com/ruby/openssl/commit/b7cde6df2a --- ext/openssl/lib/openssl/ssl.rb | 17 --------- ext/openssl/ossl_ssl.c | 69 +++++++++++++++++----------------- 2 files changed, 35 insertions(+), 51 deletions(-) diff --git a/ext/openssl/lib/openssl/ssl.rb b/ext/openssl/lib/openssl/ssl.rb index 59e379635d15e0..61573f7d476b7d 100644 --- a/ext/openssl/lib/openssl/ssl.rb +++ b/ext/openssl/lib/openssl/ssl.rb @@ -73,19 +73,6 @@ class SSLContext DEFAULT_CERT_STORE = OpenSSL::X509::Store.new # :nodoc: DEFAULT_CERT_STORE.set_default_paths - # A callback invoked when DH parameters are required for ephemeral DH key - # exchange. - # - # The callback is invoked with the SSLSocket, a - # flag indicating the use of an export cipher and the keylength - # required. - # - # The callback must return an OpenSSL::PKey::DH instance of the correct - # key length. - # - # Deprecated in version 3.0. Use #tmp_dh= instead. - attr_accessor :tmp_dh_callback - # A callback invoked at connect time to distinguish between multiple # server names. # @@ -437,10 +424,6 @@ def client_cert_cb @context.client_cert_cb end - def tmp_dh_callback - @context.tmp_dh_callback - end - def session_new_cb @context.session_new_cb end diff --git a/ext/openssl/ossl_ssl.c b/ext/openssl/ossl_ssl.c index c7b9cb74a9dcae..583396ebfc6f3b 100644 --- a/ext/openssl/ossl_ssl.c +++ b/ext/openssl/ossl_ssl.c @@ -36,8 +36,7 @@ VALUE cSSLSocket; static VALUE eSSLErrorWaitReadable; static VALUE eSSLErrorWaitWritable; -static ID id_call, ID_callback_state, id_tmp_dh_callback, - id_npn_protocols_encoded, id_each; +static ID id_call, ID_callback_state, id_npn_protocols_encoded, id_each; static VALUE sym_exception, sym_wait_readable, sym_wait_writable; static ID id_i_cert_store, id_i_ca_file, id_i_ca_path, id_i_verify_mode, @@ -134,8 +133,6 @@ ossl_client_cert_cb(SSL *ssl, X509 **x509, EVP_PKEY **pkey) #if !defined(OPENSSL_NO_DH) struct tmp_dh_callback_args { VALUE ssl_obj; - ID id; - int type; int is_export; int keylength; }; @@ -144,48 +141,36 @@ static VALUE ossl_call_tmp_dh_callback(VALUE arg) { struct tmp_dh_callback_args *args = (struct tmp_dh_callback_args *)arg; - VALUE cb, dh; - EVP_PKEY *pkey; + VALUE ctx_obj, cb, obj; + const DH *dh; - cb = rb_funcall(args->ssl_obj, args->id, 0); + ctx_obj = rb_attr_get(args->ssl_obj, id_i_context); + cb = rb_attr_get(ctx_obj, id_i_tmp_dh_callback); if (NIL_P(cb)) - return (VALUE)NULL; - dh = rb_funcall(cb, id_call, 3, args->ssl_obj, INT2NUM(args->is_export), - INT2NUM(args->keylength)); - pkey = GetPKeyPtr(dh); - if (EVP_PKEY_base_id(pkey) != args->type) - return (VALUE)NULL; + return (VALUE)NULL; + + obj = rb_funcall(cb, id_call, 3, args->ssl_obj, INT2NUM(args->is_export), + INT2NUM(args->keylength)); + // TODO: We should riase if obj is not DH + dh = EVP_PKEY_get0_DH(GetPKeyPtr(obj)); + if (!dh) + ossl_clear_error(); - return (VALUE)pkey; + return (VALUE)dh; } -#endif -#if !defined(OPENSSL_NO_DH) static DH * ossl_tmp_dh_callback(SSL *ssl, int is_export, int keylength) { - VALUE rb_ssl; - EVP_PKEY *pkey; - struct tmp_dh_callback_args args; int state; - - rb_ssl = (VALUE)SSL_get_ex_data(ssl, ossl_ssl_ex_ptr_idx); - args.ssl_obj = rb_ssl; - args.id = id_tmp_dh_callback; - args.is_export = is_export; - args.keylength = keylength; - args.type = EVP_PKEY_DH; - - pkey = (EVP_PKEY *)rb_protect(ossl_call_tmp_dh_callback, - (VALUE)&args, &state); + VALUE rb_ssl = (VALUE)SSL_get_ex_data(ssl, ossl_ssl_ex_ptr_idx); + struct tmp_dh_callback_args args = {rb_ssl, is_export, keylength}; + VALUE ret = rb_protect(ossl_call_tmp_dh_callback, (VALUE)&args, &state); if (state) { rb_ivar_set(rb_ssl, ID_callback_state, INT2NUM(state)); return NULL; } - if (!pkey) - return NULL; - - return (DH *)EVP_PKEY_get0_DH(pkey); + return (DH *)ret; } #endif /* OPENSSL_NO_DH */ @@ -2872,6 +2857,23 @@ Init_ossl_ssl(void) */ rb_attr(cSSLContext, rb_intern_const("client_cert_cb"), 1, 1, Qfalse); +#ifndef OPENSSL_NO_DH + /* + * A callback invoked when DH parameters are required for ephemeral DH key + * exchange. + * + * The callback is invoked with the SSLSocket, a + * flag indicating the use of an export cipher and the keylength + * required. + * + * The callback must return an OpenSSL::PKey::DH instance of the correct + * key length. + * + * Deprecated in version 3.0. Use #tmp_dh= instead. + */ + rb_attr(cSSLContext, rb_intern_const("tmp_dh_callback"), 1, 1, Qfalse); +#endif + /* * Sets the context in which a session can be reused. This allows * sessions for multiple applications to be distinguished, for example, by @@ -3265,7 +3267,6 @@ Init_ossl_ssl(void) sym_wait_readable = ID2SYM(rb_intern_const("wait_readable")); sym_wait_writable = ID2SYM(rb_intern_const("wait_writable")); - id_tmp_dh_callback = rb_intern_const("tmp_dh_callback"); id_npn_protocols_encoded = rb_intern_const("npn_protocols_encoded"); id_each = rb_intern_const("each"); From a8b34d9a9beb5c8edb59acf045968795c12d87b8 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Sat, 2 Aug 2025 00:48:38 +0900 Subject: [PATCH 06/11] [ruby/openssl] ssl: allow SSLContext#set_params to be used from non-main Ractors Freeze OpenSSL::SSL::SSLContext::DEFAULT_PARAMS so that it becomes Ractor-shareable. Also, prepare a new OpenSSL::X509::Store in Ractor-local storage, if called from a non-main Ractor. OpenSSL::X509::Store currently is not a shareable object. https://github.com/ruby/openssl/commit/3d5271327c --- ext/openssl/lib/openssl/ssl.rb | 12 ++++++++-- test/openssl/test_ssl.rb | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/ext/openssl/lib/openssl/ssl.rb b/ext/openssl/lib/openssl/ssl.rb index 61573f7d476b7d..3268c126b9efad 100644 --- a/ext/openssl/lib/openssl/ssl.rb +++ b/ext/openssl/lib/openssl/ssl.rb @@ -66,9 +66,10 @@ class SSLContext AES256-SHA256 AES128-SHA AES256-SHA - }.join(":"), + }.join(":").freeze, ) end + DEFAULT_PARAMS.freeze DEFAULT_CERT_STORE = OpenSSL::X509::Store.new # :nodoc: DEFAULT_CERT_STORE.set_default_paths @@ -114,7 +115,14 @@ def set_params(params={}) params.each{|name, value| self.__send__("#{name}=", value) } if self.verify_mode != OpenSSL::SSL::VERIFY_NONE unless self.ca_file or self.ca_path or self.cert_store - self.cert_store = DEFAULT_CERT_STORE + if not defined?(Ractor) or Ractor.current == Ractor.main + self.cert_store = DEFAULT_CERT_STORE + else + self.cert_store = Ractor.current[:__openssl_default_store__] ||= + OpenSSL::X509::Store.new.tap { |store| + store.set_default_paths + } + end end end return params diff --git a/test/openssl/test_ssl.rb b/test/openssl/test_ssl.rb index d843b00f639319..3ec1a710436f38 100644 --- a/test/openssl/test_ssl.rb +++ b/test/openssl/test_ssl.rb @@ -2317,6 +2317,50 @@ def test_export_keying_material end end + # OpenSSL::Buffering requires $/ accessible from non-main Ractors (Ruby 3.5) + # https://bugs.ruby-lang.org/issues/21109 + # + # Hangs on Windows + # https://bugs.ruby-lang.org/issues/21537 + if respond_to?(:ractor) && RUBY_VERSION >= "3.5" && RUBY_PLATFORM !~ /mswin|mingw/ + ractor + def test_ractor_client + start_server { |port| + s = Ractor.new(port, @ca_cert) { |port, ca_cert| + sock = TCPSocket.new("127.0.0.1", port) + ctx = OpenSSL::SSL::SSLContext.new + ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER + ctx.cert_store = OpenSSL::X509::Store.new.tap { |store| + store.add_cert(ca_cert) + } + begin + ssl = OpenSSL::SSL::SSLSocket.new(sock, ctx) + ssl.connect + ssl.puts("abc") + ssl.gets + ensure + ssl.close + sock.close + end + }.value + assert_equal("abc\n", s) + } + end + + ractor + def test_ractor_set_params + # We cannot actually test default stores in the test suite as it depends + # on the environment, but at least check that it does not raise an + # exception + ok = Ractor.new { + ctx = OpenSSL::SSL::SSLContext.new + ctx.set_params + ctx.cert_store.kind_of?(OpenSSL::X509::Store) + }.value + assert(ok, "ctx.cert_store is an instance of OpenSSL::X509::Store") + end + end + private def server_connect(port, ctx = nil) From a0c6efdea46584b816afc47cbbe938668ed14476 Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Wed, 8 Oct 2025 23:28:20 +0900 Subject: [PATCH 07/11] [ruby/zlib] Load the same loaded zlib Zlib is used by also rubygems, the built extension cannot be loaded after the default gem is loaded. https://github.com/ruby/zlib/commit/02a29dc7ea --- test/zlib/test_zlib.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/zlib/test_zlib.rb b/test/zlib/test_zlib.rb index 5a8463ad8e719e..bedf243b3e5919 100644 --- a/test/zlib/test_zlib.rb +++ b/test/zlib/test_zlib.rb @@ -9,6 +9,9 @@ begin require 'zlib' rescue LoadError +else + z = "/zlib.#{RbConfig::CONFIG["DLEXT"]}" + LOADED_ZLIB, = $".select {|f| f.end_with?(z)} end if defined? Zlib @@ -1525,7 +1528,7 @@ def test_gunzip_encoding end def test_gunzip_no_memory_leak - assert_no_memory_leak(%[-rzlib], "#{<<~"{#"}", "#{<<~'};'}") + assert_no_memory_leak(%W[-r#{LOADED_ZLIB}], "#{<<~"{#"}", "#{<<~'};'}") d = Zlib.gzip("data") {# 10_000.times {Zlib.gunzip(d)} From 5e7e6040938a37d1c320d69d2266f1b39e1b9f2a Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Wed, 8 Oct 2025 17:59:51 +0200 Subject: [PATCH 08/11] Update to ruby/mspec@6a7b509 --- spec/mspec/lib/mspec/commands/mspec-ci.rb | 1 + spec/mspec/spec/commands/mspec_ci_spec.rb | 5 +++++ spec/mspec/spec/commands/mspec_run_spec.rb | 5 +++++ spec/mspec/tool/sync/sync-rubyspec.rb | 13 ++++++++++--- 4 files changed, 21 insertions(+), 3 deletions(-) diff --git a/spec/mspec/lib/mspec/commands/mspec-ci.rb b/spec/mspec/lib/mspec/commands/mspec-ci.rb index 9959059ca1c7f2..8951572f693685 100644 --- a/spec/mspec/lib/mspec/commands/mspec-ci.rb +++ b/spec/mspec/lib/mspec/commands/mspec-ci.rb @@ -18,6 +18,7 @@ def options(argv = ARGV) options.chdir options.prefix options.configure { |f| load f } + options.repeat options.pretend options.interrupt options.timeout diff --git a/spec/mspec/spec/commands/mspec_ci_spec.rb b/spec/mspec/spec/commands/mspec_ci_spec.rb index bcbc5b422496a9..b8dc9d062f4d61 100644 --- a/spec/mspec/spec/commands/mspec_ci_spec.rb +++ b/spec/mspec/spec/commands/mspec_ci_spec.rb @@ -78,6 +78,11 @@ @script.options [] end + it "enables the repeat option" do + expect(@options).to receive(:repeat) + @script.options @argv + end + it "calls #custom_options" do expect(@script).to receive(:custom_options).with(@options) @script.options [] diff --git a/spec/mspec/spec/commands/mspec_run_spec.rb b/spec/mspec/spec/commands/mspec_run_spec.rb index 62acd49d7fefac..f96be2b43ee66c 100644 --- a/spec/mspec/spec/commands/mspec_run_spec.rb +++ b/spec/mspec/spec/commands/mspec_run_spec.rb @@ -105,6 +105,11 @@ @script.options @argv end + it "enables the repeat option" do + expect(@options).to receive(:repeat) + @script.options @argv + end + it "exits if there are no files to process and './spec' is not a directory" do expect(File).to receive(:directory?).with("./spec").and_return(false) expect(@options).to receive(:parse).and_return([]) diff --git a/spec/mspec/tool/sync/sync-rubyspec.rb b/spec/mspec/tool/sync/sync-rubyspec.rb index effccc1567c156..617123733e5442 100644 --- a/spec/mspec/tool/sync/sync-rubyspec.rb +++ b/spec/mspec/tool/sync/sync-rubyspec.rb @@ -3,7 +3,7 @@ IMPLS = { truffleruby: { - git: "https://github.com/oracle/truffleruby.git", + git: "https://github.com/truffleruby/truffleruby.git", from_commit: "f10ab6988d", }, jruby: { @@ -34,6 +34,13 @@ SOURCE_REPO = MSPEC ? MSPEC_REPO : RUBYSPEC_REPO +# LAST_MERGE is a commit of ruby/spec or ruby/mspec +# which is the spec/mspec commit that was last imported in the Ruby implementation +# (i.e. the commit in "Update to ruby/spec@commit"). +# It is normally automatically computed, but can be manually set when +# e.g. the last update of specs wasn't merged in the Ruby implementation. +LAST_MERGE = ENV["LAST_MERGE"] + NOW = Time.now BRIGHT_RED = "\e[31;1m" @@ -142,8 +149,8 @@ def rebase_commits(impl) else sh "git", "checkout", impl.name - if ENV["LAST_MERGE"] - last_merge = `git log -n 1 --format='%H %ct' #{ENV["LAST_MERGE"]}` + if LAST_MERGE + last_merge = `git log -n 1 --format='%H %ct' #{LAST_MERGE}` else last_merge = `git log --grep='^#{impl.last_merge_message}' -n 1 --format='%H %ct'` end From 50593d51997eab4aa5a148c5e131d10ea535f955 Mon Sep 17 00:00:00 2001 From: Benoit Daloze Date: Wed, 8 Oct 2025 17:59:52 +0200 Subject: [PATCH 09/11] Update to ruby/spec@3d7e563 --- spec/ruby/.rubocop_todo.yml | 1 + spec/ruby/README.md | 2 +- spec/ruby/core/array/shared/unshift.rb | 2 +- spec/ruby/core/io/readpartial_spec.rb | 5 + spec/ruby/core/kernel/Float_spec.rb | 2 + spec/ruby/core/kernel/shared/sprintf.rb | 2 +- .../ruby/core/kernel/singleton_method_spec.rb | 46 ++++- spec/ruby/core/kernel/sprintf_spec.rb | 36 +++- spec/ruby/core/kernel/warn_spec.rb | 6 + spec/ruby/core/module/autoload_spec.rb | 15 ++ spec/ruby/core/regexp/compile_spec.rb | 2 +- spec/ruby/core/regexp/new_spec.rb | 2 +- spec/ruby/core/string/to_f_spec.rb | 12 ++ spec/ruby/core/string/unpack1_spec.rb | 13 ++ spec/ruby/core/thread/fixtures/classes.rb | 1 + spec/ruby/core/time/utc_spec.rb | 4 + spec/ruby/core/warning/warn_spec.rb | 28 +++ spec/ruby/language/assignments_spec.rb | 68 ++++++++ spec/ruby/language/method_spec.rb | 160 ++++++++++++++++++ .../library/socket/socket/getaddrinfo_spec.rb | 18 ++ .../library/socket/socket/getnameinfo_spec.rb | 18 ++ .../ruby/library/stringio/readpartial_spec.rb | 30 +++- spec/ruby/optional/capi/ext/set_spec.c | 1 + spec/ruby/shared/kernel/raise.rb | 4 +- 24 files changed, 456 insertions(+), 22 deletions(-) diff --git a/spec/ruby/.rubocop_todo.yml b/spec/ruby/.rubocop_todo.yml index 6b43db9b9a4dd7..bd30f3f14af1ba 100644 --- a/spec/ruby/.rubocop_todo.yml +++ b/spec/ruby/.rubocop_todo.yml @@ -44,6 +44,7 @@ Lint/FloatOutOfRange: Lint/ImplicitStringConcatenation: Exclude: - 'language/string_spec.rb' + - 'core/string/chilled_string_spec.rb' # Offense count: 4 Lint/IneffectiveAccessModifier: diff --git a/spec/ruby/README.md b/spec/ruby/README.md index 56d6c3c5426a7a..674ada4c9e4cc9 100644 --- a/spec/ruby/README.md +++ b/spec/ruby/README.md @@ -26,7 +26,7 @@ ruby/spec is known to be tested in these implementations for every commit: * [MRI](https://rubyci.org/) on 30 platforms and 4 versions * [JRuby](https://github.com/jruby/jruby/tree/master/spec/ruby) for both 1.7 and 9.x -* [TruffleRuby](https://github.com/oracle/truffleruby/tree/master/spec/ruby) +* [TruffleRuby](https://github.com/truffleruby/truffleruby/tree/master/spec/ruby) * [Opal](https://github.com/opal/opal/tree/master/spec) * [Artichoke](https://github.com/artichoke/spec/tree/artichoke-vendor) diff --git a/spec/ruby/core/array/shared/unshift.rb b/spec/ruby/core/array/shared/unshift.rb index 4941e098f665c1..9e0fe7556a43af 100644 --- a/spec/ruby/core/array/shared/unshift.rb +++ b/spec/ruby/core/array/shared/unshift.rb @@ -49,7 +49,7 @@ -> { ArraySpecs.frozen_array.send(@method) }.should raise_error(FrozenError) end - # https://github.com/oracle/truffleruby/issues/2772 + # https://github.com/truffleruby/truffleruby/issues/2772 it "doesn't rely on Array#[]= so it can be overridden" do subclass = Class.new(Array) do def []=(*) diff --git a/spec/ruby/core/io/readpartial_spec.rb b/spec/ruby/core/io/readpartial_spec.rb index 2fcfaf520370e3..176c33cf9e43cd 100644 --- a/spec/ruby/core/io/readpartial_spec.rb +++ b/spec/ruby/core/io/readpartial_spec.rb @@ -93,6 +93,11 @@ @rd.readpartial(0).should == "" end + it "raises IOError if the stream is closed and the length argument is 0" do + @rd.close + -> { @rd.readpartial(0) }.should raise_error(IOError, "closed stream") + end + it "clears and returns the given buffer if the length argument is 0" do buffer = +"existing content" @rd.readpartial(0, buffer).should == buffer diff --git a/spec/ruby/core/kernel/Float_spec.rb b/spec/ruby/core/kernel/Float_spec.rb index 1705205996a9ad..e00fe815726318 100644 --- a/spec/ruby/core/kernel/Float_spec.rb +++ b/spec/ruby/core/kernel/Float_spec.rb @@ -163,6 +163,7 @@ def to_f() 1.2 end -> { @object.send(:Float, "+1.") }.should raise_error(ArgumentError) -> { @object.send(:Float, "-1.") }.should raise_error(ArgumentError) -> { @object.send(:Float, "1.e+0") }.should raise_error(ArgumentError) + -> { @object.send(:Float, "1.e-2") }.should raise_error(ArgumentError) end end @@ -172,6 +173,7 @@ def to_f() 1.2 end @object.send(:Float, "+1.").should == 1.0 @object.send(:Float, "-1.").should == -1.0 @object.send(:Float, "1.e+0").should == 1.0 + @object.send(:Float, "1.e-2").should be_close(0.01, TOLERANCE) end end diff --git a/spec/ruby/core/kernel/shared/sprintf.rb b/spec/ruby/core/kernel/shared/sprintf.rb index a68389a7b407a2..2b2c6c9b63ff1e 100644 --- a/spec/ruby/core/kernel/shared/sprintf.rb +++ b/spec/ruby/core/kernel/shared/sprintf.rb @@ -449,7 +449,7 @@ def obj.to_str it "is escaped by %" do @method.call("%%").should == "%" - @method.call("%%d", 10).should == "%d" + @method.call("%%d").should == "%d" end end end diff --git a/spec/ruby/core/kernel/singleton_method_spec.rb b/spec/ruby/core/kernel/singleton_method_spec.rb index 0bdf125ad84898..7d63fa7cc61223 100644 --- a/spec/ruby/core/kernel/singleton_method_spec.rb +++ b/spec/ruby/core/kernel/singleton_method_spec.rb @@ -1,7 +1,7 @@ require_relative '../../spec_helper' describe "Kernel#singleton_method" do - it "find a method defined on the singleton class" do + it "finds a method defined on the singleton class" do obj = Object.new def obj.foo; end obj.singleton_method(:foo).should be_an_instance_of(Method) @@ -38,4 +38,48 @@ def foo e.class.should == NameError } end + + ruby_bug "#20620", ""..."3.4" do + it "finds a method defined in a module included in the singleton class" do + m = Module.new do + def foo + :foo + end + end + + obj = Object.new + obj.singleton_class.include(m) + + obj.singleton_method(:foo).should be_an_instance_of(Method) + obj.singleton_method(:foo).call.should == :foo + end + + it "finds a method defined in a module prepended in the singleton class" do + m = Module.new do + def foo + :foo + end + end + + obj = Object.new + obj.singleton_class.prepend(m) + + obj.singleton_method(:foo).should be_an_instance_of(Method) + obj.singleton_method(:foo).call.should == :foo + end + + it "finds a method defined in a module that an object is extended with" do + m = Module.new do + def foo + :foo + end + end + + obj = Object.new + obj.extend(m) + + obj.singleton_method(:foo).should be_an_instance_of(Method) + obj.singleton_method(:foo).call.should == :foo + end + end end diff --git a/spec/ruby/core/kernel/sprintf_spec.rb b/spec/ruby/core/kernel/sprintf_spec.rb index 9ef7f86f16d833..5a4a90ff7a2860 100644 --- a/spec/ruby/core/kernel/sprintf_spec.rb +++ b/spec/ruby/core/kernel/sprintf_spec.rb @@ -13,28 +13,52 @@ describe "Kernel#sprintf" do it_behaves_like :kernel_sprintf, -> format, *args { - sprintf(format, *args) + r = nil + -> { + r = sprintf(format, *args) + }.should_not complain(verbose: true) + r } it_behaves_like :kernel_sprintf_encoding, -> format, *args { - sprintf(format, *args) + r = nil + -> { + r = sprintf(format, *args) + }.should_not complain(verbose: true) + r } it_behaves_like :kernel_sprintf_to_str, -> format, *args { - sprintf(format, *args) + r = nil + -> { + r = sprintf(format, *args) + }.should_not complain(verbose: true) + r } end describe "Kernel.sprintf" do it_behaves_like :kernel_sprintf, -> format, *args { - Kernel.sprintf(format, *args) + r = nil + -> { + r = Kernel.sprintf(format, *args) + }.should_not complain(verbose: true) + r } it_behaves_like :kernel_sprintf_encoding, -> format, *args { - Kernel.sprintf(format, *args) + r = nil + -> { + r = Kernel.sprintf(format, *args) + }.should_not complain(verbose: true) + r } it_behaves_like :kernel_sprintf_to_str, -> format, *args { - Kernel.sprintf(format, *args) + r = nil + -> { + r = Kernel.sprintf(format, *args) + }.should_not complain(verbose: true) + r } end diff --git a/spec/ruby/core/kernel/warn_spec.rb b/spec/ruby/core/kernel/warn_spec.rb index 00164ad90b2004..e03498c6dc44de 100644 --- a/spec/ruby/core/kernel/warn_spec.rb +++ b/spec/ruby/core/kernel/warn_spec.rb @@ -112,6 +112,12 @@ ruby_exe(file, options: "-rrubygems", args: "2>&1").should == "#{file}:2: warning: warn-require-warning\n" end + it "doesn't show the caller when the uplevel is `nil`" do + w = KernelSpecs::WarnInNestedCall.new + + -> { w.f4("foo", nil) }.should output(nil, "foo\n") + end + guard -> { Kernel.instance_method(:tap).source_location } do it "skips { ModuleSpecs::Autoload::DynClass }.should raise_error(NameError, /private constant/) + end + # [ruby-core:19127] [ruby-core:29941] it "does NOT raise a NameError when the autoload file did not define the constant and a module is opened with the same name" do module ModuleSpecs::Autoload diff --git a/spec/ruby/core/regexp/compile_spec.rb b/spec/ruby/core/regexp/compile_spec.rb index c41399cfbb3539..887c8d77dc3c15 100644 --- a/spec/ruby/core/regexp/compile_spec.rb +++ b/spec/ruby/core/regexp/compile_spec.rb @@ -14,6 +14,6 @@ it_behaves_like :regexp_new_regexp, :compile end -describe "Regexp.new given a non-String/Regexp" do +describe "Regexp.compile given a non-String/Regexp" do it_behaves_like :regexp_new_non_string_or_regexp, :compile end diff --git a/spec/ruby/core/regexp/new_spec.rb b/spec/ruby/core/regexp/new_spec.rb index 65f612df55311f..79210e9a236a04 100644 --- a/spec/ruby/core/regexp/new_spec.rb +++ b/spec/ruby/core/regexp/new_spec.rb @@ -7,11 +7,11 @@ describe "Regexp.new given a String" do it_behaves_like :regexp_new_string, :new + it_behaves_like :regexp_new_string_binary, :new end describe "Regexp.new given a Regexp" do it_behaves_like :regexp_new_regexp, :new - it_behaves_like :regexp_new_string_binary, :new end describe "Regexp.new given a non-String/Regexp" do diff --git a/spec/ruby/core/string/to_f_spec.rb b/spec/ruby/core/string/to_f_spec.rb index a91ccc168ecfee..abfd2517b69327 100644 --- a/spec/ruby/core/string/to_f_spec.rb +++ b/spec/ruby/core/string/to_f_spec.rb @@ -127,4 +127,16 @@ }.should raise_error(Encoding::CompatibilityError, "ASCII incompatible encoding: UTF-16") end end + + it "allows String representation without a fractional part" do + "1.".to_f.should == 1.0 + "+1.".to_f.should == 1.0 + "-1.".to_f.should == -1.0 + "1.e+0".to_f.should == 1.0 + "1.e+0".to_f.should == 1.0 + + ruby_bug "#20705", ""..."3.4" do + "1.e-2".to_f.should be_close(0.01, TOLERANCE) + end + end end diff --git a/spec/ruby/core/string/unpack1_spec.rb b/spec/ruby/core/string/unpack1_spec.rb index 3b3b879f75f946..cfb47fe6953365 100644 --- a/spec/ruby/core/string/unpack1_spec.rb +++ b/spec/ruby/core/string/unpack1_spec.rb @@ -31,4 +31,17 @@ it "raises an ArgumentError when the offset is larger than the string bytesize" do -> { "a".unpack1("C", offset: 2) }.should raise_error(ArgumentError, "offset outside of string") end + + context "with format 'm0'" do + # unpack1("m0") takes a special code path that calls Pack.unpackBase46Strict instead of Pack.unpack_m, + # which is why we repeat the tests for unpack("m0") here. + + it "decodes base64" do + "dGVzdA==".unpack1("m0").should == "test" + end + + it "raises an ArgumentError for an invalid base64 character" do + -> { "dGV%zdA==".unpack1("m0") }.should raise_error(ArgumentError) + end + end end diff --git a/spec/ruby/core/thread/fixtures/classes.rb b/spec/ruby/core/thread/fixtures/classes.rb index 54bd85fae30b6e..7c485660a8f522 100644 --- a/spec/ruby/core/thread/fixtures/classes.rb +++ b/spec/ruby/core/thread/fixtures/classes.rb @@ -27,6 +27,7 @@ def self.raise(*args, **kwargs, &block) thread.join ensure thread.kill if thread.alive? + Thread.pass while thread.alive? # Thread#kill may not terminate a thread immediately so it may be detected as a leaked one end end diff --git a/spec/ruby/core/time/utc_spec.rb b/spec/ruby/core/time/utc_spec.rb index 3d36e13ccfecdb..ab3c0df657e045 100644 --- a/spec/ruby/core/time/utc_spec.rb +++ b/spec/ruby/core/time/utc_spec.rb @@ -43,10 +43,14 @@ it "does not treat time with +00:00 offset as UTC" do Time.new(2022, 1, 1, 0, 0, 0, "+00:00").utc?.should == false + Time.now.localtime("+00:00").utc?.should == false + Time.at(Time.now, in: "+00:00").utc?.should == false end it "does not treat time with 0 offset as UTC" do Time.new(2022, 1, 1, 0, 0, 0, 0).utc?.should == false + Time.now.localtime(0).utc?.should == false + Time.at(Time.now, in: 0).utc?.should == false end end diff --git a/spec/ruby/core/warning/warn_spec.rb b/spec/ruby/core/warning/warn_spec.rb index 572885c2b4ee6d..12677349874e5b 100644 --- a/spec/ruby/core/warning/warn_spec.rb +++ b/spec/ruby/core/warning/warn_spec.rb @@ -97,6 +97,20 @@ def Warning.warn(msg) end end + ruby_version_is "3.4" do + it "warns when category is :strict_unused_block but Warning[:strict_unused_block] is false" do + warn_experimental = Warning[:strict_unused_block] + Warning[:strict_unused_block] = true + begin + -> { + Warning.warn("foo", category: :strict_unused_block) + }.should complain("foo") + ensure + Warning[:strict_unused_block] = warn_experimental + end + end + end + it "doesn't print message when category is :deprecated but Warning[:deprecated] is false" do warn_deprecated = Warning[:deprecated] Warning[:deprecated] = false @@ -121,6 +135,20 @@ def Warning.warn(msg) end end + ruby_version_is "3.4" do + it "doesn't print message when category is :strict_unused_block but Warning[:strict_unused_block] is false" do + warn_experimental = Warning[:strict_unused_block] + Warning[:strict_unused_block] = false + begin + -> { + Warning.warn("foo", category: :strict_unused_block) + }.should_not complain + ensure + Warning[:strict_unused_block] = warn_experimental + end + end + end + ruby_bug '#20573', ''...'3.4' do it "isn't called by Kernel.warn when category is :deprecated but Warning[:deprecated] is false" do warn_deprecated = Warning[:deprecated] diff --git a/spec/ruby/language/assignments_spec.rb b/spec/ruby/language/assignments_spec.rb index 4ad9f4167bbad0..d469459c43d727 100644 --- a/spec/ruby/language/assignments_spec.rb +++ b/spec/ruby/language/assignments_spec.rb @@ -72,6 +72,34 @@ def []=(k, v, &block) @h[k] = v; end end end end + + context "given keyword arguments" do + before do + @klass = Class.new do + attr_reader :x + + def []=(*args, **kw) + @x = [args, kw] + end + end + end + + ruby_version_is ""..."3.4" do + it "supports keyword arguments in index assignments" do + a = @klass.new + eval "a[1, 2, 3, b: 4] = 5" + a.x.should == [[1, 2, 3, {b: 4}, 5], {}] + end + end + + ruby_version_is "3.4" do + it "raies SyntaxError when given keyword arguments in index assignments" do + a = @klass.new + -> { eval "a[1, 2, 3, b: 4] = 5" }.should raise_error(SyntaxError, + /keywords are not allowed in index assignment expressions|keyword arg given in index assignment/) # prism|parse.y + end + end + end end describe 'using +=' do @@ -176,6 +204,46 @@ def []=(k, v, &block) @h[k] = v; end end end + context "given keyword arguments" do + before do + @klass = Class.new do + attr_reader :x + + def [](*args) + 100 + end + + def []=(*args, **kw) + @x = [args, kw] + end + end + end + + ruby_version_is ""..."3.3" do + it "supports keyword arguments in index assignments" do + a = @klass.new + eval "a[1, 2, 3, b: 4] += 5" + a.x.should == [[1, 2, 3, {b: 4}, 105], {}] + end + end + + ruby_version_is "3.3"..."3.4" do + it "supports keyword arguments in index assignments" do + a = @klass.new + eval "a[1, 2, 3, b: 4] += 5" + a.x.should == [[1, 2, 3, 105], {b: 4}] + end + end + + ruby_version_is "3.4" do + it "raies SyntaxError when given keyword arguments in index assignments" do + a = @klass.new + -> { eval "a[1, 2, 3, b: 4] += 5" }.should raise_error(SyntaxError, + /keywords are not allowed in index assignment expressions|keyword arg given in index assignment/) # prism|parse.y + end + end + end + context 'splatted argument' do it 'correctly handles it' do @b[:m] = 10 diff --git a/spec/ruby/language/method_spec.rb b/spec/ruby/language/method_spec.rb index 1d412a133edc30..39b245fe2a9de2 100644 --- a/spec/ruby/language/method_spec.rb +++ b/spec/ruby/language/method_spec.rb @@ -1487,3 +1487,163 @@ def greet(person) = "Hi, ".dup.concat person greet("Homer").should == "Hi, Homer" end end + +describe "warning about not used block argument" do + ruby_version_is "3.4" do + it "warns when passing a block argument to a method that never uses it" do + def m_that_does_not_use_block + 42 + end + + -> { + m_that_does_not_use_block { } + }.should complain( + /#{__FILE__}:#{__LINE__ - 2}: warning: the block passed to 'm_that_does_not_use_block' defined at #{__FILE__}:#{__LINE__ - 7} may be ignored/, + verbose: true) + end + + it "does not warn when passing a block argument to a method that declares a block parameter" do + def m_with_block_parameter(&block) + 42 + end + + -> { m_with_block_parameter { } }.should_not complain(verbose: true) + end + + it "does not warn when passing a block argument to a method that declares an anonymous block parameter" do + def m_with_anonymous_block_parameter(&) + 42 + end + + -> { m_with_anonymous_block_parameter { } }.should_not complain(verbose: true) + end + + it "does not warn when passing a block argument to a method that yields an implicit block parameter" do + def m_with_yield + yield 42 + end + + -> { m_with_yield { } }.should_not complain(verbose: true) + end + + it "warns when passing a block argument to a method that calls #block_given?" do + def m_with_block_given + block_given? + end + + -> { + m_with_block_given { } + }.should complain( + /#{__FILE__}:#{__LINE__ - 2}: warning: the block passed to 'm_with_block_given' defined at #{__FILE__}:#{__LINE__ - 7} may be ignored/, + verbose: true) + end + + it "does not warn when passing a block argument to a method that calls super" do + parent = Class.new do + def m + end + end + + child = Class.new(parent) do + def m + super + end + end + + obj = child.new + -> { obj.m { } }.should_not complain(verbose: true) + end + + it "does not warn when passing a block argument to a method that calls super(...)" do + parent = Class.new do + def m(a) + end + end + + child = Class.new(parent) do + def m(...) + super(...) + end + end + + obj = child.new + -> { obj.m(42) { } }.should_not complain(verbose: true) + end + + it "does not warn when called #initialize()" do + klass = Class.new do + def initialize + end + end + + -> { klass.new {} }.should_not complain(verbose: true) + end + + it "does not warn when passing a block argument to a method that calls super()" do + parent = Class.new do + def m + end + end + + child = Class.new(parent) do + def m + super() + end + end + + obj = child.new + -> { obj.m { } }.should_not complain(verbose: true) + end + + it "warns only once per call site" do + def m_that_does_not_use_block + 42 + end + + def call_m_that_does_not_use_block + m_that_does_not_use_block {} + end + + -> { + m_that_does_not_use_block { } + }.should complain(/the block passed to 'm_that_does_not_use_block' defined at .+ may be ignored/, verbose: true) + + -> { + m_that_does_not_use_block { } + }.should_not complain(verbose: true) + end + + it "can be disabled with :strict_unused_block warning category" do + def m_that_does_not_use_block + 42 + end + + # ensure that warning is emitted + -> { m_that_does_not_use_block { } }.should complain(verbose: true) + + warn_experimental = Warning[:strict_unused_block] + Warning[:strict_unused_block] = false + begin + -> { m_that_does_not_use_block { } }.should_not complain(verbose: true) + ensure + Warning[:strict_unused_block] = warn_experimental + end + end + + it "can be enabled with :strict_unused_block = true warning category in not verbose mode" do + def m_that_does_not_use_block + 42 + end + + warn_experimental = Warning[:strict_unused_block] + Warning[:strict_unused_block] = true + begin + -> { + m_that_does_not_use_block { } + }.should complain(/the block passed to 'm_that_does_not_use_block' defined at .+ may be ignored/) + ensure + Warning[:strict_unused_block] = warn_experimental + end + end + end +end diff --git a/spec/ruby/library/socket/socket/getaddrinfo_spec.rb b/spec/ruby/library/socket/socket/getaddrinfo_spec.rb index 9f049597d0d2b4..a2330715ad9713 100644 --- a/spec/ruby/library/socket/socket/getaddrinfo_spec.rb +++ b/spec/ruby/library/socket/socket/getaddrinfo_spec.rb @@ -106,6 +106,24 @@ ] res.each { |a| expected.should include(a) } end + + ruby_version_is ""..."3.3" do + it "raises SocketError when fails to resolve address" do + -> { + Socket.getaddrinfo("www.kame.net", 80, "AF_UNIX") + }.should raise_error(SocketError) + end + end + + ruby_version_is "3.3" do + it "raises ResolutionError when fails to resolve address" do + -> { + Socket.getaddrinfo("www.kame.net", 80, "AF_UNIX") + }.should raise_error(Socket::ResolutionError) { |e| + e.error_code.should == Socket::EAI_FAMILY + } + end + end end end diff --git a/spec/ruby/library/socket/socket/getnameinfo_spec.rb b/spec/ruby/library/socket/socket/getnameinfo_spec.rb index 4f13bf484df93e..b3105244a28f70 100644 --- a/spec/ruby/library/socket/socket/getnameinfo_spec.rb +++ b/spec/ruby/library/socket/socket/getnameinfo_spec.rb @@ -60,6 +60,24 @@ def should_be_valid_dns_name(name) name_info = Socket.getnameinfo ["AF_INET", 9, 'foo', '127.0.0.1'] name_info[1].should == 'discard' end + + ruby_version_is ""..."3.3" do + it "raises SocketError when fails to resolve address" do + -> { + Socket.getnameinfo(["AF_UNIX", 80, "0.0.0.0"]) + }.should raise_error(SocketError) + end + end + + ruby_version_is "3.3" do + it "raises ResolutionError when fails to resolve address" do + -> { + Socket.getnameinfo(["AF_UNIX", 80, "0.0.0.0"]) + }.should raise_error(Socket::ResolutionError) { |e| + e.error_code.should == Socket::EAI_FAMILY + } + end + end end describe 'Socket.getnameinfo' do diff --git a/spec/ruby/library/stringio/readpartial_spec.rb b/spec/ruby/library/stringio/readpartial_spec.rb index f25cef40148137..988c552235d5e6 100644 --- a/spec/ruby/library/stringio/readpartial_spec.rb +++ b/spec/ruby/library/stringio/readpartial_spec.rb @@ -10,13 +10,7 @@ @string.close unless @string.closed? end - it "raises IOError on closed stream" do - @string.close - -> { @string.readpartial(10) }.should raise_error(IOError) - end - it "reads at most the specified number of bytes" do - # buffered read @string.read(1).should == 'S' # return only specified number, not the whole buffer @@ -67,14 +61,34 @@ it "raises IOError if the stream is closed" do @string.close - -> { @string.readpartial(1) }.should raise_error(IOError) + -> { @string.readpartial(1) }.should raise_error(IOError, "not opened for reading") end it "raises ArgumentError if the negative argument is provided" do - -> { @string.readpartial(-1) }.should raise_error(ArgumentError) + -> { @string.readpartial(-1) }.should raise_error(ArgumentError, "negative length -1 given") end it "immediately returns an empty string if the length argument is 0" do @string.readpartial(0).should == "" end + + it "raises IOError if the stream is closed and the length argument is 0" do + @string.close + -> { @string.readpartial(0) }.should raise_error(IOError, "not opened for reading") + end + + it "clears and returns the given buffer if the length argument is 0" do + buffer = +"existing content" + @string.readpartial(0, buffer).should == buffer + buffer.should == "" + end + + version_is StringIO::VERSION, "3.1.2" do # ruby_version_is "3.4" + it "preserves the encoding of the given buffer" do + buffer = ''.encode(Encoding::ISO_8859_1) + @string.readpartial(10, buffer) + + buffer.encoding.should == Encoding::ISO_8859_1 + end + end end diff --git a/spec/ruby/optional/capi/ext/set_spec.c b/spec/ruby/optional/capi/ext/set_spec.c index 72938be1d887f0..7af922fd49ea96 100644 --- a/spec/ruby/optional/capi/ext/set_spec.c +++ b/spec/ruby/optional/capi/ext/set_spec.c @@ -47,6 +47,7 @@ VALUE set_spec_rb_set_size(VALUE self, VALUE set) { void Init_set_spec(void) { VALUE cls = rb_define_class("CApiSetSpecs", rb_cObject); + rb_define_method(cls, "rb_set_foreach", set_spec_rb_set_foreach, 2); rb_define_method(cls, "rb_set_new", set_spec_rb_set_new, 0); rb_define_method(cls, "rb_set_new_capa", set_spec_rb_set_new_capa, 1); diff --git a/spec/ruby/shared/kernel/raise.rb b/spec/ruby/shared/kernel/raise.rb index d46c4b7b15f07e..8432c835946d6e 100644 --- a/spec/ruby/shared/kernel/raise.rb +++ b/spec/ruby/shared/kernel/raise.rb @@ -191,8 +191,8 @@ def e.exception -> do @object.raise(error, cause: error) - end.should raise_error(RuntimeError, "message") do |error| - error.cause.should == nil + end.should raise_error(RuntimeError, "message") do |e| + e.cause.should == nil end end From 40d704a2bf3324f4472c4436447af391f0987681 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 8 Oct 2025 17:13:21 +0100 Subject: [PATCH 10/11] Bump RDoc (#14747) --- gems/bundled_gems | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gems/bundled_gems b/gems/bundled_gems index 4b95fa525004f7..20f4379dbbb6d7 100644 --- a/gems/bundled_gems +++ b/gems/bundled_gems @@ -39,7 +39,7 @@ ostruct 0.6.3 https://github.com/ruby/ostruct pstore 0.2.0 https://github.com/ruby/pstore benchmark 0.4.1 https://github.com/ruby/benchmark logger 1.7.0 https://github.com/ruby/logger -rdoc 6.14.2 https://github.com/ruby/rdoc +rdoc 6.15.0 https://github.com/ruby/rdoc ac2a6fbf62b584a8325a665a9e7b368388bc7df6 win32ole 1.9.2 https://github.com/ruby/win32ole irb 1.15.2 https://github.com/ruby/irb d43c3d764ae439706aa1b26a3ec299cc45eaed5b reline 0.6.2 https://github.com/ruby/reline From 77b019f656b33d8f8af359522d421d66cf4625ee Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 8 Oct 2025 17:22:56 +0100 Subject: [PATCH 11/11] ZJIT: Use type alias for num-profile and call-threshold's types (#14777) Co-authored-by: Alan Wu --- zjit/src/options.rs | 18 ++++++++++-------- zjit/src/profile.rs | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/zjit/src/options.rs b/zjit/src/options.rs index 44209b963f3bf9..4ef998acede36c 100644 --- a/zjit/src/options.rs +++ b/zjit/src/options.rs @@ -6,24 +6,26 @@ use crate::cruby::*; use std::collections::HashSet; /// Default --zjit-num-profiles -const DEFAULT_NUM_PROFILES: u32 = 5; +const DEFAULT_NUM_PROFILES: NumProfiles = 5; +pub type NumProfiles = u32; /// Default --zjit-call-threshold. This should be large enough to avoid compiling /// warmup code, but small enough to perform well on micro-benchmarks. -pub const DEFAULT_CALL_THRESHOLD: u64 = 30; +pub const DEFAULT_CALL_THRESHOLD: CallThreshold = 30; +pub type CallThreshold = u64; /// Number of calls to start profiling YARV instructions. /// They are profiled `rb_zjit_call_threshold - rb_zjit_profile_threshold` times, /// which is equal to --zjit-num-profiles. #[unsafe(no_mangle)] #[allow(non_upper_case_globals)] -pub static mut rb_zjit_profile_threshold: u64 = DEFAULT_CALL_THRESHOLD - DEFAULT_NUM_PROFILES as u64; +pub static mut rb_zjit_profile_threshold: CallThreshold = DEFAULT_CALL_THRESHOLD - DEFAULT_NUM_PROFILES as CallThreshold; /// Number of calls to compile ISEQ with ZJIT at jit_compile() in vm.c. /// --zjit-call-threshold=1 compiles on first execution without profiling information. #[unsafe(no_mangle)] #[allow(non_upper_case_globals)] -pub static mut rb_zjit_call_threshold: u64 = DEFAULT_CALL_THRESHOLD; +pub static mut rb_zjit_call_threshold: CallThreshold = DEFAULT_CALL_THRESHOLD; /// ZJIT command-line options. This is set before rb_zjit_init() sets /// ZJITState so that we can query some options while loading builtins. @@ -40,7 +42,7 @@ pub struct Options { pub mem_bytes: usize, /// Number of times YARV instructions should be profiled. - pub num_profiles: u32, + pub num_profiles: NumProfiles, /// Enable YJIT statsitics pub stats: bool, @@ -319,14 +321,14 @@ fn update_profile_threshold() { unsafe { rb_zjit_profile_threshold = 0; } } else { // Otherwise, profile instructions at least once. - let num_profiles = get_option!(num_profiles) as u64; - unsafe { rb_zjit_profile_threshold = rb_zjit_call_threshold.saturating_sub(num_profiles).max(1) }; + let num_profiles = get_option!(num_profiles); + unsafe { rb_zjit_profile_threshold = rb_zjit_call_threshold.saturating_sub(num_profiles.into()).max(1) }; } } /// Update --zjit-call-threshold for testing #[cfg(test)] -pub fn set_call_threshold(call_threshold: u64) { +pub fn set_call_threshold(call_threshold: CallThreshold) { unsafe { rb_zjit_call_threshold = call_threshold; } rb_zjit_prepare_options(); update_profile_threshold(); diff --git a/zjit/src/profile.rs b/zjit/src/profile.rs index 471ff89e763148..9588a541827f35 100644 --- a/zjit/src/profile.rs +++ b/zjit/src/profile.rs @@ -3,7 +3,7 @@ // We use the YARV bytecode constants which have a CRuby-style name #![allow(non_upper_case_globals)] -use crate::{cruby::*, gc::get_or_create_iseq_payload, options::get_option}; +use crate::{cruby::*, gc::get_or_create_iseq_payload, options::{get_option, NumProfiles}}; use crate::distribution::{Distribution, DistributionSummary}; use crate::stats::Counter::profile_time_ns; use crate::stats::with_time_stat; @@ -278,7 +278,7 @@ pub struct IseqProfile { opnd_types: Vec>, /// Number of profiled executions for each YARV instruction, indexed by the instruction index - num_profiles: Vec, + num_profiles: Vec, } impl IseqProfile {