From: "matz (Yukihiro Matsumoto) via ruby-core" Date: 2026-08-06T11:35:47+00:00 Subject: [ruby-core:126305] [Ruby Feature#22226] Ractor: class/module ownership -- restrict modification to the Ractor that created it Issue #22226 has been updated by matz (Yukihiro Matsumoto). Accepted, including the incompatibilities in section 7. Asking "did you create this?" instead of "are you the main Ractor?" is the right question, and Ractor is experimental precisely so that we can make this kind of correction. On class variables (section 6): I am not comfortable with the result that nobody can write a class variable of a class created in a non-main Ractor -- not its owner, and not the main Ractor either. That is less than "full use of the classes you create", which is the promise of the rest of the proposal. I expect this to change eventually, but it needs more thought, and I accept the current behavior as an experiment rather than hold the rest back for it. On section 8: please make `freeze` and `Module#set_temporary_name` owner-only as well. A non-owner freezing somebody else's class is a hole, and it lets a third party make a class permanently unmodifiable. Top-level class and module names in non-main Ractors, and the integration with `Ruby::Box`, should be settled together with the author of `Ruby::Box`. Let us discuss that separately. Matz. ---------------------------------------- Feature #22226: Ractor: class/module ownership -- restrict modification to the Ractor that created it https://bugs.ruby-lang.org/issues/22226#change-118407 * Author: ko1 (Koichi Sasada) * Status: Open * Assignee: ko1 (Koichi Sasada) * Target version: 4.1 ---------------------------------------- ## Abstract Every class/module records the Ractor that created it as its *owner*. Only the owner Ractor can modify it. Reading is unchanged and allowed from any Ractor. This replaces several ad-hoc "main Ractor only" rules with a single rule keyed on the creator. It **relaxes** the rules for classes a Ractor creates itself, and **tightens** them for classes created by somebody else. No Ruby-level API is added: ownership is recorded internally and is not exposed. --- ## 1. Motivation ### 1.1 The current rules ask the wrong question Today's restrictions ask *"are you the main Ractor?"*. They never ask *"did you create this class?"*. The result is inconsistent in both directions. A non-main Ractor may freely redefine **any** class in the process: ```ruby class C; end Ractor.new { C.define_method(:m) { 1 }; :done }.value #=> :done (works today) Ractor.new { class String; def foo = 1; end; :done }.value #=> :done (works today) Ractor.new { def f = 42; f() }.value #=> 42 (works today; defines Object#f) Ractor.new { Object.send(:remove_const, :FOO); :done }.value #=> :done (works today) ``` ...but it may **not** set an instance variable on a class it created itself: ```ruby Ractor.new { k = Class.new # nobody else can even name this class yet k.instance_variable_set(:@iv, 1) }.value #=> Ractor::IsolationError: # can not set instance variables of classes/modules by non-main Ractors ``` ...nor give it an unshareable constant: ```ruby Ractor.new { k = Class.new k.const_set(:X, "str".dup) }.value #=> Ractor::IsolationError: # can not set constants with non-shareable objects by non-main Ractors ``` The first group is unrestricted although it affects the whole process. The second group is forbidden although it affects nothing but the Ractor doing it. The rules are exactly inverted with respect to who is actually affected. ### 1.2 A Ractor's classes can be redefined under it by another Ractor Because any Ractor may modify any class, the code a Ractor runs can be changed by somebody else at any moment: ```ruby Ractor.new { class String; def upcase = :patched; end; :done }.value "abc".upcase #=> :patched # evaluated in the *main* Ractor ``` ```ruby FOO = 1 Ractor.new { Object.send(:remove_const, :FOO); :removed }.value FOO #=> NameError: uninitialized constant FOO ``` Both of these run fine today. There is no way for a Ractor to rely on the classes it uses. ### 1.3 Class internals have no single writer `m_tbl`, `const_tbl` and the class fields can be written by any Ractor today. This means the implementation cannot assume a single writer for any class, which stands in the way of synchronization and caching work (and it is also how the `cvar_overtaken()` bug below arose: a *read* path physically deletes a table entry, potentially of another Ractor's class). --- ## 2. Proposal A class/module records its creator Ractor as its **owner**. Only the owner may: * define/remove/undef methods, `alias`, change method visibility * `include`/`prepend` into it, and `Module#refine` targeting it * define/remove constants, register `autoload` * set instance variables on the class/module object Non-owner Ractors get `Ractor::IsolationError`. **Reading is completely unchanged**: method calls, instantiation, subclassing, constant reads and instance-variable reads all work from any Ractor as before. In exchange, the previous "main Ractor only" rules for class instance variables and unshareable constant values become "**owner Ractor only**". A Ractor gets full use of the classes it creates. Since all classes/modules created at boot or by main-Ractor code (including everything loaded by `require`) are owned by the main Ractor, this is equivalent to today's rules for programs that do not create classes inside non-main Ractors. --- ## 3. Examples ### 3.1 Newly allowed: a Ractor can fully use the classes it creates Instance variables on a class the Ractor created: ```ruby Ractor.new { k = Class.new k.instance_variable_set(:@iv, 1) # was: IsolationError, even for a shareable value! k.instance_variable_set(:@iv, "str".dup) # was: IsolationError k.instance_variable_get(:@iv) #=> "str" }.value ``` **Unshareable values in constants** of a class the Ractor created. This was the main Ractor's privilege before; now every Ractor has it for its own classes: ```ruby Ractor.new { k = Class.new k::BUF = "buffer".dup # unshareable value; was: IsolationError k::BUF << "!" # ...and the owner can freely use it k::BUF #=> "buffer!" }.value Ractor.new { k = Class.new k.const_set(:LIST, [1, 2, 3]) # an unfrozen Array is unshareable too k::LIST << 4 k::LIST #=> [1, 2, 3, 4] }.value ``` The value stays private to the owner -- reading it from another Ractor raises, which is exactly the rule that applied to the main Ractor before: ```ruby port = Ractor::Port.new r = Ractor.new(port) { |pt| k = Class.new; k::BUF = "buffer".dup; pt << k; Ractor.receive } k = port.receive k::BUF #=> Ractor::IsolationError: can not access non-shareable objects in constant # #::BUF of a class/module created by another Ractor. ``` A *shareable* value in the same position is readable from anywhere, as before: ```ruby r = Ractor.new(port) { |pt| k = Class.new; k::N = 42; pt << k; Ractor.receive } k = port.receive k::N #=> 42 ``` Classes defined under a module the Ractor created itself work fully: ```ruby Ractor.new { mod = Module.new mod.const_set(:Foo, Class.new { def m = :ok }) mod::Foo.new.m #=> :ok }.value ``` ### 3.2 Newly prohibited: modifying a class created by another Ractor ```ruby class C; end module M; end Ractor.new { def f = 42 }.value #=> can not modify Object because it is created by another Ractor # (top-level `def` defines a private method on Object) Ractor.new { class String; def foo = 1; end }.value #=> can not modify String because it is created by another Ractor Ractor.new { C.define_method(:m) { 1 } }.value Ractor.new { C.send(:alias_method, :a, :inspect) }.value Ractor.new { C.send(:private, :inspect) }.value Ractor.new { C.send(:undef_method, :inspect) }.value Ractor.new { C.include(M) }.value Ractor.new { C.prepend(M) }.value Ractor.new { Module.new { refine(C) { def z = 1 } } }.value Ractor.new { C.autoload(:Zz, "zz") }.value #=> can not modify C because it is created by another Ractor Ractor.new { C.const_set(:X, 1) }.value # note: even a *shareable* value now raises #=> can not set constants of classes/modules created by another Ractor Ractor.new { class TopCls; end }.value # a top-level class name... Ractor.new { module TopMod; end }.value # ...and a top-level module name #=> can not set constants of classes/modules created by another Ractor # (both write a constant into Object) Ractor.new { Object.send(:remove_const, :FOO) }.value #=> can not modify Object because it is created by another Ractor Ractor.new { C.instance_variable_set(:@iv, 1) }.value #=> can not set instance variables of classes/modules created by another Ractor ``` Patterns that stop working: ```ruby # a registry in Class#inherited that writes into the superclass Base = Class.new { def self.inherited(sub) = const_set(:"Sub#{sub.object_id}", sub) } Ractor.new { Class.new(Base) }.value #=> can not set constants of classes/modules created by another Ractor # lazy definition on first touch M2 = Module.new { def self.const_missing(n) = const_set(n, Class.new) } Ractor.new { M2::Foo }.value #=> can not set constants of classes/modules created by another Ractor ``` Both work if the definition happens before the Ractor is spawned (eager loading). ### 3.3 Unchanged ```ruby Ractor.new { "abc".upcase }.value #=> "ABC" Ractor.new { C.new }.value # instantiation Ractor.new { Class.new(String) { def m = :ok }.new.m }.value #=> :ok (subclassing is creation) Ractor.new { require "time"; Time.now.respond_to?(:xmlschema) }.value #=> true ``` `require` from a non-main Ractor keeps working because `Ractor#require` performs the load on the main Ractor, so the library's classes are defined by (and owned by) the main Ractor. `def` on an object a Ractor created is also unaffected -- an ordinary object's singleton class is created by, and owned by, the Ractor that triggers it: ```ruby Ractor.new { o = Object.new; def o.f = :ok; o.f }.value #=> :ok ``` ### 3.4 Summary table | operation from a non-main Ractor | today | proposal | |---|---|---| | define a method on a foreign class | OK | **IsolationError** | | top-level `def` | OK | **IsolationError** | | `include`/`prepend`/`refine` a foreign class | OK | **IsolationError** | | set a **shareable** constant on a foreign class | OK | **IsolationError** | | set an **unshareable** constant on a foreign class | IsolationError | IsolationError | | `remove_const` / `autoload` on a foreign class | OK | **IsolationError** | | set an ivar on a foreign class | IsolationError | IsolationError | | define a method on its **own** class | OK | OK | | set a **shareable** constant on its **own** class | OK | OK | | set an **unshareable** constant on its **own** class | IsolationError | **OK** | | set an ivar on its **own** class | IsolationError | **OK** | | read / call / instantiate / subclass any class | OK | OK | --- ## 4. How the proposal answers the motivation * **1.1 (rules ask the wrong question)** -- the rules now ask "did you create this?" instead of "are you main?". Every restriction is on the class of somebody else, and every relaxation is on your own class. The two inverted cases in 1.1 both flip to the right side. The old "main Ractor only" rules become the special case *"boot-time classes are owned by the main Ractor"*, so they are no longer separate rules. * **1.2 (classes changed under you)** -- no Ractor can modify a class it did not create, so the classes a Ractor uses cannot be redefined by another Ractor. * **1.3 (no single writer)** -- every class/module now has exactly one Ractor which can write its method table, constant table and fields. --- ## 5. What this does **not** give Ownership bounds *who may write*, not *what readers may see*. Reads stay unsynchronized and a class modification is not atomic, so a non-owner Ractor can still observe a class in the middle of being modified by its owner: after the first `def` of a `class ... end` body but before the second, or while `include`/`prepend` is rewiring the ancestor chain. The guarantee is **a single writer per class/module**, not a consistent view for readers. This proposal is not, and does not claim to be, full isolation of class state. --- ## 6. Details * **Singleton classes / metaclasses** are owned by the owner of the object they are attached to, not by the Ractor that happened to trigger their lazy creation. `def C.foo` is allowed exactly for the owner of `C`: ```ruby class C; end Ractor.new { C.singleton_class; :touched }.value # another Ractor touches it first class << C; def foo = :ok; end # still fine in main C.foo #=> :ok Ractor.new { class << C; def bar = 1; end }.value #=> IsolationError ``` * **Terminated owner**: a class whose owner Ractor has finished becomes permanently read-only for everybody, including the main Ractor. ```ruby K = Ractor.new { Class.new { def x = :made_in_ractor } }.value K.new.x #=> :made_in_ractor (reading is fine) K.define_method(:y) { 1 } #=> IsolationError (nobody owns it any more) ``` There is intentionally no API to look up or transfer ownership. * **Class variables** are *not* covered by the relaxation. They are shared across the whole inheritance chain and the class they are physically stored in can migrate over time (`cvar_overtaken`), so no single owner can be defined for them. Writes still require the main Ractor, and additionally must not cross the ownership boundary (the class actually written into must be owned), so class fields keep a single writer. * **Copying a foreign class**: `Class#dup`/`clone` of a class created by another Ractor produces a copy owned by the copying Ractor. It raises if the source's constants or instance variables refer to unshareable objects (they would leak across Ractors); copying classes holding only shareable values works. This gives a mutation-free alternative to monkey-patching: ```ruby S = Class.new; S.const_set(:OK, 1) Ractor.new { k = S.dup; k.define_method(:m) { 1 }; k.new.m }.value #=> 1 ``` * **Top-level class and module names**: `class Foo; end` / `module Foo; end` at the top level of a non-main Ractor writes a constant into `Object`, so both are prohibited. Define them under a module the Ractor creates itself instead (`mod = Module.new; mod.const_set(:Foo, Class.new)`). Integration with `Ruby::Box` is future work. * **Bug fix included**: `cvar_overtaken()` physically deleted a duplicated cvar entry from the `front` class, and that path is reachable from *read* operations (`rb_cvar_find`) -- i.e. a cross-Ractor write triggered by a read. The clean-up is now skipped unless the current Ractor owns `front`. --- ## 7. Incompatibility Only code that modifies classes from a non-main Ractor is affected. Concretely, the following raise `Ractor::IsolationError` where they used to work: * method definition / `alias` / visibility change on a foreign class, including top-level `def` * `include`/`prepend` into a foreign class, `refine` targeting one * setting a **shareable** constant on a foreign class, `remove_const`, `autoload` registration * `Class#inherited` and `const_missing`/`method_missing` hooks which mutate a foreign class Migration is usually "define it before spawning the Ractor" (eager loading), or "create the class inside the Ractor that uses it", or "`dup` the foreign class and modify the copy". Programs which never define classes inside non-main Ractors are unaffected. --- ## 8. Open questions / future work * `freeze` of a foreign class is still allowed. It is a behavior-changing write and should probably be owner-only too. * `Module#set_temporary_name` is not checked yet. * Singleton class ownership is not transferred by `Ractor#send(move: true)`. * Integration with `Ruby::Box`, so that non-main Ractors can define top-level class/module names. * Should the error message for a class owned by the main Ractor say "owned by the main Ractor" rather than "created by another Ractor"? The current wording is confusing for top-level `def`, where the user never mentioned `Object`. --- ## 9. Implementation https://github.com/ruby/ruby/pull/17913 The owner is stored in `rb_classext_t::owner_ractor` as the Ractor object; `0` means the main Ractor, so single-Ractor programs get no additional GC edges. It is marked from the classext and updated on compaction, so the comparison never sees a dangling or reused reference. Method-table checks funnel through `rb_class_modify_check()`; constant/ivar/cvar checks replace the previous `rb_ractor_main_p()` checks in `variable.c`. The class-ivar fast paths in `vm_insnhelper.c` switch from "main Ractor" to "owner Ractor", which keeps them valid because the owner is the only writer and its threads are serialized by the per-Ractor lock. `make btest` (2053 tests), `make test-all` (35797 tests, 0 failures / 0 errors) and `make test-spec` (32628 examples, 0 failures / 0 errors / 0 tagged) all pass. Three tests which relied on cross-Ractor method definition were rewritten so that the owner Ractor performs the definition. ## Notes * The description above was written by Claude Code based on my explanation. * The primary motivation is to eliminate the special cases for the main Ractor. The concept of an "owner Ractor" for classes and modules provides a general solution to such cases. * Another possible approach would be to prohibit class and module mutations from non-main Ractors, since most classes and modules are defined by the main Ractor during program loading. However, this would also prohibit `Class.new` in non-main Ractors, even though dynamically creating classes is a common programming pattern in Ruby. -- 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/