Files
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

148 lines
6.5 KiB
Markdown

# 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](BOOT-CHAIN.md) and [COMPATIBILITY.md](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](https://kunagisa.moe/2026/09/14/finding-treasuries-mac-os-on-wahway-qingyun-w515y/),
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](BOOT-CHAIN.md).
## Reproducing
```sh
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.