Skip to content

[pull] master from ruby:master - #1315

Merged
pull[bot] merged 5 commits into
turkdevops:masterfrom
ruby:master
Aug 16, 2026
Merged

[pull] master from ruby:master#1315
pull[bot] merged 5 commits into
turkdevops:masterfrom
ruby:master

Conversation

@pull

@pull pull Bot commented Aug 16, 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 : )

ko1 and others added 5 commits August 16, 2026 12:15
The test added by 6f176c8 warmed two ractors before dropping
RLIMIT_NPROC, assuming every subsequent Ractor.new would attempt to
spawn a shared native thread and raise ThreadError.  However,
native_thread_check_and_create_shared widens the pool only while
snt_cnt < max_cpu, so on 2-vCPU CI machines (rubyci ubuntu-no-yjit,
ubuntu2204) the two warm ractors already saturated the pool:
pthread_create was never attempted, nothing raised, and the test
failed with "0 of 20 raised".

Warm a single ractor instead.  snt_cnt (1) then stays below max_cpu
on any host with two or more CPUs, so every attempt deterministically
reaches the failing pthread_create, while one parked shared native
thread is still around to exercise the publish-before-widen race the
test guards against.  On a single-CPU host widening can never be
attempted, so accept zero raises as well; a mixed count still fails,
which is what a broken snt_cnt rollback would produce.

Verified with RUBY_MAX_CPU=1/2/unset: the new test passes 5/5 in each
configuration on master, and still segfaults 5/5 on the pre-fix code
(3c66369), so the original regression remains covered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reuse downloaded files and source repositories across runner
platforms to reduce network-dependent fetches.
Since "Ractor: the dying thread collects its own objspace post-mortem"
(e768a87), a dying Ractor thread runs rb_ractor_postmortem_free()
after thread_sched_to_dead() (its GVL is gone) and after
rb_ractor_living_threads_remove() -> vm_remove_ractor(), which unlinks
the Ractor, decrements vm->ractor.cnt and signals the terminate waiter.
From that point nothing holds the main Ractor back, but the epilogue
still frees through VM-global state: rb_fiber_free_body() -> cont_free()
takes jit_cont_lock and returns the stack to the shared fiber pool, and
both frees update the main objspace's malloc accounting.

The coroutine epilogue has the same hazard and is covered:
coroutine_thread_terminated() bumps vm->ractor.sched.winding_cnt and
ruby_vm_destruct() spins in rb_thread_sched_wait_winding() until the
reclaim is done.  The non-coroutine path -- the ordinary
Ractor.new-spawned dedicated thread -- was not counted, so a main
Ractor that exits while the dying thread is still in the epilogue tears
that state down underneath it.

Count the epilogue in winding_cnt as well.  The increment happens before
the Ractor stops being counted, so there is no window where the VM sees
neither the Ractor nor the epilogue.  vm->ractor.sched exists only on
the pthread backend, so the count gets the same shape its waiter has:
real work in thread_pthread.c, a no-op in thread_win32.c and
thread_none.c, where rb_thread_sched_wait_winding never waits either.

Reproduced deterministically by delaying the epilogue (50ms) and the
process exit (400ms) on top of e768a87: 6/6 runs of
`Ractor.new{ 1 }.value` died with

  [BUG] pthread_mutex_lock: Invalid argument (EINVAL)
    rb_native_mutex_lock (thread_pthread.c:128)
    jit_cont_free (cont.c:1419)
    cont_free (cont.c:1185)
    rb_ractor_postmortem_free (ractor.c:683)
    thread_start_func_2 (thread.c:851)

i.e. the epilogue locking jit_cont_lock after rb_jit_cont_finish()
destroyed it.  With this change the same harness is clean 6/6, and
test/ruby/test_ractor.rb, test/objspace/test_ractor.rb, test_thread.rb
and the strscan/digest/io-wait/io-console Ractor tests pass.  Without
the injected delay the window is never entered on x86_64 (0 hits in 15
runs of those test files, instrumented), which fits the failures being
confined to the slow emulated runners.

This is the most likely cause of the heap corruption CI has been
reporting since e768a87 landed (2026-08-10 18:33 UTC): every failing
case is a child process that runs one Ractor and exits.

Error:
  TestIOWaitInRactor#test_ractor [test/io/wait/test_ractor.rb:9]:
  pid 68552 exit 0
  | free(): corrupted unsorted chunks
  1. [2/2] Assertion for "stderr"
     | <[]> expected but was
     | <["free(): corrupted unsorted chunks"]>.

  (also TestStringScannerRactor#test_ractor, TestDigestRactor::*,
   TestIOConsoleInRactor#test_ractor -- 16 of the 63 master runs since
   2026-08-10, none in the 87 runs before it)

  TestObjSpaceRactor#test_copy_finalizer [test/objspace/test_ractor.rb:57]:
  ==96541==ERROR: AddressSanitizer: heap-use-after-free on address 0x520000000088
  WRITE of size 8 at 0x520000000088 thread T5

CI: https://github.com/ruby/ruby/actions/runs/31886577248/job/95016642538
CI: https://github.com/ruby/ruby/actions/runs/31427322788 (ASAN, x86_64)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pull pull Bot locked and limited conversation to collaborators Aug 16, 2026
@pull pull Bot added the ⤵️ pull label Aug 16, 2026
@pull
pull Bot merged commit 973c45f into turkdevops:master Aug 16, 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.

2 participants