Skip to content

[pull] master from ruby:master - #1390

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

[pull] master from ruby:master#1390
pull[bot] merged 36 commits into
turkdevops:masterfrom
ruby:master

Conversation

@pull

@pull pull Bot commented Sep 9, 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 : )

bquorning and others added 30 commits September 8, 2026 15:56
json 3.0 changed `JSON.parse` to take keyword arguments instead of a
positional opts Hash, so some of the documented usage would now raise
`ArgumentError`. I have updated all such examples to use kwargs instead.

Also align documented output with what is actually produced with the
current parser/generator on Ruby 4.0, plus a number of other minor
changes.

ruby/json@8bdd5179df
RB_OBJ_SET_SHAREABLE() was unusable from extensions: the declaration
was outside RBIMPL_SYMBOL_EXPORT_BEGIN/END, so the symbol was compiled
with hidden visibility and did not appear in libruby.so.

Also document it, including the cost that a shareable object is not
collected by a local GC.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(ruby/erb#139)

Changing a string encoding is more work than directly creating it
with the right encoding, as Ruby has to check if the TERM_LEN matches
etc.

ruby/erb@ae26cc80cf
…moving it

create_ractor_alloc_thread() pointed cr->objspace at the child's objspace while
it allocated the child's Thread and root Fiber wrappers, so that they would be
made of objects the child owns.  That slot is read without the VM lock by
threads that hold no GVL -- a Ractor's postmortem epilogue, which clears the
TLS EC before freeing the dead thread, thread_sched_reclaim, and any thread
that released the GVL inside rb_nogvl -- to decide which objspace to charge a
free to.  One of them could therefore be sent to the child's objspace; a
stillborn child (IsolationError) is then disowned and freed by the orphan merge
job, and the free landed in released memory (ASAN heap-use-after-free at the
malloc accounting in ruby_xfree_sized, seen once in 209 CI runs).

The window allocates exactly two objects, so hand the objspace down to them
instead: rb_newobj_in_objspace() and the two TypedData entry points built on
it carry it to thread_alloc() and fiber_alloc_in().  cr->objspace never moves,
so no other thread can observe anything, and rb_gc_get_objspace() is unchanged.

GC suppression around the wrappers now names the child's objspace too, which is
the one the allocations go to; resolving it through the current Ractor would
suppress the creator's instead and let a cycle collect the half-built child.
The creation cover goes up before the first allocation rather than after the
last one, and the allocation-failure path parks the objspace in the same
VM-lock section, so a global GC can never find it neither covered nor a zombie.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>
We can use "covers" instead of "survives" when we need to spill across
calls
and update the list of active maintainers. We don't seem to enumerate
all past maintainers for other gems in this file.

Context: ruby/erb#136 (comment)
The darwin branch starts from an empty POSTLINK to build up the
dsymutil and codesign steps, so it stays empty when neither tool is
found, unlike the `:` default of the other platforms.  A recipe that
runs it as part of a compound command needs it to be a command.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With --with-static-linked-ext, the archives, libraries and Init
functions of the extensions are known only to exts.mk, and the
top-level $(PROGRAM) rule linked ruby with a bare EXTOBJS whenever
its prerequisites changed.  The sub-make from exts.mk relinked it
correctly afterwards only because it happened to come later, and
because configuring ext/-test- deletes ext/extinit.c.  With `make -j`
under GNU make 3.81, or 4.4 with `DOT_WAIT=`, `programs` and
`test-precheck` ran that link and the exts sub-make at once, leaving
a ruby without any extension or encoding, or no ruby at all when the
two links collided in codesign.  A bare `make ruby` gave the same
broken ruby even serially.

Now the top-level $(PROGRAM) depends on `exts` instead of linking
when PROGRAM_EXTS is set, and only the sub-make, which passes
PROGRAM_EXTS empty along with the real EXTOBJS, links.  EXTOBJS at
the top level is always dmyext.o so that ext/extinit.c is generated
only with the full EXTINITS.  `.WAIT` stays in the test targets; it
still keeps the outputs of the dynamic build from interleaving, but
the correctness of the static build no longer depends on it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
win32/Makefile.sub has its own EXTOBJS and its own $(PROGRAM) rule, so
PROGRAM_EXTS never reached nmake. With --with-static-linked-ext,
`nmake ruby` regenerated ext/extinit.c with no EXTINITS and relinked
the DLL with dmyext.obj, leaving a ruby without any extension or
encoding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
digest.h declared it plain while digest.c defines it as
RUBY_FUNC_EXPORTED, which cl.exe rejects, so a build with
--with-static-linked-ext failed:

    ext/digest/digest.c(547): error C2375: 'rb_digest_wrap_metadata': redefinition; different linkage

Keeping the attribute on the definition is what exports the symbol from
libruby when extensions are linked statically.

ruby/digest@7f72a1eb43

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
When a shared root string is moved to another Ractor, it loses the T_STRING
type, which causes it to leak memory. For example, this script leaks memory:

    r = Ractor.new { loop { Ractor.receive } }

    10.times do
      100_000.times do
        str = "x" * 4096
        str.instance_variable_set(:@x, []) # make str not shareable
        str.freeze

        child_str = str.dup

        r.send(str, move: true)
      end

      puts `ps -o rss= -p #{$$}`
    end

Before:

    470516
    906212
    1303752
    1683808
    2121396
    2502312
    2898164
    3328072
    3697172
    4114284

After:

    129604
    135788
    136748
    136748
    136812
    136812
    136876
    136876
    136876
    136876
When a shared root array is moved to another Ractor, it loses the T_ARRAY
type, which causes it to leak memory. For example, this script leaks memory:

    r = Ractor.new { loop { Ractor.receive } }

    10.times do
      100_000.times do
        ary = [1] * 1000
        ary.instance_variable_set(:@x, []) # make ary not shareable
        ary.freeze

        r.send(ary, move: true)
      end

      puts `ps -o rss= -p #{$$}`
    end

Before:

    977616
    1769680
    2552492
    3335304
    4118116
    4900928
    5683744
    6466640
    7249368
    8032180

After:

    211588
    220448
    220316
    220444
    220700
    212404
    220512
    220872
    221200
    220780
Using the `container->obj = rb_gc_location(container->obj)` pattern dirties pages unconditionally.
If we're in a forked process, this is bad for CoW. If the object hasn't moved, don't reassign.
Bumps the github-actions group with 1 update in the / directory: [taiki-e/install-action](https://github.com/taiki-e/install-action).


Updates `taiki-e/install-action` from 2.87.6 to 2.87.7
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](taiki-e/install-action@7b8d471...84f5ac3)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.87.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
The call-seq and the prose both claimed the method returns 0, but it
returns self, and has since [Feature #17294] made mkpath and rmtree
chainable. test_rmtree asserts it.

Also document the keyword arguments, which the call-seq omitted.

Fixes [Bug #22288]
Co-authored-by: Luke Gruber <luke.gruber@shopify.com>
The def file is generated from $(LIBRUBY_A) alone, so a symbol that a
statically linked extension exports with RUBY_FUNC_EXPORTED reaches the
DLL through its own linker directive but never reaches the import
library, and an extension built later against that ruby cannot link it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The note added in a51b4a8 says this method does not yield
Ractor-unshareable objects in multi-Ractor mode, and points at [Bug #19387]
as an open implementation issue.  That has not been true since per-Ractor
GC landed: each Ractor walks its own objspace, so it yields all of its own
objects, plus the objects of the other Ractors that have been made
shareable.

[Bug #19387]

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
move_neutralize_source() rewrites the source's flags to
T_OBJECT | FL_FREEZE | (flags & FL_PROMOTED), which drops FL_FINALIZE
while the finalizer table entry keyed on that slot stays.  The two then
disagree, and a RUBY_DEBUG build aborts at shutdown:

    gc/default/default.c:4044: Assertion Failed:
    rb_gc_impl_shutdown_call_finalizer_i:RB_FL_TEST(obj, FL_FINALIZE)

    r = Ractor.new { Ractor.receive }
    o = Object.new
    ObjectSpace.define_finalizer(o, proc { |id| })
    r.send(o, move: true)

Carry FL_FINALIZE over to the shell.  The finalizer then runs when the
shell is collected, in the Ractor that defined it; the object rebuilt on
the other side gets fresh flags and does not inherit it, so it still runs
exactly once.

[Bug #21368]

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…aise

The test TestRequire#test_loading_fifo_threading_raise fails with the
following error in LSAN because pm_load_parse_file could raise which
will cause the pm_parse_result_t to leak.

    Direct leak of 104 byte(s) in 1 object(s) allocated from:
        #0 0x64ee1a50385d in calloc build-llvm/tools/clang/stage2-bins/runtimes/runtimes-bins/compiler-rt/lib/asan/asan_malloc_linux.cpp:74:3
        #1 0x64ee1a578206 in calloc1 gc/default/default.c:2000:12
        #2 0x64ee1a578206 in rb_gc_impl_calloc gc/default/default.c:11069:5
        #3 0x64ee1a578206 in ruby_xcalloc_body gc.c:6146:12
        #4 0x64ee1a578206 in ruby_xcalloc gc.c:6140:34
        #5 0x64ee1acfaed1 in pm_parse_result_init prism_compile.c:10639:23
        #6 0x64ee1a67bbd1 in load_iseq_eval load.c:748:13
        #7 0x64ee1a674cd4 in rb_load_internal load.c:866:9
        #8 0x64ee1a6754d6 in rb_load_entrypoint load.c:908:5
        #9 0x64ee1a6790a5 in rb_f_load load.c:950:12
        #10 0x64ee1a97043b in vm_call_cfunc_with_frame_ vm_insnhelper.c:3899:11
        #11 0x64ee1a957d0b in vm_call_method_each_type vm_insnhelper.c:4891:16
        #12 0x64ee1a95779b in vm_call_method vm_insnhelper.c
        #13 0x64ee1a91a9ad in vm_sendish vm_insnhelper.c:6213:15
        #14 0x64ee1a91a9ad in vm_exec_core insns.def:909:11
        #15 0x64ee1a9083bd in rb_vm_exec vm.c:2875:22
        #16 0x64ee1a54bccc in rb_ec_exec_node eval.c:299:9
        #17 0x64ee1a54b993 in ruby_run_node eval.c:337:30
        #18 0x64ee1a547f50 in rb_main main.c:42:12
        #19 0x64ee1a547f50 in main main.c:62:12
        #20 0x7f8507a2a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
        #21 0x7f8507a2a28a in __libc_start_main csu/../csu/libc-start.c:360:3
        #22 0x64ee1a45fb74 in _start (ruby+0x197b74) (BuildId: 5b18a734908cec0a5f93267f1f350850b0f3fb6d)

    Direct leak of 16 byte(s) in 1 object(s) allocated from:
        #0 0x64ee1a50385d in calloc build-llvm/tools/clang/stage2-bins/runtimes/runtimes-bins/compiler-rt/lib/asan/asan_malloc_linux.cpp:74:3
        #1 0x64ee1a578206 in calloc1 gc/default/default.c:2000:12
        #2 0x64ee1a578206 in rb_gc_impl_calloc gc/default/default.c:11069:5
        #3 0x64ee1a578206 in ruby_xcalloc_body gc.c:6146:12
        #4 0x64ee1a578206 in ruby_xcalloc gc.c:6140:34
        #5 0x64ee1acfaeb9 in pm_parse_result_init prism_compile.c:10638:21
        #6 0x64ee1a67bbd1 in load_iseq_eval load.c:748:13
        #7 0x64ee1a674cd4 in rb_load_internal load.c:866:9
        #8 0x64ee1a6754d6 in rb_load_entrypoint load.c:908:5
        #9 0x64ee1a6790a5 in rb_f_load load.c:950:12
        #10 0x64ee1a97043b in vm_call_cfunc_with_frame_ vm_insnhelper.c:3899:11
        #11 0x64ee1a957d0b in vm_call_method_each_type vm_insnhelper.c:4891:16
        #12 0x64ee1a95779b in vm_call_method vm_insnhelper.c
        #13 0x64ee1a91a9ad in vm_sendish vm_insnhelper.c:6213:15
        #14 0x64ee1a91a9ad in vm_exec_core insns.def:909:11
        #15 0x64ee1a9083bd in rb_vm_exec vm.c:2875:22
        #16 0x64ee1a54bccc in rb_ec_exec_node eval.c:299:9
        #17 0x64ee1a54b993 in ruby_run_node eval.c:337:30
        #18 0x64ee1a547f50 in rb_main main.c:42:12
        #19 0x64ee1a547f50 in main main.c:62:12
        #20 0x7f8507a2a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
        #21 0x7f8507a2a28a in __libc_start_main csu/../csu/libc-start.c:360:3
        #22 0x64ee1a45fb74 in _start (ruby+0x197b74) (BuildId: 5b18a734908cec0a5f93267f1f350850b0f3fb6d)
When two Ractors fail an assertion at nearly the same time both write into
the same stream, and the first report -- the interesting one -- is cut
short by the second:

    rs = 100.times.map { Ractor.new { sleep rand(1..3); Ractor.fail_assert } }

produced a 25-line report ending in "Crashed while printing bug report",
every time.

Take a claim before writing.  The first thread through writes its report
and aborts the process; a later one waits for that instead of writing its
own.  The claiming thread is let through again, so the existing
crash-while-reporting path still works.  The wait is bounded, so a writer
that hangs still ends the process as a crash rather than a hang.

rb_assert_failure_detail() writes a report without going through
report_bug(), so it needs the same claim.

The example above now produces one complete report.

[Bug #21146]

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
peterzhu2118 and others added 6 commits September 9, 2026 14:56
Fixes the following warning:

    thread_sched.c:114:1: warning: unused function 'mn_threads_enabled_p' [-Wunused-function]
      114 | mn_threads_enabled_p(void)
          | ^~~~~~~~~~~~~~~~~~~~
`JSON` has no `.new`, and options have been keyword arguments since 3.0,
so passing a positional Hash raises ArgumentError.

ruby/json@effca27964
(ruby/erb#136)

* Speedup ERB::Util.html_escape with SIMD

The code is lifted from similar work in the `json` gem.
SIMD use is limited to NEON and SSE2 as they can easily be
detected and assumed at compile time.

Using more advanced SIMD implementations would require runtime
detection, which probably isn't worth it.

```
== 1k no matches ==
ruby 4.0.6 (2026-07-14 revision ruby/erb@03b6d3f889) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
                simd     1.430M i/100ms
Calculating -------------------------------------
                simd     14.719M (± 0.5%) i/s   (67.94 ns/i) -     74.350M in   5.051379s

Comparison:
master:  2389369.8 i/s
  simd: 14718663.6 i/s - 6.16x  faster

== 1k few matches ==
ruby 4.0.6 (2026-07-14 revision ruby/erb@03b6d3f889) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
                simd   163.731k i/100ms
Calculating -------------------------------------
                simd      1.702M (± 0.6%) i/s  (587.67 ns/i) -      8.514M in   5.003388s

Comparison:
master:  1405436.9 i/s
  simd:  1701649.4 i/s - 1.21x  faster

== 1k many matches ==
ruby 4.0.6 (2026-07-14 revision ruby/erb@03b6d3f889) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
                simd   153.148k i/100ms
Calculating -------------------------------------
                simd      1.590M (± 1.2%) i/s  (629.01 ns/i) -      7.964M in   5.009249s

Comparison:
master:  1225987.4 i/s
  simd:  1589798.4 i/s - 1.30x  faster
```

NB: I don't have an x86_64 machine to benchmark SSE2.

ruby/erb@d5ddd13686
(ruby/erb#137)

`memcpy(dest, html_escape_table[c].str, len);` will hardly be able to be
inlined as the compiler has no idea how large `len` may be.

By turning it into constants, we allow the compiler to realize these
strings are very small, and it will most likely inline the `memcpy`.

It also reduce the size of `html_escape_table` from `2kiB` down to
just `256B`, which is good for caches.

```
== 1k no matches ==
ruby 4.0.6 (2026-07-14 revision ruby/erb@03b6d3f889) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
              memcpy     1.410M i/100ms
Calculating -------------------------------------
              memcpy     14.681M (± 0.6%) i/s   (68.12 ns/i) -     74.709M in   5.088841s

Comparison:
  simd: 1472696.2 i/s
memcpy: 14681042.1 i/s - same-ish: difference falls within error

== 1k few matches ==
ruby 4.0.6 (2026-07-14 revision ruby/erb@03b6d3f889) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
              memcpy   213.724k i/100ms
Calculating -------------------------------------
              memcpy      2.167M (± 2.2%) i/s  (461.54 ns/i) -     10.900M in   5.030772s

Comparison:
  simd:  1695817.9 i/s
memcpy:  2166650.4 i/s - 1.28x  faster

== 1k many matches ==
ruby 4.0.6 (2026-07-14 revision ruby/erb@03b6d3f889) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
              memcpy   201.591k i/100ms
Calculating -------------------------------------
              memcpy      1.982M (± 7.2%) i/s  (504.63 ns/i) -     10.080M in   5.086446s

Comparison:
  simd:  1578725.7 i/s
memcpy:  1981648.9 i/s - 1.26x  faster
```

ruby/erb@a26b4ef602
@pull pull Bot locked and limited conversation to collaborators Sep 9, 2026
@pull pull Bot added the ⤵️ pull label Sep 9, 2026
@pull
pull Bot merged commit 1bb0450 into turkdevops:master Sep 9, 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.