A minimal bare-metal ARM64 kernel that boots under Apple's Virtualization.framework and prints a message to the host terminal via a VirtIO PCI console.
▶ Building kernel...
Built kernel.bin (12417 bytes)
▶ Compiling runner...
▶ Starting VM...
Hello from bare-metal on Apple Virtualization.framework!
▶ Done.
Built with Claude — This project was developed interactively with Claude (Anthropic), which drove the iterative debugging process of discovering Apple VZ's undocumented VirtIO quirks.
-
boot.S— Emits a valid Linux/ARM64 Image header soVZLinuxBootLoaderaccepts the binary, installs an exception vector table, clears BSS, and callskernel_main. -
kernel.c— A single C file that:- Parses the FDT passed in
x0to find the PCI ECAM base and MMIO aperture. - Scans the PCI bus for the VirtIO console device (
vid=0x1af4, did=0x1043/0x1003). - Programs the device using the VirtIO 1.x modern PCI transport.
- Sends a string via the virtqueue TX path, polls
tx_used.idxfor completion, then calls PSCISYSTEM_OFF.
- Parses the FDT passed in
-
runner.swift— A Swift host process that creates aVZVirtualMachineConfiguration, attaches a VirtIO serial port wired to stdout, and starts the VM. When the kernel calls PSCISYSTEM_OFF, VZ invokesguestDidStop, which closes the pipe write end; the runner exits whenreadabilityHandlerobserves EOF, after all output has been forwarded.
- Apple Silicon Mac (M1 or later)
- macOS 13 Ventura or later
- Xcode Command Line Tools (provides
clang,swiftc) - LLVM's
ld.lld— install via Homebrew:brew install llvm
Make sure
ld.lldis on yourPATH. With Homebrew:export PATH="$(brew --prefix llvm)/bin:$PATH"
git clone https://github.com/r2jitu/hello-apple-vz
cd hello-apple-vz
bash run.shOr, step by step:
# 1. Build the kernel flat binary
bash build.sh
# 2. Compile and codesign the Swift runner
swiftc -framework Virtualization runner.swift -o runner
codesign --entitlements entitlements.plist --force -s - runner
# 3. Run the VM (exits cleanly when the kernel calls PSCI SYSTEM_OFF)
./runnerApple Virtualization.framework is not QEMU. Several things behave differently:
| Topic | QEMU | Apple VZ |
|---|---|---|
| PCI BAR allocation | Firmware pre-programs BARs | BARs not pre-programmed — address bits are zero; must write a valid address before use |
| VirtIO status writes | Synchronous | Asynchronous — must delay between writes and before read-back; 85% failure rate without delays |
| VirtIO DRIVER_OK | Host ready immediately | Host needs time — must pause (~1M nops) after DRIVER_OK or TX queue never becomes active |
| VirtIO transport | Legacy or modern | Modern only (device ID 0x1043) |
| UART | PL011 at 0x09000000 |
None — VirtIO console is the only output path |
From the FDT passed in x0 at boot (empirical; parsed at runtime so not hardcoded):
| Region | Address | Size |
|---|---|---|
| RAM | 0x70000000 |
1 GB |
| GIC | 0x10000000 |
— |
| pvpanic MMIO | 0x20070000 |
— |
| PCI ECAM | 0x40000000 |
256 MB |
| PCI I/O window | 0x6fff0000 |
64 KB |
| PCI MMIO32 window | 0x50000000 |
~510 MB |
| PCI MMIO64 window | 0x100000000 |
1 GB |
- Vendor/Device ID:
0x1af4/0x1043(modern VirtIO console) - Queue size: 256 (empirical; read from device at runtime)
notify_off_multiplier: 4- All VirtIO PCI caps reference
bar_idx = 0
- Parse FDT for ECAM base and MMIO32 window.
- Scan ECAM for device
vid=0x1af4, did=0x1043/0x1003. - Enable Mem + Bus Master:
mmio_w16(dbase + 0x04, cmd | 0x06)← 16-bit only. - Determine BAR0: read existing value (mask low 4 type bits); if zero (Apple VZ), write
pci_mmio32_basefrom FDT. - Walk PCI caps (
cap_ptrchain) to locatecommon_cfgandnotify_cfg. - Modern VirtIO init:
RESET→ACKNOWLEDGE→DRIVER→ negotiateVERSION_1→FEATURES_OK. - Setup queue 0 (RX); post one RX buffer into it; setup queue 1 (TX); read per-queue notify offsets.
DRIVER_OK; notify RX queue; pause ~1M nops (VZ host side needs time to become ready).- Fill TX buffer; update TX avail ring (
avail.idx = 1);dsb sy. - Ring TX doorbell; busy-poll
tx_used.idxuntil non-zero; callpsci_off.
VZ updates tx_used.idx and writes to the host pipe on the same I/O thread
but not necessarily in that order — the pipe write can land after the
used-ring update, and the pipe write can also land after guestDidStop fires.
The runner handles this by keeping readabilityHandler alive and exiting only
when the pipe reaches EOF (empty read). guestDidStop closes the runner's copy
of the write end; VZ closes its copy once the VM finishes tearing down. EOF
arrives only after both write ends are closed — by which point VZ has flushed
all pending pipe data — so no output is lost regardless of ordering.
| Feature | Why omitted | What you'd add |
|---|---|---|
| MMU / page tables | Memory is uncached by default (no SCTLR_EL1 set); D-cache flush not needed | setup_mmu() in boot.S; enable caching in SCTLR_EL1 |
| RX polling | Guest posts one RX buffer so the host CAN send, but never reads it back | Poll rx_used.idx; read rx_buf for host-to-guest data |
| VirtIO feature negotiation | Only VERSION_1 is negotiated; all device-specific features are skipped |
Read and mask device_feature before writing driver_feature |
| PCI function scan | Only function 0 of each device slot is checked | Add a function loop: `(d << 15) |
| VirtIO multiport console | VIRTIO_CONSOLE_F_MULTIPORT is not negotiated; single port only |
Negotiate feature bit 1; use control queue (queue 2/3) to open named ports |
With no UART, execution state can be encoded as behaviour:
psci_off()→ runner exits 0 immediately: reached this point cleanly.while (1) wfi→ runner hangs (kill with Ctrl-C): reached this point but did not exit.- pvpanic write to
0x20070000→ runner exits with error: explicit error signal.
This allows binary-search debugging of the boot path without any output device.
boot.S ARM64 boot stub: Linux image header, exception vectors, BSS clear
kernel.c Bare-metal C kernel: FDT parser + VirtIO PCI console driver
runner.swift Swift host: Virtualization.framework VM configuration and launcher
linker.ld LLD linker script: flat binary, BSS, 64 KB stack
build.sh Compile kernel.c + boot.S → kernel.bin
run.sh Full build + sign + run pipeline
entitlements.plist com.apple.security.virtualization entitlement for the runner
- Apple Virtualization.framework documentation
- VirtIO 1.2 specification
- Apple VZ device tree (zhuowei's gist)
- ARM64 Linux Image header format
- PSCI specification
MIT