Skip to content

macOS: implement kernel driver detach#911

Closed
osy wants to merge 5 commits intolibusb:masterfrom
utmapp:macos-force-capture
Closed

macOS: implement kernel driver detach#911
osy wants to merge 5 commits intolibusb:masterfrom
utmapp:macos-force-capture

Conversation

@osy
Copy link
Copy Markdown
Contributor

@osy osy commented Apr 24, 2021

It's a well known issue on macOS that you cannot use libusb to attach to an interface already claimed by an KEXT for exclusive access. The common workaround is to do sudo kextunload or inject a codeless KEXT. Both require admin permissions, and does not work for all devices on latest macOS. Additionally, SIP has to be disabled to inject a codeless KEXT, making it a pain to work with USB devices.

The original implementation relied on a new option but that has been superseded by a new implementation which uses the libusb_detach_kernel_driver() and libusb_set_auto_detach_kernel_driver() APIs. However there are a few caveats.

First, the libusb_detach_kernel_driver APIs expect a single USB interface to be detachable. On macOS, we can only "capture" an entire USB device (and force all interfaces to be released by any existing driver). To keep compatibility with existing code, we count the number of (successful) calls to libusb_detach_kernel_driver and when that number is balanced by libusb_attach_kernel_driver, the entire device is re-enumerated and the original kernel driver is allowed to re-attach.

Second, libusb_detach_kernel_driver is expected to be called after libusb_open (this doesn't seem to be documented but I see that is the usage from existing code). This is problematic because the effect of USBDeviceReEnumerate with kUSBReEnumerateCaptureDeviceMask doesn't seem to take place until after USBDeviceOpen. So we call darwin_restore_state to close and re-open the device.

Third, libusb_reset_device uses USBDeviceReEnumerate which will release the capture flag and allow the driver to re-claim the interfaces. I'm not sure what the reasoning for avoiding ResetDevice (I see a comment "from macOS 10.11 ResetDevice no longer does anything so just use USBDeviceReEnumerate" but it seems to work fine in 10.15...). If we are currently capturing a USB device, then ResetDevice is used instead of USBDeviceReEnumerate.

kUSBReEnumerateCaptureDeviceMask requires either the application to be running as root OR the application has the entitlement com.apple.vm.device-access AND the user has accepted the authorization for the capture with IOServiceAuthorize. To get the second part to work, we detect when the entitlement com.apple.vm.device-access is available and then call IOServiceAuthorize in darwin_detach_kernel_driver before darwin_reenumerate_device. However, as the authorization decision is cached in the plugin's start() call, it is necessary to stop and start the USB plugin again.

I've only tested this with hidtest (modified to use libusb on OSX) and with libusbredir (in spice-gtk). More testing is needed specifically with calling libusb_detach_kernel_driver on multiple interfaces. On calling libusb_detach_kernel_driver before libusb_open or libusb_attach_kernel_driver after libusb_close (I'm not sure if this should be a supported use case but the APIs don't prescribe the usage order...). I'm also not sure why ResetDevice was avoided so there may be subtle issues in darwin_reset_device. Finally, there should be some testing with multiple calls to libusb_open/libusb_close.

@hjelmn
Copy link
Copy Markdown
Member

hjelmn commented Apr 26, 2021

The issue is that for since time ResetDevice has been a noop in macOS. If I remember correctly we even confirmed that with a wireshark. Do we know if that has changed? If not we may need to figure out how to make this work with reenumerate.


if (dpriv->capture_count > 0) {
/* we have to use ResetDevice as USBDeviceReEnumerate() loses the authorization for capture */
kresult = (*(dpriv->device))->ResetDevice (dpriv->device);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is ok for now but if possible it would be better if we can be consistent and always re-enumerate the device. This ensures that the device is actually reset. I am surprised we can't just specify capture again on reset but I have to assume you have already tried this. We may want to call this out somewhere in the docs so that users are aware that reset may not actually reset when capturing a device.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup I tried that the main problem is if you need authorization (it’s not running as root and it shows the user a pop-up to approve the device), it will need it again after calling re-enumerate (with or without the release flag). This means the user needs to click “approve” after every re-enum.

Perhaps someone with a usb sniffer can check the behaviour of the reset call again?

@hjelmn
Copy link
Copy Markdown
Member

hjelmn commented Apr 30, 2021

LGTM. Please squash into a minimal set of commits and we will merge.

@osy
Copy link
Copy Markdown
Contributor Author

osy commented May 3, 2021

LGTM. Please squash into a minimal set of commits and we will merge.

Can we wait for someone to test USB reset with a sniffer?

osy added 5 commits May 11, 2021 20:36
USBDeviceReEnumerate() does not return an error code (bug?) so if it fails
we could be stuck waiting forever. Set a sane timeout to 10s.
Since it calls USBDeviceReEnumerate() and not USBDeviceReset(), we name
it accordingly.
When libusb_detach_kernel_driver() is called on the first interface, we use
the capture re-enumerate APIs to force kernel drivers to detach. On
subsequent calls, a counter is incremented to keep track of the number of
detach calls. When libusb_attach_kernel_driver() is called for the same
number of times, then we re-enumerate (reset) and let kernel drivers
re-attach.

darwin_kernel_driver_active() is changed to attempt to claim an interface
and return 1 only if a kIOReturnExclusiveAccess status is returned. The old
implementation which looks for a CFBundleID in the IORegistryEntry does not
seem to work in all cases.

darwin_reset_device() is aware of the capture status and will re-set twice
if capture mode is enabled: once to do a USB reset and again to re-capture.
To use USBDeviceReEnumerate with kUSBReEnumerateCaptureDeviceMask your app
either needs to be running as root OR have the 'com.apple.vm.device-access'
entitlement AND have the user authorization requested via
IOServiceAuthorize().

We can use the capture re-enumerate APIs if either 1) the process is running
as root or 2) the 'com.apple.vm.device-access' entitlement is used AND
IOServiceAuthorize() is called. We assume that if the entitlement is not
there then we are running as root--if this is not true, then
darwin_detach_kernel_driver will fail anyways.

