Skip to content

[pull] master from ruby:master - #1394

Merged
pull[bot] merged 45 commits into
turkdevops:masterfrom
ruby:master
Sep 10, 2026
Merged

[pull] master from ruby:master#1394
pull[bot] merged 45 commits into
turkdevops:masterfrom
ruby:master

Conversation

@pull

@pull pull Bot commented Sep 10, 2026

Copy link
Copy Markdown

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

hsbt and others added 30 commits September 10, 2026 10:13
test/ruby/test_class.rb defines a top-level TestClass, so when the two
files share a test-all worker the assertion that the box's class is
invisible from the main box finds that one and fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test_port_undelivered_message_does_not_leak asserted on the ratio of two
RSS samples, which the allocator's own behaviour moves as much as the
leak did.  It failed on macOS CI at 2.70x against a 2.0x bound, after an
earlier round of tuning (large payloads, a baseline at the high-water
mark) had already been needed to get it under the bound at all.

A bound that has to be tuned to the allocator is measuring the
allocator.  Drop the test; the fix in ae5ca45 stays.

[Bug #22122]

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A new box copies the master box's $LOAD_PATH, so the tool/lib entry that
tool/test/init.rb adds to the main box at boot never reaches it, and
requiring 'envutil' by name raised LoadError under RUBY_BOX=1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test_add_constants_in_box defines String::STR_CONST0 on the main box and
never removed it, so it leaked into every later test in the same worker
process. Those lines run only now that requiring envutil into the box
works under RUBY_BOX=1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
At MinGW (CLANGARM64) builds:
```
In file included from ../src/gc.c:617:
../src/gc/default/default.c:2543:10: warning: unused variable 'need_reuse' [-Wunused-variable]
 2543 |     bool need_reuse = false;
      |          ^~~~~~~~~~
```
It is not used on Windows:
```
../src/time.c:1048:1: warning: unused function 'zone_str' [-Wunused-function]
 1048 | zone_str(const char *zone)
      | ^~~~~~~~
```
A box copies an extension to "<box id>_<serial>_<basename>" in a
private directory, but the test still expected the flattened full path
the copy was named after before that.  The ABI error message therefore
never matched under `RUBY_BOX=1`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
None of these `assert_separately` sources interpolate, and the
interpolating form consumes backslash escapes before the child ever
parses them, so a regexp written with `\d` or `\.` silently loses it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pattern matched `<digits>_<digits>_abi.` anywhere in the message, so
an unrelated component of `TMPDIR` satisfied it just as well, and it
said nothing about the per-process directory the copy is isolated in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three tests spawn a child with `RUBY_BOX=1` and assert that the two
Ruby::Box experimental warning lines come first on stderr.  They rely on
the warning setting the parent process happens to pass down.  Pass
`-W:experimental` so they get what they assert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The test raises the child's warning level to assert that `ARGF.puts`
prints nothing, but `-W2` turns every category back on, so under
`RUBY_BOX=1` the child starts with the Ruby::Box experimental warning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
test-all already turns experimental warnings off for its own process,
but tests that spawn ruby directly with Open3, IO.popen or PTY assert
the captured output verbatim.  With `RUBY_BOX=1` in the environment
every child prints the Ruby::Box experimental warning to stderr, and 51
tests and specs fail on it.  Pass `-W:no-experimental` down through
RUBYOPT.  It has to precede the bare "-" that common.mk puts there,
because that ends the option scan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The example asserts the default value of the experimental category and
already skips when the ruby_exe command carries -W.  RUBYOPT can carry
it too, and test-spec now puts -W:no-experimental there, so clear it for
the two children rather than lose the example.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last -W switch wins, so prepending it left an inherited -w or -W2 in
control and the suppression quietly did nothing.  `make test-all
RUBYOPT=-w` reproduces it: under `RUBY_BOX=1` test_open3.rb and
test_pty.rb stay at 12 and 4 failures, and reach 0 once the switch moves
behind the inherited options and in front of the bare "-".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The -w it hands every child turns all the categories back on, so the
RUBYOPT setting test-all passes down never reaches them and a Ruby::Box
startup warning lands in output the assertions compare verbatim.  The
switch is 2.7 or later, and this file is copied verbatim into
ruby/test-unit-ruby-core, which still runs on older rubies.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The test hands -w to the ruby it spawns, so the suppression the harness
sets up does not survive into that grandchild.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…te matches

`bundle update --bundler ">= <version>"` crashes with "comparison of
NilClass with Bundler::StubSpecification failed" when the requirement is
satisfied locally but by nothing on rubygems.org.

ruby/rubygems@ad1e0d4bdb

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… one

`bundle update --bundler` and `bundle lock --update --bundler` defaulted
to `> 0.a`, so publishing 4.1.0.beta1 moved every project onto the beta.
Default to `>= <running version>` and skip prerelease candidates unless
the requirement mentions one, the same way `gem update --system` does.
Anyone already running a prerelease keeps it, since the floor makes the
default requirement a prerelease requirement in that case.

ruby/rubygems#9867

ruby/rubygems@3a6c6e9781

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`bundle update --bundler --pre` did nothing: the self manager only looked
at whether the requirement named a prerelease, so it never considered one
and never restarted, leaving the flag silently ignored.

ruby/rubygems@0d527fd5e4

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Auto switching restarts into whatever prerelease the lockfile names, so
the requirement derived from the running version is a prerelease one and
a bare `--bundler` keeps the prerelease. Naming the target explicitly is
the way out, and nothing pinned that.

ruby/rubygems@c7a375c7ea

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both man pages said a bare `--bundler` picks the latest version, which no
longer holds, and neither showed that a version or requirement can be
passed. That argument is now the way to reach a prerelease.

ruby/rubygems@ba934bef60

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Once the flag had fallen to 0 the short-circuited condition still landed
in the else, so every later call stored 0 again.  Guard the attempt so the
store happens only on the call that saw clock_gettime fail.

current_process_time carries the same idiom and is what current_thread_time
falls through to, so both are fixed together.

Pointed out by nobu in review of #18717

Co-Authored-By: Nobuyoshi Nakada <nobu@ruby-lang.org>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thread_sched_wait_events() decided "timed out" from
waiting_reason.data.result == 0, which only says that no event fired.  A
thread woken by an interrupt lands there too, so an interrupted wait was
reported as thread_sched_wait_timeout.  rb_thread_io_wait() then left
revents at 0 and IO#wait_readable returned nil even though the fd had
become readable.

Only RUBY_MN_THREADS=2 could hit it, because that is what puts the main
thread on the M:N io path; =1 and =-1 keep the 1:1 path, which retries
around EINTR and reports the fd correctly.

Hand an interrupted wait back to the caller's blocking path instead, and
charge the time the M:N wait already spent against the timeout so the
deadline stays the one the caller asked for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test asserts that exactly one SIGCHLD arrives while its trap is
installed, but it cannot guarantee that: Signal.trap does not discard a
SIGCHLD that was delivered before the handler was installed, and under
-j20 the worker process has reaped children of earlier tests whose
signal can still be delivered inside the window.

This is the same tolerance the test carried until 4a67ef0, where it
was dropped along with the RJIT branch that needed it ("It may trigger
extra SIGCHLD").  An extra true is still tolerated; an empty array,
which is the regression [ruby-core:19744] guards, still fails.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The expected message bakes the receiver's `#inspect` into a heredoc before the
block raises, so a moving GC that relocates the receiver in between makes the
two addresses disagree. That is why these tests fail intermittently under MMTk.
`GC.compact` reproduces the same failure with the default GC.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
env_copy() overwrote the SPECVAL slot of every local env with
VM_BLOCK_HANDLER_NONE. For a TOP/CLASS frame's env that slot stores
the box (VM_ENV_BOX), not a block handler, so a proc defined at a
class/module body and isolated by Ractor.make_shareable lost its box
while keeping its frame type. Any method call inside such a proc then
made rb_current_box() return NULL via current_box_on_cfp(), and the
classext lookup dereferenced the NULL box:

  RUBY_BOX=1 ruby -e \
    'module M; L = Ractor.make_shareable(->(*a){ Rational(*a) }); end;
     p M::L.call(3, 4)'
  # => SIGSEGV at 0x0 in rb_vm_search_method_slowpath

Copy the SPECVAL slot through instead when the source env belongs to
a TOP/CLASS frame, so the isolated proc keeps resolving methods in
its defining box.

[Bug #22260]
ko1 and others added 15 commits September 10, 2026 16:02
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>
Every class/module records the id of the Ractor that created it in
rb_classext_t::owner_ractor_id; only that Ractor may modify it.  0 means the
main Ractor, so a program that never leaves it stores nothing.

An id rather than the Ractor object: ids come from a monotonic counter and are
never reused, so the comparison can never meet a dangling or recycled identity,
there is nothing for the GC to trace or move, and a class does not keep its
creator alive.  It also gives the termination rule for free -- once the owner
is gone no live Ractor carries its id, so no comparison can succeed and the
class is read-only for everybody.

Singleton classes and metaclasses inherit the owner of the attached
class/module instead of the Ractor which happened to trigger their lazy
creation.

rb_class_owned_p() is inline: later commits put it on the class instance
variable and constant read paths, where it replaces what used to be a single
load of ruby_single_main_ractor.  Only the non-main-owner half is out of line.
That needs rb_ractor_main_p() in internal/class.h, hence the include guard
ractor_core.h was missing.

This commit only records the ownership and introduces the
rb_class_owned_p()/rb_class_owner_check() helpers; no behavior changes yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
Requested by matz on [Feature #22226]: a non-owner freezing somebody else's
class is a hole, because it lets a third party make a class permanently
unmodifiable.  Naming is the same kind of change to the class itself.

Module#freeze checks only when it would actually change something, so an
already frozen class stays a no-op for every Ractor and idempotent .freeze
calls keep working.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
…rship

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>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With RUBY_BOX=1 inherited from the environment, a child that clears or
overrides RUBYOPT, or that passes its own warning flags, prints the
experimental warning into the captured output and fails the assertions
on it.  Tests that hand RUBY_BOX to the child themselves still see the
warning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same reason as the EnvUtil change.  A spec that replaces RUBYOPT loses
the suppression, so the ruby mspec spawns prints the experimental
warning into the output ruby_exe returns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… input

Testing on the default tty should be performed only in the CI
environment.

ruby/io-console@0e04785a7d
@pull pull Bot locked and limited conversation to collaborators Sep 10, 2026
@pull pull Bot added the ⤵️ pull label Sep 10, 2026
@pull
pull Bot merged commit e0c6905 into turkdevops:master Sep 10, 2026
1 of 3 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants