Files
Koichi SasadaandClaude Opus 4.8 ca20425fd5 Avoid a stale TLS execution context in fiber_switch after NT migration
`fiber_current()` reads `GET_EC()`, which the compiler may inline as a
thread-pointer-relative TLS access whose base is cached in a callee-saved
register.  `coroutine_transfer` preserves callee-saved registers across the
stack switch, so when a fiber's coroutine resumes on a different native
thread (M:N scheduler migration), the cached thread pointer is stale and
`fiber_current()` returns another thread's execution context.

`fiber_switch()` calls `fiber_current()` right after the transfer.  With
`-DRUBY_DEBUG=1` this trips `VM_ASSERT(ec == rb_current_ec_noinline())` in
`rb_current_execution_context`; in release builds it silently uses the
wrong (or NULL) EC.  Only reproduces with >= 2 Ractors (a single Ractor
does not migrate the coroutine).

Read the current EC through `rb_current_ec_noinline()` in `fiber_current()`,
which forces a fresh thread-pointer load.  fiber_current() is only used by
the (cold) fiber machinery, so the non-inlined read costs nothing on hot
paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 11:01:06 +09:00

64 lines
1.3 KiB
Ruby

show_limit %q{
fibers = []
begin
fiber = Fiber.new{Fiber.yield}
fiber.resume
fibers << fiber
raise Exception, "skipping" if fibers.count >= 10_000
rescue Exception => error
puts "Fiber count: #{fibers.count} (#{error})"
break
end while true
}
assert_equal %q{ok}, %q{
Fiber.new{
}.resume
:ok
}
assert_equal %q{ok}, %q{
100.times.collect{Fiber.new{}}
:ok
}
assert_equal %q{ok}, %q{
fibers = 1000.times.collect{Fiber.new{Fiber.yield}}
fibers.each(&:resume)
fibers.each(&:resume)
:ok
}
assert_normal_exit %q{
at_exit { Fiber.new{}.resume }
}
assert_normal_exit %q{
Fiber.new(&Object.method(:class_eval)).resume("foo")
}, '[ruby-dev:34128]'
# [Bug #21400]
assert_normal_exit %q{
Thread.new { Fiber.current.kill }.join
}
# fiber_current() must not read a stale (cached) TLS execution context after a
# fiber's coroutine migrates between native threads under the M:N scheduler.
# Needs >= 2 Ractors so coroutines actually migrate; sleep drives the migration.
assert_equal 'ok', %q{
Warning[:experimental] = false
2.times.map do
Ractor.new do
200.times do
f = Fiber.new { Fiber.yield; 1 }
f.resume
sleep(0.0003) # block -> M:N native-thread migration
f.resume rescue nil
end
:ok
end
end.each(&:value)
:ok
}