{"record":{"id":"9dfa0a526cad94e9","repo":"ruby/ruby","slug":"invalid-operand-to-br-instruction","errorCode":null,"errorMessage":"Invalid operand to br instruction.","messagePattern":"Invalid operand to br instruction\\.","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"zjit/src/asm/arm64/mod.rs","lineNumber":304,"sourceCode":"\n    cb.write_bytes(&bytes);\n}\n\n/// BLR - branch with link to a register\npub fn blr(cb: &mut CodeBlock, rn: A64Opnd) {\n    let bytes: [u8; 4] = match rn {\n        A64Opnd::Reg(rn) => Branch::blr(rn.reg_no).into(),\n        _ => panic!(\"Invalid operand to blr instruction.\"),\n    };\n\n    cb.write_bytes(&bytes);\n}\n\n/// BR - branch to a register\npub fn br(cb: &mut CodeBlock, rn: A64Opnd) {\n    let bytes: [u8; 4] = match rn {\n        A64Opnd::Reg(rn) => Branch::br(rn.reg_no).into(),\n        _ => panic!(\"Invalid operand to br instruction.\"),\n    };\n\n    cb.write_bytes(&bytes);\n}\n\n/// BRK - create a breakpoint\npub fn brk(cb: &mut CodeBlock, imm16: A64Opnd) {\n    let bytes: [u8; 4] = match imm16 {\n        A64Opnd::None => Breakpoint::brk(0xf000).into(),\n        A64Opnd::UImm(imm16) => {\n            assert!(uimm_fits_bits(imm16, 16), \"The immediate operand must be 16 bits or less.\");\n            Breakpoint::brk(imm16 as u16).into()\n        },\n        _ => panic!(\"Invalid operand combination to brk instruction.\")\n    };\n\n    cb.write_bytes(&bytes);\n}","sourceCodeStart":286,"sourceCodeEnd":322,"githubUrl":"https://github.com/ruby/ruby/blob/0e5b888e1c355f3f728f2659f085820937dada48/zjit/src/asm/arm64/mod.rs#L286-L322","documentation":"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.","triggerScenarios":"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.","commonSituations":"Writing ZJIT dispatch/jump-table code on ARM64; porting jump logic from an x86 backend where absolute jumps are immediates.","solutions":["Load the target into a register first, then br(cb, A64Opnd::Reg(reg))","For in-range relative jumps use b/bl with an offset instead","If hit while running stock Ruby with --zjit, update and report the backtrace to ruby-core"],"exampleFix":"// before\nbr(cb, A64Opnd::UImm(target_addr));  // immediate operand -> panic\n\n// after\nmov(cb, x16, A64Opnd::UImm(target_addr));\nbr(cb, A64Opnd::Reg(x16));","handlingStrategy":"type-guard","validationCode":"// br wants exactly one Reg operand\nfn br_operand_ok(rn: &A64Opnd) -> bool {\n    matches!(rn, A64Opnd::Reg(_))\n}\ndebug_assert!(br_operand_ok(&target));","typeGuard":"fn as_reg(o: A64Opnd) -> Option<Reg> {\n    if let A64Opnd::Reg(r) = o { Some(r) } else { None }\n}  // jump targets must already be in a register","tryCatchPattern":"// #[should_panic(expected = \"Invalid operand to br instruction.\")]\n// unit-test that non-register operands are rejected loudly during development","preventionTips":["Load jump-table or computed targets into registers before emitting br","Encode the rule once in a jump_reg(cb, reg) wrapper used by all codegen","Keep relative-branch offset checks (b_offset_fits_bits) in mind for long jumps"],"tags":["zjit","ruby","rust","arm64","assembler","panic","branch"],"backgroundTag":"invalid-instruction-operands","analyzedSha":"0e5b888e1c355f3f728f2659f085820937dada48","analyzedAt":"2026-08-21T14:25:43.473Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}