From: "eightbitraptor (Matt V-H) via ruby-core" Date: 2026-08-28T12:48:26+00:00 Subject: [ruby-core:126538] [Ruby Feature#22277] Make `rb_gc_register_address` Ractor-local Issue #22277 has been reported by eightbitraptor (Matt V-H). ---------------------------------------- Feature #22277: Make `rb_gc_register_address` Ractor-local https://bugs.ruby-lang.org/issues/22277 * 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. However, due to potential Ractor isolation issues, we've restricted `rb_gc_register_address` and `rb_gc_unregister_address` to the main ractor. We would like to have a discussion about this restriction before going ahead with this change. ## 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 the main Ractor only restriction? 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. Should we add some documentation around `rb_gc_register_address` to document it's restricted usage, and patch the affected gems upstream? -- 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/