63 Commits
Author SHA1 Message Date
John Hawthorn f609897fa4 Add Ractor#alive?
Matches Thread#alive? and Fiber#alive?. Returns false once the ractor's
result has been recorded. Reads sync.legacy under the ractor lock, as
Ractor#monitor does, so it cannot race with termination.

[Feature #22441]
2026-10-21 16:48:38 -07:00
Luke Gruber b1d315127f Use mark ctx when marking internal ractor objects
When doing marking work with > 16 Ractors, we can see a difference by
not having to rederive the mark context when marking the Ractor object.

Benchmark: gc_balloon, 20 runs per number of ractors:

|      bench | ractors |      ctl (ms) |      exp (ms) | ctl/exp |
| ---------: | ------: | ------------: | ------------: | ------: |
| gc_balloon |       8 |  516.1 ± 2.8% |  517.3 ± 3.2% |   0.998 |
| gc_balloon |      16 |  913.4 ± 0.3% |  896.0 ± 0.3% |   1.019 |
| gc_balloon |      32 | 1818.1 ± 0.2% | 1792.1 ± 0.3% |   1.014 |

GC summary:

|      bench | ractors | mark/GC ratio | notes                     |
| ---------: | ------: | ------------: | :------------------------ |
| gc_balloon |       8 |         1.008 |                           |
| gc_balloon |      16 |         1.069 | 7% increase in mark perf  |
| gc_balloon |      32 |         1.059 | 6% increase in mark perf  |
2026-10-15 17:13:41 -04:00
Luke Gruber b5d839dbca Shrink the ractor_basket struct from 80 to 48 bytes 2026-10-09 16:04:55 -04:00
Shugo MaedaandClaude Fable 5.1 891973a944 Keep a Ractor::Port pinned while Ractor#monitor copies it
ractor_monitor() copied *rp after an allocation that can run a
compacting GC, and used rp again after taking the ractor lock.  The port
is embedded and the argument is dead once the pointer is taken, so
nothing kept the port in place.

Same pattern as https://bugs.ruby-lang.org/issues/22397

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-01 00:18:34 +09:00
Nobuyoshi Nakada 13411f495b [Bug #22386] Reject port creation after Ractor termination
Raise `Ractor::ClosedError` to prevent child threads from accessing
freed port tables during teardown.
2026-09-28 23:40:49 +09:00
Matt Valentine-House 2ad6b99a98 GC: scope rb_gc_register_address to the calling Ractor
The registered_globals list made every Ractor's GC scan every slot under
a shared lock. This commit splits it into per-Ractor lists.

We also maintain a global registry that references every Ractor that has
ever registered so that unregistering from a Ractor other than the one
that registered can look up the owning list.

When a Ractor is joined, the dead Ractor's slots are moved to the
joining Ractor's heap before the objspace merge happens.

This change tightens up the requirements for values stored in a
registered address.

Previously, Every Ractor's local GC scanned the shared list. So if a
slot held an unshareable object living in a different Ractor's heap,
that object was always kept alive regardless.

After this change, only the registering Ractor's GC scans its list. So if a
slot holds another Ractor's unshareable object, the owning Ractor's
local GC cannot see that reference, so it can collect the object while
the registered slot still points at it.

Storing such an object was already a Ractor containment violation. It
was just hidden by the shared scan.

[Feature #22277]
2026-09-25 19:24:10 +01:00
Yaroslav Markin 4133daa130 Ractor: wake receiver waiters inside the send critical section
ractor_send_basket used to enqueue the basket under the receiver's lock,
release it, and then re-take the same lock inside ractor_wakeup_all to wake
the receiver. Every sender to a Ractor contends on that one lock, so the
second acquisition doubles the contention in the send hot path.

Split ractor_wakeup_all into a _locked core plus a locking wrapper, and call
the core directly from the send path while the lock is still held. Waking
under the lock is already the established pattern (ubf_ractor_wait does it).
2026-09-24 13:17:35 -04:00
John Hawthorn dea2cdac07 Remove marshal fallback for Ractor copy
The previous commit integrated Marshal's hooks into the native courier
copying path. That should cover most cases.

One exception I've noticed that this doesn't implement is singleton
classes which have been extended by modules. I would rather we implement
that separately if we find it to be an issue.
2026-09-17 09:33:28 -07:00
John Hawthorn 73055acd57 Forbid setting ivars on Ractor shareable objects
Previously we had a check that objects which were shareable, but not
frozen on a non-main Ractor would raise an exception if instance
variables were read. This prevented a non-main Ractor reading
non-shareable objects.

This changes the behaviour to forbid all writes to shareable objects
(even when unfrozen). This is done by re-purposing the frozen flag on
the object shape to reflect (FL_FROZEN || FL_SHAREABLE).

With IV writes forbidden on all shareable objects whether or not they
are frozen (with the exception of classes and modules), we can also
remove the check against reading instance variables, since no unsafe
instance variables can be set.

[Bug #22291]
2026-09-16 12:26:11 -07:00
Koichi SasadaandClaude Opus 5 46c2853661 ractor_sync.c: remove the unused ractor_basket_new_ref()
308677c703 moved the exit tokens over to ractor_basket_new_exit(), which
left this one with no caller.  Its comment carried the reason those tokens
cannot push a tag, so that moves to ractor_basket_new_exit().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 11:37:59 +09:00
Koichi SasadaandClaude Opus 5 308677c703 Ractor#monitor: name the ractor in the exit token
A monitoring port received a bare :exited or :aborted, so a port could only
ever watch one ractor: with two, the token that arrived said what happened
but not to which.  Watching a group meant a port per ractor and a table
from the port back to the ractor -- which is what Ractor.select did, and
what anyone writing a supervisor had to do as well.

The token is now [ractor, :exited] or [ractor, :aborted], so one port can
watch a whole group and a supervisor needs nothing but monitor:

    workers.each { |r| r.monitor port }

    until workers.empty?
      r, status = port.receive
      workers.delete(r)
      workers << restart(r) if status == :aborted
    end

The pair is built by the receiver, from a basket type of its own, rather
than sent as a shareable array.  A shareable array would be simpler -- four
lines against thirty -- but it is left behind for every ractor that ever
terminates, and shareable objects are only reclaimed by a global
collection.  Creating 20,000 ractors and joining them:

                          wall       RSS held afterwards
    built on receipt     309 ms                    98 MB
    shareable array      570 ms                   533 MB

It cannot be sent as an ordinary copy either: the tokens go out from a
thread that has no VM stack left, which is what ractor_basket_new_ref
exists to avoid, and a copy pushes a tag.  Both halves are shareable
already, so the basket holds them as they are and the array is allocated
where it is read.

The array costs join, which received one symbol per call, 0.309us to
0.327us on a terminated ractor.  Ractor.select pays it back many times over
in the commit that follows.

This is an incompatible change to an experimental API, so NEWS says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 02:19:13 +09:00
Koichi SasadaandClaude Opus 5 acc5cd03d9 Ractor: widen the Ractor id to 64 bits
A Ractor id must stay unique for the life of the process, but the 32 bit
counter wraps after ~4e9 Ractors and comes back onto ids that are still in
use.  That is not out of reach: a few thousand Ractor.new/s is days, not
years.  The wrapped value is 0 too, which some callers read as "the main
Ractor".

There is no portable 64 bit atomic, so RUBY_ATOMIC_FETCH_ADD gives way to the
VM lock, which Ractor.new acquires a few frames later anyway.  That lock is a
no-op until there is a second Ractor, which is exactly when the GVL alone
already serializes the counter, so ractor_next_id asserts the same condition
vm_insert_ractor0 does.

rb_serial_t, to match ec->ractor_id and bmethod's defined_ractor_id.  Neither
struct grows: rb_ractor_pub had 4 bytes of padding after id.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 16:02:03 +09:00
Koichi SasadaandClaude Opus 5 ae5ca45944 Free messages addressed to a port that died before delivery
A message sent to a Ractor::Port is enqueued on the receiving Ractor's
recv_queue, and only moved to that port's own queue when the Ractor
receives or closes.  rb_ractor_reap_dead_ports() sweeps the port queues
held in sync.ports, so a message whose port became unreachable before any
delivery happened was never reaped: it stayed on recv_queue, off-heap and
invisible to ObjectSpace, for the life of the process.

    200.times { p = Ractor::Port.new; 1000.times { p << ("z" * 4000) } }

grew without bound (RSS 858 -> 1692 -> 2523 MB over three such passes),
and needs no Ractor at all to hit.  Closing the port, or any receive on
the same Ractor, moved the messages to the port queue and hid the leak.

Sweep recv_queue in the reap as well, dropping the baskets whose port is
no longer in the table.  The reap takes no sync lock: what keeps out the
foreign senders that write recv_queue is the caller's gate in
rb_ractor_finish_marking(), where a global GC has the world stopped and a
single objspace has exactly one live Ractor.  Assert that at the callee,
so a second caller cannot lose the guarantee silently.

[Bug #22122]

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 05:13:29 +09:00
Koichi SasadaandClaude Opus 5 9bfe1fcdb3 Ractor: wake a receiver when its port is closed
Ractor::Port#close left a receiver already waiting on that port asleep:
an untimed receive never returned, and a timed one waited out its whole
timeout and reported nil, where closing a port a thread waits on should
stop that thread.

Closing a port means nothing more can arrive on it, which is the kind of
news a send delivers by waking every waiter.  Do the same, with
wakeup_by_close -- the status already spelled out in the enum and
commented out.  The woken thread needs no new logic: ractor_close_port
deletes an emptied port from the table, and ractor_try_receive already
raises Ractor::ClosedError for a port that is no longer there.  A port
closed with messages still queued keeps them: it stays in the table until
a receiver drains it, and the last message takes it away.

Only a close that closed a port wakes anyone: ractor_queue_close reports
whether it was the call that closed the queue, so re-closing a closed
port is news to nobody.

Ractor.select over a port closed while it sleeps now raises
Ractor::ClosedError, as selecting an already-closed port does; it used to
sleep on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 04:07:37 +09:00
Koichi SasadaandClaude Opus 5 80b867e466 Ractor: keep a receive timeout when another port is woken
ractor_wait_receive reports a timeout only when nothing claimed the
wakeup: a send or an interrupt says by itself what woke the thread, so
the clock is read for wakeup_none alone.  But a wakeup goes to every
waiter of the Ractor, and one meant for another port says nothing about
this receive's deadline.  A thread waking the Ractor in a loop therefore
holds a timed receive past its time indefinitely -- it is stamped,
retries, re-registers, and is stamped again before it can ever be handed
wakeup_none:

  target = Ractor::Port.new
  other = Ractor::Port.new
  Thread.new { loop { other << 1; other.receive } }
  target.receive(timeout: 0.3)   # does not return

Keep the deadline where it belongs.  ractor_receive and the selector loop
own the timeout already; let them see that it passed and stop, after one
more look so that a message landing right at the deadline is still
returned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 04:07:37 +09:00
Koichi SasadaandClaude Opus 5 b926e79a6b Ractor: do not republish port queues a reap freed during a table rebuild
ractor_add_port grows a full port table by copying it: st_copy(old_tab), then
st_insert of the new entry, then swapping the copy in under the Ractor lock and
freeing the old table.  Both calls allocate, and an allocation is a safepoint, so
a global GC can run there.  The end of a global GC is where
rb_ractor_reap_dead_ports frees the queues of ports whose Ractor::Port went
unmarked and ST_DELETEs them -- from r->sync.ports, which is still old_tab.  The
copy already taken then carries entries the reap deleted, and swapping it in
republishes queues that have been freed.  The next global GC walks them:

  ERROR: AddressSanitizer: heap-use-after-free
      #0 ractor_queue_mark             ractor_sync.c
      #1 ractor_mark_ports_i           ractor_sync.c
      #3 ractor_sync_mark              ractor_sync.c
      #5 rb_ractor_mark_local_roots    ractor.c
      #6 gc_start_global               gc/default/default.c

Only the owning Ractor inserts into its own table, so a changed entry count means
a reap ran; take the copy again when it did.  The count is checked after each
allocation rather than once at the end: st_copy fills the header before it
allocates the entry storage, so a reap in between leaves the copy counting rows
it does not have, and st_insert must not be handed a table in that state.

The retry is bounded: each failure means the source table lost at least one
entry.

Reproduced with Ractor.select over many Ractors that finish and are absorbed,
with allocation running a global GC underneath (a supervisor loop).  A plain
build crashes only under load; under ASAN, 2 of 3 runs on master and 0 of 5 with
this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 03:29:09 +09:00
Koichi SasadaandClaude Opus 5 90e729c9bc Share one thread/ractor scheduler between POSIX and Windows
Windows carried a second, much weaker scheduler: a single Win32 mutex as
the GVL, vm->clock for preemption, its own VM barrier over
vm->ractor.sync.barrier_*, its own Ractor wait/wakeup on a per-Ractor
condvar, no ready queue and no time slice. Keeping two made the rest of
the interpreter branch on which one was compiled in, and left Ractors on
Windows without real scheduling.

Move the scheduler out of thread_pthread.c into thread_sched.c and let
both platforms use it. thread.c now includes thread_sched.c, which
declares the scheduler <-> platform contract, includes THREAD_IMPL_SRC,
and then builds the scheduler on the primitives that file provides.
thread_pthread.c keeps only those primitives: synchronization, thread
local storage, native thread creation and stacks, the communication pipe,
the timer thread backend, the fork lock and the event hooks.

The data structures the scheduler works on move to a new thread_sched.h,
included from both platform headers, so struct rb_thread_sched,
struct rb_native_thread and struct rb_thread_sched_item are now shared.

thread_pthread_mn.c becomes thread_sched_mn.c: it is the M:N scheduler,
not a pthread detail. Most of it -- the coroutine threads, the native
thread stack pool, the timer wheel and the fd -> waiters map -- is
platform independent; only the readiness backend is not, and that section
is now marked as the seam where an epoll/kqueue split (or an IOCP backend,
which is what Windows would need for M:N) would go.

What Windows had to add is small: native_thread_interrupt(),
native_thread_default_max_cpu(), timer_thread_polling(),
timer_thread_wakeup_force(), RB_NATIVE_COND_MONOTONIC_P() and
RB_NATIVE_MUTEX_TRYLOCK_DETECTS_SELF() -- the last because a
CRITICAL_SECTION is recursive, so trylock cannot back a "somebody holds
this" assertion. It sets USE_MN_THREADS to 0 and supplies the stubs
thread_sched_mn.c provides elsewhere, so every thread there is dedicated,
as it already was.

With one scheduler the platform branches go away: all 16 uses of
RUBY_THREAD_PTHREAD_H / RUBY_THREAD_WIN32_H are removed from vm_core.h,
ractor.c, ractor_core.h, ractor_sync.c, vm_sync.c, vm.c and thread.c.
ractor_check_blocking() went with them -- it only ever fed the win32
scheduler's vm->ractor.blocking_cnt -- and so did USE_VM_CLOCK, now that
the common time slice drives preemption on Windows too.

Verified on both platforms. Linux: 11 rounds of make test-all (36236
tests) and btest (2066 tests) with no failures, plus 474 runs of a
scheduler and Ractor stress soak. Windows: no test fails that does not
also fail on the same tree without this change, btest passes, and two
40-minute soaks of 438 and 460 runs are clean. Ractors now run in
parallel there -- 3.6x on four Ractors, where before they could not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 04:04:48 +09:00
Nobuyoshi Nakada 4d16abab7d Avoid GCC clobbered warnings in Ractor payload build
Keep the payload builder uninlined in optimized GCC builds because
inlining causes false-positive `-Wclobbered` warnings.
2026-08-25 23:52:27 +09:00
Nobuyoshi Nakada a5338c320a Fix clobbered warning
With gcc 15.2.0 on aarch64 Ubuntu 26.4:

```console
ractor_sync.c: In function ‘ractor_basket_new.constprop’:
ractor_sync.c:1093:82: warning: argument ‘type’ might be clobbered by ‘longjmp’ or ‘vfork’ [-Wclobbered]
 1093 | ractor_basket_new(rb_execution_context_t *ec, VALUE obj, enum ractor_basket_type type, bool exc)
      |                                                          ~~~~~~~~~~~~~~~~~~~~~~~~^~~~
```
2026-08-25 22:15:27 +09:00
Koichi Sasada d32793fe8e Ractor: let a wait for a message take a timeout
Ractor::Port#receive, Ractor.receive and Ractor.select gain a `timeout:`
keyword and return nil when it passes.

The wait itself stays where it is: rb_ractor_sched_wait() keeps parking the
thread in the thread scheduler, so an M:N thread still hands its native
thread back instead of becoming dedicated.  How the deadline is taken
depends on which kind of thread waits:

A dedicated native thread parks on its own condvar, so it takes the deadline
there, the way native_cond_sleep() does.  Nothing else is involved: the
condvar it waits on is the one a send already signals.

An M:N thread has no condvar of its own, so its deadline is armed on the
timer thread as a timeout-only wheel entry, and the timer thread wakes it
through thread_sched_to_ready_common(), which is exactly how
rb_ractor_sched_wakeup() wakes it for a send.  That gives such a waiter two
wakers, so rb_ractor_sched_wakeup() now takes an armed timeout back before
waking, skips a thread that a fired timeout already made runnable, and bumps
the scheduler event serial so a timeout that has not fired cannot wake it a
second time.

`timeout: 0` is a poll: it never blocks, converts nothing and reads no clock,
so it costs what a receive of a waiting message costs.  It still delivers the
incoming queue first, so a message another thread just sent is not missed.

A timeout that expires leaves the waiter on the Ractor's waiter list, as a
spurious wakeup does, and ractor_wait_receive() still delivers pending
messages before it reports the timeout, so a message that arrived just before
the deadline is not lost.

Both kinds of wait exist on every pthread platform, including builds without
the timer wheel (USE_MN_THREADS == 0), where every thread is dedicated.  On
win32 the wait is a condvar wait, which takes the timeout directly.
2026-08-22 00:07:09 +09:00
Koichi SasadaandClaude Opus 5 e4462a9514 Ractor: drop a port queue once nothing can name it
A port's queue lives in the receiving Ractor's table keyed by the port id, so
nothing ties it to the Ractor::Port object: when the port becomes unreachable
the queue stays, and so does every message left in it.  Nobody can receive
them -- receiving needs the port -- but they are still GC roots.

A message reaching a Ractor makes that permanent.  Port#<< of a port is the
natural shape of a credit token, and a token nobody takes holds the sender's
port, which marks the sender's Ractor object, which keeps its objspace in
zombie_objspaces for the life of the process:

  200.times { port = Ractor::Port.new
              Ractor.new(port) { |p| p << Ractor::Port.new; nil }.join }
  6.times { GC.start }
  ObjectSpace.each_object(Ractor).count   # 201 before, 1 after

Mark and sweep the table itself.  ractor_port_mark notes the ids a reachable
port still carries, and rb_ractor_finish_marking drops the queues nobody
noted, which is where the ractor-local keys are already reaped for the same
reason.  Both run only where an unmarked port really is dead: after a mark
that covered every objspace -- a global GC or a full single-objspace one --
which is also when the world is stopped enough to read the table without the
owner's lock.  Hence the new argument to rb_ractor_finish_marking: a minor
mark leaves live old objects unmarked.

The single-objspace question is asked first because mmtk marks from its GC
worker threads, which cannot reach the VM to answer the other one.  Those
threads race on the flag, but they all store the same value and the reap
reads it once marking is over.

A queue starts alive so that one created between its port being marked and
the reap, which incremental marking allows, is not taken for an orphan.  It
costs the queue one more cycle before it goes.

  2000 rounds, dropping one message each, KB of RSS retained per round:

    payload           before   after
    Integer            13.0     13.3
    Ractor::Port      leaks     14.3
    shareable 10KB     89.1     62.5

The last row is not retention: the payload is collected (0 of 500 strings
survive), but the reap only runs in a global GC, so between two of them the
dropped messages hold pages.  Collecting every 20 rounds instead brings it to
1.5 KB.

The reap runs from rb_ractor_finish_marking, which only the default GC calls
-- mmtk has never called it, so the ractor-local keys it also frees are not
freed there either.  Under mmtk a dropped message still holds its port, so
the test for that is a test-all case that omits itself, next to the other
tests for per-Ractor objspace semantics.

ractor-pipeline, which sends credit tokens this way, over 60 pipelines:
455MB growing by 5.6MB each to 272MB growing by 1.5MB, with the library
unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 04:13:52 +09:00
Koichi SasadaandClaude Opus 5 0541de372f Ractor: mark a terminated Ractor's join value through one function
rb_ractor_mark_in_flight_for_single_objspace marked r->sync.legacy and
nothing else once the pin machinery went away, which is what
rb_ractor_mark_terminated_join_value next to it already does -- two functions
over the same field, and the surviving one names it correctly.  Call that one
from the single-objspace branch and drop the other.

It also pins, which the plain rb_gc_mark did not.  sync.legacy is a C-struct
slot with no compaction update hook anywhere, so pinning is the treatment the
field needs; the comment on rb_ractor_mark_terminated_join_value says so.

The name had stopped describing the function: "in flight" meant the sending
basket and the pinned copy snapshot it used to mark, and both are gone.  The
word is also the GC-cycle sense elsewhere in this code, which is why the
wrapper it sat next to is now rb_gc_rest.

Only the ractor.c translation unit used it -- ractor.c includes
ractor_sync.c -- so the ractor_core.h declaration goes too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 13:48:51 +09:00
Koichi SasadaandClaude Opus 5 5be69f0246 Ractor: free the basket when the port turned out to be closed
ractor_send_basket freed the basket only on the path that raises
Ractor::ClosedError.  With raise_on_error false the closed port leaves the
basket neither enqueued nor freed, and nothing else holds a pointer to it, so
it leaks.

The one caller that passes false is ractor_send_exit_tokens, so a monitor port
that is closed before the Ractor it watches exits leaks one basket per exit.
Counting live baskets over 400 rounds of

  mon = Ractor::Port.new
  r = Ractor.new { Ractor.receive }
  r.monitor(mon)
  mon.close
  r.send(1)
  r.join

gives 401 live before and 1 after; receiving the token instead of closing the
port was already flat, which is what the closed path should match.

Predates this branch -- the same code is in master.  It matters more here
because a basket now sits on its holder's off_queue_baskets while it is being
built: a caller that combined ractor_basket_new with raise_on_error false
would leave it on that list, marked in every collection, rather than only
leaking the allocation.  No caller does that today.

Found by the Copilot review on ruby/ruby#18392.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 11:56:29 +09:00
Koichi SasadaandClaude Opus 5 d94c7ee6c6 Ractor: name the courier machinery after the courier, not after move
The off-heap courier started out as the move path's own structure, so every
part of it was named move_*.  Copy now travels in the same structure and
through the same capture walk, with courier_build.copy selecting whether the
source is read or taken apart, which left the prefix claiming something
untrue: move_capture is what a copy goes through too.

Rename the shared machinery to courier_*: the struct and its build/
materialize/mark/free entry points, the node kinds, the capture and
materialize walks, and the array allocators.  The two builders become
rb_ractor_courier_build_copy and rb_ractor_courier_build_move so the pair
reads as one family.  What is still move-only keeps the move_ prefix --
move_preflight (copy has its own walk, copy_courier_supported_p) and
move_neutralize_source, which is exactly the step copy skips.  In re.c the
MatchData transfer helpers become rb_match_blob_*, matching the neighbouring
rb_backtrace_blob_* it sits beside in the same node.

No behaviour change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 11:56:29 +09:00
Koichi SasadaandClaude Opus 5 4fa95746e4 Ractor: root a courier from the basket that holds it
A basket is rooted by whoever holds it, and nothing else: the port queue it
waits on, or -- while it is on no queue -- its holder's off_queue_baskets
list.  rb_ractor_sync grows that list and the basket a second ccan_list_node,
so it can sit on one or the other.

  send     alloc the basket, put it on the sender's list, then build the
           payload into it: rb_ractor_{move,copy}_courier_build publish the
           courier into the basket right after ZALLOC, so even a half-built
           one is rooted.  A move needs this -- it husks the sources as it
           captures them, so what the courier has collected loses its other
           root as the walk proceeds.
  enqueue  off the sender's list, onto the receiver's queue
  receive  off the queue, onto the receiver's list until the basket is freed

The publish is the handover of ownership too: when the capture raises, the
builder re-raises and leaves the courier where it is, and the basket frees it
exactly once as ractor_basket_new unwinds.  (The raise path was pointed out by
the Copilot review on ruby/ruby#18392.)

Before this, a courier was the one payload ractor_basket_mark skipped: it
left the whole job to vm->ractor.move_courier_registry, which only a global
GC walks.  Between the send and the next global GC nothing rooted the
payload, and a shareable object reachable from the courier alone was
collected -- a dynamic Symbol loses its fstr, a frozen Array its elements,
and reading it crashes.  A shareable String survived only by being an
fstring, rooted elsewhere.  That is already broken in master for move; it
becomes reachable from an ordinary send once copy travels by courier.  Two
reproducers, both crashing unpatched master and fine on 4.0.2, now pass:

  ports = 5.times.map do |i|
    port = Ractor::Port.new
    r = Ractor.new(port, i) { |p, n| p.send([:"sym_#{n}"], move: true) }
    r.join
    port
  end
  5.times { GC.start }
  ports.each { |p| puts p.receive[0].inspect }

(the symbol has to be interpolated: a literal one is held by the iseq)  The
second sends Ractor.make_shareable([1, "child"]) the same way.

ractor_sync_mark walks the list next to the queues it already walks, so this
needs no new root pass, and it walks both in every collection.  "What a
courier holds is shareable, and only a global GC frees a shareable" does not
hold: pinned_roots_mark, which roots a shareable from its page bit, is
skipped once the process is back to a single Ractor
(rb_gc_single_objspace_p), and then an ordinary local GC frees one that
nothing else names.  A payload in flight is named by its basket and nothing
else.  The list needs no sync lock even so -- only its owner touches it,
unlike the queues a foreign sender writes.  The one caller that does skip it
is the single-objspace root pass, where ractor_mark_unshareable_parts has
just walked the same baskets through ractor_sync_mark.

This retires the registry, its lock, its fork re-initialization and
rb_ractor_move_courier_registry_mark.  Walking the registry in every
collection instead of the basket would not do: a receiver's local GC, which
materialize's own allocation can trigger, would then walk a courier another
Ractor is inside move_alloc_ref for -- observed as a crash.

move_alloc_node and move_alloc_ref grow by swapping in a fresh array rather
than realloc, for the same reason the nodes are initialized mark-safe: the
courier is a GC root while it is built, and a realloc can leave c->nodes
pointing at a block it has already freed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 11:56:29 +09:00
Koichi SasadaandClaude Opus 5 68447b1559 Ractor: retire the on-heap copy snapshot and its in-flight pins
With MatchData and an exception's backtrace on the courier, every copy
payload is off-heap: the courier carries the core types and anything else
travels as marshaled bytes.  The sender-resident snapshot is gone, and with
it all the machinery that kept one alive across a global GC.

MatchData: rb_match_move_dump takes a release_source flag, since only a move
takes the source's onig region and char_offset apart.

Backtrace: rb_backtrace_blob_dump / _load / _mark copy the frames into an
off-heap blob.  A frame only references shareable iseq and method-entry
imemos, so the blob can carry them as they are; it has no compaction update
hook, so the mark pins them (rb_gc_mark, not _movable).

Removed, all of it unreachable once no payload lives in the sender's heap:

  rb_ractor_t   pin_capture / _cnt / _capa, sending_basket,
                gen_fields_capturing, ractor_pin_capture_push
  basket        p.pinned / p.pinned_cnt, ractor_basket_repin_in_flight,
                rb_ractor_repin_in_flight and its queue walks
  receiver      struct ractor_materialize_frame, ec->materialize_frames and
                its mark loop, sync.materializing_copies,
                rb_ractor_materializing_p
  gc            rb_gc_pin_in_flight_message and the whole
                rb_gc_impl_pin_in_flight_message hook (default, mmtk, the
                modular function table), plus the two verifier relaxations
                that existed for a snapshot being materialized

shref bits stay: the write barrier still records them for other reasons.
ractor_copy_native_try and copy_enter stay too, since
Ractor.make_shareable(obj, copy: true) still deep-copies within one objspace.

Message cost, 20k messages per trial, 9 trials, median us per message:

                        4.0.2   master   here
  4KB String              3.8     19.4     9.4
  20-key Hash             3.3     11.9     7.5
  nested Hash/Array/Str   5.3      7.1     5.1
  100-elem Integer Array  4.4      5.7     6.9
  40B String, Object      same across all three

master is well behind 4.0.2 on multi-KB payloads; this recovers about half
of that and puts nested graphs back at 4.0.2's level.

Known issue, already in master and not introduced here: the courier's
registry mark does not traverse a shareable payload, so its children are
collected if a global GC runs while the message is in flight.  Unpatched
master crashes on both repros (a dynamic Symbol loses its fstr, a frozen
Array loses its elements) and 4.0.2 does not.  It only shows up for move
today; routing copy through the courier makes it reachable from a common
path, so it wants fixing before this ships.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 11:56:29 +09:00
Koichi SasadaandClaude Opus 5 cd2ea9df6c Ractor: carry a natively copied payload off-heap too
Step 2 took the marshaled copy payload off-heap; this does the same for the
natively copied one, so an unreceived copy no longer pins a page of the
sender's heap for any payload type.

move_capture gains a copy mode: it reads the sources instead of taking them
apart (no husk, no buffer hand-over, no freeing the source's internals), and
copy_courier_supported_p decides up front whether the courier can carry the
whole graph.  MatchData, IO, any other T_DATA and a singleton class are
rejected there and keep using the on-heap snapshot path, which handles or
rejects them exactly as before.

Retention with 3000 send-and-never-receive rounds (KB per sending Ractor,
before -> after):

  String   79.5 -> 14.0     Array  79.5 -> 14.0
  Object   79.5 -> 14.0     Hash   79.5 -> 14.0

14 KB is what a Ractor that sends nothing retains, and that part plateaus.

A courier is an in-process format, so a shareable payload can be embedded as
the VALUE itself rather than costing a whole move_node: child slots now hold
either a node id or, with the top bit set, an index into a compact c->refs
array that the registry marks.  Without that, an array of immediates paid a
72-byte node, a hash lookup and an insert per element, and the message path
cost 2.5x what the native copier did (it skipped shareable children).

Message throughput (20k messages, n=9, interleaved, median us/message):

  100-element Integer array   5.1 -> 4.7
  4KB String                 16.6 -> 9.3     (one memcpy, not two copies)
  20-key Hash                12.0 -> 9.4
  nested Hash/Array/String   10.1 -> 7.2
  40B String, bare Object    unchanged

It also restores a behaviour master lost: a String or Array subclass now
survives the copy again (4.0.2 copies with #clone and keeps the class;
master's native copier builds a base-class object).  MyStr < String arrives
as MyStr, as it does under Marshal and #clone.

move_capture keeps testing the dedup table before shareability: move husks
each source as it goes, and a husk is a frozen field-less object that
rb_ractor_shareable_p answers true for, so the other order embedded the husk
instead of resolving a second occurrence to the first one's node
(bootstraptest/test_ractor.rb:661).  An immediate skips the lookup, since
only captured objects are ever inserted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 11:56:29 +09:00
Koichi SasadaandClaude Opus 5 7d5aaefc01 Ractor: keep a marshaled copy payload off-heap
An unshareable object that the native copier cannot handle is sent as a
Marshal dump.  That dump was a String in the sender's objspace, kept alive
by an in-flight pin until the receiver materialized it.  When the message
is never received the pin never lifts, so the page holding it stays
allocated for the life of the process: GC.start, GC.compact and
Port#close all leave it.  With per-Ractor GC that is one 64KB heap page
per sending Ractor, and it grows without bound.

Carry the dump in an xmalloc'd buffer instead, the way a move courier
already carries its payload (design_v2.md 4.5).  An in-flight payload
that is not a GC object needs no pin, so nothing of the sender's heap is
held while the message waits, and an unreceived message costs only the
buffer.

Measured with 3000 send-and-never-receive rounds, each from a Ractor that
then exits:

  payload          before              after
  Time             79.9 KB/Ractor      13.7 KB/Ractor   (1.00 -> 0.01 page)
  Set              79.5 KB/Ractor      13.3 KB/Ractor
  Random           83.6 KB/Ractor      15.7 KB/Ractor

13 KB/Ractor is what a Ractor that sends nothing at all retains, and that
part plateaus.  Send/receive throughput is unchanged (20k messages, n=9,
median us/message: Time 8.75 -> 8.79, Set 4.88 -> 4.84).

Natively copied payloads (String, Array, Hash, Object, Struct, MatchData)
still build a snapshot in the sender's objspace and still pin it; they
keep the old behaviour for now.

Marshal does not mark its source (mark_load_arg), and the basket is off
the queue while it materializes, so the rebuilt String is rooted from the
receiving frame's stack slot.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 11:56:29 +09:00
Nobuyoshi Nakada 9e01974ae7 Store variables which might be clobbered 2026-08-16 12:28:25 +09:00
Koichi SasadaandClaude Fable 5 247fba2efa Do not run GC while inserting to the ports table
`st_insert` to `r->sync.ports` under the ractor lock can rebuild
the table and the allocation for the rebuilding can run GC, which
acquires the VM lock (and can run the VM barrier). Acquiring the VM
lock under a ractor lock is a prohibited lock ordering: it can
deadlock with the VM barrier and it is detected by the assertion in
vm_lock_enter() on RACTOR_CHECK_MODE builds.

Introduce st_insert_no_rebuild(), which fails instead of rebuilding
(allocating) the table, and use it under the ractor lock. Only when
the table is full, rebuild the table by copying outside of the
ractor lock and swap it under the lock to exclude the readers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 11:05:14 +09:00
Koichi SasadaandClaude Opus 5 8be91fab4f Ractor: send the exit tokens after the post-mortem collection
Ractor#join waits on a monitor port, and the exit token was sent from
ractor_notify_exit as the Ractor's block ends.  Ractor#value then has to wait
for ractor_terminated on its own -- vm_remove_ractor retires the objspace the
joiner is about to absorb -- and it does so by spinning on rb_thread_schedule().

Now that the dying thread collects its own objspace post-mortem, that spin
covers a whole local GC: the joiner burns a core for as long as the child's
final collection takes, which grows with the child's heap.

Split the notification: the value is published at atexit as before, but the
tokens go out after the collection, from rb_ractor_postmortem() so that the
order of the two steps is stated in one place.  What the joiner spins over is
then only the scheduler handoff and vm_remove_ractor, which is constant.

The send cannot go through ractor_basket_new there: thread_cleanup_func has
already dropped the EC's VM stack, and EC_PUSH_TAG reads ec->cfp when ZJIT is
built in.  A token is a static Symbol, so ractor_basket_new_ref builds its
basket without a tag or any payload preparation.

Post-join spin, by the number of objects the child allocated:

     0:    13us ->   3us
  10000:   961us ->   3us
 100000:  5211us -> 199us   (the remainder is the absorb itself)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 10:53:28 +09:00
Koichi SasadaandClaude Fable 5 e768a87a17 Ractor: the dying thread collects its own objspace post-mortem
The retire GC at notify_exit could not reclaim the terminating
thread's scaffolding -- the Thread and Fiber wrappers, the ThreadGroup,
the Ractor-local stdio, whatever the machine stack still references --
because at that point they were all still rooted.  Those ~11 objects
kept ~2 heap pages alive per dead Ractor, merged into the inheritor to
wait for its next major GC, so both a spawn-and-join loop and a
fire-and-forget loop grew the heap by two pages per Ractor with
nothing reclaiming them.

Instead, run the final collection at the tail of thread_start_func_2,
just before the scheduler handoff.  The thread still holds the GVL, so
the collection runs lock-free like any local GC (a concurrent global
GC waits for its safepoint), and vm_remove_ractor keeps its light
lock-only section.  r->postmortem makes rb_ractor_mark_local_roots
root only what must survive the death -- the join value
(r->sync.legacy) and the rb_gc_register_mark_object pins -- and the
zombie_objspaces entry recorded later measures exactly the pages the
joiner will inherit.  While the Ractor is still in vm->ractor.set,
rb_vm_mark and ractor_mark keep the Ractor object itself and its
default port alive as before; everything they mark is foreign to or
pinned by this collection.

Two of the swept wrappers are the dying thread's own: its Thread (the
rest of the collection resolves GET_EC through ec->thread_ptr) and its
root Fiber (whose struct embeds that ec).  thread_free and fiber_free
recognize "the struct I am standing on" (ec == the current ec), keep
the struct, and leave an in-band mark (th->self = 0; cont.self = 0,
which already means "no wrapper" on the never-wrapped path).  Found as
a 1/30 heap corruption, pinned by ASAN.  The collecting thread
captures those two pointers right after the collection, while its GVL
still excludes any global GC, and frees exactly the captured structs
at its very last step -- after vm_remove_ractor unlinks the Ractor,
anything it did not sweep may already be freed by a concurrent global
GC, so the epilogue reads no shared memory at all.  Those frees would
resolve their objspace through the TLS ec -- which sits inside the
fiber struct being freed and leads to the retired Ractor -- so
rb_ractor_postmortem_free severs the TLS ec first (the MN epilogue
already had) and they fall back to the main objspace.

While a thread-event hook is registered the postmortem flag stays off
and the collection keeps the ordinary retire roots: callbacks may
retain any Ractor's Thread object (rb_internal_thread_event_hook_t),
and such references are invisible to the reduced roots.  The check
takes the hook rwlock, so it happens-after any completed registration;
a hook registered after it gets no event from this thread --
rb_thread_execute_hooks skips a thread whose wrapper is gone
(th->self == 0) -- so it never receives what the collection swept.
On platforms without hook support (win32, none) the check reports
false.

The collection runs after thread_cleanup_func, so the dying thread's
machine stack is already torn down: the root scan skips its machine
context (rb_gc_impl_during_postmortem_p), which holds nothing the
reduced roots do not already cover and faults when scanned on some
platforms (windows-arm).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 03:33:40 +09:00
Matt Valentine-House 6480d2236a Match the owning Ractor in Ractor#unmonitor
Port IDs are per-Ractor, so two monitoring Ractors can have the
same port ID. Ractor#unmonitor matched only by port ID, so it
could remove an entry that belonged to a different Ractor.

We should also match that the port's belong to the same Ractor.
2026-08-10 17:54:00 +01:00
Koichi SasadaandClaude Opus 5 b5da1e4856 Ractor: drop the pin list if basket allocation raises
After a native copy succeeds, ractor_basket_alloc can still raise
NoMemoryError before cr->pin_capture is handed to the basket.  The stale
list was then re-pinned by every global GC's root pass
(rb_ractor_repin_in_flight walks it unconditionally), keeping the failed
send's snapshot alive until the next copy send replaced the list.  Free
the list on that path; the nodes stay shref-pinned only until the next
global GC clears the bits.

Found by an independent review of the previous raise-path fixes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 16:27:53 +09:00
Koichi SasadaandClaude Opus 5 90fceb36d3 Fix raise-path bugs in Ractor#value and Ractor creation
Verified from an independent review's findings:

- Ractor#value: the legacy_taken check ran only before the wait for
  ractor_terminated, which yields the GVL, so a second thread of the same
  Ractor calling #value returned Qnil instead of raising "The value was
  already taken".  Re-check after the wait.
- basket_new: allocating the basket after building a move courier meant a
  NoMemoryError from the alloc orphaned the courier (sources already
  destroyed, registry entry alive forever).  Allocate the basket first and
  free it if the courier build raises.
- Ractor creation: rb_thread_alloc can raise NoMemoryError inside
  RB_VM_LOCKING, whose unlock a longjmp skips -- and it would also leave
  cr->objspace pointing at the child.  Catch inside the lock, restore, and
  re-raise outside.
- thread_start: rb_ractor_setup_default_port ran outside the tag that
  cancels creation, so a raise left an unstartable Ractor in the set and
  terminate_all waited forever.  Move it inside.
- thread_start: native_thread_create failure for a child Ractor called
  rb_ractor_living_threads_remove, which asserts the current Ractor and
  runs the Ractor-exit protocol from the wrong thread; cancel the creation
  instead, like the send-failure path.

btest 2054 PASS, test_ractor.rb 38 tests / 364 assertions PASS; the #value
race is covered by a new deterministic check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 16:27:53 +09:00
Koichi SasadaandClaude Opus 5 9d3b43fda9 Ractor: do not leak generic_fields_lock when the insert raises
Adding a generic-fields entry takes the VM-wide generic_fields_lock and then
calls st_insert, which mallocs when the table grows.  With this Ractor's GC
disabled the allocation cannot fall back to a collection, so it can raise
NoMemoryError -- and the raise skipped both gf_unlock() and the matching
rb_gc_local_enable().  A leaked generic_fields_lock hangs every later access
to generic fields, including the marking of any Ractor's local GC, so an
otherwise recoverable NoMemoryError turned into a deadlock.  Unwind through
a tag instead.

While here, drop three things that no longer match the code: a basket
payload comment describing a generic-ivar map field that was removed, the
unused st_table placeholder left in ractor_basket_new, and two comments in
variable.c still describing the per-Ractor generic-fields tables that the
single global table replaced.

This commit also carries the mechanical translation of the Japanese comments
in these two files, which was in progress in the same working tree.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 16:27:53 +09:00
Koichi SasadaandClaude Opus 5 09aab7899f Ractor: derive object belonging from the owning objspace
RACTOR_CHECK_MODE stamped every object with the id of the Ractor that
allocated it (a 4 byte suffix on every slot), and re-stamped whole object
graphs whenever a value crossed a Ractor boundary.  Per-Ractor objspaces
already record the owner: an object's page knows its objspace, so
rb_gc_obj_foreign_p() answers "is this another Ractor's object" with one
load, and the id, its suffix and the re-stamping traversals all go away.

Deriving it is also more accurate.  The stamped id was never rewritten
when a zombie objspace was absorbed, which is what ractor_reset_belonging()
had to paper over by re-stamping received and inherited values.  Page
ownership moves with the absorb, so nothing is left stale.

thread.c stamped the child's main Thread wrapper with the child's id
because upstream allocates that wrapper in the parent's objspace.  This
branch already allocates it in the child's objspace, so the site goes too.

With ractor_reset_belonging() gone, no rb_obj_traverse() caller has an
enter function that descends into arbitrary types: make_shareable raises
on T_FILE and shareable_p stops there, so obj_traverse_i() can no longer
reach T_FILE and its case goes as well.

GC impls that keep a single objspace (mmtk) cannot tell owners apart and
report every object as their own, so the check degrades to a no-op there;
in exchange, dropping the suffix re-enables mmtk's ZJIT allocation
fastpath.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 16:27:53 +09:00
Koichi SasadaandClaude Fable 5 78ec25efa2 Ractor#value: return the value only once
Ractor#value used to be repeatable: the successor could call it again
and get the same object back, because the value stayed reachable from
the dead Ractor's C struct (sync.legacy).  Keeping that slot valid
after the Ractor is gone is what forced the successor to root and pin
every Ractor it absorbed, through a VM-wide list guarded by its own
lock, with add, unlink and scan coming from three different threads.

Take the value out of the struct on the first call and raise
Ractor::Error afterwards.  The value is then rooted by the caller like
any other return value, so the list, its lock and the pinning go away.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 16:27:53 +09:00
Koichi SasadaandClaude d549b394dc GC: per-Ractor local GC — Ractor (move courier / absorb / teardown)
Implement send(move: true) with an off-heap courier that carries the
payload between objspaces, neutralizing the source into a
RactorMovedObject shell.  Handle absorbing a terminated Ractor's heap,
keeping a zombie owner alive while another Ractor still reads its value,
and per-Ractor ownership of the VM tables.

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-07 16:27:53 +09:00
himura467 d6e2feea47 Return a byte count from ractor_sync_memsize()
st_table_size() is the number of entries, but ractor_memsize() adds the result to sizeof(rb_ractor_t) as a byte count, so a Ractor was reported as costing one byte per open port. Use st_memsize(), as ractor_selector_memsize() in this file already does for its own ports table.
2026-07-29 16:56:28 +02:00
himura467 d3aee406de Raise TypeError for uninitialized Ractor::Port
Ractor::Port has a public allocator, so Ractor::Port.allocate returns a port whose owner Ractor is still NULL; only ractor_port_init() fills it in. The methods dereferenced it unchecked:

Ractor::Port.allocate.closed?  # [BUG] Segmentation fault at 0x60

Route the paths reachable from Ruby through ractor_port_ptr_check(), which raises TypeError as other uninitialized typed data objects do. The self of #initialize_copy keeps the unchecked accessor because it is legitimately uninitialized there; only orig is checked.

Ractor.select did not crash, but such a port has id 0, the same as the current Ractor's default port, so it took messages from it.

Port#closed? also loses Primitive.attr! :leaf. It can raise now, and it was not a leaf before either, since the foreign Ractor path takes RACTOR_LOCK().

[Bug #22214]
2026-07-28 12:31:24 -04:00
Koichi Sasada fc6ff6fbcc ractor: guard the port VALUE across receive/send
ractor_port_receive/send derive a raw 'struct ractor_port *rp' from self's
embedded (RUBY_TYPED_EMBEDDABLE) data and keep using it across a GC
safepoint: receive parks, and send allocates the basket. self is only kept
alive movably (rb_gc_mark_movable via the VM stack), so a concurrent
GC.compact can relocate the port and leave rp dangling -- the receiver then
polls a stale, reused slot's port id forever (a hang of Ractor#value under
IO + GC.compact). RB_GC_GUARD(self) keeps self on the machine stack, whose
conservative scan pins the port so it cannot move while rp is live.
2026-07-14 17:32:41 +09:00
Koichi SasadaandClaude Opus 4.8 10e53afd4e ractor: lock the owner when Port#closed? is queried from another Ractor
ractor_closed_port_p asserts the owning ractor's lock is held for foreign
access and reads sync.ports via st_lookup, but Ractor::Port#closed?
(ractor_port_closed_p) called it without the lock. From a foreign Ractor this
tripped the assertion and raced the owner's st_insert/st_delete on the ports
table. Take the ractor lock for foreign queries, like every other foreign
reader.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 12:24:52 +09:00
John HawthornandLuke Gruber daffaaee73 Mark most TypedData types as THREAD_SAFE_FREE
Set RUBY_TYPED_THREAD_SAFE_FREE on TypedData types whose dfree function
is trivially safe, and only frees its own memory

Types not yet marked as THREAD_SAFE_FREE:
- id2ref_tbl_type: sets the process-global id2ref_tbl to NULL.
- mutex_data_type: unlinks itself from another thread's keeping_mutexes list.
- autoload_data_type: deletes nodes from a shared intrusive list.
- autoload_const_type: deletes itself from a shared list.
- rb_cont_data_type: mutates the shared fiber pool and the global first_jit_cont list.
- rb_fiber_data_type: delegates to cont_free.
- FiberPoolDataType: mutates shared fiber pool free-lists.
- ractor_data_type: reconfigures global VM event-hook flags and call caches.
- exported_object_registry: frees a global table under the VM lock.
- rb_box_data_type (Box::Entry): unlinks classext from other live class and module objects.
- box_ext_cleanup_type: dereferences another String object and calls unlink.
- monitor_data_type: uses the default free, so the flag has no effect.

Co-authored-by: Luke Gruber <luke.gruber@shopify.com>
2026-06-23 13:30:26 -07:00
Luke Gruber fa20111333 Fix ubf_ractor_wait
There was a race between ubf_ractor_wait and `rb_ractor_sched_wakeup` (Port#send,Ractor#send).

It could occur with a sequence like this:

1) r1 t1: r2#send (rb_ractor_sched_wakeup(r2, t2))
2) ubf called for r2, but the caller gets descheduled right before acquiring RACTOR_LOCK(r2)
   in ubf_ractor_wait
3) r2 t2 wakes up from #send call, takes RACTOR_LOCK, clears ubf, waiter is
   no longer on its stack.
4) ubf resumes, acquires RACTOR_LOCK(r2), tries to touch waiter
   (use_after_return)

This was caught by ASAN:
https://ci.rvm.jp/results/trunk_asan@ruby-sp1/6247788
2026-03-23 13:56:49 -04:00
John Hawthorn c104ede546 Make Ractor::Port embeddable 2026-02-02 13:18:15 -08:00
Jean Boussier 4d5cd81165 ractor_sync.c: use ruby_sized_xfree 2026-02-01 12:17:29 +01:00
Luke Gruber aace29d485 Check for NULL fields in TYPEDDATA memsize functions (#15633)
Some TYPEDDATA objects allocate struct fields using the GC right after
they get created, and in that case the VM can try to perform a GC and join
a barrier if another ractor started one. If we're dumping the heap in another
ractor, this acquires a barrier and it will call the `rb_obj_memsize` function on this
object. We can't assume these struct fields are non-null. This also goes for C extensions,
which may cause problems with heap dumping from a ractor if their memsize functions aren't
coded correctly to check for NULL fields. Because dumping the heap from a ractor is likely a
rare scenario and it has only recently been introduced, we'll have to see how this works in
practice and if it causes bugs.
2025-12-18 15:42:49 -05:00
Luke Gruber 839410f073 Fix heap dump with ractor barrier
When a ractor was being initialized and it would join the heap dump barrier when
allocating its queue or its ports, the heap dump code calls `rb_obj_memsize` on
the ractor and this function assumed `ports` was never NULL. We need to check for
the NULL case in case the ractor is still being initialized. Hopefully other T_DATA
objects don't suffer from the same issue, otherwise we could revert the ractor barrier
during heap dump or not use `rb_obj_memsize` on T_DATA during the heap dump.
2025-12-17 11:12:57 -08:00
Koichi Sasada b9188901c0 allow Ractor::Port shareable 2025-10-30 18:04:08 +09:00