ruby/ruby · error

Invalid operand to br instruction.

Error message

Invalid operand to br instruction.

What it means

br (branch to register) mirrors blr in ZJIT's ARM64 assembler: only A64Opnd::Reg is accepted, and any other operand variant — address immediates, None, signed Imm — hits the catch-all arm and panics. ARM64 indirect jumps require the target to already live in a register.

Source

Thrown at zjit/src/asm/arm64/mod.rs:304

    cb.write_bytes(&bytes);
}

/// BLR - branch with link to a register
pub fn blr(cb: &mut CodeBlock, rn: A64Opnd) {
    let bytes: [u8; 4] = match rn {
        A64Opnd::Reg(rn) => Branch::blr(rn.reg_no).into(),
        _ => panic!("Invalid operand to blr instruction."),
    };

    cb.write_bytes(&bytes);
}

/// BR - branch to a register
pub fn br(cb: &mut CodeBlock, rn: A64Opnd) {
    let bytes: [u8; 4] = match rn {
        A64Opnd::Reg(rn) => Branch::br(rn.reg_no).into(),
        _ => panic!("Invalid operand to br instruction."),
    };

    cb.write_bytes(&bytes);
}

/// BRK - create a breakpoint
pub fn brk(cb: &mut CodeBlock, imm16: A64Opnd) {
    let bytes: [u8; 4] = match imm16 {
        A64Opnd::None => Breakpoint::brk(0xf000).into(),
        A64Opnd::UImm(imm16) => {
            assert!(uimm_fits_bits(imm16, 16), "The immediate operand must be 16 bits or less.");
            Breakpoint::brk(imm16 as u16).into()
        },
        _ => panic!("Invalid operand combination to brk instruction.")
    };

    cb.write_bytes(&bytes);
}

View on GitHub (pinned to 0e5b888e1c)

Solutions

  1. Load the target into a register first, then br(cb, A64Opnd::Reg(reg))
  2. For in-range relative jumps use b/bl with an offset instead
  3. If hit while running stock Ruby with --zjit, update and report the backtrace to ruby-core

Example fix

// before
br(cb, A64Opnd::UImm(target_addr));  // immediate operand -> panic

// after
mov(cb, x16, A64Opnd::UImm(target_addr));
br(cb, A64Opnd::Reg(x16));
Defensive patterns

Strategy: type-guard

Validate before calling

// br wants exactly one Reg operand
fn br_operand_ok(rn: &A64Opnd) -> bool {
    matches!(rn, A64Opnd::Reg(_))
}
debug_assert!(br_operand_ok(&target));

Type guard

fn as_reg(o: A64Opnd) -> Option<Reg> {
    if let A64Opnd::Reg(r) = o { Some(r) } else { None }
}  // jump targets must already be in a register

Try / catch

// #[should_panic(expected = "Invalid operand to br instruction.")]
// unit-test that non-register operands are rejected loudly during development

Prevention

When it happens

Trigger: br(cb, A64Opnd::UImm(target_addr)) — jumping to a raw address; br(cb, A64Opnd::None) from a defaulted-operand code path; operand construction refactors that stopped producing Reg.

Common situations: Writing ZJIT dispatch/jump-table code on ARM64; porting jump logic from an x86 backend where absolute jumps are immediates.

Related errors


AI-assisted analysis of ruby/ruby@0e5b888e1c (2026-08-21). Data as JSON: /api/errors/9dfa0a526cad94e9. Report an issue: GitHub.