dearblue · GitHub

After the release of mruby-3.4.0, some methods defined in mruby-array-ext were switched to C implementations.

https://github.com/mruby/mruby/commits/master/mrbgems/mruby-array-ext/mrblib?since=2025-06-01&until=2025-10-31

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 where khash is used within loops or in nested loops.

    Nested loops require careful modification.

    • Need to always check index validity in nested loops.
      Because mrb_vm_exec() may change the array length when called.
    • Need to protect array elements with mrb_gc_protect() if cached beforehand.
      This is because mrb_vm_exec() may cause array modifications that break references.
    • Need to call mrb_ary_modify() before directly modifying array elements.
      This is because mrb_vm_exec() may set the MRB_ARY_SHARED flag or cause the array to enter a frozen state.
  • Memory leaks or use-after-free issues with objects via khash.
    Objects defined with KHASH_DECLARE(ary_set, ...) have their obj.hash or obj.eql? methods called via kh_get() or kh_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 by kh_put(), and raise an exception.
    In this case, the ary_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 by kh_put() to swap objects contained in the original array with another one.
    If an object registered in the khash is garbage collected, and subsequent processing references it via the khash, 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)

Read the original on github.com ↗