Skip to content

Park the calling thread in rb_ractor_sched_barrier_join - #17804

Closed
ko1 wants to merge 3 commits into
ruby:masterfrom
ko1:bug-barrier-join-foreign-save
Closed

Park the calling thread in rb_ractor_sched_barrier_join#17804
ko1 wants to merge 3 commits into
ruby:masterfrom
ko1:bug-barrier-join-foreign-save

Conversation

@ko1

@ko1 ko1 commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

ractor_sched_barrier_join_wait_locked was passed cr->threads.sched.running, but the joining (calling) thread is not always sched.running: a terminating or blocking thread can join the barrier while another thread of the same Ractor -- e.g. one blocked in IO with live frames on its coroutine stack -- is sched.running. RB_VM_SAVE_MACHINE_CONTEXT(th) then runs on the caller's stack but writes into the other thread's ec, so machine.stack_end ends up pointing into a foreign stack. The barrier GC then scans an invalid range for the blocked thread and misses every reference living only on its machine stack; e.g. the STR_TMPLOCK'd buffer of a blocked IO#read gets swept and is used after free when the read completes (ASAN use-after-poison in rb_str_unlocktmp, 14/20 -> 0/20 with this fix).

Pass GET_THREAD() so the join saves and parks the calling thread itself.

ko1 and others added 3 commits July 11, 2026 05:58
ractor_sched_barrier_join_wait_locked was passed cr->threads.sched.running,
but the joining (calling) thread is not always sched.running: a terminating
or blocking thread can join the barrier while another thread of the same
Ractor -- e.g. one blocked in IO with live frames on its coroutine stack --
is sched.running.  RB_VM_SAVE_MACHINE_CONTEXT(th) then runs on the caller's
stack but writes into the other thread's ec, so machine.stack_end ends up
pointing into a foreign stack.  The barrier GC then scans an invalid range
for the blocked thread and misses every reference living only on its
machine stack; e.g. the STR_TMPLOCK'd buffer of a blocked IO#read gets
swept and is used after free when the read completes (ASAN
use-after-poison in rb_str_unlocktmp, 14/20 -> 0/20 with this fix).

Pass GET_THREAD() so the join saves and parks the calling thread itself.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An uncounted joiner (a terminating or blocking thread) can reach the
scheduler lock after the barrier it observed under the VM lock has
already completed.  ractor_sched_barrier_join_wait_locked then captures
the NEXT serial and sleeps until the next barrier completes -- a hang
when no further barrier ever runs (observed as test timeouts with the
GC.compact x pipe-read stress), and VM_ASSERT(barrier_waiting) fires on
assertion-enabled builds.  Re-check the barrier under the scheduler
lock and skip joining when it is stale; the caller re-evaluates.

Also count a joiner by its own membership in the VM running set instead
of the per-Ractor sched.is_running flag: the flag can already be true
for a designated successor while the terminating caller has left the
set, pushing barrier_waiting_cnt past running_cnt-1 (assertion), or
completing the barrier while a genuinely running thread has not joined.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ractor_check_blocking evaluated the will-block condition before
RB_VM_LOCKING().  Acquiring the VM lock can join a Ractor barrier and
take arbitrarily long; meanwhile a sibling thread can resume (or a
designated successor can start running), so the stale decision then
marks a Ractor that has running threads as blocking.  rb_vm_barrier on
such a Ractor fails VM_ASSERT(rb_ractor_status_p(cr, ractor_running)),
and vm->ractor.blocking_cnt drifts.  Re-evaluate the condition under
the VM lock; for a detached caller (a terminating thread whose
scheduler slot was already handed over, via
rb_ractor_living_threads_remove) additionally skip the transition when
a successor thread is running.

Pair the release side by keying the blocking->running transition on the
status instead of on count arithmetic, so a skipped transition is never
"undone" (vm->ractor.blocking_cnt underflow).

With this and the previous commit, the 8-Ractor pipe-read x GC.compact
stress runs 60/60 with no assertion failures, no hangs and no
use-after-free (previously ~40% failed), and the bootstraptest added by
the barrier-join fix passes 12/12 (previously timed out).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant