1129 Commits

Author SHA1 Message Date
Luke Gruber
1974cdddb9 Make blockparamproxy shareable
Fixes YJIT compilation bug. During YJIT compilation, it could see a
`Ractor::IsolationError` by getting the attached object of the blockparamproxy's
singleton class's (the proxy object). This object needs to be shareable in order
for the compilation to work on a non-main Ractor.
2026-11-06 15:31:56 -05:00
John Hawthorn
5c06c513ec trap: use shareable Proc semantics on non-main Ractors
Instead of rb_proc_isolate (which rejected any outer variable), dup the
handler and make the copy shareable exactly as Ractor.shareable_proc
and Ractor.new now do. With no remaining callers, remove
rb_proc_isolate, rb_proc_isolate_bang and the isolate-only code paths
in vm.c.

Method procs passed to Ractor.new now raise RuntimeError (not
supported yet), like Ractor.shareable_proc; they previously ran
unisolated with their receiver.
2026-11-04 08:13:19 -07:00
John Hawthorn
1f3e810694 Ractor.new: use Ractor.shareable_proc semantics for the block [Feature #22444]
Previously Ractor.new isolated the given block Proc in place
(rb_proc_isolate_bang) when the ractor's thread was set up. This
mutated the caller's Proc, and rejected every outer variable access.

Now Ractor.new duplicates the block and makes the copy shareable exactly
as Ractor.shareable_proc does, before any ractor resources are
allocated. The block may read outer variables that are never
reassigned and refer to shareable objects; writes, reassigned
variables, unshareable values and yield still raise
Ractor::IsolationError. The original Proc is left unchanged (not frozen,
not shareable), and instance variables on it no longer prevent use, as
the copy does not carry them.

Procs without an iseq (e.g. method procs) keep using rb_proc_isolate.
Tests and docs are updated accordingly.
2026-11-04 08:13:19 -07:00
John Hawthorn
d5d40d2749 Cache refined &:sym funcs only in the owning Ractor's table
Under refinements, vm_caller_setup_arg_block caches the func it makes for
`&:sym` in the cref's refinements table. The cref belongs to whichever
Ractor ran `using`, usually main, but any Ractor calling a method defined
there reached the cache. A non-main Ractor allocates the func in its own
objspace and stored it in main's table, where its local GC does not see
it: the func was freed on the Ractor's next GC and the next `&:sym` at
that site yielded a dangling block handler (SEGV in Array#reject from
REXML's XPath, or "stack level too deep").

Look up and store only when the table belongs to the current Ractor;
other Ractors make a fresh func, as Symbol#to_proc already does outside
the main Ractor.
2026-10-29 16:25:20 -07:00
Yusuke Endoh
bde63b65b7 Give a Ractor's main thread an ec serial from the new Ractor
Since ec serials became Ractor-local (4263f1d718), the root fiber of a
Ractor's main thread gets its serial from the creating Ractor, because it
is allocated there. The serial can then collide with one that the new
Ractor gives to its own threads. Mutex ownership is checked by ec serial,
so another thread could see a Mutex locked by the main thread as its own,
which led to "[BUG] invalid keeping_mutexes".

    Ractor.new {
      m = Mutex.new
      m.lock
      Thread.new {}.join
      Thread.new { m.unlock }.join # should raise ThreadError
    }.join
    #=> [BUG] invalid keeping_mutexes: Attempt to unlock a mutex which is not locked

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 05:40:21 +09:00
Koichi Sasada
a4131efb5a Prohibit copying a class/module created by another Ractor
Module#initialize_copy of a class/module that another Ractor owns used to
check that its constants and fields are all shareable, and copy it if so.
That check made the copying Ractor walk (and flag) the owner's objects,
and the fields were collected into a buffer and checked one by one, so a
later value sat only in that buffer, which the owner's GC cannot see.

Raise Ractor::IsolationError instead, whatever the class holds.  The
metaclass is checked as well, since its tables are copied too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 04:13:52 +09:00
Edouard CHIN
cc57f9c818 Include ivar name and class name in the Ractor isolation error message
- ### Problem

  When a Ractor isolation error happens due to setting an ivar on a
  class, the message doesn't include the name of the ivar and neither
  the name of the class. It makes it difficult to track down where the
  isolation problem comes from.

  I'd like the message to be similar as what we get when a Ractor
  access an unshareable ivar.

  This is the current behaviour

  ```ruby
  class Foo
    def self.foo
      @foo ||= "test"
    end
  end

  Ractor.new do
    Foo.foo # can not set instance variables of classes/modules by non-main Ractors
  end.join

  Foo.foo

  Ractor.new do
    Foo.foo # can not get unshareable values from instance variables of classes/modules from non-main Ractors (@foo from Foo)
  end.join
  ```

  ### Solution

  Modify the error message to include the ivar name and the class/module name.

Co-authored-by: Yaroslav Markin <yaroslav@markin.net>
2026-10-22 17:51:23 -04:00
Yusuke Endoh
053f8da910 Always use the no-inline rb_current_ec()
Under the M:N thread scheduler, a Ruby thread in a non-main Ractor may be
resumed on a different native thread after it blocks. The C compiler
assumes that the native thread never changes within a function, so it may
compute the address of ruby_current_ec once (`__tls_get_addr` in
libruby.so) and reuse it after the switch. The address then refers to the
TLS of the previous native thread, which leads to a SEGV or to using
another thread's ec.

    8.times.map do
      Ractor.new do
        50.times do
          ",,".split(/(,)/) { sleep 0.001 }
        end
        :ok
      end
    end.map(&:value)
    #=> [BUG] Segmentation fault at 0x0000000000000030 (with Clang 21)

arm64 and ppc64le already use the out-of-line rb_current_ec()
(RB_THREAD_CURRENT_EC_NOINLINE), which prevents the compiler from caching
the address. Use it on all platforms, and remove the macro.

[Bug #22453]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 00:27:26 +09:00
Yusuke Endoh
a3c7a0f169 Clear errno right before func in rb_thread_io_blocking_call
errno was cleared only once before the retry loop. Under M:N threads, the
thread may move to another native thread while it waits for the fd, and
errno is per native thread. If func then succeeded without touching errno,
a stale ETIMEDOUT left by another thread raised IO::TimeoutError on an IO
without a timeout, and the data that func had read was lost.

    8.times.map { Ractor.new {
      Thread.new { r, = IO.pipe; r.timeout = 0.0001; 30000.times { r.read(1) rescue nil } }
      r, w = IO.pipe
      Thread.new { 10000.times { w << "x"; sleep 0.0001 } }
      10000.times { r.readpartial(1) }
    } }.each(&:join)
    #=> Blocking operation timed out! (IO::TimeoutError)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 21:13:28 +09:00
John Hawthorn
0adee861cb Prevent uninitialized classes from being allocated 2026-10-13 17:02:46 -07:00
Hiroshi SHIBATA
29f3aa2f92 YJIT: Guard Fixnum results to fit in long on LLP64
A Fixnum holds a C long, which is 32-bit on Windows, so an arithmetic result or a length that did not overflow 64 bits could still be out of the Fixnum range and make an unnormalized Fixnum. Side-exit there as each instruction does on a 64-bit overflow, with opt_mult chaining to send, and treat String#length as a possible Bignum, whose allocation needs the PC and SP saved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 06:11:37 +09:00
Jeremy Evans
a8965ab695 Skip a fork bootstrap test on OpenBSD CI
I increasing the sleep from 0.2 to 0.3 in
645a0718765fbec023b20b31f819042c9cfab64e, but still saw a couple
of CI errors for this test in the last 4 days. So take the approach
used for test_autoload_fork and just skip the test. The test runs
fine on OpenBSD, it's only an issue because the OpenBSD RubyCI
machine is a software emulated virtual machine running on another
virtual machine.
2026-09-29 18:01:41 -07:00
Matt Valentine-House
3ebb1b2cab dir.c: keep GC-registered globals shareable or unregistered
last_cwd is set on init in the main Ractor, but Dir.pwd attempts to
store into it. When this is done on a child Ractor we violate the
invariant that the owning and registering Ractor must be the same. On a
normal build the use-after-free is invisible, because we only zero out
the flags, so the cache check compares the slot bytes as usual and just
thinks it's a cache miss.

It gets flagged on ASAN builds
2026-09-25 19:24:10 +01:00
Jeremy Evans
645a071876 Increase sleep in fork bootstrap test
There have been occasional failures on the OpenBSD CI machine in
this bootstrap test. There is a race condition in the test that
you can theoretically hit on a very slow machine, and the OpenBSD
CI machine is running in qemu software emulation on top of an
Ubuntu virtual machine.

I'm not sure an extra 0.1 seconds will fix all failures, but I
hope it significantly reduces the likelihood of failures.
2026-09-24 07:22:56 -07:00
John Hawthorn
6bae4d8e5f Forbid Procs with ivars from being isolated 2026-09-16 12:26:11 -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 Sasada
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
Alan Wu
7c66bea58d YJIT: Stop referencing GC internal constant in a WB test
This way it works on MMTk. The voodo tied to default GC details for
promotions and write-barriers don't test effectively on any other GC but
at least the test completes.
2026-09-11 14:42:08 -04:00
Alan Wu
0c7c2107ed YJIT: Skip btests that assume default GC on MMTk 2026-09-11 14:27:11 -04:00
Guillaume Malette
fc04a83092
YJIT: Inline Struct member writes in gen_struct_aset (GH-17477) 2026-09-11 12:41:32 -04:00
Koichi Sasada
f3ebfe5d36 Initialize as.extended before the fields object is allocated
class_allocate_complex_instance() allocates the object with an extended
shape and fills in as.extended only after allocating the fields object.
A GC during that allocation sees a shape that says "extended" and marks
whatever the reused slot left in as.extended:

  [BUG] try to mark T_NONE object (obj: out-of-heap:..., parent: ... T_OBJECT/(complex) ...)

Before 86951165b8 ("Get rid of ROBJECT_HEAP flag") the mark path keyed
off the ROBJECT_HEAP flag, which was set together with as.extended, so
the window took the embedded path and read nothing.  The shape is now
set at allocation, which makes the window live.

Reproduces with GC.stress on a class whose instances start with a
complex shape, e.g. bootstraptest/test_yjit.rb under `-s`.  Add a
bootstraptest that enables GC.stress itself so a regular btest run
covers it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 12:19:37 +09:00
Hiroshi SHIBATA
b0b21591d4 Re-enable the bootstraptest IO tests skipped on FreeBSD
Same FreeBSD 12/13 flakiness as [Bug #18613]. bootstraptest/test_io.rb ran
20 times on the rubyci FreeBSD 14.5 amd64 and 15.1 arm64 machines with no
failure. The mswin exclusion is unrelated and stays.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 08:30:28 +09:00
Koichi Sasada
f0bae5f73d Update doc/language/ractor.md and bootstraptest for class/module ownership
Document the model: only the creator Ractor can modify a class/module,
singleton class ownership follows the attached object, classes of a terminated
Ractor become read-only, and the main-Ractor-only rules for class instance
variables, constants and class variables are generalized to the owner.  For a
class variable, say which owner decides -- the class it is stored in, not the
receiver it was looked up through.

State explicitly what ownership does not give.  Reads are unsynchronized and a
class modification is not atomic, so a non-owner Ractor can observe a class
while its owner is modifying it: ownership is a single-writer guarantee, not a
consistent view for readers.  And that single writer is not always the class's
own owner -- M.include(N) rewires the ancestor chain of every class that
already includes M, whoever owns them, because modifying M is M's owner's
right.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 16:02:03 +09:00
Koichi Sasada
f5a57659d6 Ractor: move a singleton class's ownership with its object
A singleton class belongs to the owner of the object it is attached to, and
Ractor#send(move: true) hands that object over.  Without this the receiver could
define singleton methods on a moved object only when its singleton class had not
been materialized before the move.  The eigenclass chain above it goes too: each
of those is attached to the class below it, so by the same rule they follow.

The move is refused when any of them holds unshareable constants or variables:
the receiver would become their owner, and so allowed to read them, while the
sender still holds the objects themselves.  The check runs in the preflight pass,
before anything is mutated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 16:02:03 +09:00
Koichi Sasada
b8e5bf5522 Ractor: check ownership on visibility changes
set_method_visibility only called rb_check_frozen, so public/private/protected
and module_function never met the ownership check, even though the proposal
lists visibility changes as owner-only.  A non-owner Ractor could rewrite the
method entries of a foreign class through them.

The check goes next to the frozen one rather than replacing both with the
rb_class_modify_check every other method table modification uses: that one also
marks a module initialized, which this path has never done.

module_function with no arguments only sets the visibility of the current
scope, so it stays unchecked; the definitions that follow are checked as
definitions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 16:02:03 +09:00
Koichi Sasada
3cb67a2072 Ractor: check unshareable leaks when copying a foreign class
Module#initialize_copy (Class#dup etc.) of a class/module created by another
Ractor makes a copy owned by the copying Ractor.  Since an owner may read the
unshareable constant and instance variable values of its own classes, a raw
table copy would hand the copier unshareable objects belonging to the source's
Ractor.  Raise Ractor::IsolationError in that case.

The metaclass is checked as well: rb_singleton_class_clone_and_attach copies its
fields and constants into the copy's own metaclass, which the copier then owns.

Copying classes whose constants and fields are all shareable still works, which
gives a Ractor a way to "monkey-patch" a foreign class without mutating it: dup
it and modify the copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 16:02:03 +09:00
Koichi Sasada
6b9a29a13d Ractor: bring class variables under the class owner
A class variable is shared across the inheritance chain and the class it is
stored in can migrate (cvar_overtaken), so the deciding Ractor is the owner of
the class the variable is actually stored in, not of the receiver it was looked
up through.  That is a single well-defined owner at any moment, it is the same
single writer the rest of that class's fields already have, and it keeps the
cross-boundary writes out: main cannot write into a Ractor's class, and a
Ractor cannot write into main's.

Two holes turned up on the way:

- Module#remove_class_variable had no ownership check at all, so any Ractor
  could delete a class variable out of a foreign class.

- The class variable inline caches keyed the fast path on rb_ractor_main_p(),
  and the fast path writes straight into ic->entry->class_value without going
  through rb_cvar_set.  Main running a method of a foreign class therefore
  skipped the ownership check entirely.  The condition is now the owner of
  class_value, matching what the instance variable fast paths already do -- and
  a non-main owner gets the fast path it could not reach before.

cvar_overtaken() no longer deletes the duplicated entry from a class owned by
another Ractor: that deletion is reachable from read paths (rb_cvar_find), so
doing it there would be a cross-Ractor write, and it is only clean-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 16:02:03 +09:00
Koichi Sasada
2e5bf429e4 Ractor: restrict class/module modification to the creator Ractor
Only the Ractor which created a class/module may modify it: define, remove or
undefine methods, alias, include/prepend, refine, define or remove constants,
register an autoload, and set the class's own instance variables.  Reading is
unchanged.  This replaces the assorted "main Ractor only" rules with one
creator-based rule, and in exchange a Ractor gets full use of the classes it
creates itself -- unshareable constant and instance variable values included,
which even the main Ractor could not do for its own classes before.

Three paths reach a class's tables without going through rb_class_modify_check,
so they get the check of their own: Module#refine (which writes its refinement
tables into the receiver as well as into the refined class), Class#initialize
(which writes the superclass of a not-yet-initialized class, and had no frozen
check either), and Module#ruby2_keywords.

The constant inline cache has to move with the rule.  Its hit condition was
"shareable value, or we are the main Ractor", which was sound only because a
non-main Ractor could not have an unshareable constant in the first place.  Now
it can, so an owner priming the cache would let any other Ractor read that value
straight out of it, never reaching the owner check in vm_get_ev_const.  The
entry records the Ractor which filled it instead -- the one that passed the
check -- which also gives a non-main owner a fast path it never had.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 16:02:03 +09:00
Jeremy Evans
1b75fb2b7a Lower number of threads in a ractor in a bootstrap test
OpenBSD CI now runs inside a qemu virtual machine inside a Ubuntu
virtual machine, without hardware virtualization support. It is
limited to 2.5GB of RAM and only 2 vCPUs. There is a bootstrap test
that fails on it due to resource issues.

```
bootstraptest.test_ractor.rb_1536_1361.rb:8:in 'Thread#initialize':
can't create Thread: Cannot allocate memory (ThreadError)
```

Fix this by using 100 threads instead of 1000.
2026-09-09 11:29:14 -07:00
Koichi Sasada
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 Sasada
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 Sasada
6b71dd210f btest/test_thread.rb: make the NPROC test CPU-count independent
The test added by 6f176c8925 warmed two ractors before dropping
RLIMIT_NPROC, assuming every subsequent Ractor.new would attempt to
spawn a shared native thread and raise ThreadError.  However,
native_thread_check_and_create_shared widens the pool only while
snt_cnt < max_cpu, so on 2-vCPU CI machines (rubyci ubuntu-no-yjit,
ubuntu2204) the two warm ractors already saturated the pool:
pthread_create was never attempted, nothing raised, and the test
failed with "0 of 20 raised".

Warm a single ractor instead.  snt_cnt (1) then stays below max_cpu
on any host with two or more CPUs, so every attempt deterministically
reaches the failing pthread_create, while one parked shared native
thread is still around to exercise the publish-before-widen race the
test guards against.  On a single-CPU host widening can never be
attempted, so accept zero raises as well; a mixed count still fails,
which is what a broken snt_cnt rollback would produce.

Verified with RUBY_MAX_CPU=1/2/unset: the new test passes 5/5 in each
configuration on master, and still segfaults 5/5 on the pre-fix code
(3c66369561), so the original regression remains covered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 12:15:13 +09:00
Koichi Sasada
78f2945bde thread: do not clobber a sleeper's status in thread_sched_wait_events
The M:N wait set th->status to THREAD_STOPPED_FOREVER around the park
and to THREAD_RUNNABLE after it, unconditionally.  A dedicated thread's
native_cond_sleep leaves status alone: sleep_hrtime sets THREAD_STOPPED,
re-sleeps while it stays so (SLEEP_SPURIOUS_CHECK), and only a waker
sets THREAD_RUNNABLE.  Rewriting it to THREAD_RUNNABLE made every
spurious wakeup end the sleep: an interrupt deferred by
Thread.handle_interrupt cut sleep(n) short on an M:N thread.

Touch status only when the thread entered as THREAD_RUNNABLE (an io
wait), which shows "sleep" while parked as a dedicated thread's
blocking region does; a sleeper's status now belongs to its caller on
both kinds of native thread.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 01:35:08 +09:00
Koichi Sasada
6f176c8925 thread: do not publish an M:N thread before its native thread exists
native_thread_create_shared called thread_sched_to_ready before trying
to widen the shared pool, so when pthread_create failed the creator's
failure path (marking the thread killed, removing it from the living
set, rb_ractor_cancel_creation) ran against a thread that an already
parked shared native thread could pick up and run to its end
concurrently -- including rb_ractor_postmortem_free of the very
rb_thread_t the creator is still writing to.  Observed under
RLIMIT_NPROC exhaustion as
  ractor.c: rb_ractor_cancel_creation: r->status_ == ractor_blocking
  ractor.c: ractor_check_blocking: cr->threads.cnt >= cr->threads.blocking_cnt + 1
i.e. the living-set removal ran twice.

Widen the pool first and make the thread ready only after that
succeeded; the failure path then handles a thread that never became
runnable, which is what it assumes.  A never-started coroutine's stack
and context are already freed by rb_threadptr_sched_free.

Also roll snt_cnt back when native_thread_create0 fails, and free the
allocated rb_native_thread.  The count stayed raised before, so
native_thread_check_and_create_shared concluded forever that the pool
was wide enough and never tried again: permanent starvation instead of
a retry on the next timer tick.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 01:35:08 +09:00
Koichi Sasada
3c66369561 thread: wake the timer thread when an enqueued ractor has no server
ractor_sched_enq only signalled ractor.sched.cond.  The signal reaches
a parked shared native thread, and a running one revisits the queue in
ractor_sched_deq (under the same lock) before it can wait, so as long
as any shared native thread exists the entry is served.  With none --
each one dedicated to a blocking region or retired -- the signal is
lost, and if the timer thread is also in its untimed sleep
(timeslice_wait_inf), nothing serves the enqueued ractor: its runnable
thread never runs.  native_thread_check_and_create_shared, which would
repair the shortage, runs on the timer thread's timeout branch, which
is never reached again.

Wake the timer thread under the same lock its timeout decision is made
under, from the two transitions that can strand an entry: an enqueue
that finds snt_cnt == 0, and the blocking-region entry that dedicates
the last shared native thread away while the queue is non-empty.
Both are off the ordinary path, and timer_thread_wakeup_locked writes
to the communication pipe only when the timer thread sleeps untimed,
so the common case costs nothing.  [Bug #21504]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-15 01:35:08 +09:00
Koichi Sasada
5990645266 Test M:N threads waiting on one fd
Cover what an fd with more than one waiter has to do: the second waiter
must not be told the fd is ready, a read behind another waiter must sleep
rather than spin, every waiter is woken and not just the one that
registered the fd, and the read still gets its data.

The tests run inside a Ractor because M:N threads are enabled by default
only outside the main Ractor.

Against the fix reverted, two of the four fail; the spin shows up as a
full second of CPU burned while the process is idle.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 17:58:20 +09:00
Koichi Sasada
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 Sasada
5f87918fb4 GC: per-Ractor local GC — tests
Adjust bootstraptest and test-all for the per-Ractor local GC, and add
regression tests for the Ractor lifecycle and cross-objspace cases.

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-07 16:27:53 +09:00
Matt Valentine-House
1e7d46bc0d Add Ractor tests for the monitor list.
This commit adds tests to ensure that the monitor list is handled
correctly in advance or rlgc merging.

Because the monitor list can be mutated by multiple Ractors it's
possible to engineer a situation where the list contains dangling
pointers.

This test causes a SEGV under rlgc.
2026-08-05 12:06:26 +01: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
Takashi Kokubun
49cc7e3403 Revert "test_flow.rb: Skip flow tests on clang-17"
This reverts commit 9266536887.
2026-07-22 03:01:29 -04:00
Takashi Kokubun
5096c98623
test_block.rb: Skip test on clang-17 CI (#18003) 2026-07-21 19:05:20 -04:00
Takashi Kokubun
9266536887 test_flow.rb: Skip flow tests on clang-17
which are broken right now.

I'm working on a fix, but let's skip them until it's ready.
2026-07-21 16:44:36 -04:00
Ian Ker-Seymer
c7c0483efc ractor.c: preserve aliasing during Ractor move
Moving an unshareable object graph with duplicate references failed to
preserve aliasing because the traversal checked the `rec` table too late.

On a move path, `move_leave` replaces the original object with a
`Ractor::MovedObject` tombstone. Because `move_enter` classifies this
tombstone as shareable, the second visit returned `traverse_skip` before
the visited `rec` lookup was reached.

Fix this by performing the `st_lookup` on `data->rec` at the top of
`obj_traverse_replace_i`, before calling `enter_func`. We guard the lookup
with a NULL check on `data->rec` to avoid allocating `rec_hash` for
simple graphs.
2026-07-15 17:05:50 -07:00
Koichi Sasada
faa3cc8cf1 Fix Ractor move source husk breaking compaction slot size invariant
When an object is moved to another Ractor with `Ractor.send(obj, move: true)`,
the moved-out source is neutralized into a RactorMovedObject husk by move_leave.
The husk stays in the object's original, possibly larger, slot, but its shape
was reset to 0, losing the capacity. Shape 0 implies the smallest slot, so the
husk ended up with rb_obj_shape_slot_size(husk) != rb_gc_obj_slot_size(husk).

Since re-embeddable T_OBJECT fields landed, compaction requires every live
object to satisfy rb_gc_obj_slot_size == rb_obj_shape_slot_size (asserted in
gc_ref_update_object under RGENGC_CHECK_MODE). A GC.compact in the receiving
Ractor walks the husk and trips the assertion; in a release build it updates
references with a mismatched slot width and can corrupt the slot.

Give the husk an empty, frozen, RObject-layout shape while preserving its
SHAPE_ID_CAPACITY_MASK, so the slot size still matches and no field is exposed.

Co-Authored-By: Jean Boussier <jean.boussier@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 08:29:17 +09:00
Koichi Sasada
d418ff6870 Add a test for forking while other Ractors are alive
Forking with live Ractors takes a VM barrier in the parent and the child
inherits scheduler/barrier state that must be reset. General regression
coverage for the parent-side fork barrier and the child teardown, checking
the surviving Ractors still work afterwards. (Uses GC.start, not GC.compact,
so it also runs on GCs without compaction such as MMTk.)
2026-07-14 17:32:41 +09:00
Koichi Sasada
7c6a30883e Add a stress test for a thread terminating during a compaction barrier
Exercises a short-lived thread terminating while another Ractor drives
GC.compact barriers, the scenario fixed by the previous commits.
Skipped when the GC does not implement compaction (e.g. MMTk).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:32:41 +09:00
Koichi Sasada
ca20425fd5 Avoid a stale TLS execution context in fiber_switch after NT migration
`fiber_current()` reads `GET_EC()`, which the compiler may inline as a
thread-pointer-relative TLS access whose base is cached in a callee-saved
register.  `coroutine_transfer` preserves callee-saved registers across the
stack switch, so when a fiber's coroutine resumes on a different native
thread (M:N scheduler migration), the cached thread pointer is stale and
`fiber_current()` returns another thread's execution context.

`fiber_switch()` calls `fiber_current()` right after the transfer.  With
`-DRUBY_DEBUG=1` this trips `VM_ASSERT(ec == rb_current_ec_noinline())` in
`rb_current_execution_context`; in release builds it silently uses the
wrong (or NULL) EC.  Only reproduces with >= 2 Ractors (a single Ractor
does not migrate the coroutine).

Read the current EC through `rb_current_ec_noinline()` in `fiber_current()`,
which forces a fresh thread-pointer load.  fiber_current() is only used by
the (cold) fiber machinery, so the non-inlined read costs nothing on hot
paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 11:01:06 +09:00
Nobuyoshi Nakada
df0622320b
[Feature #22135] Remove obsolete ObjectSpace._id2ref 2026-07-09 17:12:21 +09:00
Ibrahim Awwal
c34cc402a3 YJIT: Fix local register mapping overflow
Previously, local indices greater than 255 were truncated when
converted to RegOpnd::Local. This could make a high-index local alias
a tracked low-index local in the register mapping and produce incorrect
values after compilation.

Cap overflowing indices to MAX_CTX_LOCALS. RegMapping treats that index
as untrackable because it is just outside the tracked local range.

Fixes [Bug #22074].

Co-authored-by: Codex <199175422+chatgpt-connector[bot]@users.noreply.github.com>
2026-05-21 12:56:21 -07:00