Bug #22120
openSegfault caused by ar_find_entry_hint() not checking for conversion to st_table
Description
While perusing code in hash.c (motivated by some crashes reported in https://github.com/DataDog/dd-trace-rb/issues/5718),
I found it suspicious that ar_find_entry_hint() didn't reread bound in the loop and yet called arbitrary code through #eql?.
Indeed, ar_find_entry_hint() does not check if bound or the storage (AR->ST) changed and would return a bin index which is not correct to access:
static unsigned
ar_find_entry_hint(VALUE hash, ar_hint_t hint, st_data_t key)
{
unsigned i, bound = RHASH_AR_TABLE_BOUND(hash);
const ar_hint_t *hints = RHASH_AR_TABLE(hash)->ar_hint.ary;
/* if table is NULL, then bound also should be 0 */
for (i = 0; i < bound; i++) {
if (hints[i] == hint) {
ar_table_pair *pair = RHASH_AR_TABLE_REF(hash, i);
if (ar_equal(key, pair->key)) {
RB_DEBUG_COUNTER_INC(artable_hint_hit);
return i;
}
else {
RB_DEBUG_COUNTER_INC(artable_hint_miss);
}
}
}
RB_DEBUG_COUNTER_INC(artable_hint_notfound);
return RHASH_AR_TABLE_MAX_BOUND;
}
Fix: https://github.com/ruby/ruby/pull/17383
With the conversion from AR to ST it can actually trigger a segfault:
This repro uses a single thread and mutates inside eql? for simplicity, but the same crash happens with another Thread mutating while the main Thread is in eql?.
class Key
attr_reader :v
def initialize(v, h = nil)
@v = v
@h = h
end
def hash = 0
def eql?(other)
if @h
# Trigger AR->ST conversion
@h[42] = 42
end
other.is_a?(Key) && @v == other.v
end
def inspect = "K(#{@v})"
end
h = {}
8.times { |i| h[Key.new(i)] = i }
# Not in the hash, so ar_find_entry_hint checks every entry.
lookup_key = Key.new(-1, h)
p h[lookup_key]
$ ./miniruby ar_find_entry_hint_bug.rb
ar_find_entry_hint_bug.rb:15: [BUG] Segmentation fault at 0x0000000000000004
ruby 4.1.0dev (2026-06-17T10:52:40Z master 0888fa8cb5) +PRISM [arm64-darwin25]
-- Crash Report log information --------------------------------------------
See Crash Report log file in one of the following locations:
* ~/Library/Logs/DiagnosticReports
* /Library/Logs/DiagnosticReports
for more details.
Don't forget to include the above Crash Report log file in bug reports.
-- Control frame information -----------------------------------------------
c:0003 p:0021 s:0018 e:000015 l:y b:0001 r:0x0 METHOD ar_find_entry_hint_bug.rb:15 [FINISH]
c:0002 p:0046 s:0011 E:0014e0 l:n b:---- r:0x0 EVAL ar_find_entry_hint_bug.rb:27 [FINISH]
c:0001 p:0000 s:0003 E:002480 l:y b:---- r:0x0 DUMMY [FINISH]
-- Ruby level backtrace information ----------------------------------------
ar_find_entry_hint_bug.rb:27:in '<main>'
ar_find_entry_hint_bug.rb:15:in 'eql?'
-- Threading information ---------------------------------------------------
Total ractor count: 1
Ruby thread count for this ractor: 1
-- Machine register context ------------------------------------------------
x0: 0x0000000000000004 x1: 0x00000000000011bf x2: 0x000000016b9ed680
x3: 0x0000000000000000 x4: 0x0000000000000001 x5: 0x0000000000000001
x6: 0x0000000104d400a0 x7: 0xfffff0003ffff800 x18: 0x0000000000000000
x19: 0x000000016b9ed6d0 x20: 0x0000000104d41200 x21: 0x0000000000000000
x22: 0x00000000000011bf x23: 0x0000000000000000 x24: 0x0000000000000000
x25: 0x0000000104a96000 x26: 0x0000000000000009 x27: 0x0000000afd00aa00
x28: 0x0000000104a96000 lr: 0x00000001046a21dc fp: 0x000000016b9ed6c0
sp: 0x000000016b9ed680
-- C level backtrace information -------------------------------------------
/Users/benoit.daloze/code/ruby/miniruby(rb_vm_bugreport+0xbfc) [0x1046aec04] /Users/benoit.daloze/code/ruby/vm_dump.c:1473
/Users/benoit.daloze/code/ruby/miniruby(rb_vm_bugreport) (null):0
/Users/benoit.daloze/code/ruby/miniruby(rb_bug_for_fatal_signal+0x10c) [0x1044c7cd4] /Users/benoit.daloze/code/ruby/error.c:1140
/Users/benoit.daloze/code/ruby/miniruby(sigsegv+0x94) [0x1045ff93c] /Users/benoit.daloze/code/ruby/signal.c:948
/usr/lib/system/libsystem_platform.dylib(_sigtramp+0x38) [0x1891a57a4]
/Users/benoit.daloze/code/ruby/miniruby(lookup_method_table+0x7c) [0x1046a21dc] ./vm_method.c:1363
/Users/benoit.daloze/code/ruby/miniruby(search_method0) ./vm_method.c:1822
/Users/benoit.daloze/code/ruby/miniruby(search_method+0x14) [0x104685960] ./vm_method.c:1845
/Users/benoit.daloze/code/ruby/miniruby(callable_method_entry_or_negative) ./vm_method.c:2049
/Users/benoit.daloze/code/ruby/miniruby(callable_method_entry+0x10) [0x104677fd0] ./vm_method.c:2078
/Users/benoit.daloze/code/ruby/miniruby(rb_callable_method_entry) ./vm_method.c:2085
/Users/benoit.daloze/code/ruby/miniruby(rb_vm_search_method_slowpath) ./vm_insnhelper.c:2039
/Users/benoit.daloze/code/ruby/miniruby(vm_search_method_slowpath0+0x8) [0x10467e6cc] ./vm_insnhelper.c:2214
/Users/benoit.daloze/code/ruby/miniruby(vm_exec_core) ./vm_insnhelper.c:2277
/Users/benoit.daloze/code/ruby/miniruby(vm_exec_loop+0x0) [0x10467a970] /Users/benoit.daloze/code/ruby/vm.c:2796
/Users/benoit.daloze/code/ruby/miniruby(rb_vm_exec) /Users/benoit.daloze/code/ruby/vm.c:2799
/Users/benoit.daloze/code/ruby/miniruby(rb_funcallv_scope+0x238) [0x10468a0c0] ./vm_eval.c:101
/Users/benoit.daloze/code/ruby/miniruby(rb_funcallv+0x8) [0x10468a4cc] ./vm_eval.c:1084
/Users/benoit.daloze/code/ruby/miniruby(rb_funcall) ./vm_eval.c:1141
/Users/benoit.daloze/code/ruby/miniruby(RB_TEST+0x0) [0x10455f248] /Users/benoit.daloze/code/ruby/object.c:161
/Users/benoit.daloze/code/ruby/miniruby(rb_eql) /Users/benoit.daloze/code/ruby/object.c:163
/Users/benoit.daloze/code/ruby/miniruby(rb_any_cmp+0xd0) [0x1044ff0e0] /Users/benoit.daloze/code/ruby/hash.c:138
/Users/benoit.daloze/code/ruby/miniruby(ar_equal+0xc) [0x104508328] /Users/benoit.daloze/code/ruby/hash.c:603
/Users/benoit.daloze/code/ruby/miniruby(ar_find_entry+0x54) [0x104500af4] /Users/benoit.daloze/code/ruby/hash.c:617
/Users/benoit.daloze/code/ruby/miniruby(ar_lookup) /Users/benoit.daloze/code/ruby/hash.c:1025
/Users/benoit.daloze/code/ruby/miniruby(rb_hash_aref) /Users/benoit.daloze/code/ruby/hash.c:2037
/Users/benoit.daloze/code/ruby/miniruby(vm_exec_core+0x4790) [0x104680c18] ./vm_insnhelper.c:6958
/Users/benoit.daloze/code/ruby/miniruby(vm_exec_loop+0x0) [0x10467a970] /Users/benoit.daloze/code/ruby/vm.c:2796
/Users/benoit.daloze/code/ruby/miniruby(rb_vm_exec) /Users/benoit.daloze/code/ruby/vm.c:2799
/Users/benoit.daloze/code/ruby/miniruby(rb_ec_exec_node+0xb4) [0x1044d317c] /Users/benoit.daloze/code/ruby/eval.c:284
/Users/benoit.daloze/code/ruby/miniruby(ruby_run_node+0x4c) [0x1044d3074] /Users/benoit.daloze/code/ruby/eval.c:322
/Users/benoit.daloze/code/ruby/miniruby(rb_main+0x1c) [0x104410980] ./main.c:42
/Users/benoit.daloze/code/ruby/miniruby(main) ./main.c:62
-- Other runtime information -----------------------------------------------
* Loaded script: ar_find_entry_hint_bug.rb
* Ruby Box: disabled
* Loaded features:
0 enumerator.so
1 monitor.so
2 thread.rb
3 fiber.so
4 rational.so
5 complex.so
6 pathname.so
7 ruby2_keywords.rb
8 set.rb
I can also repro on 3.2.11.
Reproducing locally seems harder on 3.3, 3.4 and 4.0 but I believe they are affected too since they use very similar code and miss those checks, so we should backport there as well.
Backporting to 3.2 may make sense too since this could be seen as a form of DoS.
The repro depends on how the ar_table and st_table layouts overlap and specific values written & read.
For completeness, I initially tried another repro, which shows unclear semantics but doesn't crash:
$break = false
class Key
def hash = 42
def eql?(o)
if $break
$break = false
H.shift
H[Key.new] = 42 until H.size == 8
o.equal?($key1)
else
equal?(o)
end
end
end
$key1 = key1 = Key.new
key2 = Key.new
key3 = Key.new
h = { key1 => 1, key2 => 2 }
H = h
p h
$break = true
p h[key3]
$ ./miniruby bug.rb
{#<Key:0x0000000101041200> => 1, #<Key:0x0000000101041240> => 2}
2
So reading a key considered eql? to key1 returns the value for key2, that seems quite surprising.
The underlying issue there is ar_find_entry_hint() returns an index but doesn't read the value, so the value is read after eql?, in which/during which the Hash may have changed.
Files