Make Class#attached_object respect Ractor isolation

A singleton class is shareable whatever it is attached to, so it can be sent
to another Ractor, and #attached_object then handed over the object itself:

    $ ruby -W0 -e 'Ractor.new(p(Object.new).singleton_class) { |o| p o.attached_object }.join'
    #<Object:0x0000000120768c40>
    #<Object:0x0000000120768c40>       # the same object in both Ractors

Raise Ractor::IsolationError when the attached object is unshareable and
lives in another Ractor's objspace.  Guarding the reader covers every way a
singleton class is made: Object#clone (rb_singleton_class_clone_and_attach)
and the metaclass of a singleton class (make_metaclass) allocate one without
going through make_singleton_class, so making singleton classes unshareable
instead would have to catch all of them.

An object's owner is known from the objspace of its page, so a single-objspace
GC (mmtk) cannot make this check; the test is skipped there.

[Bug #21649]

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Koichi SasadaandClaude Opus 5 committed 2026-09-09 18:38:20 +09:00
1 parent fccc3b9e81
commit 17baadc072
2 files changed
+42 -1
Notes: git 2026-09-09 09:38:51 +00:00

No files matched your search

+16 -1
View File
@@ -24,6 +24,7 @@
#include "internal/box.h"
#include "internal/class.h"
#include "internal/eval.h"
#include "internal/gc.h"
#include "internal/hash.h"
#include "internal/object.h"
#include "internal/string.h"
@@ -2261,6 +2262,9 @@ rb_mod_descendants(VALUE mod)
*
* Raises an TypeError if the class is not a singleton class.
*
* Raises a Ractor::IsolationError if the attached object is not shareable and
* belongs to another Ractor.
*
* class Foo; end
*
* Foo.singleton_class.attached_object #=> Foo
@@ -2277,7 +2281,18 @@ rb_class_attached_object(VALUE klass)
rb_raise(rb_eTypeError, "'%"PRIsVALUE"' is not a singleton class", klass);
}
return RCLASS_ATTACHED_OBJECT(klass);
const VALUE obj = RCLASS_ATTACHED_OBJECT(klass);
/* A singleton class is shareable whatever it is attached to, so another Ractor can
* hold one attached to an unshareable object. Returning it would share it. */
if (rb_objspace_foreign_object_p(obj) && !RB_OBJ_SHAREABLE_P(obj)) {
/* No klass in the message: naming a singleton class inspects the very object we
* must not touch from here. */
rb_raise(rb_eRactorIsolationError,
"can not get an unshareable attached object from another Ractor");
}
return obj;
}
static void
+26
View File
@@ -958,4 +958,30 @@ class TestRactor < Test::Unit::TestCase
$stdout.puts "sent"
RUBY
end
def test_attached_object_of_unshareable_object
omit 'objspace per Ractor is how an object\'s owner is known' unless GC.config[:implementation] == 'default'
assert_ractor(<<~'RUBY')
# A singleton class is shareable whatever it is attached to, so sending one used to
# hand the attached object to another Ractor through #attached_object.
o = Object.new
assert_equal true, Ractor.shareable?(o.singleton_class)
assert_same o, o.singleton_class.attached_object
r = Ractor.new(o.singleton_class) do |sc|
begin
sc.attached_object
rescue Ractor::IsolationError
:isolated
end
end
assert_equal :isolated, r.value
# A shareable attached object, and a Ractor's own unshareable one, are fine.
shareable = Ractor.make_shareable(Object.new)
assert_same shareable, Ractor.new(shareable.singleton_class) { |sc| sc.attached_object }.value
assert_same String, Ractor.new(String.singleton_class) { |sc| sc.attached_object }.value
assert_equal true, Ractor.new { own = Object.new; own.singleton_class.attached_object.equal?(own) }.value
RUBY
end
end