From 0f35c62863a620ee884d731207aec976ca1e5e62 Mon Sep 17 00:00:00 2001 From: Ibrahim Awwal Date: Tue, 19 May 2026 01:02:41 +0000 Subject: [PATCH] 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 u8::MAX. Since this is greater than MAX_CTX_LOCALS, YJIT treats these locals as untracked. Fixes [Bug #22074]. Co-authored-by: Codex <199175422+chatgpt-connector[bot]@users.noreply.github.com> --- bootstraptest/test_yjit.rb | 20 ++++++++++++++++++++ yjit/src/backend/ir.rs | 4 +++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/bootstraptest/test_yjit.rb b/bootstraptest/test_yjit.rb index f19629ec0e4e85..e9ce905e2c7073 100644 --- a/bootstraptest/test_yjit.rb +++ b/bootstraptest/test_yjit.rb @@ -5535,3 +5535,23 @@ def compiled_method # Resume the fiber — compiled_method's iseq must still be valid fiber.resume.to_s } + +# regression test for register mapping of methods with over 256 locals +# [Bug #22074] +assert_equal "ok", %q{ + source = +"def many_locals\n" + source << " total = 0\n" + + 128.times do |i| + source << " y#{i} = 1\n" + source << " x#{i} = Object.new\n" + end + + source << " total += 1\n" + source << " raise total.inspect unless total == 1\n" + source << "end\n" + + eval(source) + many_locals + "ok" +} diff --git a/yjit/src/backend/ir.rs b/yjit/src/backend/ir.rs index 3fb67bc7cc1584..16a6ad49b357d4 100644 --- a/yjit/src/backend/ir.rs +++ b/yjit/src/backend/ir.rs @@ -242,7 +242,9 @@ impl Opnd let last_idx = stack_size as i32 + VM_ENV_DATA_SIZE as i32 - 1; assert!(last_idx <= idx, "Local index {} must be >= last local index {}", idx, last_idx); assert!(idx <= last_idx + num_locals as i32, "Local index {} must be < last local index {} + local size {}", idx, last_idx, num_locals); - RegOpnd::Local((last_idx + num_locals as i32 - idx) as u8) + // Indices that don't fit in u8 are capped to u8::MAX, which is greater than MAX_CTX_LOCALS. + let local_idx = last_idx + num_locals as i32 - idx; + RegOpnd::Local(local_idx.try_into().unwrap_or(u8::MAX)) } else { assert!(idx < stack_size as i32); RegOpnd::Stack((stack_size as i32 - idx - 1) as u8)