[pull] master from ruby:master - #1390
Merged
Merged
Conversation
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>
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 : )