Skip to content

[pull] master from ruby:master#1194

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

[pull] master from ruby:master#1194
pull[bot] merged 13 commits into
turkdevops:masterfrom
ruby:master

Conversation

@pull

@pull pull Bot commented Jul 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 : )

XrXr and others added 13 commits July 8, 2026 15:16
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.
@pull pull Bot locked and limited conversation to collaborators Jul 9, 2026
@pull pull Bot added the ⤵️ pull label Jul 9, 2026
@pull pull Bot merged commit 583668d into turkdevops:master Jul 9, 2026
0 of 2 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.

4 participants