Prohibit copying a class/module created by another Ractor

Module#initialize_copy of a class/module that another Ractor owns used to
check that its constants and fields are all shareable, and copy it if so.
That check made the copying Ractor walk (and flag) the owner's objects,
and the fields were collected into a buffer and checked one by one, so a
later value sat only in that buffer, which the owner's GC cannot see.

Raise Ractor::IsolationError instead, whatever the class holds.  The
metaclass is checked as well, since its tables are copied too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Koichi SasadaandClaude Opus 5.5 committed 2026-10-24 04:13:52 +09:00
1 parent ac823ef8b8
commit a4131efb5a
2 files changed
+19 -107
Notes: git 2026-10-23 19:14:25 +00:00

No files matched your search

+15 -53
View File
@@ -1640,63 +1640,25 @@ assert_equal "can not access non-shareable objects in constant Object::STR of a
end
RUBY
# Copying a class/module created by another Ractor raises if its constants or
# fields refer to unshareable objects
assert_equal 'can not copy a class/module created by another Ractor because constant CONST refers to an unshareable object', <<~'RUBY', frozen_string_literal: false
class C
CONST = 'str'
end
r = Ractor.new { C.dup }
begin
r.join
rescue Ractor::RemoteError => e
e.cause.message
end
RUBY
assert_equal 'can not copy a class/module created by another Ractor because variable @iv refers to an unshareable object', <<~'RUBY', frozen_string_literal: false
class C
@iv = 'str'
end
r = Ractor.new { C.dup }
begin
r.join
rescue Ractor::RemoteError => e
e.cause.message
end
RUBY
# ... the metaclass is checked too, because the copy carries it over
assert_equal 'can not copy a class/module created by another Ractor because variable @secret refers to an unshareable object', <<~'RUBY', frozen_string_literal: false
class C; end
C.singleton_class.instance_variable_set(:@secret, 'str')
r = Ractor.new { C.dup }
begin
r.join
rescue Ractor::RemoteError => e
e.cause.message
end
RUBY
# ... and the copy of a class which refers to nothing unshareable belongs to the
# copying Ractor, which is a way to modify a foreign class without mutating it
assert_equal 'copy orig', %q{
# Copying a class/module created by another Ractor is not allowed: the copy would
# carry the owner's field values
assert_equal 'can not copy a class/module created by another Ractor', %q{
class C
CONST = 1
def m = 'orig'
end
Ractor.new do
k = C.dup
k.class_eval { def m = 'copy' }
"#{k.new.m} #{C.new.m}"
end.value
r = Ractor.new { C.dup }
begin
r.join
rescue Ractor::RemoteError => e
e.cause.message
end
}
assert_equal 'Ractor::IsolationError', %q{
module M; end
Ractor.new { M.clone rescue $!.class.to_s }.value
}
# Setting constants of classes created by other Ractors is not allowed
+4 -54
View File
@@ -1004,30 +1004,6 @@ rb_module_check_initializable(VALUE mod)
}
}
static enum rb_id_table_iterator_result
init_copy_check_const_i(ID id, VALUE v, void *data)
{
const rb_const_entry_t *ce = (const rb_const_entry_t *)v;
if (!UNDEF_P(ce->value) && !RB_SPECIAL_CONST_P(ce->value) && !RB_OBJ_SHAREABLE_P(ce->value)) {
// checked after the walk, as rb_ractor_shareable_p may allocate or raise
rb_ary_push((VALUE)data, ID2SYM(id));
}
return ID_TABLE_CONTINUE;
}
static int
init_copy_check_field_i(ID id, VALUE val, st_data_t arg)
{
if ((rb_is_instance_id(id) || rb_is_class_id(id)) && !rb_ractor_shareable_p(val)) {
rb_raise(rb_eRactorIsolationError,
"can not copy a class/module created by another Ractor because "
"variable %"PRIsVALUE" refers to an unshareable object", rb_id2str(id));
}
return ST_CONTINUE;
}
// The copy belongs to the copying Ractor, so it must not carry over unshareable
// objects owned by the source's Ractor.
static enum rb_id_table_iterator_result
move_check_const_i(ID id, VALUE v, void *data)
{
@@ -1064,39 +1040,13 @@ rb_class_check_singleton_movable(VALUE klass)
} while ((klass = class_own_metaclass(klass)) != 0);
}
static void
init_copy_check_tables(VALUE klass)
{
if (RCLASS_CONST_TBL(klass)) {
const VALUE ids = rb_ary_hidden_new(0);
RB_VM_LOCKING() { // see rb_const_lookup_copy
rb_id_table_foreach(RCLASS_CONST_TBL(klass), init_copy_check_const_i, (void *)ids);
}
for (long i = 0; i < RARRAY_LEN(ids); i++) {
const ID id = SYM2ID(RARRAY_AREF(ids, i));
rb_const_entry_t entry;
if (rb_const_lookup_copy(klass, id, &entry) && !UNDEF_P(entry.value) &&
!rb_ractor_shareable_p(entry.value)) {
rb_raise(rb_eRactorIsolationError,
"can not copy a class/module created by another Ractor because "
"constant %"PRIsVALUE" refers to an unshareable object", rb_id2str(id));
}
}
}
rb_ivar_foreach_buffered(klass, init_copy_check_field_i, 0);
}
// the copy would carry the owner's field values, which only the owner may read
static void
init_copy_owner_check(VALUE orig)
{
if (!rb_class_owned_p(orig)) {
init_copy_check_tables(orig);
// rb_singleton_class_clone_and_attach copies the metaclass's tables too
VALUE meta = METACLASS_OF(orig);
if (RCLASS_SINGLETON_P(meta)) {
init_copy_check_tables(meta);
}
VALUE meta = METACLASS_OF(orig);
if (!rb_class_owned_p(orig) || (RCLASS_SINGLETON_P(meta) && !rb_class_owned_p(meta))) {
rb_raise(rb_eRactorIsolationError, "can not copy a class/module created by another Ractor");
}
}