USBDriverKit Host Drivers: Interface Matching, Endpoint Pipes, and I/O Cancellation
Follow a macOS USBDriverKit host driver from interface matching to endpoint-pipe I/O, entitlements, asynchronous completion, and safe cancellation.
USBDriverKit is Apple’s DriverKit family for custom or non-class-compliant USB devices. Its host-side model is deliberately narrower than “talk to a USB device”: the operating system matches a driver service, provides device and interface objects, and exposes endpoint communication through pipe objects. A reliable driver must understand which object owns each part of that path and what happens when the interface disappears while requests are outstanding.
This article concentrates on interface-to-pipe I/O. It is not a general DriverKit introduction, a recipe for replacing Apple’s class drivers, or a guarantee that a specific USB device can be claimed by a third-party extension. The exact entitlement, device matching properties, endpoint descriptors, alternate settings, and current macOS support must all be verified for the product being built.
Choose the narrowest supported device contract
Apple describes USBDriverKit as intended for custom or non-class-compliant USB devices. The framework objects provide access to device configurations, interfaces, and endpoints, while the driver supplies its custom behavior. Standard devices may already be managed by system drivers; developing a dext is not a way to bypass a class driver or a user-consent boundary.
A USB device can expose multiple configurations and interfaces. An interface contains endpoint descriptors, which in turn define endpoint addresses and transfer characteristics. A driver should match only the intended hardware and interface, then validate the actual descriptors at runtime. Vendor/product identifiers can identify a device family, but they do not prove the device has the firmware revision, endpoint topology, or protocol version the driver expects.
For a custom implementation, the system can create an IOUSBHostInterface provider as part of the driver’s matching and configuration. Apple also documents obtaining interfaces from an IOUSBHostDevice provider. Prefer a narrow interface match when the driver needs only one function of a composite device; taking ownership of a whole device can interfere with unrelated functions. Record the selected interface and configuration in diagnostics so a firmware change that alters descriptor layout is visible.
A pipe is an endpoint-specific, one-way channel
IOUSBHostPipe represents communication to or from a particular USB endpoint. Apple documents synchronous and asynchronous transfers for bulk and interrupt endpoints, along with isochronous operations. A pipe is one-way; it is not a general bidirectional device connection. Control requests use the default control endpoint and have their own interface/device request methods.
Do not instantiate a pipe directly. Retrieve it from the interface with CopyPipe, using the bEndpointAddress from the selected endpoint descriptor. The method returns a pipe object through an output pointer; the caller is responsible for releasing that object when finished. This makes object ownership an explicit part of the driver lifecycle.
Conceptually, the lookup should be guarded before any transfer is submitted:
IOUSBHostPipe *pipe = nullptr;
const uint8_t endpointAddress = endpoint->bEndpointAddress;
kern_return_t kr = interface->CopyPipe(endpointAddress, &pipe);
if (kr != kIOReturnSuccess || pipe == nullptr) {
// Record the interface, endpoint address, and return code.
// Do not continue with a partially initialized I/O path.
return kr == kIOReturnSuccess ? kIOReturnError : kr;
}
This illustrates the documented call shape; add the project’s normal ownership wrapper and ensure the endpoint descriptor remains valid for the lookup. A successful pipe lookup proves that the interface has a pipe for that endpoint. It does not prove the application protocol is ready, that the device has the intended firmware, or that a transfer will complete successfully.
For each endpoint, preserve the descriptor’s direction, transfer type, maximum packet size, and interval where relevant. Bulk, interrupt, and isochronous transfers have different timing and delivery expectations. A robust service does not silently send a bulk-style request to an endpoint assumed to be interrupt-driven, nor does it infer packet size from a product model string. If the interface exposes alternate settings, select and validate the setting that supplies the endpoints the driver expects before caching pipe references.
Synchronous and asynchronous requests need different designs
A synchronous pipe operation is easier to reason about when the request is small and bounded, but it still needs an explicit timeout and error path. Do not block a DriverKit work loop indefinitely waiting for a device that has been unplugged or whose firmware has stalled. Check the return status and transferred byte count; partial progress, device errors, and timeout are not equivalent to a completed protocol message.
Asynchronous operations are appropriate when the driver must avoid tying up its execution path while hardware responds. Apple’s API exposes enqueue and completion methods for bulk and interrupt operations, and completion APIs for isochronous transfers. That split creates an ownership problem: the memory descriptor, request state, and any context used by a completion callback must remain valid until the request has reached a terminal state. Build an explicit per-request state machine rather than freeing the request context when the submit method returns.
A practical state sequence is:
- Validate that the service is started, the interface is selected, and the endpoint is still present.
- Allocate or acquire a bounded buffer and initialize its ownership record.
- Submit the request and record its identifier, endpoint, timeout, and generation.
- In completion, verify the request still belongs to the active device generation, process only the reported byte range, and release owned state exactly once.
- On stop, disconnect, or protocol reset, stop creating new work and cancel outstanding requests before releasing shared objects.
The generation check helps avoid using a completion from an old connection after the device has been removed and re-enumerated. It is an application-level guard, not a substitute for the framework’s own lifetimes. Never assume a callback is delivered on the thread that submitted the request unless the API documents that guarantee; protect state that can be reached by multiple callbacks.
Cancellation is a lifecycle transition, not a free operation
Apple documents IOUSBHostPipe::Abort as aborting pending I/O requests on that pipe. IOUSBHostInterface::AbortDeviceRequests applies to device requests previously made by the current interface client. They are separate cancellation scopes: aborting control requests on the interface is not the same as aborting a pipe’s endpoint transfers.
During teardown, first prevent new submissions, then request cancellation through the correct object, and keep the resources needed by the completion path alive until the request lifecycle is resolved according to the current API contract. The short API summary does not justify assuming that every caller-owned buffer can be freed immediately after an abort call. Test cancellation with delayed completions, device removal, timeout, repeated stop/start, and interface reconfiguration. If an operation returns an error because the device vanished, surface the error and move the service into a known stopped state rather than retrying in a tight loop.
Bound every request and define a recovery policy. For a bulk command protocol, a timeout might require discarding an incomplete frame and resetting the device state machine; blindly resending a non-idempotent command can duplicate an operation. For a telemetry endpoint, dropping a stale sample may be safer than blocking all later data. For isochronous data, missed timing windows cannot necessarily be recovered by replaying a packet later. These are protocol decisions the driver author must specify.
Entitlements and extension activation are part of the design
DriverKit drivers are user-space system extensions delivered inside an app. Apple’s entitlement guide explains that the driver needs the DriverKit entitlement and the transport-specific capabilities it uses; the host app needs the System Extension capability to activate it. Apple ties requested DriverKit entitlements to the development team’s provisioning profile, and validates entitlement and signature information during activation. USBDriverKit documentation lists com.apple.developer.driverkit.transport.usb for identifying supported USB devices.
Do not treat a development-mode machine as proof that distribution is ready. Confirm the exact platform and hardware support for the selected USBDriverKit APIs, provisioning profile, codesign identity, host-app capability, and user or management approval path. Use the supported System Extensions framework rather than copying extension files into private or legacy directories. Never weaken platform security on a customer’s Mac to compensate for an entitlement or signature error.
When activation fails, separate the stages: app signing, extension signing, provisioning and entitlements, activation request, approval, device discovery, interface matching, and driver start. Capture the activation result and relevant unified logs. A driver that is active but matched to no interface needs a different correction from an app that cannot activate its extension.
Test endpoint and failure behavior, not just enumeration
Build a test matrix around real interface descriptors and the supported host versions. Include the expected configuration, an unsupported firmware revision, missing endpoint, short transfer, timeout, unplug during active I/O, unplug while idle, device sleep/wake where applicable, and reconnect to a new instance. Verify that a failed request cannot corrupt a buffer length, complete the same request twice, or access a released pipe. Keep captures limited to the data required for diagnosis because device payloads may contain user information.
Instrument each request with a correlation identifier, interface/endpoint address, operation class, requested length, reported length, elapsed time, completion status, and connection generation. Avoid dumping payload bytes by default. In a field diagnostic, this evidence can distinguish “no matching interface,” “pipe lookup failed,” “endpoint accepted but device timed out,” and “host canceled a request during teardown.” Those failure classes call for different fixes.
The design goal is a narrow, auditable chain: match the intended interface, obtain the endpoint pipe through the documented provider, validate transfer semantics, preserve request ownership through completion or cancellation, and recover cleanly when hardware disappears. USBDriverKit makes that chain available outside the kernel; it does not remove the need to define the chain precisely.
Related:
- DriverKit on macOS: Hardware Drivers That Run in User Space
- How to Diagnose a macOS System Extension That Will Not Activate
Sources: