Project

General

Profile

Actions

Bug #21333

open

heap-use-after-free caused by rehash during update

Added by cyruscyliu (Qiang Liu) 2 days ago. Updated about 9 hours ago.

Status:
Open
Assignee:
-
Target version:
-
ruby -v:
ruby 3.5.0dev (2025-05-13T08:35:34Z master a6435befa7) +PRISM [x86_64-linux]
[ruby-core:122054]

Description

Hi, we found a heap-use-after-free caused by rehash during update

$a = (1..1337).to_h { |k| [k, k] }
$a.update($b) { |k, o, n|
    $a.rehash
}
$ git log | head -n4
commit a6435befa76c2ae0525147f934bd9cd1914ffb8a
Author: Jean Boussier <jean.boussier@gmail.com>
Date:   Mon May 12 11:02:17 2025 +0200
$ ./ruby triaged/hash_update.rb
`RubyGems' were not loaded.
`error_highlight' was not loaded.
`did_you_mean' was not loaded.
`syntax_suggest' was not loaded.
=================================================================
==227694==ERROR: AddressSanitizer: heap-use-after-free on address 0x52f00000e410 at pc 0x64f4179fa0d5 bp 0x7ffec17cce10 sp 0x7ffec17cce08
WRITE of size 8 at 0x52f00000e410 thread T0
    #0 0x64f4179fa0d4 in rb_st_update /media/test/ruby/build/../st.c:1508:23
    #1 0x64f417814018 in rb_hash_stlike_update /media/test/ruby/build/../hash.c:1686:12
    #2 0x64f417814018 in tbl_update /media/test/ruby/build/../hash.c:1727:15
    #3 0x64f417823d09 in rb_hash_update_block_i /media/test/ruby/build/../hash.c:4162:5
    #4 0x64f417810576 in hash_ar_foreach_iter /media/test/ruby/build/../hash.c:1302:18
    #5 0x64f417810576 in ar_foreach_check /media/test/ruby/build/../hash.c:944:22
    #6 0x64f417810576 in hash_foreach_call /media/test/ruby/build/../hash.c:1435:15
    #7 0x64f4177a45c6 in rb_ensure /media/test/ruby/build/../eval.c:1074:18
    #8 0x64f417810329 in rb_hash_foreach /media/test/ruby/build/../hash.c:1463:9
    #9 0x64f41781b88f in rb_hash_update /media/test/ruby/build/../hash.c:4219:13
    #10 0x64f417b409db in vm_call_cfunc_with_frame_ /media/test/ruby/build/../vm_insnhelper.c:3798:11
    #11 0x64f417b2904a in vm_call_method_each_type /media/test/ruby/build/../vm_insnhelper.c:4776:16
    #12 0x64f417b28b13 in vm_call_method /media/test/ruby/build/../vm_insnhelper.c
    #13 0x64f417af0539 in vm_sendish /media/test/ruby/build/../vm_insnhelper.c:5995:15
    #14 0x64f417af0539 in vm_exec_core /media/test/ruby/build/../insns.def:851:11
    #15 0x64f417ae5cd7 in rb_vm_exec /media/test/ruby/build/../vm.c
    #16 0x64f4177a1bf0 in rb_ec_exec_node /media/test/ruby/build/../eval.c:281:9
    #17 0x64f4177a1bf0 in ruby_run_node /media/test/ruby/build/../eval.c:319:30
    #18 0x64f41779d2b0 in rb_main /media/test/ruby/build/../main.c:42:12
    #19 0x64f41779d2b0 in main /media/test/ruby/build/../main.c:62:12
    #20 0x72d96ba2a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #21 0x72d96ba2a28a in __libc_start_main csu/../csu/libc-start.c:360:3
    #22 0x64f4176c3d94 in _start (/media/test/ruby/build/ruby+0x14bd94) (BuildId: 5c3c6444d00499cf0254892dbef6454db4a9698a)

0x52f00000e410 is located 16 bytes inside of 49152-byte region [0x52f00000e400,0x52f00001a400)
freed by thread T0 here:
    #0 0x64f41775e94a in free (/media/test/ruby/build/ruby+0x1e694a) (BuildId: 5c3c6444d00499cf0254892dbef6454db4a9698a)
    #1 0x64f4177c944b in rb_gc_impl_free /media/test/ruby/build/../gc/default/default.c:8099:9
    #2 0x64f4177c944b in ruby_sized_xfree /media/test/ruby/build/../gc.c:5225:13
    #3 0x64f4177c944b in ruby_xfree /media/test/ruby/build/../gc.c:5236:5
    #4 0x64f417811e09 in hash_st_free /media/test/ruby/build/../hash.c:1218:5
    #5 0x64f417811e09 in rb_hash_rehash /media/test/ruby/build/../hash.c:2026:9
    #6 0x64f417b409db in vm_call_cfunc_with_frame_ /media/test/ruby/build/../vm_insnhelper.c:3798:11
    #7 0x64f417b2904a in vm_call_method_each_type /media/test/ruby/build/../vm_insnhelper.c:4776:16
    #8 0x64f417b28b13 in vm_call_method /media/test/ruby/build/../vm_insnhelper.c
    #9 0x64f417aed6f3 in vm_sendish /media/test/ruby/build/../vm_insnhelper.c:5995:15
    #10 0x64f417aed6f3 in vm_exec_core /media/test/ruby/build/../insns.def:899:11
    #11 0x64f417ae5cd7 in rb_vm_exec /media/test/ruby/build/../vm.c
    #12 0x64f417b5bdcc in invoke_iseq_block_from_c /media/test/ruby/build/../vm.c:1653:12
    #13 0x64f417b5bdcc in invoke_block_from_c_bh /media/test/ruby/build/../vm.c:1667:20
    #14 0x64f417b0e2a7 in vm_yield_with_cref /media/test/ruby/build/../vm.c:1704:12
    #15 0x64f417b0e2a7 in vm_yield /media/test/ruby/build/../vm.c:1712:12
    #16 0x64f417b0e2a7 in rb_yield_0 /media/test/ruby/build/../vm_eval.c:1361:12
    #17 0x64f417b0e2a7 in rb_yield_values /media/test/ruby/build/../vm_eval.c:1400:16
    #18 0x64f417823db5 in rb_hash_update_block_callback /media/test/ruby/build/../hash.c:4148:31
    #19 0x64f417823d48 in rb_hash_update_block_callback_insert /media/test/ruby/build/../hash.c:4157:1
    #20 0x64f4178225c7 in tbl_update_modify /media/test/ruby/build/../hash.c:1696:15
    #21 0x64f4179f9bea in rb_st_update /media/test/ruby/build/../st.c:1498:14
    #22 0x64f417814018 in rb_hash_stlike_update /media/test/ruby/build/../hash.c:1686:12
    #23 0x64f417814018 in tbl_update /media/test/ruby/build/../hash.c:1727:15
    #24 0x64f417823d09 in rb_hash_update_block_i /media/test/ruby/build/../hash.c:4162:5
    #25 0x64f417810576 in hash_ar_foreach_iter /media/test/ruby/build/../hash.c:1302:18
    #26 0x64f417810576 in ar_foreach_check /media/test/ruby/build/../hash.c:944:22
    #27 0x64f417810576 in hash_foreach_call /media/test/ruby/build/../hash.c:1435:15
    #28 0x64f4177a45c6 in rb_ensure /media/test/ruby/build/../eval.c:1074:18
    #29 0x64f417810329 in rb_hash_foreach /media/test/ruby/build/../hash.c:1463:9
    #30 0x64f41781b88f in rb_hash_update /media/test/ruby/build/../hash.c:4219:13
    #31 0x64f417b409db in vm_call_cfunc_with_frame_ /media/test/ruby/build/../vm_insnhelper.c:3798:11
    #32 0x64f417b2904a in vm_call_method_each_type /media/test/ruby/build/../vm_insnhelper.c:4776:16
    #33 0x64f417b28b13 in vm_call_method /media/test/ruby/build/../vm_insnhelper.c
    #34 0x64f417af0539 in vm_sendish /media/test/ruby/build/../vm_insnhelper.c:5995:15
    #35 0x64f417af0539 in vm_exec_core /media/test/ruby/build/../insns.def:851:11
    #36 0x64f417ae5cd7 in rb_vm_exec /media/test/ruby/build/../vm.c
    #37 0x64f4177a1bf0 in rb_ec_exec_node /media/test/ruby/build/../eval.c:281:9
    #38 0x64f4177a1bf0 in ruby_run_node /media/test/ruby/build/../eval.c:319:30
    #39 0x64f41779d2b0 in rb_main /media/test/ruby/build/../main.c:42:12
    #40 0x64f41779d2b0 in main /media/test/ruby/build/../main.c:62:12
    #41 0x72d96ba2a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #42 0x72d96ba2a28a in __libc_start_main csu/../csu/libc-start.c:360:3
    #43 0x64f4176c3d94 in _start (/media/test/ruby/build/ruby+0x14bd94) (BuildId: 5c3c6444d00499cf0254892dbef6454db4a9698a)

