From: "Eregon (Benoit Daloze) via ruby-core" Date: 2026-09-05T18:40:26+00:00 Subject: [ruby-core:126576] [Ruby Feature#22277] Make `rb_gc_register_address` Ractor-local Issue #22277 has been updated by Eregon (Benoit Daloze). eightbitraptor (Matt V-H) wrote: > Given the infrequency of this usage in real world Ruby applications. Is it acceptable to restrict `rb_gc_register_address` to the main Ractor only, document it's restricted usage, and patch the affected gems upstream? We'd like to discuss this, and any potential alternative solutions. I think it might be OK, although it is a semantic change. I'd like to understand this issue better, let me ask a few questions. >From the issue title `Make rb_gc_register_address Ractor-local` I got confused about what this is proposing. I think making it Ractor-local, IIUC hold the object at that address alive only for the Ractor that called rb_gc_register_address() is less clear if that's OK semantically (notably because there might be objects from other Ractors stored there). To state it clearly, the current semantics are "keep the object at this address alive for the entire duration of the Ruby interpreter" ([docs](https://docs.ruby-lang.org/capi/en/master/db/df8/include_2ruby_2internal_2gc_8h.html#a4f1f713d5ac60f00d2bf8f362c2aa85d)), it would be ideal if we can keep that. To achieve that, isn't there still a true global GC e.g. to reclaim shareable objects? Could that take care of rb_gc_register_address()? Maybe the issue is then the Ractor-local GCs would collect the object behind the pointer? (sorry I don't have time to read more about the Ractor-local GC semantics right now) If not, how about something like a read-write lock so multiple Ractors could do this part of the Ractor-local GC at the same time as long as there are no concurrent rb_gc_register_address()? This seems the best: preserves the semantics and solves (most of) the contention. --- There is a sister API, `rb_gc_register_mark_object(VALUE)` (takes an object instead of a pointer, so simpler), what about that, is that not problematic with Ractor? There is also `rb_global_variable()` but that's literally just calling `rb_gc_register_address()` so the same thing. The name makes it even clearly it should be global and stay alive forever. --- In the context of TruffleRuby it's not possible to fully implement `rb_gc_register_address()`, instead we approximate by saving the addresses for the calls during `Init_` and then reading them only once at the end of each `Init_my_extension`. For `rb_gc_register_address()` called outside `Init_` we approximate by reading the pointer immediately (and only once). So we kind of turn `rb_gc_register_address()` into `rb_gc_register_mark_object()`, but delaying until the end of `Init_` so the order between `rb_gc_register_address()` and the assignment doesn't matter. It works pretty well in practice (no known issue with it). >From the point of view of portability, ideally we would only allow `rb_gc_register_address()` during `Init_`, raising an exception if called outside `Init_`, and it would still mean "keep the object at this address alive for the entire duration of the Ruby interpreter". (Or deprecate it and recommend `rb_gc_register_mark_object` instead, but I guess that's too hard compat-wise) ---------------------------------------- Feature #22277: Make `rb_gc_register_address` Ractor-local https://bugs.ruby-lang.org/issues/22277#change-118783 * Author: eightbitraptor (Matt V-H) * Status: Open * Assignee: eightbitraptor (Matt V-H) * Target version: 4.1 ---------------------------------------- ## Summary [Github PR #18393](https://github.com/ruby/ruby/pull/18393) `rb_gc_register_address` stores its entries in a single VM-wide list. This means that every Ractor local GC walks the list under a VM wide lock, which causes contention between Ractors attempting to GC. The associated PR changes the storage to per-ractor lists, removing the lock. ## Background Each ractor owns an objspace. A local GC marks only that ractor's roots and sweeps only its own pages. Using a VM-wide list with this design means that: - Every ractor's local GC scans every slot, under a shared lock. - A slot can name an object in another ractor's objspace. - If ractor A registers a slot and the slot's value is an unshareable object owned by ractor B, every ractor's GC marks the object. Storing it there violates the Ractor isolation guarantees, but the object stays alive, so it works. Changing this so that each Ractor maintains it's own list changes this so that only the registering Ractor's GC scans it's own list, which means we don't need to do this under a lock. However this introduces a situation where a slot on Ractor A's list holds an unshareable object owned by Ractor B, then Ractor B's local GC will not scan A's list, and A's local GC won't mark objects in B's objspace (because mark_maybe filters by objspace). This means that B can collect the object while A holds a reference to it, leading to a potential use-after-free. Because `rb_gc_register_address` and `rb_gc_unregister_address` are part of the public C API. It's tough to enforce a contract that restricts a slots value to one that's either shareable or owned by the main Ractor, because we register addresses, and any C extension can manipulate the value at that address at any time. ## Implementation detail The associated PR adds a "registered_addrs" array and a listed flag to each `rb_ractor_t` instance. When an address registered the Ractor in question is added to a VM local registry of Ractors who's lists are not empty, so that any calls to `rb_gc_unregister_address` can find the registering Ractor easily. Each Ractor local GC marks the current ractors list via `rb_ractor_mark_local_roots` and each Global GC marks every Ractors list, including terminated Ractors whose structs haven't been freed yet. The `rb_gc_register_address` and `rb_gc_unregister_address` functions now raise `Ractor::UnsafeError` when run from a non-main Ractor. This is a fairly blunt mechanism for ensuring that we can't end up with dangling pointers, but does restrict the usage of this API in ways that might be problematic. ## Concerns The main concern with this approach is potential use-after-free that can arise from a dangling pointer if the Ractor has an address registered containing a pointer to an object owned by another Ractor. One potential solution to this is to instead restrict the `rb_gc_register_address` API to the main Ractor only? This is a semantic change from how the existing API works, and does restrict the functionality somewhat. This will only affect gems with C extensions that register and unregister objects dynamically at runtime. Registration during `Init_` functions still runs on the main Ractor and is unaffected. On 2026-08-25 we ran a codesearch over an index of published gems, looking for gems that both call `rb_ext_ractor_safe(true)` to declare themselves ractor safe and also call `rb_gc_register_address`. We found a total of 14 packages that matched both calls. Of which 6 were forks of other packages also in the list. Of the remaining 8 packages, 5 contained `rb_gc_register_address` calls that were _only_ within `Init_` functions. These would be unaffected by the change. The remaining 3 are [`oj`](https://rubygems.org/gems/oj), [`ox`](https://rubygems.org/gems/ox), and [`stack_trace`](https://rubygems.org/gems/stack_trace). Given the infrequency of this usage in real world Ruby applications. Is it acceptable to restrict `rb_gc_register_address` to the main Ractor only, document it's restricted usage, and patch the affected gems upstream? We'd like to discuss this, and any potential alternative solutions. -- 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/