Project

General

Profile

Bug #22120

Updated by Eregon (Benoit Daloze) 3 months ago

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: 
 ```c 
 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?`. 
 ```ruby 
 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.3 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: 
 ```ruby 
 $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.

Back