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

12 KiB

Guest and host compatibility

Snapintosh is an XNU compatibility layer on non-Apple Arm hardware. This document describes the boundaries between the guest (Apple's kernelcache and firmware bundle), the guest ABI the monitor implements, and the host resources it runs on, and what is needed before a second guest or host is supported.

The current proof target is the stock J700 mac17p kernelcache from macOS 27 build 26A428 on the Snapdragon X2E. That is one qualified input bundle, not a promise that every XNU kernelcache or Arm CPU boots.

Three boundaries

  1. Guest identity. probes/guestprofile.py describes a complete, audited kernelcache/device-tree/SPTM/TXM bundle: its hashes, monitor ABI, vectors, initial memory attributes, boot CPU, console node, entry-path addresses and the early heap's packed-pointer encoding constraints. get_profile(name) rejects unknown names. The exact image passed to build() is verified, including an explicit --kernelcache override. --record prints candidate hashes; it cannot certify an unexamined image.

  2. Guest ABI implementation. probes/xnuboot.py, probes/xnuboot.c, compat/sptm.c, compat/sptm-unmap.c, compat/sptm-update.c, compat/sptm-refs.c, compat/sptm-slide.c and compat/txm.c implement sptm-mac-v1: the measured Mac SPTM handoff, 16 KiB kernel tables, 16/4 KiB user tables, monitor-call decoding and emulated Apple devices. A non-SPTM, embedded or changed SPTM ABI needs its own implementation; hash changes alone do not establish ABI compatibility.

    • Device models. compat/device.{c,h} is a portable, bounded register-model registry; probes/devicemodel.py derives placement from the guest device tree and rejects conflicting ranges and unsupported geometry. Mutable state belongs to each model instance.
    • Memory controller and interrupts. MCCBootSpec selects cache-capacity/quota, DCS policy and AMCC/DCS/SOCNI counter contracts. Counter APIs accept submitted virtual events; they do not manufacture host DRAM telemetry. AICBootSpec selects the interrupt controller's identification/EVENT contract. Neither grants host physical MMIO access; AIC delivery is a separate backend.
    • Clocks. CounterClockBootSpec audits the watchdog's base-clock ID binding. probes/clocktree.py supplies the running CPU's fixed-frequency, which populates base ID 4 (nclk), separately from timebase-frequency.
    • DART. The profile's DARTBootSpec selects the device identity contract. probes/iommu.py supplies validated, unique loader IDs and carries the guest register inventory in built['dart_devices']. Assignment does not enable DMA. Guest IDs and streams are translated by a backend, never passed directly to a Qualcomm SMMU.
    • NVRAM. NVRAMBootSpec selects the boot-time variable format and geometry; probes/nvram.py encodes an empty CHRP v2 store into the audited device-tree reservation. Existing nonempty data is never silently replaced.
    • Page-table operations. RETYPE uses a host-independent memory/publication callback interface. User-root allocation supports formats 0/1 with flags 2, ARM64_32 flags 0x22, and format 1/flags 6 for the x86 commpage, with unique ASIDs. Empty-root recycling requires a host retirement callback with ownership checks and translation invalidation. Typed empty table construction supports inactive format-0/1 user roots (16/4 KiB pages, 47-bit VAs, restricted to 32 bits for ARM64_32), including the commpage L3. Commpage RW/RO/RX allocation preserves the initialized payload; inactive commpage leaves enforce permissions and reference counts. Detach retains populated commpage tables and removes empty ordinary tables with parent-link accounting. Four 4 KiB subtables link and detach together and count as one 16 KiB frame link; leaf references aggregate over the containing physical frame.
    • Mapping and updates. Empty table installation and region unmapping use the same memory interface; unmapping removes both guest leaves and their hardware shadow. MAP_PAGE's output pointer is translated through a bounded guest aperture (a physical PTE address is not a guest pointer). Frame-type descriptors come from the selected profile's audited SPTM image. Region and disjoint updates use bounded guest/shadow callbacks and preflight the whole batch; the host publishes the shadow and invalidates translations. A shared reference transaction accounts for allocator data aliases and L3 entries across MAP_PAGE, updates and unmap. The physical aperture is excluded; the loader proves the initial allocator baseline before upload.
    • SLIDE_REGION uses a profile-selected segment allowlist resolved from Mach-O headers, a reserved destination window and bounded callbacks. It moves aliases and updates physical-to-virtual metadata while the guest is stopped; the host publishes tables and invalidates translations before resuming. Firmware addresses are not embedded in the portable C path.

    These are boot-compatibility semantics, not full SPTM security enforcement.

  3. Host resources. probes/bootcontract.py contains immutable HostRuntime, PhysicalRange and Framebuffer values. The q1n1 adapter derives the counter frequency, heap and GOP framebuffer from boot information. Another transport or board adapter should produce the same values from its own evidence. Host resource discovery must not select an Apple kernel profile.

configure_guest(name, directory) configures one loader instance before a build. It still uses loader globals, so simultaneous builds need separate processes. The firmware directory applies consistently to the tree and both monitors; firmware files are never modified.

Safety and evidence contracts

  • Physical ranges must be nonempty, aligned, within the implemented 36-bit stage-2 address space and nonoverlapping. This limit belongs to the current EL2 backend; a wider host must not silently truncate addresses.
  • The flat frame map covers RAM/reserve and the optional panel. Holes carry type zero; stage 2 enforces ownership and leaves the gaps absent. The handoff's monitor stack fields describe VAs, not physical boundaries; the TXM stack pages and a separate CPU-stack window are reserved and zeroed, and the profile's audited stack count must agree with the guest device tree.
  • EL2's address-translation helper preserves PAR_EL1 across instruction steps. The diagnostic SNAP_WATCH_WRITE=<guest VA> traps before writes to one mapped guest page and captures the writer; it never opens a new physical mapping.
  • Signing-related monitor calls are outside the scope of the public documentation.
  • The early-mapping recipe is derived from the selected device tree (console windows, CPU register windows, two AIC mappings and the panel) and must fit the loader's initial L3 reservation. Later table expansion is supported for empty 16 KiB children under the kernel root. It is a conservative preflight, not a bound on every future kernel mapping.
  • Framebuffer stride is in bytes at the adapter boundary. GOP formats 0/1 are accepted; invalid dimensions, insufficient backing, unsupported formats and heap overlaps are errors.
  • CPU requirements are checked by the EL2 probe. A host descriptor does not prove NV2, debug trapping, translation support, interrupt delivery or board qualification.
  • The audited guest profile supplies the virtual kernel-return HCR value; NV2's VNCR backing is initialized from it, separate from the host's actual HCR. Leaving the virtual HCR zero while CurrentEL reports EL2 would select XNU's VM-exit path on ordinary kernel exceptions.
  • compat/uat.c answers the audited GPU address-translation information queries from guest device-tree geometry and a bounded shared reserve. The Apple GPU itself is not modelled; the A16's Adreno GPU is driven by Snapintosh's own kext and Metal plug-in instead.
  • Pointer authentication: compat/pauth.c handles the architectural PACGA operation for the guest through a host backend.
  • compat/exception.c handles ordinary kernel page-fault delivery and plain ERET back to the nested kernel. Virtual EL2t/h becomes physical EL1t/h; saved flags and stack selection are preserved.
  • Runs hash the complete framebuffer inside the EL2 probe immediately before guest entry and after the stop, while q1n1's UI is suspended, which separates guest drawing from q1n1's own redraws. A profile-specific memory witness reads the console's vinfo; missing witness metadata disables the diagnostic rather than reusing another kernel's addresses.

Running the bundle checks

python3 tools/test-bootcontract.py
python3 tools/test-profile.py
python3 tools/test-classify.py
python3 probes/xnuboot.py --check --guest-profile j700ap-26A428

# Serial console, no panel mapping.
SNAP_NO_FB=1 python3 probes/run.py xnuboot --console serial
# Panel handed over, printf still to the UART.
python3 probes/run.py xnuboot --console serial
# Same mapping, serial output bit clear so printf draws on the panel.
python3 probes/run.py xnuboot --console panel

Loader and runner both accept --guest-profile, --firmware-dir and --kernelcache. A directory override relocates an audited bundle; it does not relax identity validation. --device selects the q1n1 transport endpoint (for example --device udp://<interface>). Full boots are driven by tools/cycle.py; see RUNNING.md.

Device boundary

Local reference implementations are listed in DEVICE-REFERENCES.md: emuasi's portable AIC state model and m1n1-j700's ASC/RTKit drivers. They are implementation references, not Snapdragon qualification.

  • Watchdog. The virtual WD1 primary bank has a timed counter, bite comparator, keepalive and a latched guest-reset request; a guest reset stops the harness, never the host.
  • AIC. The flat-bank model supports global enable, masks, software requests, producer levels and EVENT auto-mask/acknowledge; the host adapter drives virtual IRQs separately from the timer's virtual FIQ.
  • Timers. The kernel deadline timer is a separate portable model; a q1n1 backend multiplexes the physical timer between guest deadlines, virtual device deadlines and the run budget. A rate adapter converts the host's 19.2 MHz counter to the profile's 24 MHz guest domain; Apple counter aliases, deadlines and WFE event edges agree with that conversion. Direct CNTFRQ consumers in scaled executable images are rejected before boot.
  • Keep guest-facing AIC registers, mask/pending/acknowledge state and interrupt numbers separate from the host interrupt controller and timer adapter. Architectural timer delivery and AIC device interrupts are distinct paths.
  • ASC/mailbox transports and the services above them are separate interfaces: queues, doorbells and interrupts first, service protocols such as SMC, AOP, SEP or ANS2 storage (see FULL-INSTALL.md) above them.
  • Any driver the guest sees must receive real supported protocol behaviour or an explicit unsupported outcome; optional devices are withdrawn from the device tree instead. A SEP handshake is not secure-enclave firmware, persistent key services, Touch ID or Apple security enforcement.

Before a second guest or host

For another kernelcache: identify its monitor family and granules, audit the complete firmware bundle, re-derive the entry/vector and handoff consumers, and add an explicit profile with evidence. Move the remaining J700 diagnostic addresses in probes/run.py, the early-mapping recipe and ABI layout constants into the guest backend before registering a second profile. A missing diagnostic must disable that diagnostic, never substitute J700 addresses. Add offline negative tests and run the same hardware acceptance gates. Report source support, offline validation and live qualification separately.

For another host: implement resource discovery against HostRuntime, keep board-specific reset, USB, interrupt-controller and display operations behind its adapter, and verify the CPU's required features before entry. The C and assembly entry path and q1n1 heap reservations still need an explicit, versioned transport ABI; the Python resource contract is the first boundary, not a finished HAL.