The authorization status is cached in the device's start() so we have to
stop() and start() the device by destroying the plugin and recreating it
again.
The InterfaceVersion is what we want and there is no reason to also check
that the InterfaceVersion matches the macOS version it was introduced in.
This check inadvertently disables features for other Apple platforms that
use IOKit.
@osy osy force-pushed the macos-force-capture branch from 2cc9c38 to e31c9ad Compare May 12, 2021 04:07
@programmingkidx
Copy link
Copy Markdown

I use libusb with a computer emulator called QEMU. I was able to test out the changes in the above patches. They do work. I was able to listen to MP3 files in several of my VM's using a real attached USB sound card. Good job everyone.

@hjelmn hjelmn closed this in 00688d0 May 16, 2021
@osy
Copy link
Copy Markdown
Contributor Author

osy commented May 16, 2021

@hjelmn was USB reset tested? I’m still having some issues that I think might be related to reset but I don’t have any sniffer to test.

I’ve been locally experimenting with a patch that implements the reenumeration but the kernel drivers always reattach as soon as you re-enumerate and then you can re-capture but there is a period of time when the kernel drivers take control and can possibly modify the device state. I’m still not sure how to do a “clean” reset while keeping capture state.

@hjelmn
Copy link
Copy Markdown
Member

hjelmn commented May 16, 2021

@hjelmn was USB reset tested? I’m still having some issues that I think might be related to reset but I don’t have any sniffer to test.

I’ve been locally experimenting with a patch that implements the reenumeration but the kernel drivers always reattach as soon as you re-enumerate and then you can re-capture but there is a period of time when the kernel drivers take control and can possibly modify the device state. I’m still not sure how to do a “clean” reset while keeping capture state.

I would like to get these changes up in a 1.0.25 RC so a wider audience can start testing. I don't have the equipment to test that reset works as expected. If there is a known issue we can add a known issues list while a fix is developed. Given how capture works I don't think we can easily fix it if it is broken.

@supercairos
Copy link
Copy Markdown

