AccessoryAccess in a sandboxed helper: IOUSBHostInterface fails without a temporary-exception entitlement. Is there a supported way?

Hello,

We are building USB passthrough for virtual machines on macOS 27 and would appreciate guidance from the AccessoryAccess / IOUSBHost / App Sandbox teams on the supported way to open a USB accessory's interfaces from a sandboxed helper process.

WHAT WE ARE BUILDING

Our app is a sandboxed macOS app that runs virtual machines. For guests that use Virtualization.framework, USB passthrough already works through VZUSBPassthroughDevice, using the AAUSBAccessory we get from AccessoryAccess.

For a second kind of guest we use our own virtual machine monitor, built on Hypervisor.framework. Each VM runs in its own helper process, a nested app bundle in Contents/Helpers that the main app launches with Process. The helper's entitlements are:

com.apple.security.app-sandbox      = true
com.apple.security.inherit          = true
com.apple.security.hypervisor       = true
com.apple.vm.networking             = true

The main app's entitlements include:

com.apple.security.app-sandbox      = true
com.apple.developer.accessory-access.usb = true
com.apple.security.device.usb       = true
(plus hypervisor, networking, files, application groups)

The helper emulates an xHCI controller for the guest and forwards the guest's USB transfers to the real device. We use the same AccessoryAccess hand-off the documentation describes for worker processes:

  1. The main app registers an AAUSBAccessoryListener with AAUSBAccessoryManager and shows the consent UI. Nothing changed here from the Virtualization.framework path.
  2. When the user attaches a device to a VM, the main app calls -[AAUSBAccessory createXPCRepresentation]. We send the bytes of that xpc_data object to the helper over our existing control channel. We use the bytes rather than an XPC connection because the representation contains only data, no Mach ports or file descriptors.
  3. The helper rebuilds the accessory with -[AAUSBAccessory initWithXPCRepresentation:] and calls -openWithServiceQueue:completionHandler:. This succeeds and returns an IOUSBHostDevice. In the IORegistry the device then has an AppleUSBHostFrameworkDeviceClient created by the helper.
  4. When the guest sends SET_CONFIGURATION, the helper calls -[IOUSBHostDevice configureWithValue:matchInterfaces:NO error:], which succeeds. It then finds the device's IOUSBHostInterface children in the IORegistry and opens each one with -[IOUSBHostInterface initWithIOService:options:queue:error:interestHandler:]. Bulk and interrupt transfers then go through IOUSBHostPipe (copyPipeWithAddress:, enqueueIORequestWithData:...).

THE PROBLEM

Steps 1-3 work, and so do control requests on the default pipe. The guest enumerates the device and loads its driver. Step 4 fails: every interface fails to open with

Failed to create IOUSBHostInterface. (kIOReturnInternalError, 0xe00002c9)

With no interfaces open there are no pipes, so the guest's driver fails as soon as it moves data. A USB flash drive reported "This device cannot start (Code 10)", and a USB Ethernet adapter reported "Network cable unplugged".

What we found:

  • An unsandboxed command-line tool, run while the helper held the device through AccessoryAccess, opened the same IOUSBHostInterface with no error. The IORegistry then showed an AppleUSBHostFrameworkInterfaceClient under the interface, created by that tool. So the kernel and the accessory state allow the open; the App Sandbox blocks it.

  • /System/Library/Sandbox/Profiles/application.sb contains:

    (when (entitlement "com.apple.security.device.usb")
          (allow iokit-open-user-client
                 (iokit-user-client-class
                   "IOUSBDeviceUserClientV2"
                   "IOUSBInterfaceUserClientV3")))
    

    com.apple.security.device.usb (which the app has) therefore covers only the legacy IOUSBLib user clients, not AppleUSBHostFrameworkInterfaceClient, the class IOUSBHostInterface uses. We found no rule for AppleUSBHostFrameworkDeviceClient either. We assume AccessoryAccess authorizes the device client when the accessory is opened, but nothing does the same for the accessory's interfaces.

  • Adding com.apple.security.device.usb to the helper itself doesn't work: the helper is killed at launch (exit status 5). We understand an inherited-sandbox helper may carry only a limited set of entitlements.

HOW WE WORKED AROUND IT

We added this entitlement to the main app. The helper inherits the app's sandbox, so it gets the exception too:

<key>com.apple.security.temporary-exception.iokit-user-client-class</key>
<array>
    <string>AppleUSBHostFrameworkInterfaceClient</string>
</array>

With it, the helper opens the interfaces and passthrough works: the flash drive mounts in the guest, and the Ethernet adapter gets a DHCP address and passes traffic.

We would rather not ship a temporary-exception entitlement, especially since we also distribute through the Mac App Store.

QUESTIONS

  1. Is opening an accessory's interfaces with IOUSBHostInterface the intended way to do I/O on a device obtained through AccessoryAccess? If so, what is the supported way to authorize AppleUSBHostFrameworkInterfaceClient for a sandboxed process, or a helper inheriting its sandbox, without a temporary exception?

  2. Does AccessoryAccess grant access to an accessory's interfaces as well as the IOUSBHostDevice? For example, should a listener registered with interface matching criteria (AAUSBAccessoryMatchingCriteria initWithDeviceMatchingDictionary: interfaceMatchingDictionaries:...) be able to open those interfaces? Or is there an API we missed that returns IOUSBHostInterface or IOUSBHostPipe objects for an opened accessory?

  3. Is passing the accessory to a helper as the bytes of -createXPCRepresentation, instead of through an XPC connection, supported? The documentation describes the representation as something sent "via XPC". Our helpers are plain child processes, not XPC services.

  4. Should we use the legacy IOUSBLib interface API instead (IOUSBInterfaceInterface through IOCreatePlugInInterfaceForService, client class IOUSBInterfaceUserClientV3, which com.apple.security.device.usb allows)? Is that supported for a device AccessoryAccess has opened for exclusive access, and is it acceptable to mix IOUSBHostDevice from AccessoryAccess with IOUSBLib interface objects for the same device?

  5. Would the temporary-exception entitlement above be accepted in App Store review for this case, or should we file a Feedback Assistant request for a dedicated entitlement or an API change?

Environment:

  • macOS 27 (Golden Gate), Apple silicon
  • Xcode with the macOS 27 SDK
  • App and helper are signed with the same team; the helper is a nested app bundle in Contents/Helpers using com.apple.security.inherit
  • Tested devices: a USB 3 flash drive (mass storage, Bulk-Only Transport) and a USB 2.5GbE Ethernet adapter (Realtek, vendor 0x0BDA, product 0x8156)

We can provide a minimal sample project that reproduces the IOUSBHostInterface failure, plus IORegistry dumps.

Thank you for your time.

Answered by DTS Engineer in 910862022
Written by rbmanian75 in 910803022

New problem

If you want to discuss that, I recommend that you start a new thread for it. I’d rather we focus this thread on the ‘interacting with an accessory from a child helper process’.

Apropos that, earlier I wrote:

Written by DTS Engineer in 910423022

That’s a bit of a worry given that the method’s return return type is xpc_object_t.

I confirmed with the Accessory Access folks that you can rely on this value being an XPC data object, so my concerns we unfounded. Cool.

Written by rbmanian75 in 910803022

We still use the legacy IOUSBLib interface API.

I’m glad you got it working, but the fact that you had to use the legacy interface is less than ideal. I’d appreciate you filing a bug about that.

Please post your bug number, just for the record.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Update: we found a configuration that works without the temporary-exception entitlement. We would still like to understand whether it is the intended approach, and why the modern IOUSBHost interface API is not usable here.

WHAT WORKS NOW

We kept AccessoryAccess and IOUSBHostDevice for the device itself, and switched only the interfaces and pipes to the IOUSBLib plug-in API:

  • The helper still rebuilds the AAUSBAccessory from its XPC representation and calls -openWithServiceQueue:completionHandler:. It uses the returned IOUSBHostDevice for control requests (-sendDeviceRequest:...) and for -configureWithValue:matchInterfaces:NO error:.
  • For each IOUSBHostInterface child of the device it now calls IOCreatePlugInInterfaceForService with kIOUSBInterfaceUserClientTypeID, queries kIOUSBInterfaceInterfaceID942, and calls USBInterfaceOpen. Async completions come through CreateInterfaceAsyncEventSource on a dedicated run loop thread.
  • Bulk and interrupt transfers use ReadPipeAsync and WritePipeAsync. Stall recovery uses ClearPipeStallBothEnds and alternate settings use SetAlternateInterface.

The main app's entitlements are now only the standard ones, plus com.apple.developer.accessory-access.usb and com.apple.security.device.usb. The helper inherits the app's sandbox. With this, a USB 3 flash drive mounts in the guest and a USB 2.5GbE Ethernet adapter (Realtek 0x0BDA:0x8156) gets a DHCP address and passes traffic.

We confirmed the cause twice:

  1. With the same entitlements (com.apple.security.device.usb, no temporary exception), the IOUSBHostInterface version of our helper still fails with "Failed to create IOUSBHostInterface" (kIOReturnInternalError, 0xe00002c9) for every interface. We retested this today on the same devices.

  2. A test tool running under a sandbox profile that allows only the two user-client classes application.sb grants for com.apple.security.device.usb:

    (deny iokit-open-user-client)
    (allow iokit-open-user-client
           (iokit-user-client-class "IOUSBDeviceUserClientV2"
                                    "IOUSBInterfaceUserClientV3"))
    

    On an interface of a device AccessoryAccess currently holds, -[IOUSBHostInterface initWithIOService:...] fails with the same error, while IOUSBLib's USBInterfaceOpen succeeds and reports the interface's pipes. Without the sandbox, both APIs open the interface.

So the difference is entirely the App Sandbox rule for AppleUSBHostFrameworkInterfaceClient.

FOLLOW-UP QUESTIONS

  1. Why does com.apple.security.device.usb allow IOUSBLib's interface user client but not AppleUSBHostFrameworkInterfaceClient, the client -[IOUSBHostInterface initWithIOService:...] creates? IOUSBHost is the API the documentation recommends for new code, and the one AccessoryAccess returns for the device. Is the missing sandbox rule intentional, or an oversight we should report through Feedback Assistant?

  2. Is there a supported way for a sandboxed app, or a helper inheriting its sandbox, to obtain an IOUSBHostInterface for an accessory it opened through AccessoryAccess, without a temporary-exception entitlement? For example, a planned API on AAUSBAccessory or IOUSBHostDevice that returns its interfaces.

  3. Is it supported long term to mix the IOUSBHostDevice returned by AccessoryAccess with IOUSBLib interface objects for the same device? Should we expect it to keep working, given that IOUSBLib is the older API?

  4. If IOUSBHostInterface does gain a sandbox rule later, we would prefer to move back to it. Is there anything we should watch for, such as a new entitlement name or a release in which the rule is added?

Thank you. We are happy to provide the test tool, the sandbox profile, and IORegistry dumps.

Sorry it did not work out in the later also. It did not run the latest binary. In both cases we need to add the temporary exception to get it work.

Is there a way i can get it work wihout temporary exceptiom

I created testflight version and ran it on developement machine(m5 macbook air) with the legacy approach and it worked. But when i test it on fresh m4 macbook air it is failing to connect the usb devices. Just for the update and current status of the bug.

My initial comment is that your overall goal — a sandboxed app that uses Hypervisor framework to run VMs in a one-child-process-per-guest setup — is something that I’d expect to work. However, there are a lot of moving parts here. The first thing I’d like to nail down is this:

3- When the user attaches a device to a VM, the main app calls -[AAUSBAccessory createXPCRepresentation]. We send the bytes of that xpc_data object to the helper over our existing control channel.

That’s a bit of a worry given that the method’s return return type is xpc_object_t. My initial research suggests that it’s OK, but I need to do some more research before I can say that definitively. I’ll update this thread when I know more.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

it is working with the legacy method by using createXPC on m5 macbook air for m4 macbook air i have to do some tweaking to get it to work. usb flash drives are getting connected and disconnected safely and it works ok. but when i connect usb ethernet adapter it is not reliable. some times the adapter is getting connected but it is in disabled state. disconnect and connecting again resolves the problem. this is the only problem i have. but with the modern approach i need to have temporary exception to get it to work. without that usb devices are connecting but they are not working. so my question is there a way we can make use of the modern api without temporary exception because appstore review dont encourage using this kind of exceptions

Update: resolved on our side. We still use the legacy IOUSBLib interface API.

We still use AccessoryAccess and IOUSBHostDevice for the device, and the legacy IOUSBLib
interface API (IOUSBInterfaceInterface942) for interfaces and pipes, because the sandbox has no
rule for the IOUSBHostInterface user client. We would move back to IOUSBHost if that changes.

New problem: a Realtek 0x0BDA:0x8156 2.5GbE adapter worked twice, then failed on every third
attach in the guest (device disabled or "not connected"). A USB flash drive was never affected.
The failing sessions had one thing in common: control requests on the default pipe returned
kIOReturnNotReady (0xE00002D8) from the first request until the guest gave up. The same device,
opened again a moment later, answered normally.

Fix, in our helper's open path, before the device is shown to the guest:

  1. After -openWithServiceQueue:completionHandler: returns, poll GET_STATUS for up to 1.5 s.
    Anything other than kIOReturnNotReady counts as ready.
  2. If the device is still NotReady, call -destroy and -closeWithCompletionHandler:, wait 0.5 s
    and open a fresh AAUSBAccessory from the same XPC representation. Up to 4 attempts.

The adapter now attaches and detaches repeatedly on M4 and M5 MacBook Air.

Questions:

  1. Is NotReady on an accessory opened right after release expected? Is there a notification or
    property that signals readiness, so we need not poll?
  2. Is there a reset that keeps the AccessoryAccess handle valid? -resetWithError: invalidates it.
  3. What is the recommended modern approach for this? We would like to use IOUSBHostInterface and
    IOUSBHostPipe for interfaces and transfers instead of the legacy IOUSBLib API, from a sandboxed
    helper that inherits the app's sandbox, with only AccessoryAccess and
    com.apple.security.device.usb. Is there a supported way to obtain the interfaces of an
    accessory opened through AccessoryAccess, or a sandbox rule or entitlement for
    AppleUSBHostFrameworkInterfaceClient, so we can drop the legacy path?

Thank you.

Written by rbmanian75 in 910803022

New problem

If you want to discuss that, I recommend that you start a new thread for it. I’d rather we focus this thread on the ‘interacting with an accessory from a child helper process’.

Apropos that, earlier I wrote:

Written by DTS Engineer in 910423022

That’s a bit of a worry given that the method’s return return type is xpc_object_t.

I confirmed with the Accessory Access folks that you can rely on this value being an XPC data object, so my concerns we unfounded. Cool.

Written by rbmanian75 in 910803022

We still use the legacy IOUSBLib interface API.

I’m glad you got it working, but the fact that you had to use the legacy interface is less than ideal. I’d appreciate you filing a bug about that.

Please post your bug number, just for the record.

Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Here is the bug fb no. FB25324561