{"record":{"id":"017151492a1b41e1","repo":"ruby/ruby","slug":"invalid-operand-to-ret-instruction","errorCode":null,"errorMessage":"Invalid operand to ret instruction.","messagePattern":"Invalid operand to ret instruction\\.","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"zjit/src/asm/arm64/mod.rs","lineNumber":1146,"sourceCode":"    let bytes: [u8; 4] = match (rd, rn) {\n        (A64Opnd::Reg(rd), A64Opnd::Reg(rn)) => {\n            assert_eq!(rd.num_bits, 64, \"rd must be 64-bits wide.\");\n            assert_eq!(rn.num_bits, 32, \"rn must be 32-bits wide.\");\n\n            SBFM::sxtw(rd.reg_no, rn.reg_no).into()\n        },\n        _ => panic!(\"Invalid operand combination to sxtw instruction.\"),\n    };\n\n    cb.write_bytes(&bytes);\n}\n\n/// RET - unconditionally return to a location in a register, defaults to X30\npub fn ret(cb: &mut CodeBlock, rn: A64Opnd) {\n    let bytes: [u8; 4] = match rn {\n        A64Opnd::None => Branch::ret(30).into(),\n        A64Opnd::Reg(reg) => Branch::ret(reg.reg_no).into(),\n        _ => panic!(\"Invalid operand to ret instruction.\")\n    };\n\n    cb.write_bytes(&bytes);\n}\n\n/// TBNZ - test bit and branch if not zero\npub fn tbnz(cb: &mut CodeBlock, rt: A64Opnd, bit_num: A64Opnd, offset: A64Opnd) {\n    let bytes: [u8; 4] = match (rt, bit_num, offset) {\n        (A64Opnd::Reg(rt), A64Opnd::UImm(bit_num), A64Opnd::Imm(offset)) => {\n            TestBit::tbnz(rt.reg_no, bit_num.try_into().unwrap(), offset.try_into().unwrap()).into()\n        },\n        _ => panic!(\"Invalid operand combination to tbnz instruction.\")\n    };\n\n    cb.write_bytes(&bytes);\n}\n\n/// TBZ - test bit and branch if zero","sourceCodeStart":1128,"sourceCodeEnd":1164,"githubUrl":"https://github.com/ruby/ruby/blob/0e5b888e1c355f3f728f2659f085820937dada48/zjit/src/asm/arm64/mod.rs#L1128-L1164","documentation":"zjit's arm64 ret() encodes RET to a register and accepts only A64Opnd::None (defaults to X30, the link register) or A64Opnd::Reg. Any other variant (Imm, UImm, Mem) panics because a return target must be a register on A64.","triggerScenarios":"Calling ret(cb, rn) with A64Opnd::Imm(addr) or A64Opnd::Mem(...) — e.g. returning to an address held in memory or computed as a constant. Passing a register works: ret(cb, X0) emits RET X0; passing None emits plain RET (X30).","commonSituations":"Tail-call or trampoline code that computes the destination as an immediate constant; IRs whose 'return' node carries a generic operand that happens to be an immediate; switching an epilogue from a register to an address-in-memory scheme during refactoring.","solutions":["Call ret(cb, A64Opnd::None) for the normal case (returns via X30).","Move the target address into a register first (ldr from memory, or movz/movk for constants), then ret(cb, Xn).","Never pass a Mem or Imm operand; check with matches!(rn, A64Opnd::None | A64Opnd::Reg(_)) before emitting."],"exampleFix":"// before: address in memory -> panic\nret(cb, A64Opnd::Mem(target_slot));\n\n// after: load target then return\nldr(cb, X16, A64Opnd::Mem(target_slot));\nret(cb, X16);","handlingStrategy":"validation","validationCode":"let ok = matches!(rn, A64Opnd::None | A64Opnd::Reg(_));\nassert!(ok, \"ret target must be None or a register, got {rn:?}\");\nret(cb, rn);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Load return targets from memory or materialize constants into X16/X17 (IP registers) before ret.","Keep a single epilogue emitter so return-target handling cannot diverge per call site."],"tags":["arm64","assembler","operand-validation","control-flow"],"backgroundTag":"invalid-instruction-operands","analyzedSha":"0e5b888e1c355f3f728f2659f085820937dada48","analyzedAt":"2026-08-21T14:25:43.473Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}