OpenZFS on FSKit — Proof of Concept

Installing ZFSFSKit.appex ? /Library/ExtensionKit/Extensions/                   
  Substituting real Mach-O (libtool wrapper ? .libs/ZFSFSKit)                   
                                                                                
Installing zfs.fs ? /Library/Filesystems/                                       
  mount_zfs: Mach-O 64-bit executable arm64                                     
  Done.                                                                         
                                                                                
Signing (before pluginkit, so it sees a valid signature)...                     
Re-signing /Library/ExtensionKit/Extensions/ZFSFSKit.appex ad-hoc (no identity).
  Note: requires amfi_get_out_of_my_way=1 in boot-args.                         
  Team ID: ADHOC                                                                
/Library/ExtensionKit/Extensions/ZFSFSKit.appex: replacing existing signature   
                                                                                
Done. Signature:                                                                
Identifier=org.openzfsonosx.filesystems.zfs.fsext                               
Signature=adhoc                                                                 
TeamIdentifier=not set                                                          
Entitlements:                                                                   
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>com.apple.application-identifier</key><string>ADHOC.org.openzfsonosx.filesystems.zfs.fsext</string><key>com.apple.developer.fskit.fsmodule</key><true/><key>com.apple.developer.team-identifier</key><string>ADHOC</string><key>com.apple.security.app-sandbox</key><true/></dict></plist>                         
                                                                                
Registering with pluginkit...                                                   
pluginkit -a done.                                                              
                                                                                
Restarting fskitd...      

# sudo pluginkit -v -m -p com.apple.fskit.fsmodule
+    org.openzfsonosx.filesystems.zfs.fsext((null))     6A12A41280FB-4190-B957-FA94DC89BB1E    2026-05-29 01:17:58 +0000       /Library/ExtensionKit/Extensions/ZFSFSKit.appex
                                                      

# sudo mkdir /Volumes/tank                                                                   
# sudo mount -F -t zfs /dev/disk4 /Volumes/tank                                              
# ls -la /Volumes/tank                                                                       
total 3                                                                         
drwxr-xr-x  3 lundman  staff    4 May 29 09:21 .                                
drwxr-xr-x  4 root     wheel  128 May 29 10:18 ..                               
-rw-r--r--  1 lundman  staff   11 May 29 09:21 file.txt                         
drwxr-xr-x  2 lundman  staff    2 May 29 09:21 HelloWorld                       
# cat /Volumes/tank/file.txt                                                                 
HelloWorld                                                                      

Even though FSKit isn't quite ready, I built a proof-of-concept FSKit extension to understand what the migration path looks like. This post shares what we got working, specific technical findings that weren't documented, and the gaps we hit that would need Apple's attention for a production implementation.

Luckily, OpenZFS already compiles in userland for the "zdb" utility so not much work was required on that side.

There were certain amount of desperation applied when we came across hurdles, so possibly some assumptions we formed are not correct. (We didn't go back and confirm the problem after it started working).

Technical Findings — Things That Weren't Obvious

These cost significant debugging time and aren't documented anywhere:

  1. startCheckWithTask: must complete asynchronously

Calling [task didCompleteWithError:nil] synchronously inside startCheckWithTask: causes fskitd to receive "task completed" before "task started" over XPC.

fskitd rejects this with FSKitErrorDomain Code=27503 "Task didn't start yet" and never spawns the activate instance. The fix is to dispatch the completion with even a 1ms delay so FSKit can send the "started" notification first.

  1. app-sandbox=true is required in entitlements

ExtensionKit rejects the extension entirely without com.apple.security.app-sandbox=true, even though sandboxing a filesystem extension feels counterintuitive. This needs to be paired with com.apple.developer.fskit.fsmodule=true.

  1. Container identifier must equal volume identifier for FSUnaryFileSystem

FSContainer.h documents this but it's easy to miss: for unary file systems, the container identifier passed in probeResource: must exactly match the volume identifier used when constructing the FSVolume. A mismatch causes loadResource: to fail with EAGAIN "unexpected container state."

What's Missing for Production

We see three hard blockers and several important-but-workable gaps.

Blocker 1: No management plane

Every ZFS management tool — zpool, zfs, zdb, zed, zinject — communicates with the ZFS engine via /dev/zfs ioctls (ZFS_IOC_*). Without an equivalent:

  • No pool create, destroy, import, export, scrub, resilver, or status
  • No snapshots, clones, send/receive, or bookmark management
  • No dataset property management
  • No ZFS event daemon (zed) for fault handling and auto-replacement

The need isn't specifically "ioctls" — it's a defined IPC contract between the FSKit-hosted ZFS engine and management tools. Whether that's a character device DEXT, an XPC service exposed by the extension, or a new FSKit API, the mechanism needs to exist. The management tools would be adapted to whatever is provided.

Question: Is there a recommended pattern for a filesystem extension to expose a management interface to non-sandboxed privileged tools?

Blocker 2: No virtual block device publication

ZFS Volumes (zvols) are block devices backed by the ZFS storage pool — used for VM disks, iSCSI targets, swap, and more.

The current kext implementation (IOBlockStorageDevice subclass) works because it creates a kernel service that IOKit matches into IOMedia → /dev/diskN.

There is no userspace equivalent.

DriverKit's IOUserBlockStorageDevice is the closest analog, but IOKit matching is hardware-triggered. There's no mechanism for a filesystem extension discovering a zvol inside a pool to say "please also publish a block device for this." A static DEXT can't know what zvols exist until the pool is imported.

Question: What is the intended path for a filesystem to publish virtual block devices — for example, a software RAID layer or a volume manager that needs to create block device nodes dynamically at runtime?

Blocker 3: N:M — multiple devices per pool, multiple datasets per mount

ZFS has a fundamental mismatch with FSKit's current resource model on two axes:

N devices → 1 pool. ZFS pools span multiple block devices: a 3-disk RAIDZ, a 4-disk mirror, etc. FSKit's probe/activate model is one FSBlockDeviceResource per activation. For a RAIDZ pool, all member device fds need to be available at spa_import time. There's no multi-resource activation concept, and no way for probing one member disk to "claim" the others. (Our PoC works only because we used a single-vdev file pool.)

1 pool → M mounts. A pool typically contains many datasets, each with its own mountpoint (tank, tank/home, tank/data, tank/vm). These should each appear as separately mounted volumes. FSUnaryFileSystem is explicitly one volume per activation — there's no mechanism for one pool import to produce multiple mounted volumes.

These likely need separate primitives: something like a multi-resource pool probe that coalesces member devices, and a dataset iterator that produces multiple FSVolume instances per pool activation.

Question: Is multi-resource activation (multiple FSBlockDeviceResource objects for one filesystem instance) on the roadmap? Is there a pattern for one extension activation to produce multiple mount points?

Secondary Gaps (important but not immediate blockers)

ARC memory limits. ZFS's Adaptive Replacement Cache is designed to use a significant portion of system RAM. Sandbox memory limits constrain it. A declared "buffer cache" process role with higher memory entitlements would meaningfully improve performance.

Background operations. Pool scrub and vdev resilver (RAID rebuild) are long-running background I/O tasks essential for data integrity. There's no mechanism for an FSKit extension to run sustained background work while mounted. Resilver after a disk failure can't wait for user interaction.

No unified buffer cache integration. All reads go through the extension process with no kernel page cache sharing. mmap either goes through readFromFile: per page fault or doesn't work at all. This is significant for database and VM image workloads.

NFS/SMB re-export. ZFS is widely used as a NAS backend. Correctness of persistent file IDs, fsid stability, and server-side locking semantics under FSKit needs validation.

The code is open source at https://github.com/openzfsonosx/openzfs-fork/tree/FSKit

  • Jorgen Lundman

  • Co-Authored-By: Claude Sonnet 4.6

OpenZFS on FSKit — Proof of Concept
 
 
Q