Bug #21941
closedLocal variable becomes nil when YJIT enabled mid-method with fork/signal/ensure
Description
The following code results in the read local variable becoming nil, even though it is never reassigned:
def run
fork_safe = ->(t) { t }
RubyVM::YJIT.enable
read, wakeup = IO.pipe
Signal.trap("SIGCHLD") { wakeup.write("!") }
begin
while true
begin
fork { exit }
next if read.wait_readable
rescue Interrupt
end
end
ensure
end
end
run
Error:
repro.rb:13:in 'Object#run': undefined method 'wait_readable' for nil (NoMethodError)
next if read.wait_readable
^^^^^^^^^^^^^^
from repro.rb:21:in '<main>'
See also:
https://github.com/puma/puma/issues/3620
https://github.com/Shopify/ruby/issues/625
Updated by byroot (Jean Boussier) 21 days ago
- Assignee set to jit
- Backport changed from 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN to 3.2: DONTNEED, 3.3: DONTNEED, 3.4: REQUIRED, 4.0: REQUIRED
Reduced even further:
def run
fork_safe = ->(t) { t }
RubyVM::YJIT.enable
read, wakeup = IO.pipe
wakeup.write("!")
begin
while true
begin
next if read.wait_readable
rescue Interrupt
end
end
ensure
end
end
run
Updated by byroot (Jean Boussier) 21 days ago
Reduced some more, no IO or anything:
def run
fork_safe = ->(t) { t }
RubyVM::YJIT.enable
i = 0
begin
while i < 100
i += 1
p i
begin
next if i
rescue Interrupt
end
end
ensure
end
p :ok
end
run
Updated by ufuk (Ufuk Kayserilioglu) 21 days ago
I have a fix for this here: https://github.com/ruby/ruby/pull/16306
Updated by alanwu (Alan Wu) 9 days ago
- Status changed from Open to Closed
Applied in changeset git|8f98abfc46d48c84db2b1408fc8f14b240ec05fd.
YJIT: Fix not reading locals from cfp->ep after YJIT.enable and exceptional entry
Fix for [Bug #21941].
In case of --yjit-disable, YJIT only starts to record environment
escapes after RubyVM::YJIT.enable. Previously we falsely assumed that
we always have a full history all the way back to VM boot. This had YJIT
install and run code that assume EP=BP when EP≠BP for some exceptional
entry into the middle of a running frame, if the environment escaped
before YJIT.enable.
The fix is to reject exceptional entry with an escaped environment.
Rename things and explain in more detail how the predicate for deciding
to assume EP=BP works. It's quite subtle since it reasons about all
parties in the system that push a control frame and then run JIT code.
Note that while can_assume_on_stack_env() checks the currently running
environment if it so happens to be the one YJIT is compiling against, it
can return true for any ISEQ. The check isn't necessary for fixing the
bug, and the load bearing part of this patch is the change to
exceptional entries.
This fix is flat on speed and space on ruby-bench headline benchmarks.
Many thanks for the community effort to create a small test case for
this bug.
Updated by k0kubun (Takashi Kokubun) 9 days ago
- Backport changed from 3.2: DONTNEED, 3.3: DONTNEED, 3.4: REQUIRED, 4.0: REQUIRED to 3.2: DONTNEED, 3.3: DONTNEED, 3.4: REQUIRED, 4.0: DONE
ruby_4_0 8466e93b1d6eb85ad5952ab3a10575fa453e77e2.