From c0d86a0103de7130943d54b4a290b76ec7e0c135 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sun, 29 Mar 2026 08:47:36 +0200 Subject: 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. --- class.c | 25 ++++++++++++++----------- test/ruby/test_box.rb | 31 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/class.c b/class.c index cd07846173..e52a541c6e 100644 --- a/class.c +++ b/class.c @@ -226,14 +226,6 @@ struct duplicate_id_tbl_data { VALUE klass; }; -static enum rb_id_table_iterator_result -duplicate_classext_id_table_i(ID key, VALUE value, void *data) -{ - struct rb_id_table *tbl = (struct rb_id_table *)data; - rb_id_table_insert(tbl, key, value); - return ID_TABLE_CONTINUE; -} - static enum rb_id_table_iterator_result duplicate_classext_m_tbl_i(ID key, VALUE value, void *data) { @@ -262,8 +254,19 @@ duplicate_classext_m_tbl(struct rb_id_table *orig, VALUE klass, bool init_missin return tbl; } +static enum rb_id_table_iterator_result +duplicate_classext_cvc_tbl_i(ID key, VALUE value, void *data) +{ + struct rb_id_table *tbl = (struct rb_id_table *)data; + struct rb_cvar_class_tbl_entry *cvc_entry = (struct rb_cvar_class_tbl_entry *)value; + struct rb_cvar_class_tbl_entry *copy = ALLOC(struct rb_cvar_class_tbl_entry); + MEMCPY(copy, cvc_entry, struct rb_cvar_class_tbl_entry, 1); + rb_id_table_insert(tbl, key, (VALUE)copy); + return ID_TABLE_CONTINUE; +} + static struct rb_id_table * -duplicate_classext_id_table(struct rb_id_table *orig, bool init_missing) +duplicate_classext_cvc_tbl(struct rb_id_table *orig, bool init_missing) { struct rb_id_table *tbl; @@ -274,7 +277,7 @@ duplicate_classext_id_table(struct rb_id_table *orig, bool init_missing) return NULL; } tbl = rb_id_table_create(rb_id_table_size(orig)); - rb_id_table_foreach(orig, duplicate_classext_id_table_i, tbl); + rb_id_table_foreach(orig, duplicate_classext_cvc_tbl_i, tbl); return tbl; } @@ -411,7 +414,7 @@ rb_class_duplicate_classext(rb_classext_t *orig, VALUE klass, const rb_box_t *bo * RCLASSEXT_CC_TBL(copy) = NULL */ - RCLASSEXT_CVC_TBL(ext) = duplicate_classext_id_table(RCLASSEXT_CVC_TBL(orig), dup_iclass); + RCLASSEXT_CVC_TBL(ext) = duplicate_classext_cvc_tbl(RCLASSEXT_CVC_TBL(orig), dup_iclass); // Subclasses/back-pointers are only in the prime classext. diff --git a/test/ruby/test_box.rb b/test/ruby/test_box.rb index 5da29b4971..c6a2c5423e 100644 --- a/test/ruby/test_box.rb +++ b/test/ruby/test_box.rb @@ -155,6 +155,37 @@ class TestBox < Test::Unit::TestCase assert_include Ruby::Box.current.inspect, "main" end + def test_class_variables + # [Bug #21952] + assert_separately([ENV_ENABLE_BOX], __FILE__, __LINE__, "here = '#{__dir__}'; #{<<~"begin;"}\n#{<<~'end;'}", ignore_stderr: true) + begin; + Ruby::Box.root.eval(<<~RUBY) + module M + @@x = 1 + end + + class A + include M + end + + class B < A + end + RUBY + + code = <<~REPRO + class ::B + @@x += 1 + end + REPRO + + b1 = Ruby::Box.new + assert_equal 2, b1.eval(code) + + b2 = Ruby::Box.new + assert_equal 2, b2.eval(code) + end; + end + def test_autoload_in_box setup_box -- cgit v1.2.3