From 3c3a805c35ebcc396625499034296a63478de682 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 2 Jul 2026 20:31:46 +0000 Subject: [PATCH 1/4] mark internal cross-Ractor structures as shareable The concurrent set, managed id-table dups and symbol id-entry buckets are reachable from every Ractor via VM-global state (the frozen-string/symbol tables, shape-tree edge tables, the symbol table), so flag them shareable -- as enc_list_update already does for the encoding list. Co-Authored-By: Claude Opus 4.8 (1M context) --- concurrent_set.c | 4 ++++ id_table.c | 4 ++++ symbol.c | 3 +++ 3 files changed, 11 insertions(+) diff --git a/concurrent_set.c b/concurrent_set.c index 42ff6ef43f799e..cdc7879f4cd307 100644 --- a/concurrent_set.c +++ b/concurrent_set.c @@ -2,6 +2,7 @@ #include "internal/gc.h" #include "internal/concurrent_set.h" #include "ruby/atomic.h" +#include "ruby/ractor.h" #include "vm_sync.h" #define CONCURRENT_SET_CONTINUATION_BIT ((VALUE)1 << (sizeof(VALUE) * CHAR_BIT - 1)) @@ -102,6 +103,9 @@ rb_concurrent_set_new(const struct rb_concurrent_set_funcs *funcs, int capacity) set->funcs = funcs; set->entries = ZALLOC_N(struct concurrent_set_entry, capacity); set->capacity = capacity; + /* The set is reachable from every Ractor (e.g. via C globals such as the + * frozen-string and symbol tables), so mark it shareable. */ + RB_OBJ_SET_SHAREABLE(obj); return obj; } diff --git a/id_table.c b/id_table.c index f45cd2c61e1199..a1cc9043b6cff2 100644 --- a/id_table.c +++ b/id_table.c @@ -390,6 +390,10 @@ rb_managed_id_table_dup(VALUE old_table) { struct rb_id_table *new_tbl; VALUE obj = TypedData_Make_Struct(0, struct rb_id_table, RTYPEDDATA_TYPE(old_table), new_tbl); + /* A managed id table hangs off VM-global state (e.g. a shape tree's edge + * table grows via this dup) and is reachable from every Ractor, so mark it + * shareable. */ + RB_OBJ_SET_SHAREABLE(obj); struct rb_id_table *old_tbl = managed_id_table_ptr(old_table); rb_id_table_init(new_tbl, old_tbl->num + 1); rb_id_table_foreach(old_tbl, managed_id_table_dup_i, new_tbl); diff --git a/symbol.c b/symbol.c index b157d2677cdae3..513e7ce33774c0 100644 --- a/symbol.c +++ b/symbol.c @@ -278,6 +278,9 @@ set_id_entry(rb_symbols_t *symbols, rb_id_serial_t num, VALUE str, VALUE sym) if (idx >= (size_t)RARRAY_LEN(ids) || NIL_P(id_entry_list = rb_ary_entry(ids, (long)idx))) { rb_darray_make(&entries, ID_ENTRY_UNIT); id_entry_list = TypedData_Wrap_Struct(0, &sym_id_entry_list_type, entries); + /* Reachable from every Ractor via the global symbol table, so mark it + * shareable. */ + RB_OBJ_SET_SHAREABLE(id_entry_list); rb_ary_store(ids, (long)idx, id_entry_list); } else { From c4bda536f67c1616ae2047f9ac1538d66c98f133 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 2 Jul 2026 21:17:24 +0000 Subject: [PATCH 2/4] depend: add ruby/ractor.h for concurrent_set.o concurrent_set.c now includes ruby/ractor.h for RB_OBJ_SET_SHAREABLE. (id_table.c is #included into symbol.c, which already depends on ruby/ractor.h, so no separate entry is needed.) Co-Authored-By: Claude Opus 4.8 (1M context) --- depend | 1 + 1 file changed, 1 insertion(+) diff --git a/depend b/depend index 22ae43b21ad589..8b764bd060c49c 100644 --- a/depend +++ b/depend @@ -2353,6 +2353,7 @@ concurrent_set.$(OBJEXT): {$(VPATH)}missing.h concurrent_set.$(OBJEXT): {$(VPATH)}node.h concurrent_set.$(OBJEXT): {$(VPATH)}onigmo.h concurrent_set.$(OBJEXT): {$(VPATH)}oniguruma.h +concurrent_set.$(OBJEXT): {$(VPATH)}ractor.h concurrent_set.$(OBJEXT): {$(VPATH)}ruby_assert.h concurrent_set.$(OBJEXT): {$(VPATH)}ruby_atomic.h concurrent_set.$(OBJEXT): {$(VPATH)}rubyparser.h From 6f13d75598568b68193dede5d01813a279bd3c40 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 2 Jul 2026 20:36:43 +0000 Subject: [PATCH 3/4] vm_trace: read postponed-job table entries atomically rb_postponed_job_preregister already stores table[i].func/data with atomic CAS/EXCHANGE, but rb_postponed_job_flush read them non-atomically, which races when a job is (pre)registered on another thread while a job fires. Load them atomically. Co-Authored-By: Claude Opus 4.8 (1M context) --- vm_trace.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vm_trace.c b/vm_trace.c index 8ef805dc1fe9d4..542b1869e08a84 100644 --- a/vm_trace.c +++ b/vm_trace.c @@ -1950,8 +1950,10 @@ rb_postponed_job_flush(rb_vm_t *vm) while (triggered_bits) { unsigned int i = bit_length(triggered_bits) - 1; triggered_bits ^= ((1UL) << i); /* toggle ith bit off */ - rb_postponed_job_func_t func = pjq->table[i].func; - void *data = pjq->table[i].data; + /* Read atomically to pair with the atomic CAS/EXCHANGE stores in + * rb_postponed_job_preregister, which can run on another thread. */ + rb_postponed_job_func_t func = (rb_postponed_job_func_t)(uintptr_t)RUBY_ATOMIC_PTR_LOAD(pjq->table[i].func); + void *data = RUBY_ATOMIC_PTR_LOAD(pjq->table[i].data); (func)(data); } From 9233cc3b07cff4429538fe6336e15a8caee36c62 Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Thu, 2 Jul 2026 21:23:11 +0000 Subject: [PATCH 4/4] test: a new Ractor does not inherit the creating thread's fiber storage Co-Authored-By: Claude Opus 4.8 (1M context) --- test/ruby/test_ractor.rb | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index e7eb0cd4b34fe7..65c4756dc0bc02 100644 --- a/test/ruby/test_ractor.rb +++ b/test/ruby/test_ractor.rb @@ -372,6 +372,13 @@ def test_ractor_new_raises_isolation_error_if_proc_uses_yield end end + def test_ractor_does_not_inherit_fiber_storage + assert_ractor(<<~'RUBY') + Fiber[:key] = "creator" + assert_nil Ractor.new { Fiber[:key] }.value + RUBY + end + def assert_make_shareable(obj) refute Ractor.shareable?(obj), "object was already shareable" Ractor.make_shareable(obj)