Bug #22532
openpthread_mutex_lock: Invalid argument (EINVAL) / SEGV in rb_threadptr_join_list_wakeup when a fiber blocked in Thread#join under a Fiber scheduler is never resumed
Description
Thread#join links a struct rb_waiting_list waiter, allocated on the joining fiber's stack, into target_th->join_list. Only the rb_ensure clause (remove_from_join_list) ever unlinks it. Under a Fiber scheduler, a non-blocking fiber joining a thread suspends in Scheduler#block. If that fiber is never resumed, the waiter stays linked in two situations:
The joining thread terminates. Its rb_thread_t may later be freed by GC, and the waiter now points at freed memory and a released fiber stack.
The fiber itself is garbage-collected while its thread is still alive, so the waiter lives in a released fiber stack.
When the target thread finishes, rb_threadptr_join_list_wakeup walks the stale entry. The scheduler of the dead joiner is nil, so it calls rb_threadptr_interrupt on freed memory. Observed symptoms:
[BUG] pthread_mutex_lock: Invalid argument (EINVAL) (rb_threadptr_interrupt → rb_native_mutex_lock)
SEGV in rb_fiberptr_self(join_list->fiber)
NoMethodError: undefined method 'unblock' for false (macOS)
Reproduced on 3.3, 3.4.9, 3.4.10, 4.0.5 and master (1f2a4bd575), on x86_64-linux, aarch64-linux and arm64-darwin.
Reproduction (crashes within a few hundred iterations, every run):
class Scheduler
def initialize = @main = Fiber.current
def block(blocker, timeout = nil) = @main.transfer
def unblock(blocker, fiber) = nil
def kernel_sleep(duration = nil) = block(:sleep, duration)
def io_wait(io, events, timeout) = block(:io_wait, timeout)
def fiber_interrupt(fiber, exception) = nil
def close = nil
def fiber(&) = Fiber.new(blocking: false, &).tap(&:transfer)
end
1000.times do
target = nil
Thread.new do
Fiber.set_scheduler(Scheduler.new)
Fiber.schedule { (target = Thread.new { sleep 0.001 }).join }
end.join
GC.start
target.join
end
This was found in a real test suite whose scheduler's io_select hook does Thread.new { IO.select(...) }.value. When an exception cut the scheduler's close short, the fiber parked in that join was abandoned.
Updated by chucke (Tiago Cardoso) 29 minutes ago
Proposed fix: https://github.com/ruby/ruby/pull/19826