Project

General

Profile

Actions

Bug #22265

closed

`Thread#value` can return before the target thread completes after a spurious Fiber scheduler unblock

Bug #22265: `Thread#value` can return before the target thread completes after a spurious Fiber scheduler unblock

Added by ioquatix (Samuel Williams) 2 days ago.

Status:
Closed
Assignee:
-
Target version:
-
[ruby-core:126502]

Description

Ruby 3.3 and 3.4 can allow an untimed Thread#join to return after a spurious Fiber scheduler wakeup, even though the target thread is still alive.

Because Thread#value relies on Thread#join, it can consequently return nil before the thread completes.

This was encountered in io-event, where a stale scheduler unblock could resume a fiber waiting for a worker thread.

Reproducer

class Scheduler
  def initialize
    @root = Fiber.current
    @ready = []
    @blocking = {}
    @lock = Thread::Mutex.new
  end

  def fiber(&block)
    fiber = Fiber.new(blocking: false, &block)
    fiber.transfer
    fiber
  end

  def block(blocker, timeout = nil)
    fiber = Fiber.current
    @blocking[fiber] = true
    @root.transfer
  ensure
    @blocking.delete(fiber)
  end

  def unblock(blocker, fiber)
    @lock.synchronize {@ready << fiber}
  end

  def kernel_sleep(duration = nil)
    unblock(:sleep, Fiber.current)
    @root.transfer
    true
  end

  def io_wait(io, events, duration = nil)
    raise NotImplementedError
  end

  def fiber_interrupt(fiber, exception)
    fiber.raise(exception)
  end

  def run
    while @blocking.any? || @ready.any?
      ready = @lock.synchronize do
        ready, @ready = @ready, []
        ready
      end

      ready.each do |fiber|
        fiber.transfer if fiber.alive?
      end

      Thread.pass if ready.empty?
    end
  end
end

release = Thread::Queue.new

target = Thread.new do
  release.pop
  :success
end

Thread.pass until target.status == "sleep"

result = :unset

runner = Thread.new do
  scheduler = Scheduler.new
  Fiber.set_scheduler(scheduler)

  joining_fiber = Fiber.schedule do
    result = target.value
  end

  Fiber.schedule do
    scheduler.unblock(:spurious_wakeup, joining_fiber)
    sleep(0)
    release << true
  end

  scheduler.run
ensure
  Fiber.set_scheduler(nil)
end

runner.join
target.join

puts "ruby #{RUBY_VERSION}: result=#{result.inspect}"
abort "Thread#value returned before completion" unless result == :success

Actual result on Ruby 3.4.4

ruby 3.4.4: result=nil
Thread#value returned before completion

Tested with:

ruby 3.4.4 (2025-05-14 revision a38531fd3f) +PRISM [arm64-darwin25]

Expected result

An untimed Thread#join or Thread#value should continue waiting after a spurious wakeup:

ruby 3.4.4: result=:success

Existing fix

This is fixed in Ruby 4.0 in PR #13532. The reproducer succeeds with Ruby 4.0.5.

Proposed backports

Backport PRs have been prepared for both affected maintenance branches:

No data to display

Actions

Also available in: PDF Atom