Bug #22074
closedYJIT misaligns locals when there are > 256 local variables
Description
Hi! We ran into this issue in production with a large Slim template in a Rails app, but we found that it's reproducible in any function with a lot of local variables. It seems like YJIT tracks local variables for register allocation in an 8-bit integer, so if there are more than 256 local variables in a function, the index overflows and maps back to the wrong variable and causes issues. We reproduced the issue with the released version of Ruby 4.0.4. We have a simple reproduction script and a proposed patch. The reproduction script generates some code with 257 locals and tests that it behaves correctly over multiple iterations (to exercise YJIT since it doesn't kick in right away).
Full disclosure, the code is AI-written, and I don't know Rust, but I do know C and integer overflows, and it seems somewhat reasonable - though I'm not sure if it's the exact right fix.
With YJIT disabled:
$ ruby yjit_many_locals_simple_repro.rb
ruby=ruby 4.0.4 (2026-05-12 revision b89eb1bcbf) +PRISM [x86_64-linux]
yjit=false
iterations=100000
YJIT is not enabled. Re-run with: ruby --yjit yjit_many_locals_simple_repro.rb
iteration=10000
iteration=20000
iteration=30000
iteration=40000
iteration=50000
iteration=60000
iteration=70000
iteration=80000
iteration=90000
iteration=100000
ok iterations=100000
With YJIT enabled
$ ruby --yjit yjit_many_locals_simple_repro.rb
ruby=ruby 4.0.4 (2026-05-12 revision b89eb1bcbf) +YJIT +PRISM [x86_64-linux]
yjit=true
iterations=100000
failed_at=30
yjit_many_locals_simple_repro.rb:259:in 'Object#many_locals': undefined method '+' for an instance of Object (NoMethodError)
from yjit_many_locals_simple_repro.rb:42:in 'block in <main>'
from yjit_many_locals_simple_repro.rb:41:in 'Integer#times'
from yjit_many_locals_simple_repro.rb:41:in '<main>'
yjit_many_locals_simple_repro.rb:259:in 'Object#many_locals': undefined method '+' for an instance of Object (NoMethodError)
from yjit_many_locals_simple_repro.rb:42:in 'block in <main>'
from yjit_many_locals_simple_repro.rb:41:in 'Integer#times'
from yjit_many_locals_simple_repro.rb:41:in '<main>'
Thank you for your consideration!
Files
Updated by Eregon (Benoit Daloze) 1 day ago
Link to the PR: https://github.com/ruby/ruby/pull/17043
Updated by k0kubun (Takashi Kokubun) about 16 hours ago
- Backport changed from 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: UNKNOWN to 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: REQUIRED
Updated by Anonymous about 15 hours ago
- Status changed from Open to Closed
Applied in changeset git|c34cc402a378be826714aae67af5d494107d9814.
YJIT: Fix local register mapping overflow
Previously, local indices greater than 255 were truncated when
converted to RegOpnd::Local. This could make a high-index local alias
a tracked low-index local in the register mapping and produce incorrect
values after compilation.
Cap overflowing indices to MAX_CTX_LOCALS. RegMapping treats that index
as untrackable because it is just outside the tracked local range.
Fixes [Bug #22074].
Co-authored-by: Codex <199175422+chatgpt-connector[bot]@users.noreply.github.com>
Updated by k0kubun (Takashi Kokubun) about 10 hours ago
- Backport changed from 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: REQUIRED to 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: DONE
ruby_4_0 cc058251160135a76cfbec8bbd83ae114001228f.
Updated by ibrahima (Ibrahim Awwal) about 5 hours ago
By the way, we first encountered this bug on Ruby 3.4, so I think it affects at least 3.4, not sure about earlier versions. But the reproduction script should hopefully make it easy to check - I can run it on older versions later.
Updated by byroot (Jean Boussier) about 5 hours ago
- Backport changed from 3.3: UNKNOWN, 3.4: UNKNOWN, 4.0: DONE to 3.3: DONTNEED, 3.4: REQUIRED, 4.0: DONE
The reproducer doesn't show the bug on 3.3, so marking as DONTNEED. But 3.3 is on security only maintenance anyways, so even if it was impacted, the policy would be WONTFIX.