Can SMAppService Daemon replace SMJobBless for exclusive HID capture from keyboards?

To gain exclusive access to keyboard HID devices like Amazon Fire Bluetooth remote controls, my app has been installing a privileged helper tool with SMJobBless in the past. The app - which also has Accessibility permissions - then invoked and communicated with that helper tool through XPC.

Now I'm looking into replacing that with a daemon installed through the newer SMAppService APIs, but running into a permission problem:

If I try to exclusively open a keyboard HID device from the SMAppService-registered XPC service/daemon (which runs as root as seen in Activity Monitor), IOHIDDeviceOpen returns kIOReturnNotPermitted.

I've spent many hours now trying to get it to work, but so far didn't find a solution.

Could it be that XPC services registered as a daemon through SMAppService do not inherit the TCC permissions from the invoking process (here: Accessibility permissions) - and the exclusive IOHIDDeviceOpen therefore fails?

SMAppService.daemon(plistName:) can fully replace SMJobBless, and it’s a lot easier to use.

However, accessing user-level things from a daemon has always been tricky. You wrote:

my app has been installing a privileged helper tool with SMJobBless in the past.

Does your SMJobBless code still work? That is, if you take an old build of your app and run it a modern version of macOS, can it successfully use IOHIDDeviceOpen?

Share and Enjoy

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

Does your SMJobBless code still work? That is, if you take an old build of your app and run it a modern version of macOS, can it successfully use IOHIDDeviceOpen?

Yes, it does.

On the same system, the SMJobBlessed privileged helper is able to IOHIDDeviceOpen the device, whereas a new project using SMAppService.daemon(plistName:) is rejected from doing so with a kIOReturnNotPermitted error being returned.

Neither the app nor the XPC service are sandboxed and Hardened Runtime is used with defaults for both (everything unchecked). The SMJobBless version is signed with Developer ID, the SMAppService version is currently just signed with the Development certificate.

I'm not sure if it is relevant, but I noticed in the logs that when using SMAppService, the XPC service is resolved as anon by runningboardd:

runningboardd: [com.apple.runningboard:process] com.mycompany.myservice: RunningBoard doesn't recognize submitted process - treating as a anonymous process
runningboardd: [com.apple.runningboard:process] Resolved pid 18156 to [anon<com.mycompany.myservice>:18156]

In contrast, going through SMJobBless, the XPC service is resolved as osservice:

runningboardd: [com.apple.runningboard:process] Resolved pid 3711 to [osservice<com.mycompany.myservice>:3711]

I've since converted the project to use SMJobBless instead and the calls to IOHIDDeviceOpen immediately started to succeed.

So to me it really looks like this is a current limitation of SMAppService.daemon(plistName:) vs SMJobBless() for this purpose.

Should I file a radar or DTS incident about this?

Being able to get exclusive access of any HID device (including "keyboards" like Amazon's Fire TV remotes) is absolutely critical to my app - and it would be great to know that this will continue to be possible on macOS even when SMJobBless() goes away (or defunct) some day. (and of course I really would love to use SMAppService instead - the API is so much nicer - especially when used from Swift :-) )

Should I file a radar … about this?

Let’s hold off on that for a minute.

Should I file a … DTS incident about this?

DTS incidents are no more. Instead we have DTS code-level support request. Most of those get bounced back to the forums [1], so you’re already in the right place.


Are you testing this on a clean machine? Or testing the old code and the new code on the same machine?

I suspect you’re hitting a TCC block and TCC can present weird results on development Macs. I generally recommend that you test stuff like this on a fresh machine. Usually I do this on a VM:

  1. I take a snapshot of the machine before it’s ever seen my app.
  2. I install my app.
  3. I test it.
  4. When I’m done, I restore the snapshot.
  5. And then repeat from step 2.

Try this with the SMJobBless version of your app and make sure it works. Then restore from the snapshot and try again with the SMAppService version. What do you see?

Share and Enjoy

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

[1] The most common exception being stuff that we have to handle in private, for example, stuff that relates to the specifics of your developer account.

Thanks for the suggestions and explanations! I wasn't able to use VMs for testing here as I couldn't find a way to make a HID device connected to the host "owned" by the guest. So instead I used two Macs never used for development.

On the first, I first ran the version using SMJobBless, then the version using SMAppService.

On the second, I ran them in the opposite order (SMAppService first, SMJobBlesss second).

I got results consistent with my development Mac on both (SMJobBless works, SMAppService version call to IOHIDDeviceOpen fails with kIOReturnNotPermitted).

Regarding TCC: during debugging this earlier on my development Mac, I also tried resetting TCC for my app via

tccutil reset All com.mycompany.myapp

(and then re-assigning it the Accessibility permissions through System Settings), but this also didn't have an effect on the outcome.

I wasn't able to use VMs for testing here as I couldn't find a way to make a HID device connected to the host "owned" by the guest.

Ah, yeah, that’s a pain. Virtualization framework doesn’t support USB device access, so any VM app based on that is similarly limited.

I also tried resetting TCC for my app

The tricky part here is that, once you start messing around with code running as root, there are multiple TCC databases to contend with, namely the one for your user account and the one for the system as a whole. That complicates matters.

So, anyway, lemme clarify your actual setup. It sounds like you’re:

  1. Using an IOHIDManager to discover HID devices.
  2. That vends you various IOHIDDevice objects.
  3. You choose one and call IOHIDDeviceOpen on it.

Is that right?

Are you passing in kIOHIDOptionsTypeSeizeDevice?

Share and Enjoy

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

The tricky part here is that, once you start messing around with code running as root, there are multiple TCC databases to contend with, namely the one for your user account and the one for the system as a whole. That complicates matters.

Thanks for the insight. I didn't know that! Out of curiosity: would a

sudo tccutil reset All com.mycompany.myapp

reset the permissions for the app for the root user?

So, anyway, lemme clarify your actual setup. It sounds like you’re:

  1. Using an IOHIDManager to discover HID devices.
  2. That vends you various IOHIDDevice objects.
  3. You choose one and call IOHIDDeviceOpen on it.

Is that right?

I'm doing it a bit differently:

  1. Using IOServiceAddMatchingNotification() to look for IOKit services of IOHIDDevice class.
  2. Match the properties of the IOKit services/devices I receive in the callback with the properties in the callback. And if it fulfills my criteria, proceed to the next step.
  3. Use IORegistryEntryGetRegistryEntryID() to get the registry entry ID for the IOKit service of the HID device
  4. Send that IOKit RegistryEntryID over XPC to my service running as root.
  5. In the service running as root, use IOServiceGetMatchingServices(kIOMainPortDefault, IORegistryEntryIDMatching(entryID), …) to get the IOKit service for the HID device I want to open
  6. Use IOHIDDeviceCreate(kCFAllocatorDefault, ioService) to get the IOHIDDeviceRef for the IOKit service
  7. Pass that to IOHIDDeviceOpen() to open the device: via IOHIDDeviceOpen(hidDevice, kIOHIDOptionsTypeSeizeDevice)

Are you passing in kIOHIDOptionsTypeSeizeDevice?

Yes I do.

If it helps, I can provide a source code example via Feedback Assistant that's ready to run after adjusting the Team IDs.

Can SMAppService Daemon replace SMJobBless for exclusive HID capture from keyboards?
 
 
Q