Project

General

Profile

Actions

Bug #22403

open

Ruby::Box: a class that gets a box-specific classext is never freed

Bug #22403: Ruby::Box: a class that gets a box-specific classext is never freed

Added by shluboto (Andreas Busold) about 7 hours ago.

Status:
Open
Assignee:
-
Target version:
-
ruby -v:
ruby 4.1.0dev (2026-10-04T10:47:58Z master 76aa225e9a) +PRISM [x86_64-darwin25]
[ruby-core:126918]

Description

With RUBY_BOX=1, a class that receives a box-specific classext is never freed while its box lives, and for the main box that is the life of the process. Freezing the clone of a frozen object that extends a module creates one such class per call, so a program that does this per request grows without bound. No user box is needed: setting the environment variable is enough.

base = Object.new.extend(Module.new).freeze

GC.start
before = ObjectSpace.count_objects[:T_CLASS]
5_000.times { base.clone.freeze }
GC.start

p ObjectSpace.count_objects[:T_CLASS] - before
$ ruby repro.rb
0
$ RUBY_BOX=1 ruby repro.rb
5000

Expected: about 0 with RUBY_BOX=1 as well. The clones and their singleton classes are unreachable after the loop, and without the flag Ruby frees them.

The load-bearing parts: base has a singleton class (from extend) and is frozen, and the clone is frozen again. base.clone, Object.new.extend(M).clone, or freezing an object with a fresh singleton class leak nothing.

What the source shows (read, not traced at runtime):

  • rb_class_set_box_classext (class.c) inserts the class into box->classext_cow_classes.
  • rb_box_entry_mark (box.c) marks that table with rb_mark_set, so every class in it is reachable.
  • Entries leave the table only in rb_class_unlink_classext, which runs when the box is freed (free_classext_for_box).

Not traced: which call inside freeze gives the clone's singleton class its box-specific classext; Bug #20319 (singleton classes frozen lazily) may be that path. Bug #22339 concerns the same table at VM shutdown. A weak reference from the table to the class, dropping the entry when the class is freed, would keep the copy-on-write bookkeeping without holding the class.

Impact: Sequel before 5.67 cloned a frozen, extended dataset on every model query, and a Rails server with RUBY_BOX=1 grew from 342 MB to 505 MB over 2,400 requests (flat at 150 MB without the flag).

Also reproduced on ruby 4.0.7 (2026-09-15 revision 229531a6cf) +PRISM [x86_64-darwin25] (5000 against 1), and on x86_64-linux 4.0.7 with the same shape written as clone(freeze: false).freeze.

No data to display

Actions

Also available in: PDF Atom