previously allocated by thread T0 here:
    #0 0x64f41775ebe3 in malloc (/media/test/ruby/build/ruby+0x1e6be3) (BuildId: 5c3c6444d00499cf0254892dbef6454db4a9698a)
    #1 0x64f417805df5 in rb_gc_impl_malloc /media/test/ruby/build/../gc/default/default.c:8114:5
    #2 0x64f4177d49f2 in ruby_xmalloc_body /media/test/ruby/build/../gc.c:5116:12
    #3 0x64f4177d49f2 in ruby_xmalloc /media/test/ruby/build/../gc.c:5106:34
    #4 0x64f4179f44a1 in rb_st_init_existing_table_with_size /media/test/ruby/build/../st.c:559:39
    #5 0x64f4179f65b0 in rb_st_init_table_with_size /media/test/ruby/build/../st.c:585:5
    #6 0x64f4179f65b0 in rebuild_table /media/test/ruby/build/../st.c:753:19
    #7 0x64f4179f65b0 in rebuild_table_if_necessary /media/test/ruby/build/../st.c:1125:9
    #8 0x64f4179f6f6b in st_add_direct_with_hash /media/test/ruby/build/../st.c:1187:5
    #9 0x64f4179f9cf3 in rb_st_update /media/test/ruby/build/../st.c:1502:13
    #10 0x64f417814018 in rb_hash_stlike_update /media/test/ruby/build/../hash.c:1686:12
    #11 0x64f417814018 in tbl_update /media/test/ruby/build/../hash.c:1727:15
    #12 0x64f417813e82 in rb_hash_aset /media/test/ruby/build/../hash.c
    #13 0x64f417814510 in rb_hash_set_pair /media/test/ruby/build/../hash.c:3715:5
    #14 0x64f417ae1bf3 in vm_yield_with_cfunc /media/test/ruby/build/../vm_insnhelper.c:5161:11
    #15 0x64f417b5b7f9 in invoke_block_from_c_bh /media/test/ruby/build/../vm.c:1672:16
    #16 0x64f417b0dcca in vm_yield_with_cref /media/test/ruby/build/../vm.c:1704:12
    #17 0x64f417b0dcca in vm_yield /media/test/ruby/build/../vm.c:1712:12
    #18 0x64f417b0dcca in rb_yield_0 /media/test/ruby/build/../vm_eval.c:1361:12
    #19 0x64f417b0dcca in rb_yield /media/test/ruby/build/../vm_eval.c
    #20 0x64f4179371c7 in range_each_fixnum_loop /media/test/ruby/build/../range.c:1059:9
    #21 0x64f4179371c7 in range_each /media/test/ruby/build/../range.c:1096:16
    #22 0x64f417b53fd5 in vm_call0_cfunc_with_frame /media/test/ruby/build/../vm_eval.c:164:15
    #23 0x64f417b53fd5 in vm_call0_cfunc /media/test/ruby/build/../vm_eval.c:178:12
    #24 0x64f417b53fd5 in vm_call0_body /media/test/ruby/build/../vm_eval.c:229:15
    #25 0x64f417b57a0a in vm_call0_cc /media/test/ruby/build/../vm_eval.c:101:12
    #26 0x64f417b57a0a in rb_call0 /media/test/ruby/build/../vm_eval.c:571:12
    #27 0x64f417b0f5f5 in rb_call /media/test/ruby/build/../vm_eval.c:890:12
    #28 0x64f417b0f5f5 in iterate_method /media/test/ruby/build/../vm_eval.c:1545:12
    #29 0x64f417b0fbf5 in rb_iterate0 /media/test/ruby/build/../vm_eval.c:1487:18
    #30 0x64f417b0f287 in rb_iterate_internal /media/test/ruby/build/../vm_eval.c:1519:12
    #31 0x64f417b0f287 in rb_block_call_kw /media/test/ruby/build/../vm_eval.c:1568:12
    #32 0x64f417b0f287 in rb_block_call /media/test/ruby/build/../vm_eval.c:1554:12
    #33 0x64f417dad5e0 in enum_hashify_into /media/test/ruby/build/../enum.c:743:5
    #34 0x64f417dad5e0 in enum_hashify /media/test/ruby/build/../enum.c:750:12
    #35 0x64f417dad5e0 in enum_to_h /media/test/ruby/build/../enum.c:791:12
    #36 0x64f417b409db in vm_call_cfunc_with_frame_ /media/test/ruby/build/../vm_insnhelper.c:3798:11
    #37 0x64f417b2904a in vm_call_method_each_type /media/test/ruby/build/../vm_insnhelper.c:4776:16
    #38 0x64f417b28b13 in vm_call_method /media/test/ruby/build/../vm_insnhelper.c
    #39 0x64f417af0539 in vm_sendish /media/test/ruby/build/../vm_insnhelper.c:5995:15
    #40 0x64f417af0539 in vm_exec_core /media/test/ruby/build/../insns.def:851:11
    #41 0x64f417ae5cd7 in rb_vm_exec /media/test/ruby/build/../vm.c
    #42 0x64f4177a1bf0 in rb_ec_exec_node /media/test/ruby/build/../eval.c:281:9
    #43 0x64f4177a1bf0 in ruby_run_node /media/test/ruby/build/../eval.c:319:30
    #44 0x64f41779d2b0 in rb_main /media/test/ruby/build/../main.c:42:12
    #45 0x64f41779d2b0 in main /media/test/ruby/build/../main.c:62:12
    #46 0x72d96ba2a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
    #47 0x72d96ba2a28a in __libc_start_main csu/../csu/libc-start.c:360:3
    #48 0x64f4176c3d94 in _start (/media/test/ruby/build/ruby+0x14bd94) (BuildId: 5c3c6444d00499cf0254892dbef6454db4a9698a)

SUMMARY: AddressSanitizer: heap-use-after-free /media/test/ruby/build/../st.c:1508:23 in rb_st_update
Shadow bytes around the buggy address:
  0x52f00000e180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x52f00000e200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x52f00000e280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x52f00000e300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x52f00000e380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x52f00000e400: fd fd[fd]fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x52f00000e480: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x52f00000e500: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x52f00000e580: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x52f00000e600: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x52f00000e680: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==227694==ABORTING
triaged/hash_update.rb:3: [BUG] ASAN error
ruby 3.5.0dev (2025-05-13T08:35:34Z master a6435befa7) +PRISM [x86_64-linux]

-- Control frame information -----------------------------------------------
c:0003 p:---- s:0011 e:000010 CFUNC  :update
c:0002 p:0029 s:0006 e:000005 EVAL   triaged/hash_update.rb:3 [FINISH]
c:0001 p:0000 s:0003 E:000be0 DUMMY  [FINISH]

-- Ruby level backtrace information ----------------------------------------
triaged/hash_update.rb:3:in '<main>'
triaged/hash_update.rb:3:in 'update'

-- Threading information ---------------------------------------------------
Total ractor count: 1
Ruby thread count for this ractor: 1

-- C level backtrace information -------------------------------------------
./ruby(___interceptor_backtrace) [0x64f4177086aa]
/media/test/ruby/build/ruby(rb_print_backtrace+0x14) [0x64f417e7bc97] /media/test/ruby/build/../vm_dump.c:843
/media/test/ruby/build/ruby(rb_vm_bugreport) /media/test/ruby/build/../vm_dump.c:1175
/media/test/ruby/build/ruby(rb_bug_without_die_internal+0x23c) [0x64f417dd016c] /media/test/ruby/build/../error.c:1097
/media/test/ruby/build/ruby(rb_bug_without_die+0x127) [0x64f417dcfe87] /media/test/ruby/build/../error.c:1106
./ruby(0x64f4177832aa) [0x64f4177832aa]
./ruby(0x64f417763a2f) [0x64f417763a2f]
./ruby(0x64f417766ab5) [0x64f417766ab5]
./ruby(__asan_report_store8) [0x64f417767bdf]
/media/test/ruby/build/ruby(rb_st_update+0xcd5) [0x64f4179fa0d5] /media/test/ruby/build/../st.c:1508
/media/test/ruby/build/ruby(tbl_update+0x139) [0x64f417814019] /media/test/ruby/build/../hash.c:1686
/media/test/ruby/build/ruby(rb_hash_update_block_i+0x3a) [0x64f417823d0a] /media/test/ruby/build/../hash.c:4162
/media/test/ruby/build/ruby(hash_ar_foreach_iter+0x10) [0x64f417810577] /media/test/ruby/build/../hash.c:1302
/media/test/ruby/build/ruby(ar_foreach_check) /media/test/ruby/build/../hash.c:944
/media/test/ruby/build/ruby(hash_foreach_call) /media/test/ruby/build/../hash.c:1435
/media/test/ruby/build/ruby(rb_ensure+0x257) [0x64f4177a45c7] /media/test/ruby/build/../eval.c:1074
/media/test/ruby/build/ruby(rb_hash_foreach+0x23a) [0x64f41781032a] /media/test/ruby/build/../hash.c:1463
/media/test/ruby/build/ruby(rb_hash_update+0xe0) [0x64f41781b890] /media/test/ruby/build/../hash.c:4219
/media/test/ruby/build/ruby(vm_cfp_consistent_p+0x0) [0x64f417b409dc] ../vm_insnhelper.c:3798
/media/test/ruby/build/ruby(vm_call_cfunc_with_frame_) ../vm_insnhelper.c:3800
/media/test/ruby/build/ruby(vm_call_method_each_type+0x27b) [0x64f417b2904b] ../vm_insnhelper.c:4776
./ruby(vm_call_method+0x2d4) [0x64f417b28b14]
/media/test/ruby/build/ruby(vm_sendish+0x11f) [0x64f417af053a] ../vm_insnhelper.c:5995
/media/test/ruby/build/ruby(vm_exec_core) ../insns.def:851
./ruby(vm_exec_loop+0x0) [0x64f417ae5cd8]
/media/test/ruby/build/ruby(rb_vm_exec) /media/test/ruby/build/../vm.c:2626
/media/test/ruby/build/ruby(rb_ec_exec_node+0x53) [0x64f4177a1bf1] /media/test/ruby/build/../eval.c:281
/media/test/ruby/build/ruby(ruby_run_node) /media/test/ruby/build/../eval.c:319
/media/test/ruby/build/ruby(rb_main+0x29) [0x64f41779d2b1] /media/test/ruby/build/../main.c:42
/media/test/ruby/build/ruby(main) /media/test/ruby/build/../main.c:62
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_call_main+0x7a) [0x72d96ba2a1ca] ../sysdeps/nptl/libc_start_call_main.h:58
/lib/x86_64-linux-gnu/libc.so.6(call_init+0x0) [0x72d96ba2a28b] ../csu/libc-start.c:360
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main_impl) ../csu/libc-start.c:347
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main) (null):0
./ruby(_start) [0x64f4176c3d95]

