[pull] master from ruby:master#1194
Merged
Merged
Conversation
We always link with the miniruby now so there is no need for this dance.
…ectly A runnable thread whose Ractor was enqueued to the global ready queue can be served without a dequeue: a thread waking up in wait_running_turn hands its native thread over by a direct transfer, and the hot-thread path steals `running` back outright. Both leave the Ractor's grq entry behind, orphaned. The entry itself is harmless (the dequeuing nt sees a served Ractor and skips), but the accounting breaks: the next legitimate enqueue double-lists the one grq_node embedded in the sched, corrupting the queue (observed on master as pthread_mutex_lock EINVAL / crashes; about 1/20 runs of a Ractor x Thread x IO churn under a saturated shared-nt pool). Restore the invariant "enqueued <=> runnable and unserved" by cancelling the entry at both direct-service points (ractor_sched_cancel_enq). The grq_node is kept self-linked whenever it is not enqueued, so membership is read off the node itself: under the per-Ractor sched lock a self-linked read is decisive (enqueuers hold that lock too), and the common direct switch -- whose transition never enqueued -- pays no lock at all; a linked read can race only with a dequeue, so recheck under the grq lock before ccan_list_del_init. ractor_sched_enq now unconditionally rb_bugs on a still-linked node: the double-list is queue corruption either way, and a VM_CHECK_MODE-only assert cannot catch this race in practice (CHECK builds perturb the timing; the previous VM_CHECK-only scan never fired while plain builds reproduced the corruption). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Redesign the teardown of M:N (coroutine) threads around the natural call structure nt_start -> co_start -> thread_start_func_2: when the thread body returns, its epilogue (coroutine_thread_terminated) finishes all rb_thread_t / rb_ractor_t business while th is still valid, and co_start then makes exactly one final transfer back to the native thread's own context (nt_start's loop), where the nt reclaims the dead coroutine's context. The resume itself proves the final transfer's register save completed, so the reclaim needs no list, no handshake and no extra locking. This deletes the zombie-threads machinery (VM-global list, sched.finished publish, the GC mark/reap pass) and closes its UAF window: publishing `finished` before the terminal transfer let the GC free the Thread wrapper -- whose coroutine context the transfer still saves into -- mid-teardown. * struct rb_thread_context bundles a coroutine thread's execution-owned resources: the coroutine_context (kept first, so th->sched.context points into the block) plus its machine stack, the stashed final-transfer target (nt), and a dead flag. co_start's epilogue sets the flag and transfers via the stashed nt, touching tctx alone -- th may already be collected by then. The nt loop passes every coroutine it resumes from to thread_sched_reclaim_from(), which frees dead ones (stack back to the pool). A live coroutine cannot be resumed elsewhere, die and be freed while the nt loop still inspects it: a live yield arrives with the sched lock handed off, and no other nt can schedule that thread again until the nt loop's own unlock. * coroutine_thread_terminated (run from thread_start_func_2, where the upstream non-coroutine teardown also runs, while th is still valid and listed) does: wake the next thread as upstream's thread_sched_to_dead does; stash th->nt into tctx and detach th->sched.context; leave the living set; clear the current ec; and, for a shared-nt non-last thread, enqueue the Ractor's next runnable thread (not earlier: waking it from to_dead would let another thread of the same Ractor execute while this teardown still runs). Everything that reads th happens before the living-set removal, after which the GC may collect th (and, for a Ractor's main thread, the Ractor). * interrupt_lock is now destroyed in thread_free instead of during teardown (thread_cleanup_func): while th is in its Ractor's living set, anyone -- terminate_all on the Ractor's terminating main thread, Thread#kill / #raise -- may lock it, and destroying it mid-teardown leaves a window where a concurrent interrupter locks a destroyed mutex (EINVAL; the upstream non-coroutine path has the same, smaller, window). At thread_free time the wrapper is unreachable AND off the living set (listed threads are marked), so no interrupter can exist. The atfork cleanup reinitializes the copied lock, which may have been held at the instant of fork. * The Thread wrapper's dfree only reclaims the context of a thread that never started; a terminated one cleared th->sched.context in its epilogue. VM shutdown needs no extra synchronization: past the living-set removal the epilogue touches only its own context block, the TLS and nt->nt_context, none of which VM destruct frees (SNT native threads and the machine-stack pool are intentionally not torn down at exit, as before). Verified: bootstraptest 2050 PASS; test_thread / test_ractor / test_fiber / test_gc / test_gc_compact / thread_queue 282 tests 0F0E; Ractor x Thread x IO teardown churn 150/150 + 60/60 (exercises DNT promotion, terminate_all racing dying threads, and the double-enqueue fixed in the previous commit); fork with live Ractors + child GC 20/20; ASAN clean including unreferenced Thread/Ractor collection (the old zombie window) and mixed stress; RGENGC_CHECK_MODE=2 btest 2050 + churn; TSan warning profile identical to master (the new functions appear in no race report); YJIT move-under-GC.stress; kill/join races; Thread object retention across Ractor death; exit-while-winding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
thread_sched_atfork empties the global ready queue with ccan_list_head_init but leaves vm->ractor.sched.grq_cnt at whatever the parent had, so a child forked while any Ractor was enqueued keeps a stale non-zero count for an empty list. VM_CHECK_MODE builds then fail "grq_size(vm, cr) == vm->ractor.sched.grq_cnt" in ractor_sched_deq (reproducible on master with RUBY_MN_THREADS=1: fork in a loop that also creates Ractors); on regular builds the counter only feeds assertions and debug logs, so the corruption stays silent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This extends the ZJIT GC fast path to the newhash instruction for empty
hashes.
This was verified in a stripped down benchmarking environment, using the
following micro-benchmark in ruby-bench:
```
def run(max)
i = 0
while i < max
h = {}
i += 1
end
end
require_relative '../harness/loader.rb'
run_benchmark(50) do
50.times do
run(500_000)
end
end
```
This patch resulted in a ~30% speedup.
```
master: ruby 4.1.0dev (2026-07-07T14:04:07Z master c51b159) +ZJIT +PRISM [x86_64-linux]
experiment: ruby 4.1.0dev (2026-07-07T19:27:04Z mvh-zjit-inline-fa.. 60730f1) +ZJIT +PRISM [x86_64-linux]
----- ------------ --------------- ------------------ -----------------
bench master (ms) experiment (ms) experiment 1st itr master/experiment
hash 682.3 ± 0.7% 518.0 ± 1.0% 1.082 1.317
----- ------------ --------------- ------------------ -----------------
Legend:
- experiment 1st itr: ratio of master/experiment time for the first benchmarking iteration.
- master/experiment: ratio of master/experiment time. Higher is better for experiment. Above 1 represents a speedup.
```
Co-authored-by: Tavian Barnes <tavianator@tavianator.com>
It was leftover and we don't actually need it.
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 : )