6 Commits
Author SHA1 Message Date
Samuel Williams e7551c0883 coroutine: allow pthread resumption across threads. (#18820) 2026-09-14 22:15:24 +12:00
Samuel Williams d568c61094 coroutine: unlock pthread guard on setup failure. (#18818) 2026-09-14 04:56:14 +00:00
Samuel Williams 84a8652f01 coroutine: consistently return the context that resumed transfer. (#18814) 2026-09-14 03:04:04 +00:00
Koichi SasadaandClaude Opus 5 8c7c80f4da coroutine: do not write to the target after switching away
coroutine_transfer() restores target->from after the switch, that is,
after this coroutine is resumed -- by then it may have been resumed by
someone else and target may be freed.

With M:N threads it usually is: a Ruby thread parks by transferring to
its shared native thread's context (thread_sched_wait_running_turn), and
that thread, finding the ready queue empty, retires and frees nt and
nt->nt_context.  Over 40 retires in the reproducer below, the number of
threads still parked on the retiring context was a median of 19 (max 61,
zero in four cases); each writes to it when it is next resumed.
sizeof(ucontext_t) is 968, so the write lands in a freed 1008-byte
region, and once that is reused it corrupts the new owner:

  free(): invalid next size (fast)
    ruby_xfree_sized ... st_free_entries -> rb_st_free_table
    ractor_free_all_ports -> ractor_notify_exit -> ractor_atexit
    thread_start_func_2 -> co_start -> coroutine_trampoline

  AddressSanitizer: heap-use-after-free
    WRITE of size 8 in coroutine_transfer (Context.h:97), thread T3
    968 bytes inside a 1008-byte region freed in nt_start ->
    native_thread_destroy_self, allocated in native_thread_alloc

from is read only by the trampoline, when target starts, so the restore
is dead by the time the switch returns.  Skip it under
COROUTINE_TARGET_MAY_BE_FREED, which thread_pthread.c defines; cont.c
and other embedders keep the original behaviour.  Only ucontext runs
M:N threads today, but the lifetime rule is not specific to them, so
guard every backend that has the restore.

Fibers never hit this: a suspended fiber still references its target, so
it cannot be collected.  A native thread's context is not a Ruby object
and is freed by hand.

Reproducer: clang-20 -O1 --enable-shared --with-coroutine=ucontext,
bootstraptest/test_ractor.rb (the case at line 2603 spawns 2000 Ractors)
four times in parallel on four CPUs: 21 of 48 runs died before, 0 of 48
after.  Under ASAN, the isolated case eight at a time: 120 of 120
reported before, 0 of 120 after.

CI: https://github.com/ruby/ruby/actions/runs/31942932752/job/95154613098

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 04:57:21 +09:00
Samuel Williams 1862d961a9 Ignore dead threads in coroutine_join. 2021-07-02 12:36:14 +12:00
Samuel Williams 42130a64f0 Replace copy coroutine with pthread implementation. 2021-07-01 11:23:03 +12:00