-- Other runtime information -----------------------------------------------

* Loaded script: triaged/hash_update.rb

* Loaded features:

    0 enumerator.so
    1 thread.rb
    2 fiber.so
    3 rational.so
    4 complex.so
    5 ruby2_keywords.rb
    6 set.rb

Updated by byroot (Jean Boussier) 1 day ago

The reproduction script seem missing something? $b is nil here so the script simply fail.

Updated by byroot (Jean Boussier) 1 day ago

$a = (1..1337).to_h { |k| [k, k] }
$b = (1..1337).to_h { |k| [k, k * 2] }
$a.update($b) { |k, o, n|
    $a.rehash
}

Seem to do the trick.

Updated by byroot (Jean Boussier) 1 day ago

So #update with block isn't considered iteration, so it doesn't increase the iterlevel hence #rehash is mistakenly allowed.

At first sight the fix is to treat #update like an iteration when yielding.

Updated by cyruscyliu (Qiang Liu) 1 day ago

byroot (Jean Boussier) wrote in #note-3:

So #update with block isn't considered iteration, so it doesn't increase the iterlevel hence #rehash is mistakenly allowed.

At first sight the fix is to treat #update like an iteration when yielding.

byroot (Jean Boussier) wrote in #note-2:

$a = (1..1337).to_h { |k| [k, k] }
$b = (1..1337).to_h { |k| [k, k * 2] }
$a.update($b) { |k, o, n|
    $a.rehash
}

Seem to do the trick.

byroot (Jean Boussier) wrote in #note-2:

$a = (1..1337).to_h { |k| [k, k] }
$b = (1..1337).to_h { |k| [k, k * 2] }
$a.update($b) { |k, o, n|
    $a.rehash
}

Seem to do the trick.

My bad. Forgot one line. Here is my poc.

$b = {"a" => 1, 1 => "b", 2 => 1}
$a = (1..1337).to_h { |k| [k, k] }
$a.update($b) { |k, o, n|
    $a.rehash
}
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0