Files
Snapintosh/tools/test-pauth-native.py
T
Acelogic 1510d8c6ce Snapintosh public release
An EL2 monitor/hypervisor and toolchain that boots macOS 27 on the ASUS A16
(Snapdragon X2 Elite), with an Adreno Metal plug-in and IOGPU kext, and
air2ir3, an AIR to Mesa ir3 shader compiler.

MIT, with GPL-2.0 components listed in LICENSE and THIRD_PARTY_NOTICES.md.
No Apple, Qualcomm, ASUS or Microsoft binaries are included.
2026-10-21 21:26:36 -04:00

192 lines
7.6 KiB
Python
Executable File

#!/usr/bin/env python3
# SPDX-License-Identifier: MIT
"""Exercise native PA helper control flow with modeled privileged operations.
Runs the built AArch64 instructions. System registers, TLB maintenance and
PAC/AUT are modeled; this verifies guards, key selection and restoration, not
the hardware PAC algorithm or translation behavior.
"""
import hashlib
import itertools
import json
from pathlib import Path
import struct
import sys
ROOT = Path(__file__).resolve().parents[1]
sys.path[:0] = [str(ROOT / 'build/test-python-deps'), str(ROOT / 'probes')]
import run
from capstone import Cs, CS_ARCH_ARM64, CS_MODE_ARM
from unicorn import Uc, UC_ARCH_ARM64, UC_MODE_ARM, UC_HOOK_CODE
import unicorn.arm64_const as reg
BASE = 0x1000000
BUFFER = BASE + 0x10000
STACK = BASE + 0x18000
RETURN = BASE + 0x1f000
KEYS = ('apia', 'apib', 'apda', 'apdb')
FIELDS = ('currentel', 'id_aa64isar1_el1', 'id_aa64isar2_el1',
'sctlr_el2', 'tcr_el2', 'hcr_el2', 'sctlr_el1', 'tcr_el1',
'id_aa64mmfr0_el1', 'id_aa64isar0_el1')
CFG = dict(zip(FIELDS, (8, 0x110111100311002, 0x11005112,
0x10003000100d, 0x80843514, 0x88000010,
0x100030800180, 0, 0x2100122100001024, 0x1221111110212120)))
CFG['daif'] = 0x80
for n, key in enumerate(KEYS):
CFG[key + 'keylo_el1'] = 0x11110000 + n
CFG[key + 'keyhi_el1'] = 0x22220000 + n
def test_case(blob, request, override=None, rejected=False, inspect=False,
corrupt_read=None, raw_sign=False):
uc = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
uc.mem_map(BASE, 0x20000)
uc.mem_write(BASE, blob)
uc.mem_write(BUFFER, b'\xa5' * 0x200)
packed = struct.pack('<7Q', *request)
uc.mem_write(BUFFER + 0x100, packed)
initial = dict(CFG, **(override or {}))
registers = initial.copy()
code = {i.address: i for i in Cs(CS_ARCH_ARM64, CS_MODE_ARM).disasm(blob, BASE)}
writes, events, signed = [], [], []
counts = {}
def get(name):
return uc.reg_read(getattr(reg, 'UC_ARM64_REG_' + name.upper()))
def put(name, value):
uc.reg_write(getattr(reg, 'UC_ARM64_REG_' + name.upper()), value)
def hook(u, address, size, _):
insn = code[address]
op, operands = insn.mnemonic, insn.op_str.split(', ')
if op == 'mrs':
dst, src = operands
value = registers[src]
# Corrupt only a restoration readback, after the restore write.
if src == corrupt_read and counts.get(src, 0) >= 2:
value ^= 1
put(dst, value)
elif op == 'msr':
dst, src = operands
if dst == 'daifset':
registers['daif'] |= int(src[1:], 0) << 6
else:
value = get(src)
if dst != 'daif':
assert registers['daif'] & 0x3c0 == 0x3c0
registers[dst] = value
counts[dst] = counts.get(dst, 0) + 1
writes.append((dst, value))
events.append(op + ':' + dst)
elif op == 'tlbi':
assert insn.op_str == 'alle2'
assert registers['daif'] & 0x3c0 == 0x3c0
events.append('tlbi')
elif op.startswith(('pac', 'aut')):
selector = {'ia': 0, 'ib': 1, 'da': 2, 'db': 3}[op[3:]]
assert selector == request[4]
dst, src = operands
key = KEYS[selector]
assert registers[key + 'keylo_el1'] == request[2]
assert registers[key + 'keyhi_el1'] == request[3]
mask = 63 | (1 << 20) | (1 << 29)
expected_tcr = ((initial['tcr_el2'] & ~mask) | 17 |
(request[5] << 20) | (request[6] << 29))
assert registers['tcr_el2'] == expected_tcr
assert registers['sctlr_el2'] == initial['sctlr_el2'] | 0xc8002000
assert registers['daif'] & 0x3c0 == 0x3c0
assert get(src) == request[1]
if op.startswith('pac'):
assert get(dst) == request[0]
value = request[0] ^ (0x35 << 48)
signed.append(value)
put(dst, value)
else:
assert not raw_sign, 'sign-only entry executed authentication'
assert get(dst) == signed[-1]
put(dst, request[0])
events.append(op)
else:
return
put('pc', address + size)
uc.hook_add(UC_HOOK_CODE, hook)
saved = {n: 0x31415000 + n for n in range(19, 30)}
for n, value in saved.items():
put('x' + str(n), value)
put('sp', STACK)
put('x30', RETURN)
put('x0', BUFFER)
put('x1', 0 if inspect else 1)
uc.emu_start(BASE, RETURN, count=500)
assert get('pc') == RETURN
assert get('sp') == STACK and get('x30') == RETURN
assert all(get('x' + str(n)) == v for n, v in saved.items())
assert registers == initial
after = bytes(uc.mem_read(BUFFER, 0x200))
expected = bytearray(b'\xa5' * 0x200)
expected[:80] = struct.pack('<10Q', *(initial[k] for k in FIELDS))
expected[0x100:0x138] = packed
if rejected or inspect:
assert get('x0') == int(rejected)
assert not writes and not signed
else:
restored = 15
if corrupt_read:
restored &= ~({'tcr_el2': 2, 'sctlr_el2': 4}.get(corrupt_read, 1))
assert get('x0') == int(bool(corrupt_read))
expected[0x180:0x198] = struct.pack('<3Q', signed[0], 0 if raw_sign else request[0], restored)
assert len(signed) == 1
if raw_sign:
assert not any(event.startswith('aut') for event in events)
assert events.count('tlbi') == 2
assert events[0] == 'msr:daifset' and events[-1] == 'msr:daif'
selected = KEYS[request[4]]
assert {n for n, _ in writes if 'key' in n} == {
selected + 'keylo_el1', selected + 'keyhi_el1'}
assert after == expected, 'unexpected output or buffer clobber'
def main():
blob = run.build('pauth-address', ROOT / 'build/pauth-address-probe')
cases = 0
default = [0x1cf5382f0, 0x9abf0001cf5382a8,
0x0123456789abcdef, 0xfedcba9876543210, 0, 0, 0]
for key, tbi, tbid, pointer, t0sz in itertools.product(
range(4), range(2), range(2),
(0, 0x1cf5382f0, (1 << 47) - 1, 0xab000001cf5382f0),
(17, 20, 24)):
req = default.copy()
req[0], req[4], req[5], req[6] = pointer, key, tbi, tbid
tagged = bool(pointer >> 56)
rejected = tagged and not (tbi and (key >= 2 or not tbid))
tcr = (CFG['tcr_el2'] & ~63) | t0sz
test_case(blob, req, {'tcr_el2': tcr}, rejected=rejected)
cases += 1
for override in (
{'currentel': 4}, {'hcr_el2': 1 << 34}, {'sctlr_el2': 0},
{'tcr_el2': 0x14 | (1 << 14)}, {'tcr_el2': 16}, {'tcr_el2': 25},
{'id_aa64isar1_el1': 0, 'id_aa64isar2_el1': 0}):
test_case(blob, default, override, rejected=True)
cases += 1
for index, value in ((0, 1 << 47), (0, 1 << 55), (4, 4), (4, 1 << 63),
(5, 2), (6, 2), (5, 1 << 63), (6, 1 << 63)):
req = default.copy()
req[index] = value
test_case(blob, req, rejected=True)
cases += 1
for field in ('tcr_el2', 'sctlr_el2', 'apiakeylo_el1', 'apiakeyhi_el1'):
test_case(blob, default, corrupt_read=field)
cases += 1
test_case(blob, default, inspect=True)
cases += 1
print(json.dumps(dict(cases=cases, probe_bytes=len(blob),
probe_sha256=hashlib.sha256(blob).hexdigest(),
boundary='control flow with modeled system registers and PAC/AUT'),
indent=2))
if __name__ == '__main__':
main()