macOS Tahoe 26 running in Qubes 4.3 HVM: OpenCore, QEMU in the stubdomain, no Xen patches (unmaintained, as-is)

I got macOS Tahoe 26 running as an ordinary HVM qube on Qubes OS 4.3. It’s booted by OpenCore, with QEMU kept in its stubdomain like every other HVM. There are no Xen patches, dom0 never gets network, and there’s no nested virtualisation. It installs from Apple’s Recovery, reaches the desktop, takes 26.x updates (26.7 → 26.7.1 tested), and keeps its settings across restarts.

As far as I can find, this is the first macOS that installs and runs as a Qubes HVM. The MacOS VM on qubes thread has been asking since 2020, and earlier attempts stopped at OpenCore or the installer. One earlier macOS-on-Xen success needed a patched Xen; this doesn’t.

I’m not maintaining this. I’m sharing it as-is, as a record of what worked, together with everything someone would need to pick it up. I won’t be providing support or fixes. If you take it further, please say so here so others can find you.

[Screenshot: the macOS desktop in its Qubes window]

Downloads

For users: the scripts, the libvirt template, the install guide and the docs, plus FINDINGS.md: the whole project condensed (what boots, every wall and its fix, what was ruled out, what is still open).
[macos-qubes-0.89.zip]
sha256 4ee984abbf2915af6ca37f16f757328210829fedf0dca8cc847fd729b9f778db

For anyone who wants to maintain or change it: the whole project. That’s the full source and test suite, the build and release tools, MAINTAINING.md, and the project notes: every run’s evidence (RUNS.md), the traps (GOTCHAS.md), the roadmap and what has been ruled out.
[macos-qubes-dev-0.89.zip]
sha256 da93196e88fdd9b44c6c7df186ceb62d344c35a5f47a70ab973ad3a8d60c8930

If the forum will take them I can add the zips as base64 text. Each could be turned back into the zip, and check the sha256:
base64 -d macos-qubes-0.89.zip.b64.txt > macos-qubes-0.89.zip && sha256sum macos-qubes-0.89.zip

Check the hash, then read README.md and INSTALL.md inside. Everything that runs in dom0 is plain bash or XML: read it before you run it.

After copying the zip into dom0 (INSTALL.md step 1), the install is one command: bash ~/macos-v2/macos.sh new. It first asks the new qube’s settings (vCPUs, memory, disk, network qube; Enter keeps each default) and confirms once, then builds the boot disk in a media qube, makes the qube and follows the whole install. You type one command in Recovery’s Terminal, go through Setup Assistant, and optionally apply the speed settings. macos.sh new NAME makes further macOS qubes, each with its own serial number. macos.sh adopt OLD lets a renamed qube take over (say a test qube that becomes macos), and macos.sh clean deletes, one item at a time, what no macOS qube uses any more.

How easy is it? Honestly: not very

  • It has run on one machine: mine, an Intel Core Ultra 7 165H (Meteor Lake) on Qubes 4.3 with Xen 4.19.
  • The from-scratch install path (macos.sh new) has run end to end three times, on my machine (0.83, 0.84, 0.85; a second macOS qube): boot disk, Recovery download, install (about 3 hours), Setup Assistant, speed settings. Earlier tries stopped at OpenCore’s config check (0.79), the Recovery download (0.80), a half-built boot disk (0.81) and OpenCore booting its own launcher (0.81 again). Every full run hit the same wall at the end: Setup Assistant spins for good on “Update Mac Automatically”, with network or without, and one restart gets past it. Since 0.85 the script does that restart when you answer n, and the 0.85 run went through cleanly that way (with 16 vCPUs, too). It has run on nobody else’s machine.
  • After the install, Setup Assistant may come back once after login with a FileVault page that will not let you past. A restart, or macos.sh setupdone (one command typed in Recovery marks Setup Assistant done), got rid of it; I can’t say which of the two did it.
  • Two pieces depend on the host CPU: the CPU-model spoof and two small kernel patches for CPU topology. macos-ctl.sh hostcheck says whether your machine looks like mine. Expect trouble on anything else. AMD is out of scope.
  • You need about 150 GB free in the pool, 8 GB of RAM for macOS, about 3 hours (Apple’s installer alone took 2¼), and to be comfortable reading and running scripts in dom0.
  • If it breaks, the scripts leave good evidence: a VERDICT line, one .tar.gz, and the full kernel boot log. But nobody is on the other end to read it.

What works, what doesn’t

Works Doesn’t
Install, Setup Assistant, desktop, keyboard and mouse GPU acceleration: the CPU draws everything, so it’s slow for video and animations
Network through your netvm and firewall Sleep, Handoff/Continuity
Restart and Shut Down with settings kept (emulated NVRAM) qrexec, the Qubes GUI agent, seamless windows
26.x updates, with a snapshot first and rollback offered Live clipboard Qubes → macOS
Snapshot and rollback of the whole qube; files in and out Anything after macOS 26: Tahoe is Apple’s last Intel release
Clipboard macOS → Qubes; sound (optional); quiet boot
How it works (the walls, and the fix for each)
  • Disks: over QEMU’s AHCI controller, not the PV path, which OVMF livelocks on.
  • CPU model: OpenCore reports an Intel model macOS knows (Comet Lake), since Meteor Lake isn’t in its table.
  • CPU topology: Xen zeroes the cache-sharing field of CPUID leaf 4, which made the kernel divide by zero. Two small kernel byte patches fix it, rewritten for the qube’s vCPU count at every boot.
  • Sleep: ACPI S3 is hidden, so macOS can’t suspend itself.
  • Disk discovery: a small kext without code stops X86PlatformPlugin from blocking it.
  • Restart: OpenCore’s FadtEnableReset.
  • SMC: VirtualSMC, because QEMU’s isa-applesmc is invisible under Xen’s ACPI tables.
  • Settings: OpenCore’s emulated NVRAM, saved at shutdown.
  • Restarts: Qubes turns a guest restart into a stop, so a small watcher starts the qube again.
  • dom0: gets one per-VM libvirt template and three bash scripts. Every boot-disk edit happens in a separate media qube, never in dom0.
  • Logging: a serial port in the stubdomain sends OpenCore’s and the kernel’s boot log to dom0’s console log.
Pros and cons

For it:

  • macOS stays inside Qubes’ compartments: QEMU in the stubdomain, network through your firewall, snapshots and rollback.
  • Nothing in Xen, Qubes or your other qubes changes.
  • Every run ends with one VERDICT line and one .tar.gz of evidence.
  • About 3,400 offline tests run before every build.

Against it:

  • Slow graphics, because there’s no GPU.
  • Tested on one host.
  • The kernel byte patches could stop matching after an Apple update. The update command checks every boot and offers the snapshot back.
  • About 9,800 lines of bash in dom0, run with sudo. Much of it is research tooling, more than everyday use needs.
  • About 1 start in 5 freezes in the firmware for a reason I never found. The scripts restart it automatically.
  • Lilu and VirtualSMC run in the guest kernel.
  • Unmaintained.
  • Apple’s licence allows macOS VMs only on Apple hardware. The package ships no Apple software, no SMC key and no serial numbers; you download macOS from Apple yourself.
How it was made: with an AI coding agent

I built this with Claude Code (Anthropic’s AI coding agent) over about three weeks: 88 builds and about 300 recorded runs. Claude wrote the scripts, the libvirt template, the tests and the docs. It read the logs after every run and planned the next one. That included disassembling Apple’s kernel to find the topology divide-by-zero, and reading the OpenCore, QEMU, libvirt, libxl and Xen sources.

It never touched dom0 or the Qubes machine. It worked on a separate computer. I carried each build across with its sha256, ran one command in dom0, watched the screen, answered the script’s questions, and carried one log file back.

  • Good: one variable changed per run, a test for every behaviour change, and a written record of why each piece exists, wrong turns included.
  • Bad: it’s AI-written code that has run on one machine. Review it as you would any dom0 script from the internet. Mistakes were made and corrected along the way, and some of the docs are reasoning rather than measurement; where that’s so, they say so.

[Screenshot: the boot etc. in progress]

If you try it

There’s no support. Posting what happened here may help the next person:

  • the output of bash ~/macos-v2/macos-ctl.sh hostcheck;
  • your CPU and your Qubes and Xen versions;
  • which INSTALL steps worked;
  • for a failure, the VERDICT line and bash ~/macos-v2/macos-ctl.sh lastboot.

Read any .tar.gz before sharing it: it holds logs from your machine.

Licence: MIT for this project’s files (Ros Mac). OpenCore, Lilu and VirtualSMC come from Acidanthera under their own BSD licence; the builder downloads them. Thanks to Acidanthera, OSX-KVM, the Dortania guides, and DarwinXen, an earlier macOS-on-Xen guide that documented several of these walls first. It used a patched Xen; this needs none.

5 Likes

Things others could take upstream

Getting macOS to boot here meant working around gaps in Xen, libvirt, Qubes, QEMU and OVMF. Several are small, self-contained fixes that would help more than macOS. I’m not going to send them myself (this project is unmaintained), so here they are for anyone who wants to. Each was seen on Xen 4.19, Qubes 4.3, QEMU 9.0.2 and OpenCore 1.0.7. I have not checked current upstream, so look there first: some may already be fixed. The run names (b21, b40, …) point at the evidence in RUNS.md in the maintainer kit.

Xen

1. CPUID topology is inconsistent in HVM guests (the big one, b38-b40)

  • Leaf 4 is present, but EAX[25:14] (logical CPUs sharing each cache) is zeroed on every level, so every sharing count reads 1.
  • Leaf 1 EBX[23:16] and leaf 4 EAX[31:26] pass the host’s package counts through (128 logical, 64 cores here), whatever the domain’s vCPU count.
  • The max basic leaf is 0xD, so leaves 0xB/0x1F (x2APIC topology) are hidden.
  • macOS computes cores sharing LLC = 1 / (128/64) = 0 and divides by it during boot. We work around it with two kernel byte patches, rewritten for the vCPU count at every boot.
  • Upstream fix: give HVM guests a self-consistent topology (sharing counts, package counts matching the vCPUs, leaf 0xB). This would also help any other OS that trusts these leaves. Check first whether a newer Xen already does this.
  • Related: vCPU APIC ids are 2n (hvmloader/config.h, vlapic.c), which caps a consistent topology at 64 vCPUs here.

2. The FADT has a reset register but doesn’t say so (b52)

  • tools/libacpi/static_tables.c fills in RESET_REG (0xCF9, value 6) but leaves RESET_REG_SUP out of the FADT flags. macOS believes the flags and spins in halt_all_cpus(reboot) instead of restarting. OpenCore’s FadtEnableReset sets the bit for us.
  • Upstream fix: set RESET_REG_SUP (one flag). Check first that every HVM device model handles the 0xCF9 write.

3. \_S3 is always offered (b40, b42)

  • tools/libacpi/ssdt_s3.asl always defines \_S3. An idle macOS suspends itself after ~10 minutes, and Xen parks it for good. libxl already has acpi_s3=0; the gap is in libvirt (next item).

libvirt (libxl driver)

4. Map <pm><suspend-to-mem enabled='no'/> to libxl’s acpi_s3 = false. The libxl driver ignores <pm> today (libxl_conf.c, no acpi_s3 handling), so a Qubes per-VM template cannot turn S3 off. We rename \_S3 in OpenCore instead.

5. Map SATA disks to libxl’s hdtype = ahci. bus='sata' is accepted but never selects AHCI: Xen’s QEMU still builds IDE. Without AHCI, OVMF reaches the disks only over Xen PV (see item 11), and IDE runs at ~234 KiB/s. Our per-VM libvirt template builds the AHCI controller itself; native support would be cleaner.

Qubes

6. An optional “restart on guest reboot” for HVMs (b71, b75)

  • Qubes starts HVMs with on_reboot=destroy, so a guest’s own Restart (Apple menu, installer, updates) becomes a stop. We watch the device-model log for the reset and start the qube again from dom0.
  • A qube feature that has qubesd start the qube again after a guest reset would help every HVM that restarts itself (installers, Windows updates). libvirt’s on_reboot=restart was not the answer: it left a grey, dead window (b75).

7. An optional serial console for HVMs, logged in dom0 (b76)

  • An HVM has no serial port whose output leaves the stubdomain. We add a 16550 in the stubdomain QEMU whose output goes to the stubdomain’s console, which dom0 already logs (guest-NAME-dm.log). That gives the whole firmware and kernel boot log, in dom0, with no network and no agent.
  • As an opt-in feature this would make debugging any non-booting HVM (BSDs, Windows, other OSes) much easier.

8. Stubdomain memory (b63-b66)

  • With virtio-net, the default stubdomain (~144 MiB) OOM-killed QEMU 9 s after the guest resized its window to 2560x1440. The guest then runs on with no devices. 256 MiB fixed it.
  • Worth either a larger default for HVMs that change resolution, or a clear error in the qube’s log instead of a silent dead window.

9. efi-vmxnet3.rom is missing from the stubdomain (d16, d17). With vmxnet3 the stubdomain QEMU refuses to start. -global vmxnet3.romfile= works around it. Small packaging fix, or document that vmxnet3 needs it.

10. One broken block attachment breaks qvm-block list everywhere. qvm-block list walks every domain for frontend info, even when you name one backend (qubes-core-admin-client, qvm_device.py:135), and it catches access errors outside the per-VM loop (:169-174). One qube with a bad attachment fails every listing (“Got empty response from qubesd”). It should skip the bad domain and carry on.

edk2 / OVMF (Xen build)

11. XenPvBlkDxe livelocks under OpenCore (b21)

  • OVMF sees every disk twice, over Xen PV and over the emulated controller. When OpenCore read its kexts and the kernel over the PV path, the boot wedged in the block front-end (vCPU0 spinning, no I/O), 4 of 4 trials. Over AHCI the same boot runs.
  • I don’t have a minimal reproducer, but it’s worth a bug report with the evidence. Anything that reads a lot over PV before ExitBootServices may hit it.

12. No persistent UEFI variables. Xen’s OVMF keeps no NVRAM, so the startup disk, boot order and update stages are lost at every boot. We use OpenCore’s emulated NVRAM (a plist on the ESP). A persistent variable store for Qubes HVMs would help every UEFI guest.

QEMU

13. applesmc lacks GetKeyInfo (b59)

  • With an ACPI APP0001 node added (Xen’s DSDT has none, so macOS never even finds the device), macOS 26’s AppleSMC attaches, then gets kSMCBadCommand (0x82) for every GetKeyInfo (0x13): DPLM, RGEN, $Num, REV, …. Without an SMC, DSMOS never decrypts loginwindow and the screen stays grey.
  • Implementing GetKeyInfo (and whatever else current macOS asks) in hw/misc/applesmc.c would let macOS run without VirtualSMC injected. Under Xen it would also need the ACPI node from libacpi.

OpenCore

14. ProvideCurrentCpuInfo takes Xen’s host counts at face value (b40)

  • Under Xen it builds the MSR 0x35 value from CPUID, so it reports the host’s 64 cores / 128 threads (OCAK: Patching MSR 35h to 00400080, CpuidPatches.c:1207-1388 in 1.0.7).
  • A config option for the core/thread count (or reading it from the MADT under a hypervisor), plus the leaf-4 sharing count, would let Xen guests boot without our hand-made kernel byte patches. As quirks it would also survive Apple’s kernel updates better than byte patches.

Still unexplained (not ready to report)

  • About 1 start in 5 froze in OVMF in early runs (before BdsDxe, or at OpenCore’s “Connecting drivers” on the root disk); 0 of 10 later. Cause unknown; the scripts just restart it.

If you pick any of these up, please link the upstream issue or patch here so others can follow it.

2 Likes

wow that’s incredible piece of work! its a shame that macos for intel is almost dead :frowning:

1 Like

Fantastic!