Measured on 2026-08-05 with ./Scripts/validate-usb-entitlements.sh, macOS 26.5.2,
Apple silicon:
| Device | Kernel driver | Interfaces opened | Verdict |
|---|---|---|---|
SEGGER J-Link 1366:0101 |
none | 1/1, kIOReturnSuccess |
usable today |
USB Keyboard 2109:d101 |
AppleUserHIDDevice |
0/1, kIOReturnExclusiveAccess |
blocked |
Toshiba flash drive 0930:1400 |
IOUSBMassStorageDriver |
0/1, kIOReturnExclusiveAccess |
blocked |
Logitech C910 webcam 046d:0821 |
mixed, see below | 0/4, kIOReturnExclusiveAccess |
blocked |
Pixel 10a 18d1:4ee7 (ADB mode) |
none | 1/1, kIOReturnSuccess |
usable today |
The J-Link opened from an unsigned, unentitled, non-root process. No System
Extension, no DriverKit, no Apple approval. The project's own IOKitUSBInterface was
then driven against the same device and completed open() and close().
So the blocker is per-device-class, not project-wide. The README's "will not work until approved" is wrong for a meaningful slice of embedded hardware.
| Works today | Still blocked |
|---|---|
| Debug probes — J-Link, ST-Link, CMSIS-DAP | HID — keyboards, mice |
| USB-serial — FTDI, CP210x (measured, see below) | Mass storage |
| Android devices in ADB mode | Audio and cameras |
| DFU / bootloader modes | Boards exposing CDC ACM (most Arduino-likes) |
| Vendor-specific bulk interfaces generally | HID, mass storage, audio |
Three devices have been measured: an unbound debug probe, a HID keyboard, and a mass
storage drive. The first is usable and the other two are blocked, which is what the
mechanism predicts. USB-serial specifically is still inferred rather than measured —
no FTDI or CDC device has been available — but it is bound by the same
AppleUSBACMData-style driver attachment, so there is no reason to expect a different
outcome.
The escape hatch does not exist. USBInterfaceOpenSeize was the hope that the
right-hand column might move. Measured against a mass storage device on 2026-08-06, it
returns kIOReturnExclusiveAccess — the identical error a plain USBInterfaceOpen
returns. Seizing does not displace a kernel driver.
Two weaker forms of the same idea also fail:
| Attempt | Result |
|---|---|
USBInterfaceOpen |
kIOReturnExclusiveAccess |
USBInterfaceOpenSeize |
kIOReturnExclusiveAccess — no different |
diskutil unmountDisk then open |
still kIOReturnExclusiveAccess; the filesystem detaches but IOUSBMassStorageDriver stays bound |
diskutil eject then open |
still kIOReturnExclusiveAccess; the media stack drops from 20 IOKit nodes to 7, and the interface remains owned |
Worth noting the device/interface split: after unmounting, USBDeviceOpen succeeds
(kIOReturnSuccess) while USBInterfaceOpen on the same device still fails. Opening
the device is not the operation that matters — claiming its interface is, and that is
what the kernel driver holds.
So the split is final for the hardware available: nothing short of DriverKit rebinding releases an interface macOS has bound. Scope the release accordingly rather than waiting on a workaround.
The webcam is the case that shows why the distinction matters. Its four interfaces are not all held by the same kind of owner:
| Interface | Class | Held by |
|---|---|---|
| 0 | 1 (audio control) | AppleUSBAudioControlNub — a kernel driver |
| 1 | 1 (audio streaming) | AppleUSBHostFrameworkInterfaceClient — a userspace process |
| 2, 3 | 14 (video) | AppleUSBHostFrameworkInterfaceClient — a userspace process |
Both refuse a claim with kIOReturnExclusiveAccess, so the immediate outcome is the
same, but the remedies are not: a userspace holder is released by quitting whatever has
the device open, while a kernel driver needs an entitlement Apple has to grant. The
probe now labels each interface accordingly rather than calling everything a kernel
driver, which would send a reader after a DriverKit entitlement that could not help.
bind should draw the same distinction when it starts refusing devices.
bind now determines ownership up front and refuses with an explanation naming the
owner, distinguishing a kernel driver from a userspace holder. Verified against
hardware: a J-Link binds, a keyboard is refused with
macOS has bound a driver to it (AppleUserHIDDevice).
The original description follows.
Today bind allow-lists anything and only discovers the problem when a transfer fails.
The daemon should determine ownership up front, using the logic already proven in
Scripts/entitlement-validation/USBClaimProbe.swift: walk the IORegistry children of
the device node and treat any non-structural class as a claiming driver.
Structural classes to ignore (already established by the probe):
IOUSBHostDevice, IOUSBHostInterface, AppleUSBHostCompositeDevice,
AppleUSBHostDeviceUserClient, and the legacy shims.
That last one matters: AppleUSBHostDeviceUserClient is a userspace client, not a
kernel driver. Counting it as one made a fully usable device report as kernel-owned
during harness development. A different message is warranted — "another process has
this device open" is actionable in a way "a kernel driver owns it" is not.
bind should then either refuse with a specific message naming the owning driver, or
allow-list with an explicit warning. Refusing is preferable: silently allow-listing a
device that cannot be served is what the previous behaviour did.
Replaced with the measured split: which classes work, which do not, that seizing was measured and does not work, and a pointer at the harness.
Replace the blanket warning with the split. State plainly which classes work now, that
the DriverKit entitlement gates the rest, and point at
./Scripts/validate-usb-entitlements.sh so users can check their own hardware rather
than guess.
The system extension resource, its staging under prefix/Library/SystemExtensions,
and the post_install instruction to run usbipd install-system-extension were
removed from the tap on 2026-08-06, along with the assert_path_exists for the bundle.
Both update paths were fixed too, so a future release cannot reintroduce them.
post_install now states which devices can be shared. license "MIT" was added.
Nothing to drop yet, and nothing to add. The formula already installs the CLI to
bin. The System Extension bundle it stages under
prefix/Library/SystemExtensions is inert — OSSystemExtensionRequest resolves
extensions inside the calling app's bundle and requires that bundle to live in
/Applications, so a Homebrew prefix is never consulted. See
Sources/USBIPDCore/SystemExtension/README.md.
Done: the system extension resource, its staging, and the post_install instruction
were removed from the tap on 2026-08-06.
The formula is now kept current by formula-sync.yml in the tap, which polls this
repository's latest release every six hours and commits with its own GITHUB_TOKEN.
That replaced a repository_dispatch which needed a personal access token here — the
token expired after a year, the dispatch returned 401, and the sending step reported
success anyway, so v0.5.0 shipped with the formula pointing at a deleted release. That is a change in the tap repository, not here.
A cask is not needed for this release. It becomes the right vehicle only if a real
DriverKit extension appears, since a dext must live in an app bundle in /Applications.
No System Extension, no DriverKit entitlement, no provisioning profile, no notarization beyond what already exists. The 20,445-line System Extension subsystem stays quarantined.
Measured on 2026-08-09 against an FTDI Quad RS232-HS (0403:6011) and a Silicon Labs
CP2102N (10c4:ea60), both carrying IOUserSerial:
control transfer → idVendor=0403 idProduct=6011 PASS
bulk IN ep1 → status=0 actual=2 data=0260 FTDI modem-status bytes
Real bulk data, over USB/IP, from an adapter macOS has a driver attached to.
This document previously listed USB-serial as impossible. That was inference, not
measurement: a driver was attached, so the device was assumed held. The inference is
sound for HID and mass storage and wrong for serial, because IOUserSerial does not
take exclusive access of the interface.
The tempting refinement — "DriverKit dexts do not block, in-kernel drivers do" — is
also wrong. AppleUserHIDDevice is a dext and does block. Neither the driver's name
nor its kind predicts the outcome.
| Device | Driver | Interface open |
|---|---|---|
| FTDI Quad RS232-HS | IOUserSerial |
success |
| SiLabs CP2102N | IOUserSerial |
success |
| Keychron keyboard | AppleUserHIDDevice |
kIOReturnExclusiveAccess |
| Toshiba flash drive | IOUSBMassStorageDriver |
kIOReturnExclusiveAccess |
| Logitech webcam | mixed | kIOReturnExclusiveAccess |
So ownership is now decided by attempting the open, which is what the entitlement
harness always did and what bind did not. See DeviceOwnershipInspector.
One caveat this raises. macOS still has its serial driver attached and will still
publish a /dev/cu.* node. Nothing stops a local program opening that while a remote
client drives the same UART. That is a genuine hazard and is not guarded against.
A device can be fully claimable and still not work with its usual client, if that client's protocol keys off connection or reset events.
Measured with a Pixel 10a: the ADB interface claims cleanly and moves bulk traffic in
both directions, but adb devices reports the phone offline. On USB the phone
announces itself with CNXN once per connection, and macOS received that announcement
when the phone was plugged in. Attaching from a USB/IP client causes no bus reset the
phone can observe, so it never repeats it.
This follows from sharing a device macOS has already enumerated while not owning the
port, so it applies to any such protocol. Request/response devices — a debug probe, a
device in DFU mode — are unaffected, because the host initiates every exchange and
there is no announcement to miss. See android-adb-validation.md.
- Interop is proven for the handshake, not the full session. The wire format was
verified against
linux/drivers/usb/usbip/usbip_common.h, and a real Linuxusbipclient (Docker LinuxKit, which hasvhci_hcdbuilt in) completes bothlistandattachagainst this server. Note the QEMU harness is not what established this — it starts a local test server and inspects its log; it boots no VM and runs no client. - One device measured per category. J-Link and a keyboard.
- Only control transfers have moved real data. A GET_DESCRIPTOR control transfer
against the J-Link returns its genuine 18-byte device descriptor over USB/IP
(
Scripts/verify-usb-transfer.py). Bulk, interrupt, and isochronous transfers remain unexercised against hardware.
Interop validation is the prerequisite for calling this shippable. Everything else here is ready.