From d87ffb271a80fb108de3465d12ada829f3c130af Mon Sep 17 00:00:00 2001 From: Nobuyoshi Nakada Date: Sun, 20 Sep 2026 01:18:43 +0900 Subject: [PATCH 1/2] [Bug #22335] Use identity hashes for keyword argument tracking With more than 31 keyword arguments, tracking unevaluated defaults uses an integer-keyed Hash whose collisions can invoke methods when `Integer#==` is refined or redefined, corrupting argument locals above `cfp->sp`. Use identity hashes to prevent these calls during argument setup and `checkkeyword` execution. Making `rb_any_cmp` compare special constants directly would also bypass redefined `eql?` methods in ordinary hashes. Limit identity comparison to the internal keyword tracking hashes to preserve that behavior for now. --- test/ruby/test_keyword.rb | 25 +++++++++++++++++++++++++ vm_args.c | 4 ++-- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/test/ruby/test_keyword.rb b/test/ruby/test_keyword.rb index 576f219dac295d..cfc3dc11da511f 100644 --- a/test/ruby/test_keyword.rb +++ b/test/ruby/test_keyword.rb @@ -4273,6 +4273,31 @@ def test_many_kwargs assert_equal(:ok, many_kwargs(e0: :ok)[i], "#{i}: e0"); i+=1 end + def test_many_kwargs_with_integer_refinement + assert_separately([], <<~'RUBY') + module Ref + refine Integer do + def ==(other) super; end + end + end + + # 257 indices guarantee a collision in the 8-bit hint of a small Hash. + # Keep both indices above the keyword bitmask limit. + indices = (32...289).to_a + a, b = indices.combination(2).find { |x, y| (x.hash & 0xff) == (y.hash & 0xff) } + expected = (0...289).to_a + expected[a] = {} + expected[b] = {} + params = expected.each_with_index.map { |value, i| "k#{i}: #{value.inspect}" } + eval "def victim(#{params.join(', ')}) [#{expected.each_index.map { |i| "k#{i}" }.join(', ')}]; end" + + assert_equal(expected, victim, '[Bug #22335]') + assert_equal(expected, victim(k0: 0), '[Bug #22335]') + kwargs = {k0: 0} + assert_equal(expected, victim(**kwargs), '[Bug #22335]') + RUBY + end + def test_splat_empty_hash_with_block_passing assert_valid_syntax("bug15087(**{}, &nil)") end diff --git a/vm_args.c b/vm_args.c index 2610d99c229b5e..2ecf6eb216c7f0 100644 --- a/vm_args.c +++ b/vm_args.c @@ -362,7 +362,7 @@ args_setup_kw_parameters(rb_execution_context_t *const ec, const rb_iseq_t *cons if (NIL_P(unspecified_bits_value)) { /* fixnum -> hash */ int j; - unspecified_bits_value = rb_hash_new(); + unspecified_bits_value = rb_ident_hash_new(); for (j=0; j hash */ int j; - unspecified_bits_value = rb_hash_new(); + unspecified_bits_value = rb_ident_hash_new(); for (j=0; j Date: Sun, 20 Sep 2026 01:46:06 +0900 Subject: [PATCH 2/2] Refactor keyword parameter default handling --- vm_args.c | 83 +++++++++++++++++++++++-------------------------------- 1 file changed, 35 insertions(+), 48 deletions(-) diff --git a/vm_args.c b/vm_args.c index 2ecf6eb216c7f0..33a41c6445737c 100644 --- a/vm_args.c +++ b/vm_args.c @@ -320,6 +320,37 @@ args_setup_kw_parameters_lookup(const ID key, VALUE *ptr, const VALUE *const pas return FALSE; } +static inline void +args_setup_kw_parameters_not_found(const VALUE *default_values, VALUE *locals, int i, int di, + int *unspecified_bits, VALUE *unspecified_bits_value) +{ + if (UNDEF_P(default_values[di])) { + locals[i] = Qnil; + + if (LIKELY(i < VM_KW_SPECIFIED_BITS_MAX)) { + *unspecified_bits |= 0x01 << di; + } + else { + VALUE bits_value = *unspecified_bits_value; + if (NIL_P(bits_value)) { + /* fixnum -> hash */ + int bits = *unspecified_bits; + *unspecified_bits_value = bits_value = rb_ident_hash_new(); + + for (int j=0; j hash */ - int j; - unspecified_bits_value = rb_ident_hash_new(); - - for (j=0; j hash */ - int j; - unspecified_bits_value = rb_ident_hash_new(); - - for (j=0; j