I tested this with various devices (gamepads, mouse, kbd, camera, USB Disk, ...) and it works like a charm! Thanks a lot.

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Jul 5, 2021

This pull request seems to cause regression as mentioned in #945 .

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Aug 8, 2021

#945 has been fixed. Another improvement #961 has been merged as well.

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Aug 8, 2021

Ref: Device Capture
https://developer.apple.com/documentation/iousbhost/iousbhostobjectinitoptions/3181672-devicecapture

Declaration
static var deviceCapture: IOUSBHostObjectInitOptions { get }

Discussion
Callers must have either the com.apple.vm.device-access entitlement and the IOUSBHostDevice object from IOServiceAuthorize(::) authorization, or have root privileges. Using this option terminates all clients and drivers of the IOUSBHostDevice and associated IOUSBHostInterface clients, as well as the caller. Upon destroy() of the IOUSBHostDevice, the device resets and IOUSBHostInterface reregisters for IOKit matching.

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Aug 8, 2021

Ref:
https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_vm_device-access

Property List Key: com.apple.vm.device-access
A Boolean value that indicates whether the app captures USB devices and uses them in the guest-operating system.
Availability

Discussion
The entitlement is required to use the IOUSBHost APIs for USB device capture.

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Aug 8, 2021

https://developer.apple.com/documentation/bundleresources/entitlements

Discussion
An entitlement is a right or privilege that grants an executable particular capabilities. For example, an app needs the HomeKit Entitlement — along with explicit user consent — to access a user’s home automation network. An app stores its entitlements as key-value pairs embedded in the code signature of its binary executable.

You configure entitlements for your app by declaring capabilities for a target in Xcode. Xcode records capabilities that you add in a property list file with the .entitlements extension. You can also edit the entitlements file directly. When code signing your app, Xcode combines the entitlements file, information from your developer account, and other project information to apply a final set of entitlements to your app.

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Aug 8, 2021

@osy Could you provide more details how you modify hidtest (HIDAPI test application) to use libusb and use this new feature under macOS? Thanks.

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Aug 8, 2021

Yes latest git head is good.

libusb on  master [?] took 9s ❯ sudo ./examples/xusb 0781:5595
Password:
Using libusb v1.0.24.11648

Opening device 0781:5595...

Reading device descriptor:
            length: 18
      device class: 0
               S/N: 3
           VID:PID: 0781:5595
         bcdDevice: 0100
   iMan:iProd:iSer: 1:2:3
          nb confs: 1

Reading BOS descriptor: 2 caps
    USB 2.0 extension:
      attributes             : 02
    USB 3.0 capabilities:
      attributes             : 00
      supported speeds       : 000E
      supported functionality: 01

Reading first configuration descriptor:
             nb interfaces: 1
              interface[0]: id = 0
interface[0].altsetting[0]: num endpoints = 2
   Class.SubClass.Protocol: 08.06.50
       endpoint[0].address: 81
           max packet size: 0200
          polling interval: 00
       endpoint[1].address: 02
           max packet size: 0200
          polling interval: 00

Kernel driver attached for interface 0: 1

Claiming interface 0...
libusb: info [darwin_detach_kernel_driver] no capture entitlements. may not be able to detach the kernel driver for this device

Reading string descriptors:
   String (0x01): "SanDisk"
   String (0x02): "Ultra USB 3.0"
   String (0x03): "0101e6a66ef16708e075e3c49050ce3f9585a63516548d5130a8c0815d0296f0a86e0000000000000000000062f48ad4ff1b71009555810739a8d6f9"
Reading Max LUN:
   Max LUN = 0
Sending Inquiry:
   sent 6 CDB bytes
   received 36 bytes
   VID:PID:REV " SanDisk":"Ultra US":"1.00"
   Mass Storage Status: 00 (Success)
Reading Capacity:
   sent 10 CDB bytes
   received 8 bytes
   Max LBA: 0729BFFF, Block Size: 00000200 (57.30 GB)
   Mass Storage Status: 00 (Success)
Attempting to read 512 bytes:
   sent 10 CDB bytes
   READ: received 512 bytes
   Mass Storage Status: 00 (Success)

  00000000  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000020  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000030  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000040  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000050  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000060  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000070  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000080  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000090  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  000000a0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  000000b0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  000000c0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  000000d0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  000000e0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  000000f0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000100  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000110  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000120  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000130  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000140  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000150  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000160  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000170  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000180  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00000190  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  000001a0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  000001b0  00 00 00 00 00 00 00 00 3b e2 ae 66 00 00 00 00  ........;..f....
  000001c0  21 00 0c fe ff ff 20 00 00 00 e0 bf 29 07 00 00  !..... .....)...
  000001d0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  000001e0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  000001f0  00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa  ..............U.

Releasing interface 0...
libusb: info [darwin_claim_interface] USBInterfaceOpen: another process has device opened for exclusive access
libusb: info [darwin_capture_release_interface] on attempt to reattach the kernel driver got ret=-5
Closing device...

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Aug 8, 2021

And hidtest works as well.

hidapi_test/libusb on  master [!?] ❯ sudo ./hidtest-libusb 
hidapi test/example tool. Compiled with hidapi version 0.10.1, runtime version 0.10.1.
Compile-time version matches runtime version of hidapi.

Device Found
  type: 04d8 003f
  path: 0002:001e:00
  serial_number: (null)
  Manufacturer: Microchip Technology Inc.
  Product:      Simple HID Device Demo
  Release:      2
  Interface:    0
  Usage (page): 0x0 (0x0)

Device Found
  type: 046d c52b
  path: 0002:000a:00
  serial_number: (null)
  Manufacturer: Logitech
  Product:      USB Receiver
  Release:      2410
  Interface:    0
  Usage (page): 0x0 (0x0)

Device Found
  type: 046d c52b
  path: 0002:000a:01
  serial_number: (null)
  Manufacturer: Logitech
  Product:      USB Receiver
  Release:      2410
  Interface:    1
  Usage (page): 0x0 (0x0)

Device Found
  type: 046d c52b
  path: 0002:000a:02
  serial_number: (null)
  Manufacturer: Logitech
  Product:      USB Receiver
  Release:      2410
  Interface:    2
  Usage (page): 0x0 (0x0)

Device Found
  type: 047f c025
  path: 0002:0008:03
  serial_number: CB13A3E40E8E47D6A40769C27E90A38E
  Manufacturer: Plantronics
  Product:      Plantronics C320-M
  Release:      135
  Interface:    3
  Usage (page): 0x0 (0x0)

Manufacturer String: Microchip Technology Inc.
Product String: Simple HID Device Demo
Serial Number String: (208) ?
Indexed String 1: Microchip Technology Inc.
Unable to send a feature report.
Unable to get a feature report.
hid_error is not implemented yetwaiting...
waiting...
waiting...
Unable to read()
Data read:

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Aug 8, 2021

And it is also okay with USB Composite Devices.


libusb on  master [!+?] took 5s ❯ sudo ./examples/xusb 046d:c52b
Password:
Using libusb v1.0.24.11648

Opening device 046D:C52B...

Reading device descriptor:
            length: 18
      device class: 0
               S/N: 0
           VID:PID: 046D:C52B
         bcdDevice: 2410
   iMan:iProd:iSer: 1:2:0
          nb confs: 1

Reading BOS descriptor: no descriptor

Reading first configuration descriptor:
             nb interfaces: 3
              interface[0]: id = 0
interface[0].altsetting[0]: num endpoints = 1
   Class.SubClass.Protocol: 03.01.01
       endpoint[0].address: 81
           max packet size: 0008
          polling interval: 08
              interface[1]: id = 1
interface[1].altsetting[0]: num endpoints = 1
   Class.SubClass.Protocol: 03.01.02
       endpoint[0].address: 82
           max packet size: 0008
          polling interval: 02
              interface[2]: id = 2
interface[2].altsetting[0]: num endpoints = 1
   Class.SubClass.Protocol: 03.00.00
       endpoint[0].address: 83
           max packet size: 0020
          polling interval: 02

Kernel driver attached for interface 0: 1

