Files
Snapintosh/docs/FEASIBILITY.md
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

6.5 KiB

Can the Snapdragon X2E-96-100 host XNU?

Historical feasibility study. This records the initial hardware probes and the options considered before the stock-kernel route was chosen. The project went on to boot the stock kernelcache with translated page tables; see BOOT-CHAIN.md and COMPATIBILITY.md for the implemented approach.

Result: the hardware cannot walk a 16 KiB translation table. That is measured with a control, not read from a register. Everything else on the checklist passes, including pointer authentication and nested virtualization.

Why the granule decides the project

XNU on arm64 uses 16 KiB pages (PAGE_SHIFT 14); the pmap, the VM system and the dyld shared cache are built around it. Virtualization does not route around a missing granule: a guest's stage-1 translation is walked by the same MMU as the host's, and stage 2 only translates its output. A table walk raises no exception to trap, so an unsupported granule silently does the wrong thing.

The claim being tested

ID_AA64MMFR0_EL1 reads 0x2100122100001024:

Field Bits Value Meaning
TGran4 31:28 0x0 4K supported (0 means supported)
TGran64 27:24 0x0 64K supported
TGran16 23:20 0x0 16K not supported (0 means not supported here)
TGran16_2 35:32 0x1 16K not supported at stage 2

Note the inverted convention between TGran16 and TGran4/TGran64.

A register alone is not enough: the project that inspired this one, macOS on a Huawei Qingyun laptop, found a vendor had hidden PAuth in ID_AA64ISAR1_EL1 while the silicon had it. So behaviour had to be checked.

Two probes that do not settle it

  • Writing the granule to TCR_EL1 and reading it back. All encodings read back unchanged. TCR fields are storage; the granule is validated only when a walk happens.
  • Mapping entry 0 and translating VA 0x1000. At T0SZ=28 that address is index 0 under both granules, so a silent 4K fallback (which the architecture permits for an unsupported granule) passes identically. A test that cannot fail in the interesting direction is not a test.

The method

AT S1E1R runs the hardware table walker over the EL1&0 regime and reports the result in PAR_EL1 without entering EL1. q1n1 runs the probe at EL2. The probe sets, and each omission silently invalidates the result:

  • HCR_EL2.TGE = 0, or AT S1E1R is routed to EL2's regime;
  • HCR_EL2.VM = 0, or stage 2 is folded in;
  • HCR_EL2.RW = 1, or EL1 is AArch32 and walks 32-bit descriptors (RES1 on the A16, so the omission was caught only by the QEMU control);
  • SCTLR_EL1.M = 1, or the walk is bypassed and every trial passes.

One 16 KiB-aligned table serves both granules. At T0SZ=28 the 4K regime starts at level 1 (index VA[35:30]) and the 16K regime at level 2 (index VA[35:25]). The discriminator: map only entry 1, to PA 0x4000000, and translate VA 0x2001000. Under 16K that is index 1 and resolves to PA 0x4001000; under 4K it is index 0 and faults.

Trial Granule Table VA Expected
A 4K entry 0 mapped 0x1000 resolves (harness works)
B 4K empty 0x1000 faults (SCTLR_EL1.M took effect)
C 4K entry 1 mapped 0x2001000 faults (discriminator works)
D 16K entry 1 mapped 0x2001000 the question

Results

Identical blob and harness on two machines:

Trial QEMU -cpu max (has 16K) A16, X2E-96-100
A PA 0x1000 PA 0x1000
B fault, FST 0x05 fault, FST 0x05
C fault, FST 0x05 fault, FST 0x05
D PA 0x4001000 fault, FST 0x05

The fault code corroborates it. FST 0x05 is a level-1 translation fault. A genuine 16K walk at T0SZ=28 starts at level 2 and would have resolved. A level-1 fault identical to the 4K trials means the walker substituted the 4K structure for TG0 = 0b10.

What else the silicon says

From probes/run.py idregs; MIDR_EL1 = 0x512f0021 (implementer 0x51, Qualcomm).

Feature Value Note
PAuth APA3 = 5, GPA3 = 1 present and honestly advertised (QARMA3, the ISAR2 variant)
FEAT_NV2 MMFR2.NV = 2 nested virtualization
VHE MMFR1.VH = 1
VMID 16-bit
DIT / UAO / SSBS / SHA3 present

The options considered

1. Build XNU with 4 KiB hardware pages. XNU separates the VM page from the hardware page: with __ARM_16K_PG__ undefined and PAGE_SHIFT still 14, PAGE_RATIO == 4 backs each 16 KiB VM page with four 4 KiB PTEs, and user space still sees 16 KiB pages. But XNU_MONITOR builds assert against it, so it requires a non-PPL, non-SPTM kernel built from source: not a route to booting stock macOS.

2. Shadow paging at EL2. Never give the guest's tables to the MMU; build 4 KiB shadow tables that reproduce each 16 KiB mapping. In the classic form this is expensive, because every guest page-table store has to be caught and a shadow miss surfaces as a guest EL1 fault that no bit routes to EL2. Its one advantage is that it boots an unmodified kernel.

3. A different target. The Huawei project ran Apple's vmapple guest (the Virtualization.framework kernel) under KVM, which needs no Secure Enclave, AIC, DART or PMGR model. Bare-metal XNU needs all of those, plus SPTM and TXM on current macOS.

What was chosen, and why it was cheaper than expected

The project boots the stock bare-metal kernelcache with shadow paging. On an SPTM kernel the classic cost largely disappears: XNU does not write its own page tables but asks SPTM to, with arguments. The monitor stands in for SPTM, so it is told about every mapping change as a call and never has to trap stores into tables. XNU still reads its 16 KiB tables, so the monitor keeps both XNU's 16 KiB view and the hardware's 4 KiB shadow. FEAT_NV2 lets XNU, which expects to run at EL2, run at EL1 under the monitor. Details are in BOOT-CHAIN.md.

Reproducing

python3 probes/run.py idregs              # feature registers
python3 probes/run.py tgran16             # the granule walk, on hardware
python3 probes/run.py tgran16 --qemu      # the same blob against -cpu max

Both probes are flat AArch64 blobs uploaded to q1n1 and executed at EL2; they restore every register they touch. --device selects the q1n1 link. Sources: probes/tgran16.c, probes/idregs.S, probes/run.py. To qualify another candidate host, run trial D on it.