From c293c36d7968409b01de921a761ae2e03ce1205f Mon Sep 17 00:00:00 2001 From: Mark Rowe Date: Tue, 14 Jul 2026 12:50:07 -0700 Subject: [PATCH 1/2] Rework arm64test.py so that its tests can be consumed by pytest --- arch/arm64/README.md | 3 +- arch/arm64/arm64test.py | 179 ++++++++++++++++++++++++++++++++-------- 2 files changed, 146 insertions(+), 36 deletions(-) diff --git a/arch/arm64/README.md b/arch/arm64/README.md index 3deaa5c802..74ff7cc367 100644 --- a/arch/arm64/README.md +++ b/arch/arm64/README.md @@ -22,9 +22,10 @@ Personal Binary Ninja users can test via the built in console: >>> sys.path.append('C:/users/x/onedrive/documents/binja/binaryninja-api/arch/arm64') # Path directory containing arm64test.py >>> from importlib import reload >>> import arm64test +>>> arm64test.run_all() success! >>> # Add or fix any testcases ->>> reload(arm64test) +>>> reload(arm64test).run_all() success! ``` diff --git a/arch/arm64/arm64test.py b/arch/arm64/arm64test.py index eadab3f3f6..b05ff90dfc 100755 --- a/arch/arm64/arm64test.py +++ b/arch/arm64/arm64test.py @@ -12773,6 +12773,9 @@ (b'\x20\xb8\xe0\x4e', 'LLIL_UNIMPL()'), ] +disasm_test_cases = [ +] + test_cases = \ tests_cssc + \ tests_shll + \ @@ -12830,6 +12833,29 @@ tests_ldrsw + \ tests_grab_bag +# Instructions whose lifted IL depends on the address they are lifted at: adrp forms an address from +# the page the instruction sits in, and the literal load addresses its operand relative to the +# instruction. Each case gives the address to lift at and the IL that must be produced there, which +# pins the address arithmetic rather than just the fact that it changes. Every other test expects the +# IL produced at address 0, so these instructions are excluded from the shared view the rest are +# lifted from. +tests_position_dependent = [ + # adrp x8, 0x4364000 -- (address & ~0xfff) + 0x4364000 + (b'\x28\x1b\x02\x90', 0x0, 'LLIL_SET_REG.q(x8,LLIL_CONST.q(0x4364000))'), + (b'\x28\x1b\x02\x90', 0x8000, 'LLIL_SET_REG.q(x8,LLIL_CONST.q(0x436C000))'), + (b'\x28\x1b\x02\x90', 0x8004, 'LLIL_SET_REG.q(x8,LLIL_CONST.q(0x436C000))'), + # adrp x0, 0x11000 -- (address & ~0xfff) + 0x11000 + (b'\x80\x00\x00\xb0', 0x0, 'LLIL_SET_REG.q(x0,LLIL_CONST.q(0x11000))'), + (b'\x80\x00\x00\xb0', 0x8000, 'LLIL_SET_REG.q(x0,LLIL_CONST.q(0x19000))'), + # ldrsw x6, 0x20 -- address + 0x20 + (b'\x06\x01\x00\x98', 0x0, 'LLIL_SET_REG.q(x6,LLIL_SX.q(LLIL_LOAD.d(LLIL_CONST.q(0x20))))'), + (b'\x06\x01\x00\x98', 0x8000, 'LLIL_SET_REG.q(x6,LLIL_SX.q(LLIL_LOAD.d(LLIL_CONST.q(0x8020))))'), + (b'\x06\x01\x00\x98', 0x8004, 'LLIL_SET_REG.q(x6,LLIL_SX.q(LLIL_LOAD.d(LLIL_CONST.q(0x8024))))'), +] + +# The encodings above, which must not be lifted from the shared view. +position_dependent_encodings = {data for data, _, _ in tests_position_dependent} + def il2str(il): sz_lookup = {1:'.b', 2:'.w', 4:'.d', 8:'.q', 16:'.o'} if isinstance(il, lowlevelil.LowLevelILInstruction): @@ -12849,47 +12875,104 @@ def il2str(il): else: return str(il) -# TODO: make this less hacky -def lift(data, disasm=False): - EPILOG = b'\xa0\xd5\x9b\xd2' + b'\xc0\x03\x5f\xd6' # mov x0, 0xDEAD; return +EPILOG = b'\xa0\xd5\x9b\xd2' + b'\xc0\x03\x5f\xd6' # mov x0, 0xDEAD; return + +# Layout of the view every position-independent case is lifted from. Each case is its instruction +# followed by the epilogue, padded out to a fixed stride. The first case sits at BATCH_BASE rather +# than at 0 so that no case shares the page that address 0 is in, which is what makes an adrp that has +# lost its dependence on the address show up as a mismatch instead of agreeing with the IL expected at +# address 0. +BATCH_BASE = 0x1000 +BATCH_STRIDE = 16 - platform = binaryninja.Platform['linux-aarch64'] - # make a pretend function that returns - bv = binaryview.BinaryView.new(data + EPILOG) - bv.add_function(0, plat=platform) - assert len(bv.functions) == 1 +def platform(): + return binaryninja.Platform['linux-aarch64'] - asm = [] +def new_view(data): + """A view of data that carries analysis no further than the IL these tests read.""" + bv = binaryview.BinaryView.new(data) + binaryninja.Settings().set_string('analysis.mode', 'basic', bv) + return bv +def function_il(func): tokens = [] attributes = set() - #for block in bv.functions[0].low_level_il: - for block in bv.functions[0].lifted_il: + for block in func.lifted_il: for il in block: attributes = attributes.union(il.attributes) tokens.append(il2str(il)) - if disasm: - info = block.arch.get_instruction_info(bv[il.address:il.address+block.arch._get_max_instruction_length(None)], il.address) - if info: - asm.append(''.join(map(str, block.arch.get_instruction_text(bv[il.address:il.address + info.length], il.address)[0]))) - else: - asm.append(''.join(map(str, block.arch.get_instruction_text(bv[il.address:il.address+4], il.address)[0]))) il_str = '; '.join(tokens) i = len(il_str) try: i = il_str.rindex('; LLIL_SET_REG.q(x0,LLIL_CONST.q(0xDEAD))') - except: - # ValueError: substring not found + except ValueError: pass - il_str = il_str[0:i] - asm = asm[0:1] + return il_str[0:i], attributes + +def disassemble(data, addr=0): + arch = binaryninja.Architecture['aarch64'] + tokens = arch.get_instruction_text(data, addr)[0] + return ''.join(map(str, tokens)) if tokens else '' + +def lift_at(data, addr): + """Lift a single instruction placed at addr, in a BinaryView of its own.""" + bv = new_view(b'\x00' * addr + data + EPILOG) + bv.add_function(addr, plat=platform()) + func = bv.get_function_at(addr) + assert func is not None + return function_il(func) + +def lift(data, disasm=False): + il_str, attributes = lift_at(data, 0) if disasm: - return il_str, attributes, asm + return il_str, attributes, [disassemble(data)] else: return il_str, attributes +class BatchLift: + """Every test case as its own function within a single BinaryView. + + Constructing a BinaryView costs far more than lifting an instruction does, so the cases are laid + out together and lifted from one view. Each function is created at a known address rather than + left to be discovered, so nothing depends on what analysis finds on its own. + """ + def __init__(self, cases): + blob = bytearray(b'\x00' * BATCH_BASE) + for data, *_ in cases: + body = data + EPILOG + assert len(body) <= BATCH_STRIDE + blob += body + b'\x00' * (BATCH_STRIDE - len(body)) + + self.bv = new_view(bytes(blob)) + for index in range(len(cases)): + self.bv.add_function(self.address(index), plat=platform()) + self.bv.update_analysis_and_wait() + + def address(self, index): + return BATCH_BASE + index * BATCH_STRIDE + + def il(self, index): + func = self.bv.get_function_at(self.address(index)) + assert func is not None + return function_il(func) + +batch_lift = None + +def lifted_case(index): + """The lifted IL and attributes for test_cases[index], as produced at address 0.""" + global batch_lift + + data = test_cases[index][0] + if data in position_dependent_encodings: + return lift_at(data, 0) + + if batch_lift is None: + batch_lift = BatchLift(test_cases) + + return batch_lift.il(index) + def il_str_to_tree(ilstr): result = '' depth = 0 @@ -12916,7 +12999,8 @@ def test_all_lifts(no_fail=False): data, expected_lift = test_info[0], test_info[1] il_attrs = test_info[2] if len(test_info) == 3 else None - actual_lift, actual_attrs, asm = lift(data, True) + actual_lift, actual_attrs = lifted_case(test_i) + asm = [disassemble(data)] if actual_lift != expected_lift: print('LIFT MISMATCH AT TEST %d!' % test_i) print('\t input: %s %s' % (data.hex(), '\n'.join(asm))) @@ -12941,17 +13025,42 @@ def test_all_lifts(no_fail=False): return True -if __name__ == '__main__': - no_fail = False - if len(sys.argv) > 1: - if sys.argv[1] == '--no-fail': - no_fail = True - if test_all_lifts(no_fail): +def test_all_disassembly(no_fail=False): + for test_i, (data, expected_text) in enumerate(disasm_test_cases): + actual_text = disassemble(data) + if actual_text != expected_text: + print('DISASSEMBLY MISMATCH AT TEST %d!' % test_i) + print('\t input: %s' % data.hex()) + print('\texpected: %s' % expected_text) + print('\t actual: %s' % actual_text) + if not no_fail: + return False + + return True + +def test_all_position_dependent(no_fail=False): + for test_i, (data, addr, expected_lift) in enumerate(tests_position_dependent): + actual_lift, _ = lift_at(data, addr) + if actual_lift != expected_lift: + print('POSITION-DEPENDENT LIFT MISMATCH AT TEST %d!' % test_i) + print('\t input: %s %s at %#x' % (data.hex(), disassemble(data, addr), addr)) + print('\texpected: %s' % expected_lift) + print('\t actual: %s' % actual_lift) + if not no_fail: + return False + + return True + +def run_all(no_fail=False): + lifts_ok = test_all_lifts(no_fail) + position_dependent_ok = test_all_position_dependent(no_fail) + disassembly_ok = test_all_disassembly(no_fail) + if lifts_ok and position_dependent_ok and disassembly_ok: print('success!', file=sys.stderr) - sys.exit(0) - else: - sys.exit(-1) + return True -if __name__ == 'arm64test': - if test_all_lifts(): - print('success!') + return False + +if __name__ == '__main__': + no_fail = len(sys.argv) > 1 and sys.argv[1] == '--no-fail' + sys.exit(0 if run_all(no_fail) else -1) From da3ff7a2e1c05535a9e5f689000c2e5aec957ac6 Mon Sep 17 00:00:00 2001 From: Mark Rowe Date: Mon, 6 Jul 2026 20:52:44 -0700 Subject: [PATCH 2/2] Add support for Apple's vendor-specific aarch64 instructions The following instructions are disassembled and lifted to intrinsics: * `genter #imm5` * `gexit` * `sdsb ` * `wkdmc , ` * `wkdmd , ` * `at_as1elx ` * `mul53lo .2d, .2d` * `mul53hi .2d, .2d` Additionally, AMX instructions are disassembled but not yet lifted. Fixes https://github.com/Vector35/binaryninja-api/issues/5933. These instructions are in encoding space that is unallocated by ARM. As we do not currently have any mechanism for architecture variants (https://github.com/Vector35/binaryninja-api/issues/979) and these encodings are not currently used by any other vendor, I've opted to have `Arm64Architecture` decode these unconditionally. In hopes of a better future, I've structured the code such that the vendor-specific instruction handling is entirely separate from the primary disassembler / lifting logic. When support for architecture variants is added in the future, this logic can all be moved into an Apple-specific variant. --- arch/arm64/apple_vendor.cpp | 405 +++++++++++++++++++++++++++++++++++ arch/arm64/apple_vendor.h | 80 +++++++ arch/arm64/arch_arm64.cpp | 52 +++++ arch/arm64/arm64test.py | 108 ++++++++++ arch/arm64/neon_intrinsics.h | 2 + 5 files changed, 647 insertions(+) create mode 100644 arch/arm64/apple_vendor.cpp create mode 100644 arch/arm64/apple_vendor.h diff --git a/arch/arm64/apple_vendor.cpp b/arch/arm64/apple_vendor.cpp new file mode 100644 index 0000000000..93eec078a9 --- /dev/null +++ b/arch/arm64/apple_vendor.cpp @@ -0,0 +1,405 @@ +// Copyright (c) 2026 Vector 35 Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +#include "apple_vendor.h" + +#include + +#include "disassembler/regs.h" + +using namespace BinaryNinja; +using namespace std; + +namespace { + +enum AppleVendorOp +{ + AV_NONE, + AV_GENTER, + AV_GEXIT, + AV_SDSB, + AV_WKDMC, + AV_WKDMD, + AV_AT_AS1ELX, + AV_MUL53LO, + AV_MUL53HI, + AV_AMX, + AV_APPLE_UNKNOWN_SPTM, +}; + +struct AppleVendorInsn +{ + AppleVendorOp op = AV_NONE; + uint32_t imm = 0; // genter selector, sdsb domain + uint32_t rd = 0; // destination register field, or the single operand of at + uint32_t rs = 0; // source register field +}; + +const char* const SDSB_DOMAIN[4] = {"osh", "nsh", "ish", "sy"}; + +// AMX operation names indexed by the op field (bits [9:5]) +// Op 17 is the enable/disable pair (set/clr) and is rendered specially. +// https://github.com/corsix/amx +const char* const AMX_OPS[23] = { + "ldx", "ldy", "stx", "sty", "ldz", "stz", "ldzi", "stzi", "extrx", "extry", "fma64", "fms64", + "fma32", "fms32", "mac16", "fma16", "fms16", "set", "vecint", "vecfp", "matint", "matfp", "genlut", +}; + +bool Decode(uint32_t insn, AppleVendorInsn& out) +{ + if ((insn & 0xffffffe0) == 0x00201420) // genter #imm5 + { + out.op = AV_GENTER; + out.imm = insn & 0x1f; + return true; + } + if (insn == 0x00201400) // gexit + { + out.op = AV_GEXIT; + return true; + } + if ((insn & 0xfffffffc) == 0x00201460) // sdsb + { + out.op = AV_SDSB; + out.imm = insn & 0x3; + return true; + } + if ((insn & 0xffffffe0) == 0x00201440) // at_as1elx + { + out.op = AV_AT_AS1ELX; + out.rd = insn & 0x1f; + return true; + } + if ((insn & 0xfffffc00) == 0x00200800) // wkdmc , + { + out.op = AV_WKDMC; + out.rd = (insn >> 5) & 0x1f; + out.rs = insn & 0x1f; + return true; + } + if ((insn & 0xfffffc00) == 0x00200c00) // wkdmd , + { + out.op = AV_WKDMD; + out.rd = (insn >> 5) & 0x1f; + out.rs = insn & 0x1f; + return true; + } + if ((insn & 0xfffffc00) == 0x00201000) // AMX: 0x00201000 | op<<5 | operand + { + uint32_t op = (insn >> 5) & 0x1f; + if (op <= 22) // ops 23-31 are reserved holes that fault as undefined + { + out.op = AV_AMX; + out.imm = op; // AMX operation number + out.rd = insn & 0x1f; // GPR operand (or set/clr selector for op 17) + return true; + } + } + if ((insn & 0xfffff800) == 0x00200000) // mul53lo/mul53hi .2d, .2d + { + // Bit 10 selects hi vs lo. The source field is at bits [9:5]. This placement (rather than + // [12:8]) is the only one consistent with bit 10 acting as the lo/hi selector. + // + // TODO: Confirm how the source vector register is encoded. + out.op = (insn & 0x400) ? AV_MUL53HI : AV_MUL53LO; + out.rd = insn & 0x1f; + out.rs = (insn >> 5) & 0x1f; + return true; + } + if ((insn & 0xffe0001f) == 0xd4e00000) // Apple exception-generation instruction (opc 0b111) + { + // An SPTM/TXM monitor entry, per kernelcache symbols such as _txm_enter. It is an + // exception-based alternative to genter, chosen at runtime by _libsptm_has_perms_overlay, and + // occupies the opc == 0b111 slot that ARM leaves unallocated in the encoding class holding + // svc/hvc/smc/brk. The imm16 at bits [20:5] is zero in every observed use, with the selector + // passed in x16 as it is for genter. + // + // TODO: `apple_unknown_sptm` is a temporary name. Replace it and add lifting once there's more + // information available. + out.op = AV_APPLE_UNKNOWN_SPTM; + out.imm = (insn >> 5) & 0xffff; + return true; + } + return false; +} + +// Immediates render the way the base disassembler's `%#x` format does: a bare `0` for zero, and a +// `0x` prefix otherwise. +string ImmText(uint32_t imm) +{ + return imm ? fmt::format("{:#x}", imm) : "0"; +} + +string GprName(uint32_t field) +{ + return field == 31 ? "xzr" : "x" + to_string(field); +} + +string VecName(uint32_t field) +{ + return "v" + to_string(field); +} + +uint32_t GprReg(uint32_t field) +{ + return field == 31 ? REG_XZR : (REG_X0 + field); +} + +uint32_t VecReg(uint32_t field) +{ + return REG_V0 + field; +} + +void EmitMnemonic(const char* mnemonic, vector& result) +{ + result.emplace_back(InstructionToken, mnemonic); + size_t len = strlen(mnemonic); + string pad = len < 8 ? string(8 - len, ' ') : string(1, ' '); + result.emplace_back(TextToken, pad); +} + +void EmitRegister(const string& name, vector& result) +{ + result.emplace_back(RegisterToken, name); +} + +} // namespace + +bool AppleVendorGetInstructionInfo(uint32_t insn, uint64_t addr, InstructionInfo& result) +{ + (void)addr; + AppleVendorInsn decoded; + if (!Decode(insn, decoded)) + return false; + + result.length = 4; + switch (decoded.op) + { + case AV_GENTER: + case AV_APPLE_UNKNOWN_SPTM: + // These enter the monitor like an exception entry. Control returns after the handler for the + // common (non-terminal) selectors. + result.AddBranch(SystemCall); + break; + case AV_GEXIT: + // gexit returns from guarded mode and does not fall through, like eret. + result.AddBranch(FunctionReturn); + break; + default: + break; + } + return true; +} + +bool AppleVendorGetInstructionText(uint32_t insn, vector& result) +{ + AppleVendorInsn decoded; + if (!Decode(insn, decoded)) + return false; + + switch (decoded.op) + { + case AV_GENTER: + EmitMnemonic("genter", result); + result.emplace_back(TextToken, "#"); + result.emplace_back(IntegerToken, ImmText(decoded.imm), decoded.imm); + break; + case AV_GEXIT: + EmitMnemonic("gexit", result); + break; + case AV_SDSB: + EmitMnemonic("sdsb", result); + result.emplace_back(TextToken, SDSB_DOMAIN[decoded.imm]); + break; + case AV_WKDMC: + case AV_WKDMD: + EmitMnemonic(decoded.op == AV_WKDMC ? "wkdmc" : "wkdmd", result); + EmitRegister(GprName(decoded.rd), result); + result.emplace_back(OperandSeparatorToken, ", "); + EmitRegister(GprName(decoded.rs), result); + break; + case AV_AT_AS1ELX: + EmitMnemonic("at_as1elx", result); + EmitRegister(GprName(decoded.rd), result); + break; + case AV_MUL53LO: + case AV_MUL53HI: + EmitMnemonic(decoded.op == AV_MUL53LO ? "mul53lo" : "mul53hi", result); + EmitRegister(VecName(decoded.rd), result); + result.emplace_back(TextToken, ".2d"); + result.emplace_back(OperandSeparatorToken, ", "); + EmitRegister(VecName(decoded.rs), result); + result.emplace_back(TextToken, ".2d"); + break; + case AV_AMX: + { + // Op 17 is the enable/disable pair (amx_set/amx_clr) and carries no register operand. + // Every other op takes a single GPR that holds a packed pointer/configuration word. + string mnemonic = decoded.imm == 17 ? (decoded.rd == 1 ? "amx_clr" : "amx_set") + : string("amx_") + AMX_OPS[decoded.imm]; + EmitMnemonic(mnemonic.c_str(), result); + if (decoded.imm != 17) + EmitRegister(GprName(decoded.rd), result); + break; + } + case AV_APPLE_UNKNOWN_SPTM: + EmitMnemonic("apple_unknown_sptm", result); + result.emplace_back(TextToken, "#"); + result.emplace_back(IntegerToken, ImmText(decoded.imm), decoded.imm); + break; + default: + return false; + } + return true; +} + +optional AppleVendorGetInstructionLowLevelIL(uint32_t insn, LowLevelILFunction& il) +{ + AppleVendorInsn decoded; + if (!Decode(insn, decoded)) + return nullopt; + + switch (decoded.op) + { + case AV_GENTER: + il.AddInstruction(il.Intrinsic({}, APPLE_INTRIN_GENTER, {il.Const(4, decoded.imm)})); + break; + case AV_GEXIT: + // gexit returns from guarded mode like eret and does not fall through. + il.AddInstruction(il.Intrinsic({}, APPLE_INTRIN_GEXIT, {})); + il.AddInstruction(il.Trap(0)); + return false; + case AV_SDSB: + il.AddInstruction(il.Intrinsic({}, APPLE_INTRIN_SDSB, {il.Const(4, decoded.imm)})); + break; + case AV_WKDMC: + case AV_WKDMD: + // Xd and Xs are addresses. The result is written to the memory at Xd, and the only register + // written is Xs, which receives a status word. + il.AddInstruction(il.Intrinsic({RegisterOrFlag::Register(GprReg(decoded.rs))}, + decoded.op == AV_WKDMC ? APPLE_INTRIN_WKDMC : APPLE_INTRIN_WKDMD, + {il.Register(8, GprReg(decoded.rd)), il.Register(8, GprReg(decoded.rs))})); + break; + case AV_AT_AS1ELX: + il.AddInstruction(il.Intrinsic({RegisterOrFlag::Register(GprReg(decoded.rd))}, APPLE_INTRIN_AT_AS1ELX, + {il.Register(8, GprReg(decoded.rd))})); + break; + case AV_MUL53LO: + case AV_MUL53HI: + il.AddInstruction(il.Intrinsic({RegisterOrFlag::Register(VecReg(decoded.rd))}, + decoded.op == AV_MUL53LO ? APPLE_INTRIN_MUL53LO : APPLE_INTRIN_MUL53HI, + {il.Register(16, VecReg(decoded.rd)), il.Register(16, VecReg(decoded.rs))})); + break; + case AV_AMX: + case AV_APPLE_UNKNOWN_SPTM: + // Not yet lifted. + il.AddInstruction(il.Unimplemented()); + break; + default: + break; + } + return true; +} + +bool AppleVendorIsIntrinsic(uint32_t intrinsic) +{ + return intrinsic >= APPLE_INTRIN_GENTER && intrinsic < APPLE_INTRIN_END; +} + +void AppleVendorGetAllIntrinsics(vector& result) +{ + for (uint32_t id = APPLE_INTRIN_GENTER; id < APPLE_INTRIN_END; id++) + result.push_back(id); +} + +string AppleVendorGetIntrinsicName(uint32_t intrinsic) +{ + switch (intrinsic) + { + case APPLE_INTRIN_GENTER: + return "__genter"; + case APPLE_INTRIN_GEXIT: + return "__gexit"; + case APPLE_INTRIN_SDSB: + return "__sdsb"; + case APPLE_INTRIN_WKDMC: + return "__wkdmc"; + case APPLE_INTRIN_WKDMD: + return "__wkdmd"; + case APPLE_INTRIN_AT_AS1ELX: + return "__at_as1elx"; + case APPLE_INTRIN_MUL53LO: + return "__mul53lo"; + case APPLE_INTRIN_MUL53HI: + return "__mul53hi"; + default: + return ""; + } +} + +BNIntrinsicClass AppleVendorGetIntrinsicClass(uint32_t intrinsic) +{ + switch (intrinsic) + { + case APPLE_INTRIN_WKDMC: + case APPLE_INTRIN_WKDMD: + return MemoryIntrinsicClass; + default: + return GeneralIntrinsicClass; + } +} + +vector AppleVendorGetIntrinsicInputs(uint32_t intrinsic) +{ + switch (intrinsic) + { + case APPLE_INTRIN_GENTER: + return {NameAndType("imm", Type::IntegerType(4, false))}; + case APPLE_INTRIN_SDSB: + return {NameAndType("domain", Type::IntegerType(4, false))}; + case APPLE_INTRIN_WKDMC: + case APPLE_INTRIN_WKDMD: + return {NameAndType("dest", Type::PointerType(8, Type::VoidType())), + NameAndType("src", Type::PointerType(8, Type::VoidType()))}; + case APPLE_INTRIN_AT_AS1ELX: + return {NameAndType(Type::IntegerType(8, false))}; + case APPLE_INTRIN_MUL53LO: + case APPLE_INTRIN_MUL53HI: + return {NameAndType(Type::IntegerType(16, false)), NameAndType(Type::IntegerType(16, false))}; + default: + return {}; + } +} + +vector>> AppleVendorGetIntrinsicOutputs(uint32_t intrinsic) +{ + switch (intrinsic) + { + case APPLE_INTRIN_WKDMC: + case APPLE_INTRIN_WKDMD: + case APPLE_INTRIN_AT_AS1ELX: + return {Type::IntegerType(8, false)}; + case APPLE_INTRIN_MUL53LO: + case APPLE_INTRIN_MUL53HI: + return {Type::IntegerType(16, false)}; + default: + return {}; + } +} diff --git a/arch/arm64/apple_vendor.h b/arch/arm64/apple_vendor.h new file mode 100644 index 0000000000..b6498360fe --- /dev/null +++ b/arch/arm64/apple_vendor.h @@ -0,0 +1,80 @@ +// Copyright (c) 2026 Vector 35 Inc +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. + +#pragma once + +#include +#include +#include +#include + +#include "binaryninjaapi.h" +#include "neon_intrinsics.h" // for ARM64_INTRIN_NEON_END + +// Apple vendor-specific AArch64 instructions occupy the 0x0020xxxx encoding space (bits [31:16] == +// 0x0020), which is unallocated in the base ARM A64 instruction set. They appear in Apple firmware +// such as the iOS kernelcache and SPTM. This module decodes, renders, and lifts them independently +// of the generated ARM disassembler, so no generated file or ARM spec data is touched. +// +// Encodings follow the AsahiLinux reverse-engineering documentation +// (https://asahilinux.org/docs/hw/cpu/apple-instructions/). + +// Vendor intrinsic IDs begin immediately after the base ARM64 intrinsic range (NEON included) so +// they never collide with the base architecture's intrinsics. They are session-local (IL is +// regenerated per session and intrinsic IDs are not persisted). +enum AppleVendorIntrinsic : uint32_t +{ + APPLE_INTRIN_GENTER = ARM64_INTRIN_NEON_END + 1, + APPLE_INTRIN_GEXIT, + APPLE_INTRIN_SDSB, + APPLE_INTRIN_WKDMC, + APPLE_INTRIN_WKDMD, + APPLE_INTRIN_AT_AS1ELX, + APPLE_INTRIN_MUL53LO, + APPLE_INTRIN_MUL53HI, + APPLE_INTRIN_END, +}; + +// True if the instruction word might be an Apple vendor instruction. Most live in the 0x0020xxxx +// space (bits [31:16] == 0x0020), but at least one occupies the exception-generation encoding class +// in the opc == 0b111 slot (0xd4e0_0000) that ARM leaves unallocated. +// This is only a cheap pre-filter. The decoder makes the precise determination. +inline bool IsAppleVendorEncoding(uint32_t insn) +{ + return (insn & 0xffff0000) == 0x00200000 || (insn & 0xffe00000) == 0xd4e00000; +} + +// The info and text paths return false if the word is not a recognized Apple vendor instruction. On +// success the info path sets result.length to 4. +bool AppleVendorGetInstructionInfo(uint32_t insn, uint64_t addr, BinaryNinja::InstructionInfo& result); +bool AppleVendorGetInstructionText(uint32_t insn, std::vector& result); +// Returns nullopt if the word is not a recognized Apple vendor instruction. Otherwise the contained +// value is what Architecture::GetInstructionLowLevelIL should return: true if the block continues past +// this instruction, false if it ends the block. +std::optional AppleVendorGetInstructionLowLevelIL(uint32_t insn, BinaryNinja::LowLevelILFunction& il); + +// Metadata for vendor intrinsics. +bool AppleVendorIsIntrinsic(uint32_t intrinsic); +void AppleVendorGetAllIntrinsics(std::vector& result); +std::string AppleVendorGetIntrinsicName(uint32_t intrinsic); +BNIntrinsicClass AppleVendorGetIntrinsicClass(uint32_t intrinsic); +std::vector AppleVendorGetIntrinsicInputs(uint32_t intrinsic); +std::vector>> AppleVendorGetIntrinsicOutputs( + uint32_t intrinsic); diff --git a/arch/arm64/arch_arm64.cpp b/arch/arm64/arch_arm64.cpp index b09e2ab32d..0f11a40de2 100644 --- a/arch/arm64/arch_arm64.cpp +++ b/arch/arm64/arch_arm64.cpp @@ -8,6 +8,7 @@ #include #include +#include "apple_vendor.h" #include "arm64dis.h" #include "binaryninjaapi.h" #include "il.h" @@ -782,6 +783,13 @@ class Arm64Architecture : public Architecture if (maxLen < 4) return false; + if (m_onlyDisassembleOnAlignedAddresses && (addr % 4 != 0)) + return false; + + uint32_t insn = *(const uint32_t*)data; + if (IsAppleVendorEncoding(insn) && AppleVendorGetInstructionInfo(insn, addr, result)) + return true; + Instruction instr; if (!Disassemble(data, addr, maxLen, instr)) return false; @@ -795,6 +803,14 @@ class Arm64Architecture : public Architecture vector& result) override { len = 4; + + if (m_onlyDisassembleOnAlignedAddresses && (addr % 4 != 0)) + return false; + + uint32_t insn = *(const uint32_t*)data; + if (IsAppleVendorEncoding(insn) && AppleVendorGetInstructionText(insn, result)) + return true; + Instruction instr; bool tokenizeSuccess = false; char buf[9]; @@ -932,6 +948,9 @@ class Arm64Architecture : public Architecture virtual string GetIntrinsicName(uint32_t intrinsic) override { + if (AppleVendorIsIntrinsic(intrinsic)) + return AppleVendorGetIntrinsicName(intrinsic); + switch (intrinsic) { case ARM64_INTRIN_AUTDA: @@ -1102,12 +1121,17 @@ class Arm64Architecture : public Architecture result.push_back(id); } + AppleVendorGetAllIntrinsics(result); + return result; } virtual vector GetIntrinsicInputs(uint32_t intrinsic) override { + if (AppleVendorIsIntrinsic(intrinsic)) + return AppleVendorGetIntrinsicInputs(intrinsic); + switch (intrinsic) { case ARM64_INTRIN_MRS: @@ -1165,6 +1189,9 @@ class Arm64Architecture : public Architecture virtual vector>> GetIntrinsicOutputs(uint32_t intrinsic) override { + if (AppleVendorIsIntrinsic(intrinsic)) + return AppleVendorGetIntrinsicOutputs(intrinsic); + switch (intrinsic) { case ARM64_INTRIN_MSR: @@ -1195,6 +1222,15 @@ class Arm64Architecture : public Architecture } + virtual BNIntrinsicClass GetIntrinsicClass(uint32_t intrinsic) override + { + if (AppleVendorIsIntrinsic(intrinsic)) + return AppleVendorGetIntrinsicClass(intrinsic); + + return GeneralIntrinsicClass; + } + + virtual bool IsNeverBranchPatchAvailable(const uint8_t* data, uint64_t addr, size_t len) override { Instruction instr; @@ -1315,6 +1351,22 @@ class Arm64Architecture : public Architecture virtual bool GetInstructionLowLevelIL( const uint8_t* data, uint64_t addr, size_t& len, LowLevelILFunction& il) override { + if (m_onlyDisassembleOnAlignedAddresses && (addr % 4 != 0)) + { + il.AddInstruction(il.Undefined()); + return false; + } + + uint32_t insn = *(const uint32_t*)data; + if (IsAppleVendorEncoding(insn)) + { + if (optional handled = AppleVendorGetInstructionLowLevelIL(insn, il)) + { + len = 4; + return *handled; + } + } + Instruction instr; if (!Disassemble(data, addr, len, instr)) { diff --git a/arch/arm64/arm64test.py b/arch/arm64/arm64test.py index b05ff90dfc..47ae6c8c60 100755 --- a/arch/arm64/arm64test.py +++ b/arch/arm64/arm64test.py @@ -12773,10 +12773,118 @@ (b'\x20\xb8\xe0\x4e', 'LLIL_UNIMPL()'), ] +# Apple's vendor-specific instructions, which occupy encoding space that ARM leaves unallocated. +# See arch/arm64/apple_vendor.cpp. +tests_apple_vendor = [ + # genter #0 + (b'\x20\x14\x20\x00', 'LLIL_INTRINSIC([],__genter,[LLIL_CONST.d(0x0)])'), + # genter #5 + (b'\x25\x14\x20\x00', 'LLIL_INTRINSIC([],__genter,[LLIL_CONST.d(0x5)])'), + # genter #31 + (b'\x3f\x14\x20\x00', 'LLIL_INTRINSIC([],__genter,[LLIL_CONST.d(0x1F)])'), + # gexit (leaves guarded mode like eret, so the block ends here) + (b'\x00\x14\x20\x00', 'LLIL_INTRINSIC([],__gexit,[]); LLIL_TRAP(0)'), + # sdsb osh + (b'\x60\x14\x20\x00', 'LLIL_INTRINSIC([],__sdsb,[LLIL_CONST.d(0x0)])'), + # sdsb nsh + (b'\x61\x14\x20\x00', 'LLIL_INTRINSIC([],__sdsb,[LLIL_CONST.d(0x1)])'), + # sdsb ish + (b'\x62\x14\x20\x00', 'LLIL_INTRINSIC([],__sdsb,[LLIL_CONST.d(0x2)])'), + # sdsb sy + (b'\x63\x14\x20\x00', 'LLIL_INTRINSIC([],__sdsb,[LLIL_CONST.d(0x3)])'), + # at_as1elx x0 + (b'\x40\x14\x20\x00', 'LLIL_INTRINSIC([x0],__at_as1elx,[LLIL_REG.q(x0)])'), + # at_as1elx x3 + (b'\x43\x14\x20\x00', 'LLIL_INTRINSIC([x3],__at_as1elx,[LLIL_REG.q(x3)])'), + # at_as1elx xzr + (b'\x5f\x14\x20\x00', 'LLIL_INTRINSIC([xzr],__at_as1elx,[LLIL_REG.q(xzr)])'), + # wkdmc x0, x1 (reads both addresses, writes a status back into the source) + (b'\x01\x08\x20\x00', 'LLIL_INTRINSIC([x1],__wkdmc,[LLIL_REG.q(x0),LLIL_REG.q(x1)])'), + # wkdmc x14, xzr + (b'\xdf\x09\x20\x00', 'LLIL_INTRINSIC([xzr],__wkdmc,[LLIL_REG.q(x14),LLIL_REG.q(xzr)])'), + # wkdmd x2, x3 + (b'\x43\x0c\x20\x00', 'LLIL_INTRINSIC([x3],__wkdmd,[LLIL_REG.q(x2),LLIL_REG.q(x3)])'), + # mul53lo v4.2d, v5.2d + (b'\xa4\x00\x20\x00', 'LLIL_INTRINSIC([v4],__mul53lo,[LLIL_REG.o(v4),LLIL_REG.o(v5)])'), + # mul53hi v6.2d, v7.2d + (b'\xe6\x04\x20\x00', 'LLIL_INTRINSIC([v6],__mul53hi,[LLIL_REG.o(v6),LLIL_REG.o(v7)])'), + + # AMX and the SPTM monitor entry are disassembled but not yet lifted + # amx_ldx x3 + (b'\x03\x10\x20\x00', 'LLIL_UNIMPL()'), + # amx_set + (b'\x20\x12\x20\x00', 'LLIL_UNIMPL()'), + # amx_genlut x3 + (b'\xc3\x12\x20\x00', 'LLIL_UNIMPL()'), + # apple_unknown_sptm #0 + (b'\x00\x00\xe0\xd4', 'LLIL_UNIMPL()'), + # apple_unknown_sptm #0x1234 + (b'\x80\x46\xe2\xd4', 'LLIL_UNIMPL()'), + + # Encodings adjacent to the vendor instructions that must stay undefined + # AMX op 23, one of the reserved holes above amx_genlut + (b'\xe0\x12\x20\x00', 'LLIL_UNDEF()'), + # 0x00201401, unallocated between gexit and genter + (b'\x01\x14\x20\x00', 'LLIL_UNDEF()'), + # the SPTM encoding requires a zero LL field, so 0xd4e00001 is not one + (b'\x01\x00\xe0\xd4', 'LLIL_UNDEF()'), +] + disasm_test_cases = [ + # genter's immediate renders like the base disassembler's, bare for zero and 0x-prefixed otherwise + (b'\x20\x14\x20\x00', 'genter #0'), + (b'\x25\x14\x20\x00', 'genter #0x5'), + (b'\x3f\x14\x20\x00', 'genter #0x1f'), + (b'\x00\x14\x20\x00', 'gexit '), + (b'\x60\x14\x20\x00', 'sdsb osh'), + (b'\x61\x14\x20\x00', 'sdsb nsh'), + (b'\x62\x14\x20\x00', 'sdsb ish'), + (b'\x63\x14\x20\x00', 'sdsb sy'), + (b'\x40\x14\x20\x00', 'at_as1elx x0'), + (b'\x5f\x14\x20\x00', 'at_as1elx xzr'), + (b'\x01\x08\x20\x00', 'wkdmc x0, x1'), + (b'\xdf\x09\x20\x00', 'wkdmc x14, xzr'), + (b'\x43\x0c\x20\x00', 'wkdmd x2, x3'), + (b'\xa4\x00\x20\x00', 'mul53lo v4.2d, v5.2d'), + (b'\xe6\x04\x20\x00', 'mul53hi v6.2d, v7.2d'), + (b'\x1f\x00\x20\x00', 'mul53lo v31.2d, v0.2d'), + (b'\xe0\x07\x20\x00', 'mul53hi v0.2d, v31.2d'), + (b'\x80\x46\xe2\xd4', 'apple_unknown_sptm #0x1234'), + + # every AMX operation, indexed by the op field at bits [9:5], each with a different GPR so that + # the operand field is exercised across its range + (b'\x00\x10\x20\x00', 'amx_ldx x0'), + (b'\x21\x10\x20\x00', 'amx_ldy x1'), + (b'\x42\x10\x20\x00', 'amx_stx x2'), + (b'\x63\x10\x20\x00', 'amx_sty x3'), + (b'\x84\x10\x20\x00', 'amx_ldz x4'), + (b'\xa5\x10\x20\x00', 'amx_stz x5'), + (b'\xc6\x10\x20\x00', 'amx_ldzi x6'), + (b'\xe7\x10\x20\x00', 'amx_stzi x7'), + (b'\x08\x11\x20\x00', 'amx_extrx x8'), + (b'\x29\x11\x20\x00', 'amx_extry x9'), + (b'\x4a\x11\x20\x00', 'amx_fma64 x10'), + (b'\x6b\x11\x20\x00', 'amx_fms64 x11'), + (b'\x8c\x11\x20\x00', 'amx_fma32 x12'), + (b'\xad\x11\x20\x00', 'amx_fms32 x13'), + (b'\xce\x11\x20\x00', 'amx_mac16 x14'), + (b'\xef\x11\x20\x00', 'amx_fma16 x15'), + (b'\x10\x12\x20\x00', 'amx_fms16 x16'), + (b'\x52\x12\x20\x00', 'amx_vecint x18'), + (b'\x73\x12\x20\x00', 'amx_vecfp x19'), + (b'\x94\x12\x20\x00', 'amx_matint x20'), + (b'\xb5\x12\x20\x00', 'amx_matfp x21'), + (b'\xd6\x12\x20\x00', 'amx_genlut x22'), + # operand field 31 is xzr, not x31 + (b'\x1f\x10\x20\x00', 'amx_ldx xzr'), + + # op 17 is the enable/disable pair, whose operand selects between them + (b'\x20\x12\x20\x00', 'amx_set '), + (b'\x21\x12\x20\x00', 'amx_clr '), ] test_cases = \ + tests_apple_vendor + \ tests_cssc + \ tests_shll + \ tests_udf + \ diff --git a/arch/arm64/neon_intrinsics.h b/arch/arm64/neon_intrinsics.h index 9cade45ff3..b63d618604 100644 --- a/arch/arm64/neon_intrinsics.h +++ b/arch/arm64/neon_intrinsics.h @@ -1,3 +1,5 @@ +#pragma once + #include #include