Skip to content

DriverKit: how Apple is moving every driver out of the kernel

Same IOKit object model, userland process. Why kexts are dying, what DriverKit gives you, and how a USB driver actually crosses the boundary.

Published 6 min read
DriverKit: kernel ↔ userland dext boundaryThe DriverKit boundary. A dext (driver extension) runs as a userspace process. The kernel-side IOUserServer proxies its IOService presence into the IORegistry. Every method call becomes a Mach message across the boundary.Kernel (xnu)iokit/Kernel/IOUserServer.cppIORegistrytree of every IOService instanceIOService (parent driver)e.g. IOPCIDevice for the busIOUserServerkernel-side proxy for the dext belowFrom IOKit's POV this is just an IOService.It forwards method calls over Mach IPC.MACH IPCboundarystart, read, interrupt...register, complete, notify...Userland — dext processlaunchd-spawned · sandboxedDriverKit.frameworkdispatches Mach messages → methodsMyDriver : IOUserServiceyour subclass · the real driver codehardware accessIOMemoryDescriptor · IOInterruptDispatchSourceA crash here kills only this process.The kernel restarts the dext; the rest ofthe system continues unaffected.

For thirty years, third-party drivers on macOS were kexts — Mach-O bundles loaded into the kernel address space. Since macOS 10.15 (Catalina) Apple has been steadily, deliberately killing them. The replacement is DriverKit: the same IOKit object model, but each driver runs as a user-mode process. This is the article on what changed, why, and how it actually works.

What was wrong with kexts

A kext is code in the kernel. Three problems followed:

  1. Crashes panic the system. A null pointer dereference in a Wi-Fi driver doesn't just disconnect Wi-Fi; it takes the machine down. For decades this was the dominant cause of kernel panics on consumer Macs.
  2. The kernel C++ ABI is fragile. Adding a virtual method to IOService in a major macOS release would silently break every kext that subclassed it. Apple maintains an elaborate metaclass system (see the IOKit article) to soften this, but every release still breaks something somewhere.
  3. Kexts are a security boundary that can be subverted. A signed kext runs at ring 0 with full memory access. Code-signing requirements help but don't eliminate the attack surface.

DriverKit fixes all three: a crash kills only the driver process; the ABI is a stable C interface across a Mach boundary; the driver runs sandboxed without kernel privileges.

The bridge: IOUserServer

Here's the part that's clever. DriverKit doesn't rewrite IOKit. The driver still subclasses IOService, still has an Info.plist with a personality dictionary, still matches against hardware the same way. The only difference is where the instance lives — in a userspace process instead of in kernel memory.

The kernel half of this is a class called IOUserServer. When the kernel decides a DriverKit driver matches some hardware, instead of instantiating the C++ class in kernel space, it:

  1. Forks the DriverKit driver as a userspace process via launchd.
  2. The driver process loads DriverKit.framework and creates the driver class instance there.
  3. The kernel creates an IOUserServer instance that proxies for the userspace driver.
  4. From the rest of IOKit's perspective, the driver is just another IOService — it doesn't know (or care) that the methods are getting forwarded over Mach IPC.

apple-oss-distributions/xnuiokit/Kernel/IOUserServer.cppThe kernel side of DriverKit — how an IOService in userland looks like an IOService in the kernel.View on GitHub(line ) apple-oss-distributions/xnuiokit/Kernel/IOUserClient.cppIOUserClient — the mechanism every userland↔driver call goes through, kext or DriverKit.View on GitHub(line )

Every call from "kernel IOKit" into the driver — start, read, interrupt, anything — becomes a Mach message. The DriverKit framework on the userspace side dispatches the message to the matching method on the driver instance.

What a DriverKit driver can and can't do

DriverKit narrows the API surface to a curated subset. Things you can do:

  • Subclass IOUserService, IOUserClient, IOInterruptDispatchSource, IOTimerDispatchSource.
  • Read from and write to your assigned hardware through abstractions (IOMemoryDescriptor, IOAddressSegment).
  • Service interrupts, but through a queued dispatch — your handler runs in your userspace process, not in the kernel interrupt context.
  • Expose a user client to apps, just like a kext-era driver did.

