I unlocked Hypervisor.framework on my jailbroken phone and modified UTM, a popular QEMU port for iOS, to run arm64 Linux in a VM at full native speed. …for the clickbait - and to show iPhone’s untapped potential.

iPhone 12’s A14 CPU supports virtualization, just like Apple Silicon Macs. Virtualization support is disabled in the kernel, but can be re-enabled with a jailbreak. VMs on iPhone 12 are limited to 900MB of RAM, however.

Here’s a video of my iPhone 12 running the modified UTM, booting a Fedora 36 VM, and showing the requisite Neofetch and LibreOffice demo.

Information

Is this practical?

Absolutely not. This is a proof-of-concept that targets iPhone 12 on iOS 14.1 only. It’s also really unstable (VMs can only use 900MB of RAM, and if it goes over, often the whole phone crashes and reboots).

How much faster is hardware accelerated virtualization compared to UTM’s JIT mode?

Single-core score in Geekbench 5:

This is almost native speed.

I did not try running Geekbench in JIT-only mode on my phone. However, an Apple Silicon Mac gets a Geekbench score of 68 when emulating x86 with QEMU/UTM JIT. Emulating arm64 would have a smaller overhead, but I’d still expect a 5x to 10x slowdown.

One disadvantage of hardware accelerated virtualization: double the RAM overhead. iOS terminates the VM when it uses more than 1GB of RAM. In non-hardware accelerated mode, VMs can use 2GB of RAM. (I tried increasing this limit, but this caused kernel panics)

Can this be ported to other devices?

A14 (iPhone 12) on jailbroken iOS 14.7 and below:

  • The kernel unlock can be ported to iOS versions supposed by Fugu14
  • (the unlock won’t work on iOS 15 since the hypervisor heap was moved to read-only memory)
  • I did not attempt to handle different kernel versions in my proof-of-concept
  • to make this usable on other devices/versions, you’ll need to implement patchfinding for offsets
  • if you have any questions, I’ll be happy to help.

M1 (iPad Pro 2021/iPad Air 2022) on jailbroken iOS 14/15:

  • These devices already have Hypervisor support unlocked in the kernel
  • so any jailbreak should work, not just Fugu14
  • sign with the com.apple.private.hypervisor entitlement and include the decompiled Hypervisor.framework

All other devices:

  • CPUs before A14/M1 do not have hardware accelerated virtualization support

Will it run Crysis?

No.

Believe me, I tried. Windows arm64 refused to boot on my decompiled Hypervisor.framework, and I don’t have time to troubleshoot why.

Even if Windows were to boot, software rendered Crysis runs at 1fps at 640x480 on my M1 Mac Mini with 4 performance cores… so on an iPhone with 2 performance cores, it’d be 0.5fps.

(I also tried booting Android - I spent several days trying to run Waydroid in my Linux VM without success. The lack of RAM prevents it from working anyways.)

How this works

Unlocking Hypervisor.framework required three parts:

Why Hypervisor syscalls don’t work on iPhones

Hypervisor support is not included in the open source XNU release, but is present in the kernel itself (unlike on Intel platforms, where it’s a separate kext).

Hypervisor support is initialized during kernel boot here:

  kernel_bootstrap_thread_log("hv_support_init");
  HvEnabled = hv_initialize();

hv_initialize checks the CPU’s midr register for its model number.

If it’s an Apple processor (implementor = 0x61) and the model number is 32 (A14 Icestorm) or 33 (A14 Firestorm), the function returns false. Otherwise - if, say, the processor is an M1 - the function creates a heap and returns true.

(The iOS 15 version of this additionally checks for A15’s performance and efficiency cores, and also returns false.)

undefined8 hv_initialize(void)

{
  ulong uVar1;
  
  uVar1 = cRead_8(currentel);
  if (((uVar1 & 0xc) == 8) && (HvProcessorMidr >> 0x18 == 0x61)) {
    if ((HvProcessorMidr & 0xffe0) == 0x200) {
      _HvCheckStatusAble = _HvCheckStatusAble | 3;
      return 0;
    }
    HvHeap = zone_create_ext("hv_vm",0x2080,0x10000000,0xffff,0);
    return 1;
  }
  return 0;
}

