Project

General

Profile

Actions

Bug #21941

closed

Local variable becomes nil when YJIT enabled mid-method with fork/signal/ensure

Bug #21941: Local variable becomes nil when YJIT enabled mid-method with fork/signal/ensure
1

Added by nicholasdower (Nick Dower) 21 days ago. Updated 9 days ago.

Status:
Closed
Assignee:
Target version:
-
ruby -v:
ruby 4.0.0 (2025-12-25 revision 553f1675f3) +PRISM [arm64-darwin25]
[ruby-core:124920]

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 Actions #1 [ruby-core:124921]

  • 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 Actions #2 [ruby-core:124922]

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 alanwu (Alan Wu) 9 days ago Actions #4

  • 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 Actions #5 [ruby-core:125016]

  • 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
Actions

Also available in: PDF Atom