Things you can't do:

  • Allocate kernel memory directly.
  • Walk the IORegistry freely (you only see your provider chain and descendants).
  • Make arbitrary mach_msg calls.
  • Run code with kernel privileges, period.

The categories of devices DriverKit supports: USB, PCI, network, audio, HID, SCSI, serial. That covers most third-party device classes. Drivers for the SoC itself (interrupt controllers, the system management unit) remain in-kernel because they need privileged hardware access.

Process lifecycle

A DriverKit driver doesn't ship as an executable bundle the way a kext does. It ships as a driver extension (.dext) embedded inside a regular app bundle. The app contains a regular GUI for setup/preferences; the dext contains the driver itself.

When the user opts in via System Settings → Privacy & Security → "Allow system extensions", the dext is registered with the kernel. From then on:

  1. Hardware matching it appears (e.g., a specific USB device is plugged in).
  2. The kernel asks launchd to start the dext process.
  3. launchd execs the dext under a tight sandbox, with only the entitlements declared in its plist.
  4. The dext registers with the kernel's IOUserServer and becomes the matching driver.
  5. When the hardware goes away (unplugged, suspended), the kernel terminates the dext process.
apple-oss-distributions/xnuiokit/Kernel/IOServicePM.cppService power management — drives the lifecycle of every driver, dext or kext.View on GitHub(line )

The "process per device" model has real costs: there's IPC overhead on every call, the memory footprint is a few MB per dext, and developer ergonomics are worse than a kext (more boilerplate, no debugger attach-to-kernel). The trade is worth it for the crash isolation alone.

What does this mean for users

Practically:

  • Third-party kexts now require booting into Reduced Security mode and manually approving each kext. The user-facing flow is hostile by design.
  • Most familiar third-party drivers (USB serial adapters, audio interfaces, USB→Ethernet dongles, NVMe over Thunderbolt, fan controllers, etc.) ship as dexts now. If you've used "System Extension" approval dialogs in System Settings, you've enabled DriverKit drivers.
  • The crash class "kernel panic from a third-party driver" is becoming rare on modern macOS. A misbehaving dext shows up as "DriverKit extension crashed" in Console, the app restarts the dext, and the rest of the system carries on.

Reading the DriverKit headers

There's no public open-source mirror of DriverKit itself — it's part of the macOS SDK, not Darwin. You can read the headers in /Applications/Xcode.app/Contents/Developer/Platforms/DriverKit.platform/Developer/SDKs/DriverKit.sdk/System/DriverKit/usr/include/DriverKit/ once you've installed Xcode.

For the kernel side that is open source, the entry points to read:

apple-oss-distributions/xnuiokit/Kernel/IOUserServer.cppIOUserServer — how kernel IOKit talks to userland DriverKit.View on GitHub(line ) apple-oss-distributions/xnuiokit/Kernel/IOServicePM.cppPower management lifecycle — drives dext spawn/terminate.View on GitHub(line )

What surprises newcomers

  • Existing apps don't need changes to use DriverKit-backed hardware. The user client API is identical.
  • Notification interrupts are slower than in-kernel interrupts. The latency is fine for USB peripherals but unacceptable for, say, a high-end audio interface — which is why Core Audio's most timing-critical paths still run in-kernel.
  • A dext can have multiple matching personalities in the same bundle. One dext can drive a whole device family.
  • DriverKit was first shipped on iOS (where third-party kernel code has been forbidden since day one). Bringing it to macOS in 10.15 was Apple making the iOS model the default everywhere.

IOKit and the driver model — the foundation DriverKit is built on. Read it first if you haven't.

For the long view, Apple's WWDC sessions on DriverKit (2019 onward) are the most accessible deep dive — they walk through writing a USB dext end-to-end. Search the developer site for "DriverKit".

Related

Embedded C++, an object tree, and matching dictionaries — IOKit is how every driver on macOS gets loaded, paired with hardware, and called.
clonefile, fclonefileat, fs_snapshot — three syscalls that let you copy 50 GB in 50 milliseconds. Here's what happens under each one, and what doesn't get copied.
What changed in XNU when Apple shipped its own ARM silicon — P/E cores, APRR page-permission switching, the AMX matrix coprocessor, and Rosetta 2.