[ruby-dev:52206] [Ruby Bug#21952] Ruby::Box double free at process exit when `fiddle/import` is required in multiple boxes
Issue #21952 has been reported by katsyoshi (Katsuyoshi MATSUMOTO). ---------------------------------------- Bug #21952: Ruby::Box double free at process exit when `fiddle/import` is required in multiple boxes https://bugs.ruby-lang.org/issues/21952 * Author: katsyoshi (Katsuyoshi MATSUMOTO) * Status: Open * ruby -v: ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [x86_64-linux] * Backport: 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- I found what looks like a separate `Ruby::Box` bug from the existing `require` and `LoadError` issues such as `#21760`. This is not a `LoadError` case. I was able to reduce it to a reproducer where requiring `fiddle/import` from multiple boxes causes Ruby to abort at process exit with a double free. Environment: - ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [x86_64-linux] - Linux x86_64 - `RUBY_BOX=1` ## Reproducer Create `/tmp/fiddle_require.rb`: ```ruby require "rubygems" $:.unshift(*Gem::Specification.find_by_name("fiddle").full_require_paths) require "fiddle/import" ``` Then run: ```sh RUBY_BOX=1 ruby -e 'b1 = Ruby::Box.new; b1.require("/tmp/fiddle_require.rb"); b2 = Ruby::Box.new; b2.require("/tmp/fiddle_require.rb")' ``` ## Expected behavior Both `Ruby::Box#require` calls succeed, and Ruby exits normally. ## Actual behavior Ruby aborts at process exit with: ```text free(): double free detected in tcache 2 [BUG] Aborted ``` The C backtrace points into Ruby's `Ruby::Box` cleanup path, including: - `free_classext_for_box` - `cleanup_all_local_extensions` - `box_entry_free` - `rb_class_classext_free` - `cvar_table_free_i` - `ruby_sized_xfree` ## ASAN I also rebuilt Ruby with AddressSanitizer and reran the same reproducer. ASAN reports: - `AddressSanitizer: attempting to call malloc_usable_size() for pointer which is not owned` - the pointer was originally allocated by `rb_cvar_set` - it was then freed once via `cvar_table_free_i` - and later reached the same `cvar_table_free_i` cleanup path again through `free_classext_for_box` and `box_entry_free` This makes it look like a class-variable-related allocation created while loading `fiddle/import` is being freed twice during `Ruby::Box` cleanup. ## Notes - I first noticed this while testing `fiddle` together with shared libraries, but shared library loading is not required for the crash. - `dlload` is not necessary. - Reusing the same Ruby module name is not necessary. - As a control case, one box requiring `fiddle/import` and another box requiring a plain Ruby file exits normally. - The explicit `$:` adjustment above is only there to avoid the separate `Ruby::Box#require` issue where `require "fiddle/import"` may otherwise fail with `LoadError` under `RUBY_BOX=1`. So this seems to be a separate crash bug in `Ruby::Box` cleanup triggered by loading `fiddle/import` in multiple boxes. ---Files-------------------------------- asan-sample.txt (13.7 KB) -- https://bugs.ruby-lang.org/
Issue #21952 has been updated by byroot (Jean Boussier). Assignee set to tagomoris (Satoshi Tagomori) Backport changed from 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN to 3.2: DONTNEED, 3.3: DONTNEED, 3.4: DONTNEED, 4.0: REQUIRED So it appears that when duplicating a class in another box, we copy the class variables table, but not its entries, causing both boxes to think they own that memory, resulting in a double free. I have a fix for the specific reproducer: https://github.com/ruby/ruby/pull/16594, however I'm not familiar enough with box design to know for sure if there isn't another way this situation could occur. ---------------------------------------- Bug #21952: Ruby::Box double free at process exit when `fiddle/import` is required in multiple boxes https://bugs.ruby-lang.org/issues/21952#change-116877 * Author: katsyoshi (Katsuyoshi MATSUMOTO) * Status: Open * Assignee: tagomoris (Satoshi Tagomori) * ruby -v: ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [x86_64-linux] * Backport: 3.2: DONTNEED, 3.3: DONTNEED, 3.4: DONTNEED, 4.0: REQUIRED ---------------------------------------- I found what looks like a separate `Ruby::Box` bug from the existing `require` and `LoadError` issues such as `#21760`. This is not a `LoadError` case. I was able to reduce it to a reproducer where requiring `fiddle/import` from multiple boxes causes Ruby to abort at process exit with a double free. Environment: - ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [x86_64-linux] - Linux x86_64 - `RUBY_BOX=1` ## Reproducer Create `/tmp/fiddle_require.rb`: ```ruby require "rubygems" $:.unshift(*Gem::Specification.find_by_name("fiddle").full_require_paths) require "fiddle/import" ``` Then run: ```sh RUBY_BOX=1 ruby -e 'b1 = Ruby::Box.new; b1.require("/tmp/fiddle_require.rb"); b2 = Ruby::Box.new; b2.require("/tmp/fiddle_require.rb")' ``` ## Expected behavior Both `Ruby::Box#require` calls succeed, and Ruby exits normally. ## Actual behavior Ruby aborts at process exit with: ```text free(): double free detected in tcache 2 [BUG] Aborted ``` The C backtrace points into Ruby's `Ruby::Box` cleanup path, including: - `free_classext_for_box` - `cleanup_all_local_extensions` - `box_entry_free` - `rb_class_classext_free` - `cvar_table_free_i` - `ruby_sized_xfree` ## ASAN I also rebuilt Ruby with AddressSanitizer and reran the same reproducer. ASAN reports: - `AddressSanitizer: attempting to call malloc_usable_size() for pointer which is not owned` - the pointer was originally allocated by `rb_cvar_set` - it was then freed once via `cvar_table_free_i` - and later reached the same `cvar_table_free_i` cleanup path again through `free_classext_for_box` and `box_entry_free` This makes it look like a class-variable-related allocation created while loading `fiddle/import` is being freed twice during `Ruby::Box` cleanup. ## Notes - I first noticed this while testing `fiddle` together with shared libraries, but shared library loading is not required for the crash. - `dlload` is not necessary. - Reusing the same Ruby module name is not necessary. - As a control case, one box requiring `fiddle/import` and another box requiring a plain Ruby file exits normally. - The explicit `$:` adjustment above is only there to avoid the separate `Ruby::Box#require` issue where `require "fiddle/import"` may otherwise fail with `LoadError` under `RUBY_BOX=1`. So this seems to be a separate crash bug in `Ruby::Box` cleanup triggered by loading `fiddle/import` in multiple boxes. ---Files-------------------------------- asan-sample.txt (13.7 KB) -- https://bugs.ruby-lang.org/
participants (2)
-
byroot (Jean Boussier) -
katsyoshi (Katsuyoshi MATSUMOTO)