From: "shluboto (Andreas Busold) via ruby-core" Date: 2026-10-04T11:30:55+00:00 Subject: [ruby-core:126918] [Ruby Bug#22403] Ruby::Box: a class that gets a box-specific classext is never freed Issue #22403 has been reported by shluboto (Andreas Busold). ---------------------------------------- Bug #22403: Ruby::Box: a class that gets a box-specific classext is never freed https://bugs.ruby-lang.org/issues/22403 * Author: shluboto (Andreas Busold) * Status: Open * ruby -v: ruby 4.1.0dev (2026-10-04T10:47:58Z master 76aa225e9a) +PRISM [x86_64-darwin25] * Backport: 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN ---------------------------------------- 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. ```ruby 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`. -- https://bugs.ruby-lang.org/ ______________________________________________ ruby-core mailing list -- ruby-core@ml.ruby-lang.org To unsubscribe send an email to ruby-core-leave@ml.ruby-lang.org ruby-core info -- https://ml.ruby-lang.org/mailman3/lists/ruby-core.ml.ruby-lang.org/