Then, in handle_svc, an extra switch case checks for -5, the hypervisor Mach trap:

      if (iVar4 == -5) {
        lVar5 = -0x516bfff;
        if (HvEnabled == 0) {
          *(undefined8 *)(param_1 + 2) = 0xfffffffffae9400f;
        }
        else {
          if (*(ulong *)(param_1 + 2) < 0xe) {
            iVar4 = (*(code *)(&PTR_HvGetCapabilitiesHandler_fffffff007818990)
                              [*(ulong *)(param_1 + 2)])(*(undefined8 *)(param_1 + 4));
            lVar5 = (long)iVar4;
          }
          *(long *)(param_1 + 2) = lVar5;
        }
      }

If hypervisor support is enabled, this code dispatches to the hypervisor Mach trap handler. If hypervisor support is disabled, this code simply returns 0xfae9400f - HV_UNSUPPORTED.

You can test this - even without a jailbreak - by running this code, which attempts to create a VM.

  • on a Mac, or an iPad Pro/Air with M1, you’ll get HV_DENIED (0xfae94007)
  • on iPhone 12, HV_UNSUPPORTED (0xfae9400f)
  • on iPhone 11 and below, EXC_SYSCALL

To access Hypervisor.framework on an iPad Pro/Air with an M1, all you need is the com.apple.private.hypervisor entitlement, and everything should work.

That’s no fun, though: we already know that the M1 in an iPad Pro/Air gets the same benchmark scores as an M1 in a Mac, so virtualization on an iPad would probably be similar to a MacBook.

What I want to try is running on iPhone. To do this, we need to get around the HV_UNSUPPORTED error.

Unlocking the hypervisor syscalls on iPhone: modifying Fugu14

Unfortunately, HvEnabled, the flag that sets whether hypervisor support can be used, is in read-only kernel memory. Once the device boots, there’s no way to re-enable the normal syscall route.

However, we can just directly call the hypervisor functions in the kernel, bypassing the disabled syscall, with a jailbreak that supports kernel calls.

Linus Henze’s Fugu14 is the only iOS 14 jailbreak with kernel call/PAC signing support.

However, my device is iOS 14.1, and does not have the vulnerable CreateMemoryDescriptorFromClient method.

I decided to jailbreak with Taurine, then use Fugu14 to call kernel functions.

To do so, I replaced Fugu14’s kernel read/write exploit with calls to Taurine’s libkernrw.

To my surprise, the kernel call function worked fine using libkernrw as a backend, even though Fugu14’s kernel bug gives physical memory access via memory mapping, while libkernrw gives kernel virtual memory access via IPC and syscall.

The only changes I needed to make were:

  • remove all physical memory access functions; replace kernel virtual memory access functions with calls to libkernrw
  • replace all physical memory accesses with virtual memory accesses
  • replace anything that maps a physical page into userspace with calls to read/write through libkernrw
  • made the patchfinder load the kernel from disk instead of dumping from memory, which takes minutes using libkernrw

all uses of physical addresses was easily replaced… except one:

The exploit starts a thread that used a physical memory mapping to overwrite its own kernel stack pointer (machine.kstackptr) before making a syscall.

I didn’t know whether calling libkernrw - which would result in an extra IPC call to jailbreakd before the start of the exploit - would break it.

So, out of caution, I made the exploit thread wait in a loop, and did the write from the main thread instead.

This modularity of Fugu14 is a real testament to Linus Henze’s software engineering skills… and a boon to script kiddles like me: I can just mix and match jailbreaks to get what I want :D

Exporting the kernel call

Fugu14 gives researchers kernel call capability in one process and one thread. However, for running virtual machines, I need to make kernel calls from multiple threads.

I decided to use the traditional way to call kernel functions: a modified IOUserClient - which can be sent across processes and used simultaneously on multiple threads.

The steps to make an IOUserClient for kernel calls is well known: make a fake IOUserClient object, make a fake Vtable, override getExternalTrapForIndex to point to your function. I used Electra’s code as a guide.

However, PAC requires signing. every. single. pointer. Which was rather annoying - it takes over a minute to sign each of the ~100 pointers in the vtable.

But at the end of it, I have a Mach port that I can use with IOConnectTrap6 to call PAC-signed pointers with two arguments.

I then register the jailbreakd task point with launchd using bootstrap_register, so that apps can grab the IOUserClient directly out of jailbreakd with mach_port_extract_right.

(Yes, I should’ve used an XPC service, but, hey, proof of concept.)

Decompiling Hypervisor.framework

iOS does not ship with the userspace code for Hypervisor.framework, and I can’t just copy macOS’s Hypervisor.framework over (for one thing, it can’t be extracted from the dyld cache, and I also needed to replace the syscall with my IOUserClient.)

Thankfully, the library is tiny (30KB), so I threw it into Ghidra, used its decompiler to get pseudo-code of each function, and hand-translated it back to Objective-C.

Hypervisor.framework is a very thin wrapper around the kernel functionality. It uses two pages mapped into userspace to communicate with the kernel.

Apple made my life super easy by including the structures of those two pages in the macOS Kernel Debug Kit. I simply dumped the structures using lldb: running

type lookup arm_guest_context_t

gives me a nice dump of the structures.

These kernel structures changed slightly between macOS 11.0/iOS 14.1 and macOS 12.3.1, so I had to compare the struct definition from macOS 11.0 and 12.3.1’s kernel symbols, then add #defines to my header to allow me to test on both macOS 12 and iOS 14.1

My library isn’t a full decompile - only enough to boot Linux in QEMU.

For example, some registers such as aa64pfr0_el1 are emulated in userspace instead of in kernel/hardware. Instead of emulating this register access, I just pass the vmexit event to QEMU, which handles it anyways.

In another example, there’s an optimization for getting/setting system registers to avoid calling HV_CALL_VCPU_SYSREGS_SYNC unnecessarily. I didn’t bother decompling this since QEMU doesn’t set/get registers often.

Unfortunately, it seems Windows arm64 breaks my decompiled library, so I guess the parts I omitted were used at least by one guest operating system… oh well.

I tested this by using DYLD_FRAMEWORK_PATH= to replace the system Hypervisor.framework when running QEMU on macOS. Once this worked on macOS, I started bringing it to iOS.

Modifying UTM

I decided to modify the excellent UTM, a port of QEMU to macOS and iOS. Since QEMU and UTM already support Hypervisor.framework on Apple Silicon, all I needed to do was:

  • remove a few os(macOS) checks in UTM
  • add the entitlements to access Hypervisor.framework and to communicate with the modified Fugu14
  • work around an issue with UTM and Taurine

and it just worked!

After enabling “Hypervisor” in UTM’s VM Settings -> QEMU -> Hypervisor, I saw my code printing logging messages, and Fedora Linux booted in seconds instead of minutes.

Conclusion

Putting Linux in a VM on my iPhone 12 definitely increases its resale value (One-of-a-kind, No lowball offers: I know what I have!)… but there’s not enough jailbroken iPhone 12/iPad Pros with iOS 14.x to justify more work on this.

Instead, I only aimed to show that Apple has the power to enable VMs on iPhones, and that they should offer this feature to remain competitive with power users, now that other devices, such as the Pixel 6 on Android 13, is about to launch virtualization support.

I honestly doubt Apple’ll ever enable virtual machines on iPhones, seeing that they intentionally check for A14/A15 to disable virtualization.

However, there’s no check for M1 iPads, so there’s hope… if we find the right way to convince Apple.

Apple, you like service revenue, right? I will pay $10/month to run virtual machines on my iPad. I’m sure I’m not the only one. Think about it…

Thanks

  • Linux Henze for building Fugu14. It’s the most powerful jailbreak in recent memory, yet it’s also the most well-documented jailbreak I’ve ever seen. It’s also modular enough that a script kiddie like me can reuse it for different tasks.
  • The UTM developers for their excellent QEMU port to iOS.
  • Everyone in the community for their support and encouragement.

What I learned

  • How to modify Fugu14 to use its kernel execute functionality without its actual jailbreak
  • How to share kernel execute functionality between processes and threads by modifying an IOUserClient
  • How Hypervisor.framework communicates with the kernel
  • How to extract structs from Kernel Debug Kit
  • How not to run Android (spoiler alert: neither Ranchu (the Android Studio emulator) nor Cuttlefish (the cloud emulator) works in vanilla QEMU)
  • How to build Waydroid, how long it takes on a cloud VM (3 hours - 1 h to download and 2 h to build), how large it is (190GB), and how much it costs ($10). (I didn’t end up using the build, alas…)