Skip to content

ZJIT: Fix crashes and a miscomp for param spilling #13802

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions test/ruby/test_zjit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -776,6 +776,11 @@ def test

test
}

assert_compiles '1', %q{
def a(n1,n2,n3,n4,n5,n6,n7,n8,n9) = n1+n9
a(2,0,0,0,0,0,0,0,-1)
}
end

def test_opt_aref_with
Expand Down
5 changes: 5 additions & 0 deletions zjit/src/backend/arm64/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ impl Assembler
vec![X1_REG, X9_REG, X10_REG, X11_REG, X12_REG, X13_REG, X14_REG, X15_REG]
}

/// How many bytes a call and a [Self::frame_setup] would change native SP
pub fn frame_size() -> i32 {
0x10
}

/// Split platform-specific instructions
/// The transformations done here are meant to make our lives simpler in later
/// stages of the compilation pipeline.
Expand Down
15 changes: 12 additions & 3 deletions zjit/src/backend/lir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ pub enum MemBase
pub struct Mem
{
// Base register number or instruction index
pub(super) base: MemBase,
pub base: MemBase,

// Offset relative to the base pointer
pub(super) disp: i32,
pub disp: i32,

// Size in bits
pub(super) num_bits: u8,
pub num_bits: u8,
}

impl fmt::Debug for Mem {
Expand Down Expand Up @@ -2150,6 +2150,7 @@ impl Assembler {
}

pub fn load_into(&mut self, dest: Opnd, opnd: Opnd) {
assert!(matches!(dest, Opnd::Reg(_) | Opnd::VReg{..}), "Destination of load_into must be a register");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a little too lax. Isn't there a chance the vreg is allocated to memory?

Copy link
Member Author

@XrXr XrXr Jul 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot of code around register allocation that assume VReg means reg and I think there's no way a VReg becomes memory at the moment (because of no auto spills). I figure when they could become memory LIR would make some rewrites to completely erase these.

Also, maybe this is beside the point and we should remove these constrained movs (like Store) and use the general mov for everything. I guess these are still useful in spots where we don't run the split (legalization) backend pass...

match (dest, opnd) {
(Opnd::Reg(dest), Opnd::Reg(opnd)) if dest == opnd => {}, // skip if noop
_ => self.push_insn(Insn::LoadInto { dest, opnd }),
Expand Down Expand Up @@ -2303,5 +2304,13 @@ mod tests {

assert!(matches!(opnd_iter.next(), None));
}

#[test]
#[should_panic]
fn load_into_memory_is_invalid() {
let mut asm = Assembler::new();
let mem = Opnd::mem(64, SP, 0);
asm.load_into(mem, mem);
}
}

6 changes: 6 additions & 0 deletions zjit/src/backend/x86_64/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ impl Assembler
vec![RAX_REG, RCX_REG, RDX_REG, RSI_REG, RDI_REG, R8_REG, R9_REG, R10_REG, R11_REG]
}

/// How many bytes a call and a [Self::frame_setup] would change native SP
pub fn frame_size() -> i32 {
0x8
}

// These are the callee-saved registers in the x86-64 SysV ABI
// RBX, RSP, RBP, and R12–R15

Expand Down Expand Up @@ -475,6 +480,7 @@ impl Assembler
// (e.g. with Linux `perf record --call-graph fp`)
Insn::FrameSetup => {
if false { // We don't support --zjit-perf yet
// TODO(alan): Change Assembler::frame_size() when adding --zjit-perf support
push(cb, RBP);
mov(cb, RBP, RSP);
push(cb, RBP);
Expand Down
53 changes: 37 additions & 16 deletions zjit/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,14 @@ fn gen_iseq_entry_point(iseq: IseqPtr) -> *const u8 {
// Compile the High-level IR
let cb = ZJITState::get_code_block();
let (start_ptr, mut branch_iseqs) = match gen_function(cb, iseq, &function) {
Some((start_ptr, gc_offsets, branch_iseqs)) => {
Some((start_ptr, gc_offsets, jit)) => {
// Remember the block address to reuse it later
let payload = get_or_create_iseq_payload(iseq);
payload.start_ptr = Some(start_ptr);
payload.gc_offsets.extend(gc_offsets);

// Compile an entry point to the JIT code
(gen_entry(cb, iseq, &function, start_ptr), branch_iseqs)
(gen_entry(cb, iseq, &function, start_ptr, jit.c_stack_bytes), jit.branch_iseqs)
},
None => (None, vec![]),
};
Expand Down Expand Up @@ -145,11 +145,11 @@ fn gen_iseq_entry_point(iseq: IseqPtr) -> *const u8 {
}

/// Compile a JIT entry
fn gen_entry(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function, function_ptr: CodePtr) -> Option<CodePtr> {
fn gen_entry(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function, function_ptr: CodePtr, c_stack_bytes: usize) -> Option<CodePtr> {
// Set up registers for CFP, EC, SP, and basic block arguments
let mut asm = Assembler::new();
gen_entry_prologue(&mut asm, iseq);
gen_entry_params(&mut asm, iseq, function.block(BlockId(0)));
gen_entry_params(&mut asm, iseq, function.block(BlockId(0)), c_stack_bytes);

// Jump to the first block using a call instruction
asm.ccall(function_ptr.raw_ptr(cb) as *const u8, vec![]);
Expand Down Expand Up @@ -185,17 +185,17 @@ fn gen_iseq(cb: &mut CodeBlock, iseq: IseqPtr) -> Option<(CodePtr, Vec<(Rc<Branc

// Compile the High-level IR
let result = gen_function(cb, iseq, &function);
if let Some((start_ptr, gc_offsets, branch_iseqs)) = result {
if let Some((start_ptr, gc_offsets, jit)) = result {
payload.start_ptr = Some(start_ptr);
payload.gc_offsets.extend(gc_offsets);
Some((start_ptr, branch_iseqs))
Some((start_ptr, jit.branch_iseqs))
} else {
None
}
}

/// Compile a function
fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function) -> Option<(CodePtr, Vec<CodePtr>, Vec<(Rc<Branch>, IseqPtr)>)> {
fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function) -> Option<(CodePtr, Vec<CodePtr>, JITState)> {
let c_stack_bytes = aligned_stack_bytes(max_num_params(function).saturating_sub(ALLOC_REGS.len()));
let mut jit = JITState::new(iseq, function.num_insns(), function.num_blocks(), c_stack_bytes);
let mut asm = Assembler::new();
Expand Down Expand Up @@ -249,7 +249,7 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, function: &Function) -> Optio
}

// Generate code if everything can be compiled
asm.compile(cb).map(|(start_ptr, gc_offsets)| (start_ptr, gc_offsets, jit.branch_iseqs))
asm.compile(cb).map(|(start_ptr, gc_offsets)| (start_ptr, gc_offsets, jit))
}

/// Compile an instruction
Expand Down Expand Up @@ -548,7 +548,7 @@ fn gen_entry_prologue(asm: &mut Assembler, iseq: IseqPtr) {
}

/// Assign method arguments to basic block arguments at JIT entry
fn gen_entry_params(asm: &mut Assembler, iseq: IseqPtr, entry_block: &Block) {
fn gen_entry_params(asm: &mut Assembler, iseq: IseqPtr, entry_block: &Block, c_stack_bytes: usize) {
let self_param = gen_param(asm, SELF_PARAM_IDX);
asm.mov(self_param, Opnd::mem(VALUE_BITS, CFP, RUBY_OFFSET_CFP_SELF));

Expand All @@ -557,14 +557,34 @@ fn gen_entry_params(asm: &mut Assembler, iseq: IseqPtr, entry_block: &Block) {
asm_comment!(asm, "set method params: {num_params}");

// Allocate registers for basic block arguments
let params: Vec<Opnd> = (0..num_params).map(|idx|
gen_param(asm, idx + 1) // +1 for self
).collect();
for idx in 0..num_params {
let param = gen_param(asm, idx + 1); // +1 for self

// Funky offset adjustment to write into the native stack frame of the
// HIR function we'll be calling into. This only makes sense in context
// of the schedule of instructions in gen_entry() for the JIT entry point.
//
// The entry point needs to load VALUEs into native stack slots _before_ the
// frame containing the slots exists. So, we anticipate the stack frame size
// of the Function and subtract offsets based on that.
//
// native SP at entry point ─────►┌────────────┐ Native SP grows downwards
// │ │ ↓ on all arches we support.
// SP-0x8 ├────────────┤
// │ │
// where native SP SP-0x10├────────────┤
// would be while │ │
// the HIR function ────────────► └────────────┘
// is running
let param = if let Opnd::Mem(lir::Mem { base, disp, num_bits }) = param {
Opnd::Mem(lir::Mem { num_bits, base, disp: disp - c_stack_bytes as i32 - Assembler::frame_size() })
} else {
param
};

// Assign local variables to the basic block arguments
for (idx, &param) in params.iter().enumerate() {
// Assign local variables to the basic block arguments
let local = gen_entry_param(asm, iseq, idx);
asm.load_into(param, local);
asm.mov(param, local);
}
}
}
Expand Down Expand Up @@ -1093,11 +1113,12 @@ fn gen_push_frame(asm: &mut Assembler, argc: usize, state: &FrameState, frame: C
/// Return an operand we use for the basic block argument at a given index
fn param_opnd(idx: usize) -> Opnd {
// To simplify the implementation, allocate a fixed register or a stack slot for each basic block argument for now.
// Note that this is implemented here as opposed to automatically inside LIR machineries.
// TODO: Allow allocating arbitrary registers for basic block arguments
if idx < ALLOC_REGS.len() {
Opnd::Reg(ALLOC_REGS[idx])
} else {
Opnd::mem(64, NATIVE_STACK_PTR, -((idx - ALLOC_REGS.len() + 1) as i32) * SIZEOF_VALUE_I32)
Opnd::mem(64, NATIVE_STACK_PTR, (idx - ALLOC_REGS.len()) as i32 * SIZEOF_VALUE_I32)
}
}

Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy