Fix crash when $LOADED_FEATURES is modified during require

[Bug #21567]

When we require an object that is not a string, it will attempt to convert
it to a string by calling to_str on it. If we modify the $LOADED_FEATURES
array while it calls to_str, Ruby can crash because it can end up inserting
the string in the wrong index in the array.

For example, the following script crashes:

    require "tempfile"

    class MyString
      def initialize(path)
        @path = path
      end

      def to_str
        $LOADED_FEATURES.clear
        @path
      end

      def to_path = @path
    end

    def create_ruby_file = Tempfile.create(["test", ".rb"]).path

    require MyString.new(create_ruby_file)
    $LOADED_FEATURES.unshift(create_ruby_file)
    $LOADED_FEATURES << MyString.new(create_ruby_file)
    require create_ruby_file

Crash log:

    test.rb:21: [BUG] Segmentation fault at 0x0000000000000004
    ruby 3.5.0dev (2025-09-09T09:29:35Z master ce94add7fb) +PRISM [arm64-darwin24]

    -- 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:---- s:0011 e:000010 CFUNC  :require
    c:0002 p:0076 s:0006 e:000005 EVAL   test.rb:21 [FINISH]
    c:0001 p:0000 s:0003 E:0001b0 DUMMY  [FINISH]

    -- Ruby level backtrace information ----------------------------------------
    test.rb:21:in '<main>'
    test.rb:21:in 'require'

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

    -- Machine register context ------------------------------------------------
      x0: 0x0000000000000004  x1: 0x000000000000c800  x2: 0x0000000000000000
      x3: 0x0000000000000000  x4: 0x0000000000000205  x5: 0x0000000000000000
      x6: 0x0000000000000000  x7: 0x0000000000000001 x18: 0x0000000000000000
    x19: 0x0000000209dfc0b0 x20: 0x0000000209dfc018 x21: 0x000000016ee8ab58
    x22: 0x0fffffff0009d71d x23: 0x0000000209dfc018 x24: 0x0000000209dfc150
    x25: 0x000000016ee8acc0 x26: 0x0000000000000000 x27: 0x0000000000000000
    x28: 0x0000000000000000  lr: 0x0000000101244140  fp: 0x000000016ee887f0
      sp: 0x000000016ee887d0

    -- C level backtrace information -------------------------------------------
    miniruby(rb_print_backtrace+0x24) [0x101317b08] vm_dump.c:843
    miniruby(rb_print_backtrace) (null):0
    miniruby(rb_vm_bugreport+0x26c) [0x101317d94] vm_dump.c:1175
    miniruby(rb_bug_for_fatal_signal+0xa4) [0x10105ddac] error.c:1130
    miniruby(sig_do_nothing+0x0) [0x1012278c0] signal.c:948
    miniruby(sigsegv) (null):0
    /usr/lib/system/libsystem_platform.dylib(_sigtramp+0x38) [0x19c1216a4]
    miniruby(rb_str_new_frozen+0x1c) [0x101244140] string.c:1495
    miniruby(rb_check_realpath_internal+0x68) [0x101077804] file.c:4679
    miniruby(rb_check_realpath+0x2c) [0x101077aa4] file.c:4765
    miniruby(get_loaded_features_index+0x37c) [0x1010f9c94] load.c:467
    miniruby(rb_feature_p+0xd0) [0x1010f8174] load.c:582
    miniruby(search_required+0xac) [0x1010f6ad4] load.c:1193
    miniruby(require_internal+0x274) [0x1010f7518] load.c:1424
    miniruby(rb_require_string_internal+0x94) [0x1010f6830] load.c:1571
    miniruby(rb_require_string+0x58) [0x1010f66e8] load.c:1557
    miniruby(rb_f_require+0x1c) [0x1010f6684] load.c:1150
    miniruby(ractor_safe_call_cfunc_1+0x38) [0x101306c28] vm_insnhelper.c:3696
    miniruby(vm_call_cfunc_with_frame_+0x250) [0x1012f857c] vm_insnhelper.c:3873
    miniruby(vm_call_cfunc_with_frame+0x6c) [0x1012f8834] vm_insnhelper.c:3919
    miniruby(vm_sendish+0x1a8) [0x1012c990c] vm_insnhelper.c:6087
    miniruby(vm_exec_core+0x4050) [0x1012cfb48] insns.def:900
    miniruby(vm_exec_loop+0x80) [0x1012e5448] vm.c:2666
    miniruby(rb_vm_exec+0x134) [0x1012c9b40] vm.c:2645
    miniruby(rb_iseq_eval_main+0x34) [0x1012e5628] vm.c:2919
    miniruby(rb_ec_exec_node+0xe4) [0x10106d094] eval.c:282
    miniruby(ruby_run_node+0x94) [0x10106cf64] eval.c:320
    miniruby(rb_main+0x40) [0x100f7499c] main.c:42
    miniruby(main+0x60) [0x100f74928] main.c:62
This commit is contained in:
Peter Zhu 2025-09-09 13:48:49 -04:00
parent 624538ba92
commit 928fea3bfa
2 changed files with 33 additions and 0 deletions

9
load.c
View File

@ -447,6 +447,11 @@ get_loaded_features_index(vm_ns_t *vm_ns)
VALUE previous_realpath_map = rb_hash_dup(realpath_map);
rb_hash_clear(realpaths);
rb_hash_clear(realpath_map);
/* We have to make a copy of features here because the StringValue call
* below could call a Ruby method, which could modify $LOADED_FEATURES
* and cause it to be corrupt. */
features = rb_ary_resurrect(features);
for (i = 0; i < RARRAY_LEN(features); i++) {
VALUE entry, as_str;
as_str = entry = rb_ary_entry(features, i);
@ -456,6 +461,10 @@ get_loaded_features_index(vm_ns_t *vm_ns)
rb_ary_store(features, i, as_str);
features_index_add(vm_ns, as_str, INT2FIX(i));
}
/* The user modified $LOADED_FEATURES, so we should restore the changes. */
if (!rb_ary_shared_with_p(features, CURRENT_NS_LOADED_FEATURES(vm_ns))) {
rb_ary_replace(CURRENT_NS_LOADED_FEATURES(vm_ns), features);
}
reset_loaded_features_snapshot(vm_ns);
features = CURRENT_NS_LOADED_FEATURES_SNAPSHOT(vm_ns);

View File

@ -840,6 +840,30 @@ class TestRequire < Test::Unit::TestCase
p :ok
end;
}
# [Bug #21567]
assert_separately(%w[-rtempfile], "#{<<~"begin;"}\n#{<<~"end;"}")
begin;
class MyString
def initialize(path)
@path = path
end
def to_str
$LOADED_FEATURES.clear
@path
end
def to_path = @path
end
def create_ruby_file = Tempfile.create(["test", ".rb"]).path
require MyString.new(create_ruby_file)
$LOADED_FEATURES.unshift(create_ruby_file)
$LOADED_FEATURES << MyString.new(create_ruby_file)
require create_ruby_file
end;
end
def test_loading_fifo_threading_raise