Bug #22403
openRuby::Box: a class that gets a box-specific classext is never freed
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
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 intobox->classext_cow_classes.rb_box_entry_mark(box.c) marks that table withrb_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