Claiming interface 0...
libusb: info [darwin_detach_kernel_driver] no capture entitlements. may not be able to detach the kernel driver for this device

Kernel driver attached for interface 1: 0

Claiming interface 1...

Kernel driver attached for interface 2: 0

Claiming interface 2...

Reading string descriptors:
   String (0x01): "Logitech"
   String (0x02): "USB Receiver"

Releasing interface 0...
libusb: info [darwin_claim_interface] USBInterfaceOpen: another process has device opened for exclusive access
libusb: info [darwin_capture_release_interface] on attempt to reattach the kernel driver got ret=-5
Releasing interface 1...
Releasing interface 2...
Closing device...

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Aug 8, 2021

@hjelmn @osy
BTW, initially I thought there is a regression for the libusb_kernel_driver_active (#959) in the above output since there is no driver attached to interface 1/2. However, I realized that the driver detach is per device and not per interface.

We may need to improve the documentation here.

The other thing is to document how to get the entitlement (I am not using Xcode so I have not tried it myself).

@osy
Copy link
Copy Markdown
Contributor Author

osy commented Aug 8, 2021

There actually isn’t a documented way. I had to manually contact Apple to get them. They are included with the VM entitlements and there’s no signup form or anything. You just have to purchase an Apple Developer membership and contact their technical support.

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Aug 8, 2021

There actually isn’t a documented way. I had to manually contact Apple to get them. They are included with the VM entitlements and there’s no signup form or anything. You just have to purchase an Apple Developer membership and contact their technical support.

I see. Thanks. Maybe I will just mention this in the wiki.

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Aug 8, 2021

@osy I think the Developer provisioning profile process should be similar to the following. Is that correct?
https://github.com/pqrs-org/Karabiner-DriverKit-VirtualHIDDevice

@osy
Copy link
Copy Markdown
Contributor Author

osy commented Aug 8, 2021

You need com.apple.vm.device-access and if you search across the web for how to obtain it, you'll find there's no page on Apple's site to do so.

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Aug 8, 2021

You need com.apple.vm.device-access and if you search across the web for how to obtain it, you'll find there's no page on Apple's site to do so.

A bit of mentioning in Apple websites. But I agree it is not well documented.
https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_vm_device-access

https://developer.apple.com/documentation/bundleresources/entitlements
You configure entitlements for your app by declaring capabilities for a target in Xcode. Xcode records capabilities that you add in a property list file with the .entitlements extension. You can also edit the entitlements file directly. When code signing your app, Xcode combines the entitlements file, information from your developer account, and other project information to apply a final set of entitlements to your app.

@hjelmn
Copy link
Copy Markdown
Member

hjelmn commented Aug 8, 2021

Improved the error message a bit:

Claiming interface 0...
libusb: info [darwin_detach_kernel_driver] no capture entitlements. may not be able to detach the kernel driver for this device
libusb: warning [darwin_detach_kernel_driver] device capture requires either an entitlement (com.apple.vm.device-access) or root privilege
libusb: info [darwin_capture_claim_interface] failed to auto-detach the kernel driver for this device, ret=-3
libusb: info [darwin_claim_interface] USBInterfaceOpen: another process has device opened for exclusive access
   Failed.

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Aug 8, 2021

Yes, the new warning and info message are good.

@zer0err
Copy link
Copy Markdown

zer0err commented Oct 29, 2021

Yes, the new warning and info message are good.

It's took me a while for reading history above, and a little bit confused. Is there any solution? macOS12 has released its official version, the libuvc still can't access camera anyhow. I did use the com.apple.vm.device-access in the entitlements.

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Oct 30, 2021

Yes, the new warning and info message are good.

It's took me a while for reading history above, and a little bit confused. Is there any solution? macOS12 has released its official version, the libuvc still can't access camera anyhow. I did use the com.apple.vm.device-access in the entitlements.

@llinshenzhen
I will create a new ticket on this topic. Please continue in the new ticket here, thanks.
#1014

@mcuee
Copy link
Copy Markdown
Member

mcuee commented Oct 30, 2021

This is probably not a good place to continue the discussion as it has already been closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants