Project

General

Profile

Actions

Bug #21952

closed

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) 3 months ago. Updated 16 days ago.

Status:
Closed
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

Related issues 1 (0 open1 closed)

Related to Ruby - Bug #22072: [BUG] should have cvar cache entryClosedActions

Updated by katsyoshi (Katsuyoshi MATSUMOTO) 2 months 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) about 2 months 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.

Updated by tagomoris (Satoshi Tagomori) about 2 months ago Actions #3

As I commented, https://github.com/ruby/ruby/pull/16595 looks good to me.

Updated by byroot (Jean Boussier) about 2 months ago Actions #4

  • 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 Actions #5

  • 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

Updated by byroot (Jean Boussier) 10 days ago Actions #6

  • Related to Bug #22072: [BUG] should have cvar cache entry added
Actions

Also available in: PDF Atom