Project

General

Profile

Actions

Bug #21952

open

Ruby::Box double free at process exit when `fiddle/import` is required in multiple boxes

Bug #21952: Ruby::Box double free at process exit when `fiddle/import` is required in multiple boxes

Added by katsyoshi (Katsuyoshi MATSUMOTO) 24 days ago. Updated 8 days ago.

Status:
Open
Target version:
-
ruby -v:
ruby 4.0.1 (2026-01-13 revision e04267a14b) +PRISM [x86_64-linux]
[ruby-dev:<unknown>]

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_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) asan-sample.txt AddressSanitizer output for the minimal Ruby::Box + fiddle/import reproducer. katsyoshi (Katsuyoshi MATSUMOTO), 03/12/2026 02:07 PM

Updated by katsyoshi (Katsuyoshi MATSUMOTO) 8 days ago Actions #1

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) 8 days ago Actions #2

  • 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.

Actions

Also available in: PDF Atom