Project

General

Profile

Actions

Bug #22532

open

pthread_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

Bug #22532: pthread_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

Added by chucke (Tiago Cardoso) 15 minutes ago. Updated 4 minutes ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:127406]

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.

Actions

Also available in: PDF Atom