Skip to content

Instantly share code, notes, and snippets.

@steven-michaud
Last active October 8, 2026 03:53
Show Gist options
  • Select an option

  • Save steven-michaud/fda019a4ae2df3a9295409053a53a65c to your computer and use it in GitHub Desktop.

Select an option

Save steven-michaud/fda019a4ae2df3a9295409053a53a65c to your computer and use it in GitHub Desktop.
Running Third Party Kernel Extensions on Virtualization Framework macOS Guest VMs

Running Third Party Kernel Extensions on Virtualization Framework macOS Guest VMs

As of macOS 12 (Monterey), Apple's Virtualization framework has nice support for macOS guest virtual machines, but with severe limitations: For example you can't install a macOS guest on Intel Macs, install guests with newer versions of macOS than the host, copy and paste between the host and the guest, or install third party kernel extensions in the guest. As usual for Apple, the functionality they do support is nicely implemented, but they've left out so much that the result is only marginally useful -- at least compared to third-party implementations available on Intel Macs, like VMware Fusion and Parallels.

I've been working on the last of these limitations, and have found a workaround. It's rather complex, but should be very useful for those developing kernel extensions for macOS on Apple Silicon, or (more likely) porting existing Intel kexts to Apple Silicon. Developing kernel extensions on bare metal is a pain -- not least because you might end up damaging your nice new Apple Silicon Mac. But doing it in a virtual machine isolates the potential damage to the VM itself -- at worst you'll only need to trash it and create a replacement.

Parts of my workaround are interesting in themselves, even if you don't use them to run third party kernel extensions on macOS guest VMs. For example I've found a way to patch the VM's kernel cache (containing the kernel and built-in kernel extensions), which should be very useful in reverse engineering macOS.

Most of the discoveries here are ones I've made on my own, by trial and error. But I had a very helpful starting point -- NyanSatan's Virtual-iBoot-Fun project.

Prerequisites

  • An Apple Silicon Mac running macOS 12 or higher. Mine is a 2020 Mac Mini (Macmini9,1).

  • A decent virtual machine host that uses Apple's Virtualization framework. I use UTM.

  • One or more decent disassemblers. I use Ghidra and Hopper Disassembler. I also installed my fork of Nick Botticelli's ghidra-iboot plugin

  • Tihmstar's img4tool.

  • A decent hex editor. I use Hex Fiend. I change its default edit mode to "Overwrite" and its line number format to "Hexadecimal".

  • A binary diff tool. I use VBinDiff.

  • A calculator with support for radix modes. Apple's Calculator in Programmer mode will do.

Background

My workaround patches three iBoot modules and the VM's kernel cache.

The iBoot modules load early in the macOS (and IOS) boot sequence, before the kernel. There are at least three variants of iBoot, used in three diffent "stages" -- Stage 0 (as I call it), Stage 1 and Stage 2. On bare metal, Stage 0 is implemented in hardware (in Apple's "Secure Enclave"). Stage 1 (aka LLB) and Stage 2 (iBoot properly so called) are implemented in software, though only the Stage 2 iBoot exists (inside iBoot.img4) in the macOS file system.

By design, the Secure Enclave Processor (SEP) is inaccessible to ordinary mortals. And on bare metal the LLB and iBoot modules are encrypted (using a key held in the SEP). But for a VM, the modules for all three stages are unencrypted. Stage 0 is implemented (on the host) in /System/Library/Frameworks/Virtualization.framework/Resources/AVPBooter.vmapple2.bin. Stage 1 is embedded (in img4 format) in an AuxiliaryStorage file (on the host) associated with the DMG file (with an img extension) that stores the VM's image. Stage 2 (inside iBoot.img4) exists in the VM's file system.

None of the iBoot variants call any external functions, and none have any symbols. So they're difficult to decipher. But the fact that the Virtualization framework uses them without encryption is a golden opportunity to reverse engineer them, and understand them better.

It's apparently common knowledge that the iBoot modules all contain a function to check the "digests" (DGST) of the many img4 images that govern the macOS boot process (including LLB, iBoot.img4 and the kernel cache itself). Each "digest" is a hash, used to ensure that none of these images has changed since they were "signed" by Apple. The source code for a similar function (image4_validate_property_callback()) is available here.

The images in question are those to be loaded at the next stage. So (among other things) Stage 0 iBoot checks the digest of the Stage 1 iBoot img4 image, Stage 1 iBoot checks the digest of the Stage 2 iBoot img4 file, and Stage 2 iBoot checks the digest of the kernelcache file. Aside from the file system itself, nothing checks the integrity of the stage 0 iBoot file (AVPBooter.vmapple2.bin). So all you need to stop these integrity checks is to defeat the file system's protection and patch this function (call it image4_validate_property_callback()) at every "stage".

As NyanSatan mentions, the standard approach is to patch out image4_validate_property_callback() completely -- to replace it with code that "returns 0". I find this causes problems, so my patch is more surgical -- I allow image4_validate_property_callback() to perform all its normal checks, and only patch its return value (to '0') just before it returns.

Some time ago I discovered that failed calls to _validate_acm_context() in the VM's AppleVPBootPolicy.kext stop the VM from creating an auxiliary kext cache. Since third-party kexts are stored here, this prevents them from loading in a Virtualization framework VM. The failed calls originate from two other functions in AppleVPBootPolicy.kext -- _command_create_linked_manifest() and _command_update_local_policy_for_kcos() (or boop_create_linked_manifest() and boop_update_local_policy_for_kcos() on macOS 27). My workaround is to patch out the calls to _validate_acm_context() from both of these functions. This is a more "surgical" approach than patching out _validate_acm_context() itself, which is called from many additional functions in AppleVPBootPolicy.kext.

But this isn't enough by itself, on versions of macOS prior to 27. An auxiliary kext cache does get created (you can see it using kmutil inspect). But earlier versions of macOS still complain that your third-party kext needs to be rebuilt. To fix this you need to go back to the Stage 2 iBoot module (inside iBoot.img4). One of its purposes is to fill the "device tree" with all the appropriate devices. But it (or the variant available in a Virtualization framework VM) refuses to allow any "AuxKC" entries to be added to chosen/memory-map in a VM, even when the auxiliary kext cache is present. Fixing this requires finding the function that checks whether a given boot object (originally in img4 format) should be processed, and patching out the call to it. Its first parameter is a four-character code -- for example illb, ibot, krnl or auxk. validate_boot_object() (my name for this function) indirectly calls image4_validate_property_callback(). So if you patch out the former you don't need to patch the latter. At some point I'll describe how I found validate_boot_object().

This workaround causes problems of its own on macOS 27: On reboot, it forces you to "re-sign" your VM and remove all your changes to the Stage 2 iBoot module and AppleVPBootPolicy.kext. Fortunately it's no longer needed on macOS 27.

To display the contents of chosen/memory-map, run the following command in a Terminal prompt:

ioreg -p IODeviceTree -n "memory-map" -w 0 -r -t

Settings Changes (Host and Guest)

Some settings changes are required on the host by How to Defang macOS System Protections, which allows changes to AVPBooter.vmapple2.bin.

  • In System Settings, under Software Update, disable "Download new updates when available". Otherwise your host can become unbootable.

  • Boot into Recovery Mode, run Terminal and do the following:

csrutil disable
csrutil authenticated-root disable

You also, of course, need to create a macOS guest VM. Make sure it uses the Virtualization framework. In UTM this is accomplished by choosing "Virtualize" and then "macOS 12+".

Terminal will be used heavily below. So, to avoid lots of annoying prompts to give it permission to access files, it's best to give the Terminal app "full disk access" on the guest (in Privacy and Security in System Settings).

Then boot into Recovery Mode on the guest. On macOS 12 you'll need to use my hack to do this.

  • Run the Startup Security Utility. Choose Reduced Security and "Allow user management of kernel extensions from identified developers".

  • Run Terminal and do the following:

  • csrutil disable, then y to "Allow booting unsigned operating systems and any kernel extensions".

Special Considerations Once You've Made the Changes Described Here

Below you'll create (and boot from) an APFS snapshot on your host computer to accomodate changes to the Stage 0 iBoot module (AVMBooter.vmapple2.bin). You need to keep your host computer in this state as long as you're using third party kernel extensions in your macOS guest VM. (Without the patched AVPBooter.vmapple2.bin it will simply refuse to start.) You also need to keep csrutil's "authenticated-root" disabled, and keep "Download new updates when available" disabled under Software Update. But don't use Software Update to update macOS on your host while it's booted from this custom snapshot. I don't know that this will cause trouble, but I expect it might.

Use the following command to revert your snapshot (and AVPBooter.vmapple2.bin), then reboot your computer:

sudo bless --mount / --last-sealed-snapshot

Guest VMs running release versions of macOS 13 and up can be upgraded to newer versions of macOS. But before you do so you should revert this document's changes to the Stage 2 iBoot module (iBoot.img4) and the kernel cache (kernelcache): Boot into Recovery Mode, run the Startup Security Utility, and choose Full Security. Afterwards you'll need to go through all the following steps again, from scratch. Among other things you'll need to create new iBoot.img4.org and kernelcache.org files, and possibly also a new LLB.img4.org file.

macOS 12 guests can't be upgraded. The Virtualization process (com.apple.Virtualization.VirtualMachine) crashes and your VM becomes unbootable. This is an Apple bug. I've seen it myself, and have seen it documented here. The same is true for beta-version macOS 14 and macOS 15 guests, though the consequences are less severe.

In this case your only option is to create a new guest VM directly from an IPSW file, then work through the following steps.

Finding the Modules to be Patched

iBoot Stage 0 (AVPBooter.vmapple2.bin)

This one's on the host, and is easy to find. Copy it and rename the copy to AVPBooter.vmapple2.bin.org.

/System/Library/Frameworks/Virtualization.framework/Resources/AVPBooter.vmapple2.bin

iBoot Stage 1 (LLB)

This one's also on the host, but is harder to find. By default, VMs created by UTM are stored in the following directory:

~/Library/Containers/com.utmapp.UTM/Data/Documents

Each VM is stored in a "package" with the extension .utm. Inside the package, in its Data subdirectory, is a file named AuxiliaryStorage. LLB is embedded here, in img4 format. It's followed immediately by an img4 format image of the "logo". You'll need to get copies of both.

If you've upgraded your guest VM at least once to a newer version of macOS, there will be two copies of the LLB image in the AuxiliaryStorage file, each followed by a "logo" image. The first LLB image is always at offset 0x24000. The second, if it exists, should be at offset 0x224000. To be sure you've found all the LLB images, and their correct locations, search (in your hex editor) on "illb" (the four character code for the LLB image).

Only one of these two sets of images is active. Each time you upgrade macOS, the "other" LLB/logo image set becomes active. The original installation uses the set at offset 0x24000. The first upgrade switches to the one at offset 0x224000 (and leaves the set at offset 0x24000 unchanged). The second upgrade switches back to the one at offset 0x24000 and overwrites it (leaving the one at 0x224000 unchanged). And so forth.

Here's how to find out which set is active.

At offsets 0x4000 and 0x5000 in AuxiliaryStorage are two more file images, each starting with "HUFA". Each corresponds to one of the LLB/logo image sets. If you only have one LLB/logo image set, only one "HUFA" image exists, at offset 0x4000. The structure of these "HUFA" images is as follows:

struct HUFA {
  unsigned char magic[4];   // Always "HUFA" (unknown meaning).
  uint32_t file_version;    // Always '1'.
  uint32_t upgrade_count;   // 'n' == which (re)installation of macOS
                            // created this HUFA image and its
                            // corresponding LLB/logo images. '1'
                            // means the original installation. '2'
                            // and up mean each subsequent upgrade.
  uint32_t LLB_offset;      // Offset of corresponding LLB/logo images
                            // from the start of the first HUFA image
                            // at offset 0x4000 -- either 0x20000 or
                            // 0x220000.
  uint32_t unknown[4];
  unsigned char hash[32];   // Some kind of hash value -- type and
                            // target unknown.
  unsigned char fill[4032]; // Filled with 0xff.
};

If there are two sets of LLB/logo images, the one that's active has the highest upgrade_count. This is the one you'll need to work on.

  • Open AuxiliaryStorage in a hex editor. I use Hex Fiend, and will tailor my steps to it specifically.

  • Jump to the offset of the active set of LLB/logo images -- 0x24000 or 0x224000. If an LLB image exists at this location, its first two bytes should be 3083.

  • The img4 image is in DER format (used by ASN1), so the first five bytes are 3083 followed by a three digit hexadecimal number in big endian format (for example 037FD1). This number is the length of the image, exclusive of the length of its header. So the total length is 0x37FD6.

  • Select the first five bytes of the image, then choose "Extend Selection". In this case, you'd extend it by 0x37FD1 bytes. Scroll down to the end of the selection (without disturbing it) and check that it's in the correct location (just before the "logo" image). If it is, CMD-C to copy the image, CMD-N to open a new window, CMD-V to paste in its contents, and save the file as LLB.img4.org.

  • The header for the "logo" image should be 3082 followed by a two digit hexdecimal number (for example 36DA). Select the first four bytes, extend the selection by 0x36DA bytes and scroll down to its end. The following bytes should be a bunch of NULLs (00). CMD-C to copy the image, CMD-N to open another new window, CMD-V to paste in its contents, and save this second new file as logo.img4.org.

iBoot Stage 2 (iBoot.img4) and the Kernel Cache (kernelcache)

These modules are stored in files on the VM. So run the VM and do the following in it, at a Terminal prompt:

  • Run kmutil inspect, and observe where the boot kernel cache exists in the VM's file system. The following is an example, which will get used in the following steps. The path's exact contents will differ from case to case.
/System/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/System/Library/Caches/com.apple.kernelcaches/kernelcache
  • Note the long hexadecimal number just before /System/Library/Caches -- in this case FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83. This is the "Next Stage Image4 Hash (nsih)", observable in the output of sudo bputil -d.

  • Copy this file and rename it to kernelcache.org.

  • cd / and sudo find . -name iBoot.img4 -exec ls -al \{\} \;. There may be more than one hit. If so, choose the one whose path contains the Next Stage Image4 Hash. Copy it and rename the copy to iBoot.img4.org. You might find the original iBoot.img4 here, for example:

/System/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/usr/standalone/firmware/iBoot.img4

Patching These Modules

iBoot Stage 0 (AVPBooter.vmapple2.bin.org)

For this I use Ghidra with the ghidra-iboot plugin. Copy AVPBooter.vmapple2.bin.org to AVPBooter.vmapple2.bin. Then run Ghidra and create a new project. Then:

  • Run Ghidra's CodeBrowser and import AVPBooter.vmapple2.bin.

  • Choose Search : Program Text : All Fields. Type "0x4447" ('DG' of DGST) in "Search for", then choose "Search All". You should find two hits, both in the same function. Click on one to move the cursor to its location.

  • Once again choose Search : Program Text : All Fields, and type "retab" in "Search For". Then choose "Next". Now you should see the code that runs just before the function returns. It will look something like as follows. In this example, you want to change the instruction at address 0x00102a0c (mov x0,x20) to mov x0,#0x0. (AARCH64 machine code uses the X0 register to store a return value.)

001029f4 a8 83 5a f8     ldur       x8,[x29, #local_68]
001029f8 29 f9 37 d0     adrp       x9,0x70028000
001029fc 1f 20 03 d5     nop
00102a00 29 a1 40 f9     ldr        x9,[x9, #0x140]=>DAT_70028140
00102a04 3f 01 08 eb     cmp        x9,x8
00102a08 21 06 00 54     b.ne       LAB_00102acc
00102a0c e0 03 14 aa     mov        x0,x20
00102a10 fd 7b 4c a9     ldp        x29=>local_10,x30,[sp, #0xc0]
00102a14 f4 4f 4b a9     ldp        x20,x19,[sp, #local_20]
00102a18 f6 57 4a a9     ldp        x22,x21,[sp, #local_30]
00102a1c f8 5f 49 a9     ldp        x24,x23,[sp, #local_40]
00102a20 fa 67 48 a9     ldp        x26,x25,[sp, #local_50]
00102a24 fc 6f 47 a9     ldp        x28,x27,[sp, #local_60]
00102a28 ff 43 03 91     add        sp,sp,#0xd0
00102a2c ff 0f 5f d6     retab
  • Right click on the mov x0,0x20 instruction and choose "Patch Instruction". Then change x0,0x20 to x0,#0x0.

  • In Ghidra's CodeBrowser, choose File : Export Program. Then choose Format : Raw Bytes, and overwrite AVPBooter.vmapple2.bin.

  • Use vbindiff AVPBooter.vmapple2.bin.org AVPBooter.vmapple2.bin to check your results. There should be just one change, to a four-byte value -- the length of one AARCH64 instruction.

iBoot Stage 1 (LLB.img4)

All the other modules that need patching are "wrapped" in img4 format. So to get at their actual content you need to use img4tool to unpack them. Then you'll patch them and rewrap them in new img4 format files.

  • img4tool -e -p LLB.im4p.org LLB.img4.org

  • img4tool -e -m LLB.im4m.org LLB.img4.org

  • img4tool -e -o LLB.bin.org LLB.im4p.org

The iBoot Stage 1 module should now be in LLB.bin.org. Copy it to LLB.bin and patch it according to the instructions for the iBoot Stage 0 binary (AVPBooter.vmapple2.bin) above.

  • Run img4tool LLB.im4p.org, which will produce output something like what follows. Use the information from it to run the next command. The value for "desc" differs from one version of macOS to another. Note that you do not want to use compression, even though img4tool supports it. img4tool uses Apple's libcompression.dylib to implement its compression (and decompression). But, even though it's the same type ("bvx2"), it's incompatible with the decompression used by the iBoot binaries, which don't have access to any external modules.
img4tool version: 0.199-ed194718f9d6a035a432f2fdfe9fc639b72cba6c-RELEASE
Compiled with plist: YES
IM4P: ---------
type: illb
desc: iBoot-8422.141.2
size: 0x000369df

Compression: bvx2
Uncompressed size: 0x0006dc90
IM4P does not contain KBAG values
  • img4tool -c LLB.im4p -t illb -d "iBoot-8422.141.2" LLB.bin

  • img4tool -c LLB.img4 -p LLB.im4p -m LLB.im4m.org

The patched iBoot Stage 1 module should now be in LLB.img4.

iBoot Stage 2 (iBoot.img4)

Once again you'll need to unwrap the iBoot Stage 2 binary, patch it, and then rewrap it in img4 format. But this time the process is more complicated: iBoot.im4p.org contains a PAYP structure, tacked on to its end, that you'll need to append to iBoot.im4p by hand.

  • img4tool -e -p iBoot.im4p.org iBoot.img4.org

  • img4tool -e -m iBoot.im4m.org iBoot.img4.org

  • img4tool -e -o iBoot.bin.org iBoot.im4p.org

Here's how to copy the PAYP structure from iBoot.im4p.org to a seperate file, which you'll later append to iBoot.im4p.

  • openssl asn1parse -inform der -in iBoot.im4p.org -i -dump

This outputs the entire iBoot.im4p.org file in human-readable ASN1 format. The PAYP structure appears at the end, and looks something like this. Use the structure's offset (259719 in this case) in the next command.

259719:d=1  hl=2 l=  28 cons:  cont [ 0 ]        
259721:d=2  hl=2 l=  26 cons:   SEQUENCE          
259723:d=3  hl=2 l=   4 prim:    IA5STRING         :PAYP
259729:d=3  hl=2 l=  18 cons:    SET               
259731:d=4  hl=7 l=  11 cons:     priv [ 1768907638 ] 
259738:d=5  hl=2 l=   9 cons:      SEQUENCE          
259740:d=6  hl=2 l=   4 prim:       IA5STRING         :iocv
259746:d=6  hl=2 l=   1 prim:       INTEGER           :03
  • xxd -p -s 259719 iBoot.im4p.org iBoot.payp.hex

iBoot.payp.hex is a hex dump. The following command will convert it to a binary (in DER format):

  • xxd -p -r iBoot.payp.hex iBoot.payp.bin

Check the contents of iBoot.payp.bin by running the following command:

openssl asn1parse -inform der -in iBoot.payp.bin -i

Now copy iBoot.bin.org to iBoot.bin. On macOS 27, patch it according to the instructions for the iBoot Stage 0 binary (AVPBooter.vmapple2.bin) above. Then skip ahead to For all macOS versions.

For macOS versions prior to 27

On earlier versions of macOS you'll need to patch out the call(s) to validate_boot_object(). As mentioned above, you can do this instead of patching image4_validate_property_callback().

e5 03 04 aa 
04 00 80 52 
  • In the Ghidra CodeBrowser, choose Search : For Instruction Patterns, then Edit Bytes and Input Mode Hex. Copy the above two lines of hexadecimal code and paste it into the Edit Bytes box, then choose Apply. Choose Search All and you should find one hit, with code that looks like the following:
                     FUN_700ac1fc                                    XREF[1]:     FUN_70063a78:70063fbc(c)  
700ac1fc e5 03 04 aa     mov        x5,x4
700ac200 04 00 80 52     mov        w4,#0x0
700ac204 01 00 00 14     b          LAB_700ac208

                     LAB_700ac208                                    XREF[1]:     700ac204(j)  
700ac208 7f 23 03 d5     pacibsp
700ac20c ff 03 03 d1     sub        sp,sp,#0xc0
  • Double-click on the cross reference (FUN_70063a78:70063fbc(c) in this case). That should take you to code that looks like this:
70063fb0 e1 e3 0c 91     add        param_2,sp,#0x338
70063fb4 e4 43 03 91     add        param_5,sp,#0xd0
70063fb8 e3 03 16 aa     mov        param_4,x22
70063fbc 90 20 01 94     bl         FUN_700ac1fc                                     undefined FUN_700ac1fc()
70063fc0 a0 03 00 34     cbz        param_1,LAB_70064034
70063fc4 14 7b 00 51     sub        w20,w24,#0x1e
70063fc8 07 00 00 14     b          LAB_70063fe4
  • In this example, right-click on the bl FUN_700ac1fc instruction and change it to mov x0,#0x0.

  • On recent versions of macOS, there may be more than one cross reference. In this case you should double-click on each one, in turn, and change its target to mov x0,#0x0.

For all macOS versions

  • Choose File : Export Program. Then choose Format : Raw Bytes, and overwrite iBoot.bin.

  • Run img4tool iBoot.im4p.org, which will produce output something like this. Use the information from it to run the next command. The value of "desc" differs from one version of macOS to another.

img4tool version: 0.199-ed194718f9d6a035a432f2fdfe9fc639b72cba6c-RELEASE
Compiled with plist: YES
IM4P: ---------
type: ibot
desc: iBoot-8422.141.2
size: 0x0003f655

Compression: bvx2
Uncompressed size: 0x0007dde8
PAYP:
iocv: iocv: 3

IM4P does not contain KBAG values
  • img4tool -c iBoot.im4p -t ibot -d "iBoot-8422.141.2" iBoot.bin

  • dd if=iBoot.payp.bin >> iBoot.im4p

  • Use Hex Fiend to open iBoot.im4p and correct its length value. Make sure File : Mode is set to Override.

  • Observe iBoot.im4p's five-byte header and length value -- for example 308307DE0B. Convert the length (0x7DE0B) to decimal (515595) and add 30 (for the length of iBoot.payp.bin in this case). So the new length in this case is 515625 (== 0x7DE29).

  • Correct the length value. The header in this case will now be 308307DE29. Save iBoot.im4p.

  • img4tool -c iBoot.img4 -p iBoot.im4p -m iBoot.im4m.org

The patched Stage 2 module should now be in iBoot.img4.

The Kernel Cache (kernelcache)

As with the iBoot Stage 2 module, you'll need to unwrap the kernel cache, patch it, and rewrap it in img4 format.

  • img4tool -e -p kernelcache.im4p.org kernelcache.org

  • img4tool -e -m kernelcache.im4m.org kernelcache.org

  • img4tool -e -o kernelcache.bin.org kernelcache.im4p.org

kernelcache.im4p.org, like iBoot.im4p.org, has a PAYP structure at its end. Copy the PAYP structure to a separate file, which you'll later append to kernelcache.im4p.

  • openssl asn1parse -inform der -in kernelcache.im4p.org -i -dump

This outputs the entire kernelcache.im4p.org file in human-readable ASN1 format. The PAYP structure appears at the end, and looks something like this. Use the structure's offset (18030138 in this case) in the next command.

18030138:d=1  hl=3 l= 186 cons:  cont [ 0 ]        
18030141:d=2  hl=3 l= 183 cons:   SEQUENCE          
18030144:d=3  hl=2 l=   4 prim:    IA5STRING         :PAYP
18030150:d=3  hl=3 l= 174 cons:    SET               
18030153:d=4  hl=7 l=  19 cons:     priv [ 1801676144 ] 
18030160:d=5  hl=2 l=  17 cons:      SEQUENCE          
18030162:d=6  hl=2 l=   4 prim:       IA5STRING         :kcep
18030168:d=6  hl=2 l=   9 prim:       INTEGER           :FFFFFE0007B7D488
18030179:d=4  hl=7 l=  14 cons:     priv [ 1801677926 ] 
18030186:d=5  hl=2 l=  12 cons:      SEQUENCE          
18030188:d=6  hl=2 l=   4 prim:       IA5STRING         :kclf
18030194:d=6  hl=2 l=   4 prim:       INTEGER           :030E4000
18030200:d=4  hl=7 l=  19 cons:     priv [ 1801677935 ] 
18030207:d=5  hl=2 l=  17 cons:      SEQUENCE          
18030209:d=6  hl=2 l=   4 prim:       IA5STRING         :kclo
18030215:d=6  hl=2 l=   9 prim:       INTEGER           :FFFFFE0007004000
18030226:d=4  hl=7 l=  14 cons:     priv [ 1801677946 ] 
18030233:d=5  hl=2 l=  12 cons:      SEQUENCE          
18030235:d=6  hl=2 l=   4 prim:       IA5STRING         :kclz
18030241:d=6  hl=2 l=   4 prim:       INTEGER           :AF0000
18030247:d=4  hl=7 l=  11 cons:     priv [ 1801679462 ] 
18030254:d=5  hl=2 l=   9 cons:      SEQUENCE          
18030256:d=6  hl=2 l=   4 prim:       IA5STRING         :kcrf
18030262:d=6  hl=2 l=   1 prim:       INTEGER           :00
18030265:d=4  hl=7 l=  14 cons:     priv [ 1801679482 ] 
18030272:d=5  hl=2 l=  12 cons:      SEQUENCE          
18030274:d=6  hl=2 l=   4 prim:       IA5STRING         :kcrz
18030280:d=6  hl=2 l=   4 prim:       INTEGER           :02C64000
18030286:d=4  hl=7 l=  14 cons:     priv [ 1801680742 ] 
18030293:d=5  hl=2 l=  12 cons:      SEQUENCE          
18030295:d=6  hl=2 l=   4 prim:       IA5STRING         :kcwf
18030301:d=6  hl=2 l=   4 prim:       INTEGER           :02C64000
18030307:d=4  hl=7 l=  13 cons:     priv [ 1801680762 ] 
18030314:d=5  hl=2 l=  11 cons:      SEQUENCE          
18030316:d=6  hl=2 l=   4 prim:       IA5STRING         :kcwz
18030322:d=6  hl=2 l=   3 prim:       INTEGER           :480000
  • xxd -p -s 18030138 kernelcache.im4p.org kernelcache.payp.hex

kernelcache.payp.hex is a hex dump. The following command will convert it to a binary (in DER format):

  • xxd -p -r kernelcache.payp.hex kernelcache.payp.bin

Check the contents of kernelcache.payp.bin by running the following command:

openssl asn1parse -inform der -in kernelcache.payp.bin -i

Now copy kernelcache.bin.org to kernelcache.bin. Ghidra doesn't work well with kernel cache files, so I use Hopper Disassembler to patch kernelcache.bin.

  • In Hopper choose Read Executable to Disassemble (CMD-SHIFT-O) and select kernelcache.bin. Then click on Loader, scroll down to com.apple.security.AppleVPBootPolicy and click OK.

  • Select Labels, type "_validate_acm_context" and click on it in the list below. This should take you to the function, whose top few lines should look like this:

                     __validate_acm_context:
fffffe0008be57ec         pacibsp
fffffe0008be57f0         sub        sp, sp, #0x40
fffffe0008be57f4         stp        x20, x19, [sp, #0x20]
fffffe0008be57f8         stp        fp, lr, [sp, #0x30]
fffffe0008be57fc         add        fp, sp, #0x30
fffffe0008be5800         sturb      wzr, [fp, var_11]
fffffe0008be5804         cbz        w0, loc_fffffe0008be5868
  • You want to find all calls to _validate_acm_context() from boop_create_linked_manifest() and boop_update_local_policy_for_kcos() on macOS 27, or _command_create_linked_manifest() and _command_update_local_policy_for_kcos() on earlier versions of macOS. You'll patch each one out by replacing it with a nop instruction.

  • Click on _validate_acm_context()'s first line (pacibsp), then press the "x" key. This should open a dialog listing all the calls to _validate_acm_context() from elsewhere in AppleVPBootPolicy. Find [...]_create_linked_manifest() and [...]_update_local_policy_for_kcos() in this list, and click on each of them in turn. The calls to _validate_acm_context() should look something like this:

fffffe0008bd4e74         add        x22, x21, #0x66
fffffe0008bd4e78         mov        w0, #0x1
fffffe0008bd4e7c         mov        x1, x22
fffffe0008bd4e80         bl         __validate_acm_context
fffffe0008bd4e84         tbnz       w0, 0x0, loc_fffffe0008bd4ea8
  • Select the call to _validate_acm_context() (bl __validate_acm_context) and change to Hexadecimal mode (from ASM mode). For each of the following four hex values, double-click on it and replace it with values from the following list (binary code for a nop instruction). (Hit Enter to update each value after you've typed in its replacement.)
1f 20 03 d5
  • Repeat the previous two steps until you've patched all the calls to _validate_acm_context() from both [...]_create_linked_manifest() and [...]_update_local_policy_for_kcos().

  • Choose File : New Executable and overwrite kernelcache.bin

  • Use vbindiff kernelcache.bin.org kernelcache.bin to check your results. There should be two or more changes, each to a four-byte value -- the length of a single AARCH64 instruction.

  • Run img4tool kernelcache.im4p.org, which will produce output something like this. Use the information from it to run the next command. The value of "desc" differs from one version of macOS to another. Note that some versions of img4tool can't yet deal with kernelcache.im4p.org's PAYP structure.

img4tool version: 0.199-ed194718f9d6a035a432f2fdfe9fc639b72cba6c-RELEASE
Compiled with plist: YES
IM4P: ---------
type: krnl
desc: KernelManagement_host-354.140.3
size: 0x01131df6

Compression: bvx2
Uncompressed size: 0x03bd4000
PAYP:
kcep: kcep: [Error] img4tool: failed with exception:
[exception]:
what=assure failed
code=15597586
line=238
file=ASN1DERElement.cpp
commit count=199
commit sha  =ed194718f9d6a035a432f2fdfe9fc639b72cba6c
  • img4tool -c kernelcache.im4p -t krnl -d "KernelManagement_host-354.140.3" kernelcache.bin

  • dd if=kernelcache.payp.bin >> kernelcache.im4p

  • Use Hex Fiend to open kernelcache.im4p and correct its length value. Make sure File : Mode is set to Override.

  • Observe kernelcache.im4p's six-byte header and length value -- for example 308403BD4033. Convert the length (0x03BD4033) to decimal (62734387) and add 189 (for the length of kernelcache.payp.bin in this case). So the new length in this case is 62734576 (== 0x3BD40F0).

  • Correct the length value. The header in this case will now be 308403BD40F0. Save kernelcache.im4p.

  • img4tool -c kernelcache -p kernelcache.im4p -m kernelcache.im4m.org

The patched kernel cache should now be in kernelcache.

Copying the Patched Modules to their Final Destinations

iBoot Stage 0 (AVPBooter.vmapple2.bin)

AVPBooter.vmapple2.bin is a system file. By default it's protected by the macOS file system, and can't be changed. To get around this I borrow from How to Defang macOS System Protections. Before you follow these steps, you must make the settings changes I described above under Settings Changes.

  • mkdir /tmp/mount

  • Run mount at a Terminal prompt and observe its results, for example as follows. Use the contents of the first line in the next command.

/dev/disk5s1s1 on / (apfs, sealed, local, read-only, journaled)
devfs on /dev (devfs, local, nobrowse)
/dev/disk5s6 on /System/Volumes/VM (apfs, local, noexec, journaled, noatime, nobrowse)
/dev/disk5s2 on /System/Volumes/Preboot (apfs, local, journaled, nobrowse)
/dev/disk5s4 on /System/Volumes/Update (apfs, local, journaled, nobrowse)
/dev/disk1s2 on /System/Volumes/xarts (apfs, local, noexec, journaled, noatime, nobrowse)
/dev/disk1s1 on /System/Volumes/iSCPreboot (apfs, local, journaled, nobrowse)
/dev/disk1s3 on /System/Volumes/Hardware (apfs, local, journaled, nobrowse)
/dev/disk5s5 on /System/Volumes/Data (apfs, local, journaled, nobrowse, protect)
/dev/disk2s3 on /Volumes/Boot3 (apfs, sealed, local, read-only, journaled)
/dev/disk7s3 on /Volumes/Boot2 (apfs, sealed, local, read-only, journaled)
/dev/disk4s1 on /Volumes/Boot4 - Data (apfs, local, journaled, nobrowse, protect)
/dev/disk2s1 on /Volumes/Boot3 - Data (apfs, local, journaled, nobrowse, protect)
/dev/disk4s3 on /Volumes/Boot4 (apfs, sealed, local, read-only, journaled)
/dev/disk6s1 on /Volumes/Boot1 - Data (apfs, local, journaled, nobrowse, protect)
/dev/disk6s3 on /Volumes/Boot1 (apfs, sealed, local, read-only, journaled)
/dev/disk7s1 on /Volumes/Boot2 - Data (apfs, local, journaled, nobrowse, protect)
map auto_home on /System/Volumes/Data/home (autofs, automounted, nobrowse)
  • sudo mount -o nobrowse -t apfs /dev/disk5s1 /tmp/mount

  • cd /tmp/mount/System/Library/Frameworks/Virtualization.framework/Resources

  • sudo cp /path/to/patched/AVPBooter.vmapple2.bin .

  • sudo bless --mount /tmp/mount --bootefi --create-snapshot

  • Reboot your host computer.

On reboot, check the contents of /System/Library/Frameworks/Virtualization.framework/Resources/AVPBooter.vmapple2.bin to make sure it's what you expect.

You need to keep your host computer in this state as long as you're using third party kernel extensions in your macOS guest VM. (Without the patched AVPBooter.vmapple2.bin it will simply refuse to start.)

If need be, use the following command to revert your snapshot (and AVPBooter.vmapple2.bin), then reboot your host computer:

sudo bless --mount / --last-sealed-snapshot

iBoot Stage 1 (LLB.img4)

You'll need to copy both LLB.img4 and logo.img4.org over their original contents in AuxiliaryStorage. This is because LLB.img4 might be a different length than LLB.img4.org, and Apple's virtualization infrastructure expects the logo image to immediately follow the LLB image. Above you determined which set of these images to work with. Now you'll be copying your changes over the original set.

  • Use Hex Fiend to open LLB.img4, logo.img4.org and AuxiliaryStorage. Make sure Edit : Mode for AuxilaryStorage is Overwrite.

  • Jump to offset of the active set of LLB/logo images (either 0x24000 or 0x224000) in AuxiliaryStorage. If an LLB image exists at this location, its first two bytes should be 3083.

  • Copy the contents of LLB.img4 (CMD-A, CMD-C) and paste them into AuxilaryStorage.

  • Copy the contents of logo.img4.org and paste them into AuxiliaryStorage, then save the file.

iBoot Stage 2 (iBoot.img4) and the Kernel Cache (kernelcache)

Copy these files to your macOS guest VM and do the following there:

  • Run kmutil inspect, and observe where the boot kernel cache exists in the VM's file system. The following is an example, which will get used in the following steps. The path's exact contents will differ from case to case.
/System/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/System/Library/Caches/com.apple.kernelcaches/kernelcache
  • Note the "Next Stage Image4 Hash" just before /System/Library/Caches -- in this case FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83.

  • sudo cp kernelcache /System/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/System/Library/Caches/com.apple.kernelcaches/kernelcache

  • Run cd / and sudo find . -name iBoot.img4 -exec ls -al \{\} \;. Observe the hit whose path contains the Next Stage Image4 Hash. Use the results in the following command. For example:

  • sudo cp iBoot.img4 /System/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/usr/standalone/firmware/iBoot.img4

Shut down your your macOS guest VM and reboot it into Recovery Mode. Then perform the following steps. You'll be looking for the same Next Stage Image4 Hash as in the previous steps.

Run Terminal in Recovery Mode, then do the following:

  • Run cd / and find . -name iBoot.img4 -exec ls -al \{\} \;. Expect two hits this time, which should look like the following:
/System/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/usr/standalone/firmware/iBoot.img4
/System/Volumes/Data/private/tmp/Recovery/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/usr/standalone/firmware/iBoot.img4
  • Copy iBoot.img4 over both of them.

You may also see another hit that looks like the following. Ignore it. You can tell by its date and file size that you already copied the patched iBoot.img4 over it above.

/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/usr/standalone/firmware/iBoot.img4
  • Run cd / and find . -name kernelcache -exec ls -al \{\} \;. Once again expect two hits, which should look like the following (maybe plus one superfluous hit, as above).
/System/Volumes/Preboot/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/System/Library/Caches/com.apple.kernelcaches/kernelcache
/System/Volumes/Data/private/tmp/Recovery/8467D650-E8D9-4F4C-9403-F51E736C25B0/boot/FB72884642D3490C1D6A0C25D6901AD49BFF7A168B2A851361DD40B393D5FE8E730EF7417B83303610CA04EF15C5CD83/System/Library/Caches/com.apple.kernelcaches/kernelcache
  • Copy kernelcache over both of them.

    Sometimes the first command fails with the following error: fcopyfile failed: No space left on device. But this doesn't matter if the second command succeeds. Whatever's in /System/Volumes/Data/private/tmp/Recovery/ gets automatically copied to /System/Volumes/Preboot/ on reboot, if need be.

  • Reboot your macOS guest VM and start playing with third-party kernel extensions on it!

@steven-michaud

Copy link
Copy Markdown
Author

Am I doing everything right?

I really have no way to tell. Maybe you should put the whole thing aside for a couple of weeks, and then try again, step by step, very carefully. Maybe you'll see something you missed. Sorry I can't offer more help. Let me know if you do find out what the problem was.

@csrXamfi

Copy link
Copy Markdown

I figured it out. I'm copying LLB.img4 to the aux storage incorrectly.

@steven-michaud

Copy link
Copy Markdown
Author

Glad to hear it!

@csrXamfi

Copy link
Copy Markdown

I can load the kext into the virtual machine. Thank you so much for this method! The error was in LLB. Thank you, you're the best.

@dariaphoebe

Copy link
Copy Markdown

macOS Golden Gate dropped in the last few minutes. I'm currently 4000 miles from the machine I'd test on; I aim to try before the WWDC Virtualization Q&A session tomorrow at 1pm US/Eastern but it's unclear I'll be in a position to actually go and ask questions if they still haven't fixed things so auxKC works.

@dariaphoebe

dariaphoebe commented Jun 10, 2026 •

Copy link
Copy Markdown

Host Tahoe guest Golden Gate both unmodified, still fails, just as a point of information. SDErrorDomain error 104 when you try to change security policy.

@dariaphoebe

Copy link
Copy Markdown

It looks like the symbols have changed in the current Golden Gate beta’s “com.apple.security.AppleVPBootPolicy”; _validate_acm_context has callers which are named in a new namespace, _boop, _boop_create_linked_manifest has 2 call sites for _validate_acm_context. System boots and did not need to be re-signed, with the mods, but my kext did not immediately work. I need to investigate further. A Tahoe VM (modified) on the same host works fine for 3rd party kexts so it’s not a host issue.

@dariaphoebe

Copy link
Copy Markdown

Fresh Golden Gate RC vm, host system is running 26.6.2 with an updated DeviceSupport from July 31. AVPBooter mod is in place and works correctly with a 26.6.2 guest on the same system. AuxiliaryStorage has just one LLB in it, and it's updated as described here. iBoot has two sites where validate_boot_object needs to be patched out, as has been true in recent versions. Having done so on the guest where csrutil disable has been run and kexts enabled, iBoot copied into the NSIH location during the normal boot, booted into recovery and also copied into the Preboot and Recovery locations, then booting, forces me to re-sign the OS during the following normal boot. No changes made to kernelcache (nor, obviously, copied in).

I had gotten to this state with the 8th beta but wanted to confirm it, and since the release candidate dropped yesterday I did so with a fresh install of that. It seems something else has also changed, beyond the boop_ function family changes I noted in kernelcache.

@lundman

lundman commented Sep 12, 2026 •

Copy link
Copy Markdown

OK, so been working on a PoC with Claude, and here is its summary:


Following up on my earlier post — I've spent some time (with a lot of AI-assisted heavy lifting) trying to automate @steven-michaud's method so I can smoke-test the OpenZFS kext on each new macOS release without buying hardware for every version. Sharing concrete progress and one genuine wall, in case it's useful.

Environment: macOS 26.5 host, macOS 26 (Tahoe) guest, UTM 4.7.5, Apple/Virtualization.framework backend, Apple Silicon.

1. Patching AVPBooter without touching the host (no snapshot/bless)

On macOS 26 the VM runs inside the com.apple.Virtualization.VirtualMachine XPC service, which opens AVPBooter with a plain open() of .../Versions/A/Resources/AVPBooter.vmapple2.bin (confirmed with dtrace). The check is path-based: redirecting that open() via lldb to a copy at a different path makes VZ hit a brk/assert. But @saagarjha's union-mount idea still works on Tahoe:

sudo mount -t hfs -o union,nobrowse /dev/diskN <framework>/Versions/A/Resources

with a tiny HFS image carrying the patched AVPBooter.vmapple2.bin. VZ opens the genuine system path and gets our bytes. It's ephemeral (reverts on reboot), needs no bless/snapshot, and never modifies the host file. Verified both ways: identical overlay → boots; corrupted overlay → "Internal Virtualization error".

2. Toolchain

pyimg4 + capstone + radare2. Patched payloads are rewrapped uncompressed, reusing the original .im4m (no re-signing needed — the parent stage's patched digest check ignores the now-stale manifest).

3. Patch targets — one clarification for LLB

  • AVPBooter + iBoot Stage 2: validate_boot_object (byte sig e5 03 04 aa 04 00 80 52), null the bl callers → mov x0,#0. As documented.
  • LLB (Stage 1): validate_boot_object is the wrong target here — nulling its callers corrupts the boot (I got a single-core spin, no Apple logo). The correct target is image4_validate_property_callback (the DGST validator), which I found via the DGST fourcc constant (mov w8,#0x5354; movk w8,#0x4447,lsl#16 = 0x44475354, built twice in the function — matching the gist's "two hits in the same function"). Patch the move-into-x0 right before the retab epilogue (on this build it was mov x0,x27, not x20). This matches the gist's AVPBooter DGST-return method; just flagging that the validate_boot_object shortcut is iBoot-Stage-2-only.
  • kernelcache: nop the bl _validate_acm_context inside _command_create_linked_manifest and _command_update_local_policy_for_kcos. On Tahoe (26) these still use the old names; the _boop_* rename @dariaphoebe noted is Golden Gate (27).

4. Result — past the ACM wall

Full chain boots to desktop, and BOOTPOLICY_ERROR_ACM / "SEP command 33 returned 7" is gone. The auxKC now actually builds and signs: kernelmanager_helper logs Collection wrapped successfully into im4p, and a fresh linked-manifest .img4 appears in iSCPreboot/.../LocalPolicy/. The kext (Developer-ID signed + notarized) is approved in the policy DB and correctly in the build recipe.

5. Tahoe-era gotchas beyond the gist

  • kcgend now carries an AMFI Launch Constraint — invoking it manually is SIGKILLed (AMFI: Launch Constraint Violation). The old "just run sudo /usr/libexec/kcgend" trick now needs amfi_get_out_of_my_way=1 (which in turn needs csrutil disable so boot-args are settable).
  • hw.osenvironment is empty in the guest — the VM never enters the kcgen boot that kcgend's LimitLoadToHardware requires.
  • Worth double-checking SIP: my cloned guest was "Reduced Security + allow 3rd-party kexts" but SIP was still enabled; csrutil disable (the gist prerequisite) had to be redone.

6. Where it's stuck (the real wall)

The auxKC builds + signs, but the kernel won't finalize/load it:

kernel reports daemon is not active: Kernel request failed: (iokit/common) not ready (-536870184)
Auxkc not found: no auxiliary collection found
Prompting for complete auxiliary collection rebuild because of: 'error'

No AuxiliaryKernelExtensions.kc is ever persisted, and auxi stays absent → rebuild loop. AppleKIS and AppleARMWatchdogTimer are both absent from the VM kernelcache.

So at this point every signature/validation gate is defeated — custom-ROM (AVPBooter), all four img4 digest checks (LLB/iBoot/kernelcache), the ACM check, and the kext itself is valid+notarized+approved. What's left isn't a lock to pick; it's the kernel-side kcgend↔kernel coordination that VZ guests simply don't wire up (on bare metal this leans on AppleARMWatchdogTimer). That lines up with the "auxKC isn't supported on macOS VMs / works as designed" FB response.

Next thing I may try is @steven-michaud's separate AppleARMWatchdogTimer.kext-into-guest-kernelcache experiment. If anyone has the auxKC actually blessing on a Tahoe guest, I'd love to know what's different (KDK installed? recovery-area copies present? something that gets osenvironment=kcgen?).

Huge thanks to @steven-michaud and @dariaphoebe — all of this is built on your reverse engineering.


edit:

Possibly the real error

Kcgen roundtrip failed with: Invalid argument: bless2_install_custom_obj() failed: 103

@steven-michaud

steven-michaud commented Sep 12, 2026 •

Copy link
Copy Markdown
Author

Frankly, it sounds like Claude has gone down a rabbit hole. The analysis's individual pieces make sense, at least sort of. But the whole leads nowhere. My own adventures in reverse engineering often end up in rabbit holes. And even successful ones can look like rabbit holes until close to the end. But I'm a much better judge than Claude of whether or not this has happened.

I'm very skeptical that this document can be simplified in any significant way, or that large parts of it can be turned into scripts. But if the work is going to be done, I suspect it'll need to be the old fashioned way, without AI. Later, when/if the work's succeeded, parts of Claude's analysis may seem prescient. And they may even be helpful. But I think it's decades, at least, before LLM AIs can handle this kind of problem on their own.

@steven-michaud

Copy link
Copy Markdown
Author

I've been busy with other things, and haven't even tried my document on macOS 27. I'll be working on that over the next few weeks.

@lundman

lundman commented Sep 14, 2026

Copy link
Copy Markdown

OK right, so proof-of-concept, making Claude follow your instructions:

  125    0 0                  0x722b8    0x722b8    org.openzfsonosx.zfs (2.4.3) AE7B354C-79D2-36C1-BDF0-5E8241A69F95 <19 7 5 4 3 1>
  126    0 0xfffffe00076e2660 0x1727     0x1727     com.apple.filesystems.autofs (3.0) FB1D8255-93D2-3EB0-8AE3-83ED61577D26 <101 7 6 5 4 3 2 1>
lundman@tahoe(/Users/lundman) zpool version
zfs-2.4.3-rc3
zfs-kmod-2.4.3-rc3

And that is without any patches to Host system, just SIP/authenticated-root disabled. Which is nice.

Or to have Claude say it with many more words:

⏺ Verified. The e5 03 04 aa 04 00 80 52 signature is unique (one hit) in both AVPBooter and iBoot, which is what makes
those two robust. Here's the honest per-anchor assessment for your post.

Robust — deterministic, no fuzz needed

  • Active LLB selection in AuxiliaryStorage. Parsed structurally: the two HUFA headers at 0x4000/0x5000, pick the set with
    the higher upgrade_count, then walk the img4 DER length to find the logo. This is math on a documented layout, not
    pattern-matching. It's held across macOS 12 through 27.
  • AVPBooter and iBoot validate_boot_object. Anchored on the unique 8-byte prologue e5 03 04 aa 04 00 80 52 (mov x5,x4;
    mov w4,#0), which occurs exactly once in each. From there you decode its bl callers and rewrite each to mov x0,#0 (00
    00 80 d2). The one guard a tool needs: assert the caller count is what you expect (2 for iBoot), and refuse if not.
  • Kernelcache ACM defeat. Symbol-anchored, not offset-anchored: find _validate_acm_context, then nop the bl to it inside
    the two functions that call it in range (_command_create_linked_manifest, _command_update_local_policy_for_kcos). As
    long as the symbol table is present, this is deterministic.

Needs fuzz — real disassembly plus human judgment

  • The LLB digest gate. This is the fragile one. The validate_boot_object signature also appears in LLB, but patching
    there breaks boot — LLB enforces the digest in a different function, image4_validate_property_callback. You locate it
    by scanning for the DGST constant (0x44475354, loaded as mov w8,#0x5354; movk w8,#0x4447,lsl16), then patch the return:
    the mov x0, immediately before the shared retab epilogue, rewritten to mov x0,#0. The catch is the source
    register varies (it was x27 for us, x20 in Michaud's notes), so you can't hardcode the instruction bytes. A tool has to
    disassemble the function and identify the return mov by position, and it's worth a human eyeball.

Cross-version caveats worth stating

  • Every raw offset above is per-build. The anchoring methods (unique-signature scan, DGST-constant scan, symbol xref) are
    what carry across builds; the numbers do not.
  • On macOS 27 (Golden Gate), the ACM functions were renamed into a boop_* namespace, so a symbol-name match needs
    updating, and the LLB there reportedly lacks a plaintext iBoot version string, so any string-based sanity check has to
    fall back to reading the version out of the DER.
  • The recovery-volume iBoot and kernelcache were byte-identical to the main-OS originals on Tahoe, so the same offsets
    applied. That may not hold on every release — a tool should re-derive them, not assume, even though copying the patched
    main-OS files worked here.

Net: four of the five patch points are anchored on stable, verifiable features and are safe to automate with a
refusal-on-mismatch check. The LLB DGST return patch is the one that genuinely needs disassembly and a human sanity check
per version.

@steven-michaud

Copy link
Copy Markdown
Author

This doesn't mean much without context. So, what did you do? Did Claude write scripts for you? If so, I think everyone here would like to see them.

@dariaphoebe

Copy link
Copy Markdown

What @steven-michaud said about showing Claude’s scripts, @lundman: the Claude summary seems like it’s hitting all the right details but I’m not sure what the point is if it’s not generating scripts for you.

also this is all Steven’s work. I just happen to be using it for a similar use case to you: it’s a convenient way to test VFS-based kernel extensions in a VM.

@lundman

lundman commented Sep 14, 2026

Copy link
Copy Markdown

Step one was just making sure we could successfully replicate your work, just a verification. Next would be automating it for this version (Tahoe), then expanding to other versions.

https://github.com/openzfsonosx/vz-kext-loader

@steven-michaud

Copy link
Copy Markdown
Author

Later this week I'll work through my document on macOS 27, and add whatever revisions I need to. After that I'll work through your document, @lundman, and see what I think of it.

@lundman

lundman commented Sep 16, 2026 •

Copy link
Copy Markdown

OK Day 2 update.

  • Clone existing Tahoe VM -> PLAY4
  • UI points out to boot Recovery and make the 3 changes. (Permissive, SIP and Authenticated-root)
  • UI click on Patch, authentication popup, patching is done
  • UI click on Boot, authentication popup for overlay mount on Host
  • PLAY4 boot
  • Click to rebuild kext cache in System Settings / Privacy & Security
  • Reboot PLAY4
  • kext loaded.

Also tested

  • UI click Unpatch VM
  • Boot VM into normal macOS
  • Click Unmount overload on Host.

Only tested for the specific Tahoe VM version I have, which is 26.0 25A354.

Tomorrow I will try other macOS versions. I have macOS-12 to macOS-27.
vz-kext-loader3

@lundman

lundman commented Sep 16, 2026

Copy link
Copy Markdown

I have successfully tested:

macOS-12 ✅
macOS-13 ✅
macOS-14 ✅
macOS-15 ✅
macOS-26 ✅

But it does fail on macOS-27 Golden Gate as predicted. We tried a few things, but it is not clear yet why.

Here is Claude's (always so verbose) summary of where it stands, take it with a grain of salt:


Walls hit, in order, and what we did about each

  1. Kernelcache symbol wall — symbol __command_create_linked_manifest not found.
  • Found: GG kept __validate_acm_context (our stable anchor) but moved the two bless-path worker functions into a boop
    namespace: _boop_create_linked_manifest (now has two acm calls) and _boop_update_local_policy_for_kcos.
  • Verified by disasm that nopping is safe at all three sites (each leaves w0=1 = success path).
  • Fixed: kernelcache.py resolves targets via newest-first alias lists and nops all acm calls per function.
    Static-validated on both GG and Tahoe kernelcaches (no regression).
  1. mBoot rename + compression — while investigating your "stripped bits" hypothesis.
  • Found: GG renamed Stage-2 iBoot to mBoot (mBoot-20457.1.27) and ships it LZFSE-compressed; every earlier iBoot was
    uncompressed. Our patch hardcoded compress=False, so we were shipping an unfaithful uncompressed object.
    (Properties/structure were not stripped — that hypothesis was disproven; the compression was the real bug.)
  • Fixed: rewrap now preserves original compression. Regression-safe.
  1. The rollback — GG boots our patched chain fine, but macOS then shows "Authentication is required to verify startup
    disk," and authenticating reverts our patches and re-enables SIP. Permissive-first (the Tahoe fix) does not suppress it
    on GG. With the compression fix, GG got further than ever — it entered kcgenOS, built the auxKC, did the commit-reboot —
    then rolled back at the bless step.

  2. Third copy + checksum (your final instinct — correct) — enumerated every boot-object copy on the GG disk.

  • Found: a full restore bundle in Preboot holding pristine third copies (iBoot.vma2.RELEASE.im4p,
    kernelcache.release.vma2 — confirmed unpatched) plus a signed Bootability.trustcache + BuildManifest.plist, consumed by
    BootabilityBrain.framework.
  • Tried: patched those third copies too (new bare-IM4P support in the engine; validated on the real files). Re-ran,
    restore copies patched → booted → SIP came back on.

Conclusion

The rollback is a restore-integrity check, not a secure-boot check — which is why Permissive can't suppress it. Patching
the repair source didn't help, so BootabilityBrain verifies the active boot objects against the IMG4-signed trustcache
(digests of the originals) and repairs + re-seals on any mismatch. Since our patches change the digests and the
trustcache is signed, we can't produce matching measurements. That's the true GG wall — and it lines up with upstream's
"GG not fully solved."

Defeating it means one of: neuter BootabilityBrain's verification, defeat/replace the signed trustcache, or bless the
auxKC fully offline bypassing this path (Michaud's serial-console-UTM territory) — all deep RE against a moving target.
GG is parked until GA.

@steven-michaud

Copy link
Copy Markdown
Author

I'm having a hard time getting my procedure to work on macOS 27. I'm stuck at the iBoot module -- if I make any changes to it, I'm forced to "re-sign". It's very likely the changes I make to the LLB.img4 module are no longer sufficient to allow changes to iBoot. To get around this will require serious debugging. I'm not sure how long that will take.

@steven-michaud

Copy link
Copy Markdown
Author

Actually I didn't need "serious debugging", just more trial and error. It's not that we need more changes to the LLB.img4 module (iBoot Stage 1) on macOS 27. Instead we need fewer changes to iBoot Stage 2 (in iBoot.img4). Previously I had you always patch out calls to validate_boot_object() in iBoot Stage 2. But doing this breaks the iBoot Stage 2 module on macOS 27. It forces you to "re-sign" the VM on reboot, which erases all the changes you've made to iBoot Stage 2 and AppleVPBootPolicy.kext.

Fortunately, the Apple bug that the validate_boot_object() procedure works around no longer seems to be present in macOS 27. So iBoot Stage 2 can be patched there exactly like stages 0 and 1.

I've revised my document to reflect this, and added a few more refinements. For example, I needed to fork Nick Botticelli's ghidra-iboot plugin to get it to work with macOS 27 and the latest version of Ghidra.

As best I can tell, it now works just fine on macOS 27.

My next step will be to fork UTM and publish my debugging patch. This is what I'd have needed for "serious debugging". It'll be nice to have it ready for next time, and available to others who might like to use it.

@lundman

lundman commented Oct 7, 2026

Copy link
Copy Markdown

Works like a charm,

lundman@goldengate(/Users/lundman) sw_vers
ProductName:		macOS
ProductVersion:		27.0
BuildVersion:		26A5406e
lundman@goldengate(/Users/lundman) kextstat | grep zfs
  127    0 0                  0x6ed20    0x6ed20    org.openzfsonosx.zfs (2.3.1) F0E29E38-3B79-3502-AD8C-8ADB589AE592 <20 7 5 4 3 1>
lundman@goldengate(/Users/lundman) zpool version
zfs-2.3.1-rc1
zfs-kmod-2.3.1-rc1

I pushed out 0.1.2 of the app with golden gate support.

@steven-michaud

Copy link
Copy Markdown
Author

Glad to hear it!

@dariaphoebe

Copy link
Copy Markdown

Thanks for the update; I've been mostly offline preparing for a family funeral but I will try this. While doing other debugging I discovered that ipsw's img4 creation may well support the special iBoot variant of compression needed (iboot_lzfse) such that the reconstructed img4s can be compressed, when I have more info I will share (https://blacktop.github.io/ipsw/docs/guides/img4/)

The other thing I found is my previous issue with copying the modified AVPBooter (error 75), where mount was showing e.g. /dev/disk3s8s1, the issue is that the last blessed reversion still reverted to a snapshot, not the original, and the attempt to mount was attempting to mount a snapshot. mounting e.g. /dev/disk3s8, modifying that and resealing/blessing that works.

@dariaphoebe

Copy link
Copy Markdown

And I can confirm the updated procedure works fine on 27.2 beta3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment