How a USB device gets driven on macOS
From plug-in to working app — IOUSBHost enumeration, IOKit matching, the DriverKit dext load, the user-space SDK. A complete trace of one device's journey through the stack.
You plug a USB device into a Mac. A few seconds later, an app can talk to it. In between, the IOKit subsystem walks a long sequence: detect the device, enumerate its descriptors, match it against registered drivers, load (or spawn) the driver, and finally expose it to user-space.
This article traces one device's journey end to end, with the right files in apple-oss-distributions cited at every stop.
Step 1: hardware notifies the host controller
The USB host controller (an XHCI device on modern Macs, AOAP on Apple Silicon) raises an interrupt when a device is connected. The interrupt goes through the AIC and lands in the host controller driver's interrupt handler.
The host controller driver:
- Reads the controller's status registers to identify which port has activity.
- Resets the port and powers it up.
- Performs USB enumeration: sends
GET_DESCRIPTORto address 0, gets back the device descriptor, assigns the device an address, then reads the configuration descriptors. - Publishes a new
IOUSBHostDevicein the IORegistry representing the freshly-enumerated device.
The device descriptor includes:
- Vendor ID (e.g. 0x05AC for Apple).
- Product ID (e.g. 0x024F for a Magic Trackpad).
- Class / Subclass / Protocol codes (e.g. 0x03/0x01/0x02 = HID / Boot Interface / Mouse).
- USB version, max packet size, number of configurations.
Step 2: IOKit matching
The publication of IOUSBHostDevice triggers IOKit's matching engine (see the IOKit overview). Every registered driver with IOProviderClass = IOUSBHostDevice (or a more specific class for matched interfaces) gets a chance to claim the device.
For a USB device, matching keys typically include:
idVendorandidProductfor exact device matches.bDeviceClass/bDeviceSubClass/bDeviceProtocolfor class-based matches.bInterfaceClass/ etc. for matching against a specific interface within a multi-interface device.
A vendor-specific driver matching by VID/PID outscores a generic class driver. So Apple's AppleHIDMagicTrackpad driver beats the generic IOHIDFamily driver for a Magic Trackpad; both can match, but the vendor-specific one wins.
See the IOKit matching diagram for how scoring works.
Step 3: driver load (kext or dext)
The winning driver is invoked. What happens next depends on whether it's an in-kernel kext or a userspace DriverKit dext:
For an in-kernel kext
If the driver is part of the kernelcache (Apple's USB drivers are; some third-party kexts can be, with user approval):
IOService::probeis called. The driver reads the device descriptors via IOUSBHost and confirms it can handle this specific device.- If probe returns a positive score,
initis called, thenattach, thenstart. startis where the driver does its real work: claim interfaces, open pipes, configure the device, register itself as a service.
For a DriverKit dext
If the driver is a .dext (newer third-party USB drivers, e.g. for novel USB audio interfaces):
- The kernel asks
launchdto spawn the dext process if not already running. - The dext process loads
DriverKit.frameworkand instantiates the driver class. - The kernel-side
IOUserServerproxies the driver'sIOServicepresence into the IORegistry. - The same
probe→init→attach→startlifecycle runs, but every callback crosses the Mach-IPC boundary.
See the DriverKit article for the full mechanism.
For our Magic Trackpad example, AppleHIDMagicTrackpad is a kext (Apple's own HID drivers are in-kernel for latency reasons). Its start method:
- Opens the HID interrupt-in pipe.
- Schedules a recurring read on the pipe to get HID input reports.
- Publishes itself as an
IOHIDDeviceso the HID framework can use it.
Step 4: HID processing
IOHIDFamily is itself a registered driver layered on top. It matches against IOHIDDevice services and provides:
- Report descriptor parsing — turning the device's HID descriptor into a structured "this device produces a 4-byte report containing 2 buttons + an X delta + a Y delta + a wheel."
- Event generation — converting incoming reports into HID events the system understands.
- Multi-touch support — for trackpads, this is where finger contacts get tracked across frames.
The HID events feed up to the WindowServer (which owns mouse and trackpad event delivery to apps) via a private XPC interface.
apple-oss-distributions/xnuiokit/DriversIOHIDFamily lives here in spirit; the kernel-side bits are closed-source for HID-specific reasons.View on GitHub(line —)Step 5: user-space app opens the device (alternative path)
For non-HID devices — say, a USB serial adapter — the user-space SDK is the natural endpoint. An app opens the device via IOServiceOpen:
- The app calls
IOServiceMatching("AppleUSBSerial")to get matching dictionary for the driver class. - Iterates the IORegistry, finds the device.
- Calls
IOServiceOpen(device, mach_task_self(), 0, &connection). - The kernel verifies the app has the right entitlements (
com.apple.developer.driverkit.usbfor DriverKit clients). - The driver's
IOUserClientsubclass is instantiated, exposed back to the app as a Mach port. - The app calls
IOConnectCallMethodto invoke driver-defined methods through the user client.
For USB specifically, the modern path is IOUSBHost.framework in user-space, which wraps the IOUserClient machinery. An app uses IOUSBHostObject (Objective-C) to open the device, claim an interface, do bulk/control transfers.
Step 6: hot-unplug — running everything in reverse
When the user pulls the device out:
- Host controller raises a port-down interrupt.
- Host controller driver publishes the disappearance: terminates the
IOUSBHostDevicein the IORegistry. - Every driver that matched the device gets its
willTerminatecallback. They stop in-flight operations, close pipes. didTerminatefollows; the drivers free their resources.- The IORegistry node is removed.
- For DriverKit dexts: when the last device the dext was driving disappears, the kernel terminates the dext process (saves memory).
- User-space apps using the device get notifications via their IOServiceMatching subscriptions and clean up.
The lifecycle from plug-in to working — and back — is fully automated. Userspace apps never have to poll for device presence; they subscribe to matching notifications and react.
Notable USB-specific bits
- USB power negotiation: a Mac can supply 500 mA at 5V by default; some Macs and USB-C devices can do USB Power Delivery for more. IOKit's power-management graph integrates with USB PD negotiations.
- USB hubs are themselves devices. They get their own IOUSBHostDevice and driver, which then enumerates downstream devices.
- USB-C identity: USB-C cables and adapters identify themselves; the host queries which alternate mode (DisplayPort, Thunderbolt, plain USB) to use.
- USB On-The-Go on iPad: an iPad can host devices, so IOUSBHost runs on iOS too — same code path.
Observing on a live system
ioreg -p IOUSB -l— shows the USB part of the IORegistry. Every plugged-in device with its descriptor properties.system_profiler SPUSBDataType— pretty-printed device list with the same info.- Console.app filtered to
com.apple.iousbfamily— driver match/load events in real time. USB Prober(in additional developer tools) — packet-level USB tracing.
What surprises newcomers
- Enumeration happens before driver matching. The kernel knows what the device is (vendor, product, descriptors) before any driver gets to see it.
- A device may have multiple drivers. A composite USB device (interfaces for audio + video + control) can have a different driver bound to each interface.
- DriverKit dexts spawn and terminate on hardware presence. Plug a device, dext starts; unplug, dext exits. The process isn't long-lived.
- HID has its own framework on top of USB. For mice/keyboards/trackpads, the USB driver is the lower half; the HID processing layer adds the input-event semantics.
What to read next
apple-oss-distributions/xnuiokit/Kernel/IOService.cppIOService::probe / init / attach / start — the lifecycle every driver implements.View on GitHub(line —) apple-oss-distributions/xnuiokit/Kernel/IOUserClient.cppIOUserClient — the kernel ↔ user-space bridge for every driver's app-facing API.View on GitHub(line —)
And the IOKit overview for the matching engine this article assumes.