[ruby-core:126502] [Ruby Bug#22265] `Thread#value` can return before the target thread completes after a spurious Fiber scheduler unblock
Issue #22265 has been reported by ioquatix (Samuel Williams). ---------------------------------------- Bug #22265: `Thread#value` can return before the target thread completes after a spurious Fiber scheduler unblock https://bugs.ruby-lang.org/issues/22265 * Author: ioquatix (Samuel Williams) * Status: Closed * Backport: 3.3: REQUIRED, 3.4: REQUIRED ---------------------------------------- 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 ```ruby 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 ```text ruby 3.4.4: result=nil Thread#value returned before completion ``` Tested with: ```text 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: ```text 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. - Commit: `f0cf4dce65ba7bf3dc6787e10b88e08b411e2c92` - PR: https://github.com/ruby/ruby/pull/13532 ## Proposed backports Backport PRs have been prepared for both affected maintenance branches: - Ruby 3.3: https://github.com/ruby/ruby/pull/18502 - Ruby 3.4: https://github.com/ruby/ruby/pull/18503 -- https://bugs.ruby-lang.org/
Issue #22265 has been updated by nagachika (Tomoyuki Chikanaga). Backport changed from 3.3: REQUIRED, 3.4: REQUIRED to 3.3: REQUIRED, 3.4: DONE merged to rubu_3_4 at https://github.com/ruby/ruby/commit/c5a0ddafbbe1ad4477e6c9c60612d5b34ac41f01. ---------------------------------------- Bug #22265: `Thread#value` can return before the target thread completes after a spurious Fiber scheduler unblock https://bugs.ruby-lang.org/issues/22265#change-119098 * Author: ioquatix (Samuel Williams) * Status: Closed * Backport: 3.3: REQUIRED, 3.4: DONE ---------------------------------------- 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 ```ruby 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 ```text ruby 3.4.4: result=nil Thread#value returned before completion ``` Tested with: ```text 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: ```text 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. - Commit: `f0cf4dce65ba7bf3dc6787e10b88e08b411e2c92` - PR: https://github.com/ruby/ruby/pull/13532 ## Proposed backports Backport PRs have been prepared for both affected maintenance branches: - Ruby 3.3: https://github.com/ruby/ruby/pull/18502 - Ruby 3.4: https://github.com/ruby/ruby/pull/18503 -- https://bugs.ruby-lang.org/
participants (2)
-
ioquatix (Samuel Williams) -
nagachika (Tomoyuki Chikanaga)