After the release of mruby-3.4.0, some methods defined in mruby-array-ext were switched to C implementations.
I only briefly glanced at the code and haven't thoroughly examined it, but there are some issues.
-
After calling
mrb_vm_exec(), references to invalidated pointers or comparisons against array lengths.
This is particularly observed in multiple locations wherekhashis used within loops or in nested loops.Nested loops require careful modification.
- Need to always check index validity in nested loops.
Becausemrb_vm_exec()may change the array length when called. - Need to protect array elements with
mrb_gc_protect()if cached beforehand.
This is becausemrb_vm_exec()may cause array modifications that break references. - Need to call
mrb_ary_modify()before directly modifying array elements.
This is becausemrb_vm_exec()may set theMRB_ARY_SHAREDflag or cause the array to enter a frozen state.
- Need to always check index validity in nested loops.
-
Memory leaks or use-after-free issues with objects via
khash.
Objects defined withKHASH_DECLARE(ary_set, ...)have theirobj.hashorobj.eql?methods called viakh_get()orkh_put().
In these methods, causing an exception leads to memory leaks, while modifying the array contents results in use-after-free conditions for the object.
Regarding issues involving the khash, I couldn't find any recent reports, so I tried to demonstrate the issue.
memory leaks with khash
-
summary
Define the
obj.eql?method, which is indirectly called bykh_put(), and raise an exception.
In this case, theary_destroy_temp_set()function is not called, so the khash structure data is not freed.The demonstration is performed with
Array#intersect?, but I consider that similar issues arise with other methods as well. -
build:
rake -m MRUBY_CONFIG=host-debug clean all -
file: memleak.rb
a = [nil] * 40 a[30] = Object.new class << a[30] def eql?(o) 0 / 0 end end a.intersect?((0...50).to_a)
-
result with valgrind:
% valgrind --leak-check=full -- build/host/bin/mruby memleak.rb ==33886== Memcheck, a memory error detector ==33886== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al. ==33886== Using Valgrind-3.25.1 and LibVEX; rerun with -h for copyright info ==33886== Command: build/host/bin/mruby memleak.rb ==33886== trace (most recent call last): [2] memleak.rb:10 [1] memleak.rb:10:in intersect? memleak.rb:6:in eql?: divided by 0 (ZeroDivisionError) ==33886== ==33886== HEAP SUMMARY: ==33886== in use at exit: 528 bytes in 1 blocks ==33886== total heap usage: 774 allocs, 773 frees, 230,684 bytes allocated ==33886== ==33886== 528 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==33886== at 0x4853371: realloc (vg_replace_malloc.c:1807) ==33886== by 0x27E4A3: mrb_realloc_simple (gc.c:202) ==33886== by 0x27E4A3: mrb_realloc (gc.c:216) ==33886== by 0x27E4A3: mrb_malloc (gc.c:232) ==33886== by 0x2D989B: kh__alloc_ary_set (array.c:26) ==33886== by 0x2D989B: kh_init_data_ary_set (array.c:26) ==33886== by 0x2D989B: ary_init_temp_set (array.c:404) ==33886== by 0x2D989B: ary_intersect_p (array.c:788) ==33886== by 0x2A5ED9: mrb_vm_exec (vm.c:0) ==33886== by 0x2B0D81: mrb_load_exec (parse.y:7342) ==33886== by 0x2B109F: mrb_load_detect_file_cxt (parse.y:7385) ==33886== by 0x260065: main (mruby.c:353) ==33886== ==33886== LEAK SUMMARY: ==33886== definitely lost: 528 bytes in 1 blocks ==33886== indirectly lost: 0 bytes in 0 blocks ==33886== possibly lost: 0 bytes in 0 blocks ==33886== still reachable: 0 bytes in 0 blocks ==33886== suppressed: 0 bytes in 0 blocks ==33886== ==33886== For lists of detected and suppressed errors, rerun with: -s ==33886== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
object use-after-free with khash
-
summary
Define the
obj.eql?method indirectly called bykh_put()to swap objects contained in the original array with another one.
If an object registered in thekhashis garbage collected, and subsequent processing references it via thekhash, a use-after-free condition is established.The demonstration is performed with
Array#intersect?, but I consider that similar issues arise with other methods as well. -
build:
rake -m MRUBY_CONFIG=host-debug clean all -
file: uaf.rb
$a = Array.new(50) { Object.new } $b = Array.new(40) { Object.new } class << $b.last def eql?(o) $b.map! { nil } GC.start super end end $a.intersect?($b)
-
result with valgrind:
% valgrind -- build/host/bin/mruby uaf.rb ==65146== Memcheck, a memory error detector ==65146== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al. ==65146== Using Valgrind-3.25.1 and LibVEX; rerun with -h for copyright info ==65146== Command: build/host/bin/mruby uaf.rb ==65146== ==65146== Invalid read of size 8 ==65146== at 0x2BD2A9: mrb_class (class.h:30) ==65146== by 0x2BCA41: mrb_funcall_with_block (vm.c:807) ==65146== by 0x2BC43F: mrb_funcall_argv (vm.c:861) ==65146== by 0x2A7B0D: mrb_eql (object.c:859) ==65146== by 0x30B1A2: ary_set_equal_func (array.c:22) ==65146== by 0x30AFE4: kh_get_ary_set (array.c:26) ==65146== by 0x30D24A: ary_intersect_p (array.c:792) ==65146== by 0x2C3F2E: mrb_vm_exec (vm.c:2254) ==65146== by 0x2BDDCD: mrb_vm_run (vm.c:1613) ==65146== by 0x2D55BD: mrb_top_run (vm.c:3441) ==65146== by 0x2DFCF5: mrb_load_exec (parse.y:7342) ==65146== by 0x2DFFE9: mrb_load_detect_file_cxt (parse.y:7385) ==65146== Address 0x5666230 is 49,136 bytes inside a block of size 49,184 free'd ==65146== at 0x48502BC: free (vg_replace_malloc.c:993) ==65146== by 0x28E63F: mrb_basic_alloc_func (allocf.c:30) ==65146== by 0x28BABC: mrb_free (gc.c:265) ==65146== by 0x28DADB: incremental_sweep_phase (gc.c:1100) ==65146== by 0x28D489: incremental_gc (gc.c:1149) ==65146== by 0x28C6E3: incremental_gc_finish (gc.c:1165) ==65146== by 0x28C7AC: clear_all_old (gc.c:1189) ==65146== by 0x28B83B: mrb_full_gc (gc.c:1253) ==65146== by 0x28CC9B: gc_start (gc.c:1341) ==65146== by 0x2C3F2E: mrb_vm_exec (vm.c:2254) ==65146== by 0x2BDDCD: mrb_vm_run (vm.c:1613) ==65146== by 0x2BD65D: mrb_run (vm.c:3411) ==65146== Block was alloc'd at ==65146== at 0x4853371: realloc (vg_replace_malloc.c:1807) ==65146== by 0x28E656: mrb_basic_alloc_func (allocf.c:35) ==65146== by 0x28B770: mrb_realloc_simple (gc.c:202) ==65146== by 0x28B914: mrb_realloc (gc.c:216) ==65146== by 0x28B9A0: mrb_malloc (gc.c:232) ==65146== by 0x28BA4B: mrb_calloc (gc.c:251) ==65146== by 0x28BF32: add_heap (gc.c:305) ==65146== by 0x28BC98: mrb_obj_alloc (gc.c:512) ==65146== by 0x277E6E: mrb_instance_alloc (class.c:2816) ==65146== by 0x2C3F2E: mrb_vm_exec (vm.c:2254) ==65146== by 0x2BDDCD: mrb_vm_run (vm.c:1613) ==65146== by 0x2BD65D: mrb_run (vm.c:3411) ==65146== ==65146== ==65146== HEAP SUMMARY: ==65146== in use at exit: 0 bytes in 0 blocks ==65146== total heap usage: 860 allocs, 860 frees, 365,158 bytes allocated ==65146== ==65146== All heap blocks were freed -- no leaks are possible ==65146== ==65146== For lists of detected and suppressed errors, rerun with: -s ==65146== ERROR SUMMARY: 3 errors from 1 contexts (suppressed: 0 from 0)