Bug #21952
closedRuby::Box double free at process exit when `fiddle/import` is required in multiple boxes
Description
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:
require "rubygems"
$:.unshift(*Gem::Specification.find_by_name("fiddle").full_require_paths)
require "fiddle/import"
Then run:
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:
free(): double free detected in tcache 2
[BUG] Aborted
The C backtrace points into Ruby's Ruby::Box cleanup path, including:
free_classext_for_boxcleanup_all_local_extensionsbox_entry_freerb_class_classext_freecvar_table_free_iruby_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_icleanup path again throughfree_classext_for_boxandbox_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
fiddletogether with shared libraries, but shared library loading is not required for the crash. -
dlloadis not necessary. - Reusing the same Ruby module name is not necessary.
- As a control case, one box requiring
fiddle/importand another box requiring a plain Ruby file exits normally. - The explicit
$:adjustment above is only there to avoid the separateRuby::Box#requireissue whererequire "fiddle/import"may otherwise fail withLoadErrorunderRUBY_BOX=1.
So this seems to be a separate crash bug in Ruby::Box cleanup triggered by loading fiddle/import in multiple boxes.
Files
Updated by katsyoshi (Katsuyoshi MATSUMOTO) 2 months ago
I managed to reduce this further.
This is reproducible with pure Ruby now, without fiddle/import. The current reduced reproducer is along these lines:
Ruby::Box.root.eval(<<~RUBY)
module M
@@x = 0
end
class A
include M
end
class B < A
end
RUBY
code = <<~REPRO
class ::B
@@x += 1
end
REPRO
b1 = Ruby::Box.new
b1.eval(code)
b2 = Ruby::Box.new
b2.eval(code)
On unpatched 4.0.1 with RUBY_BOX=1, this aborts at process exit with a double free.
So fiddle/import was not the root requirement here. What seems to matter is reopening a class from multiple boxes and
touching a class variable that comes from an ancestor/include chain.
Updated by byroot (Jean Boussier) about 2 months ago
- 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.
Updated by tagomoris (Satoshi Tagomori) about 2 months ago
As I commented, https://github.com/ruby/ruby/pull/16595 looks good to me.
Updated by byroot (Jean Boussier) about 2 months ago
- Status changed from Open to Closed
Applied in changeset git|c0d86a0103de7130943d54b4a290b76ec7e0c135.
class.c: rb_class_duplicate_classext also dup content of cvc_tbl
[Bug #21952]
Shallow copying the table result in the same memory being shared
between multiple box, causing double free when one of the box
is garbage collected.
Updated by k0kubun (Takashi Kokubun) 16 days ago
- Backport changed from 3.2: DONTNEED, 3.3: DONTNEED, 3.4: DONTNEED, 4.0: REQUIRED to 3.2: DONTNEED, 3.3: DONTNEED, 3.4: DONTNEED, 4.0: DONE
ruby_4_0 8539f0b386e2e42f8fe5ac12a2fd9e84872d8c7c merged revision(s) c0d86a0103de7130943d54b4a290b76ec7e0c135, 47e061277ac194a36659510bcf4f3190bde629a6.
Updated by byroot (Jean Boussier) 10 days ago
- Related to Bug #22072: [BUG] should have cvar cache entry added