iPAD: achieve UserClient's privileges in background to have robust IOServiceAddMatchingNotification(...) as if ~[Background modes][Enable external communications].

1) The circumstances: iPADOS~>18, USB still-imaging-gadget, 1-3 gadgets might connect simultaneously via USB-hub, proprietary DExt, UserClient App in developers' iPADs or in TestFlight.

= 1 variation A) UserClientApp has attribute [Background modes][Enable external communications].

= 1 variation B) "active USB-hub" vs "passive".

= 1 variation C) "ConsoleApp logs iPAD" vs "ConsoleApp is not started".

= 1 the term "zombie" below assume issue:

== after plug-in ConsoleApp logs "IOUsbUserInterface::init", "::start";

== UserClient never receives respective callback from IOServiceAddMatchingNotification

== further IOKit APIs "teardown", "restart" and "re-enumeration of connected gadgets" doesn't reveal the zombie (while it sees another simultaneous gadget);

== unplug of the gadget logs "IOUsbUserInterface::stop".

=2) The situation when UserClient is in foreground: ~ everything is fine. Note in MAC OS everything (same DExt and UserClient-code) works fine in background and in foreground.

=3) The situation when UserClient is in background (beneath ~"Files" or "Safari"):

=3A) "1A=true" phys plugin causes zombie in ~1/20 cases (satisfactory)

=3B) "1A=false" phys plugin causes zombie in ~1/4 cases (non-satisfactory)

=3C) "1 variation B" and "1 variation C" reduce zombies by ~>30%.

=3D) ConsoleApp logs with filter "IOAccessory" (~about USB-energy) always look similar by my eyes.

=4) From Apple-store: "The app declares support for external-accessory in the UIBackgroundModes"..."The external accessory background mode is intended for apps that communicate with hardware accessories through the External Accessory framework."..."Additionally, the app must be authorized by MFi,"

=5) My wish/ask:

=5 option A) A clue to resolve the "zombie-plugin-issue in background" in a technical manner.

= E.g. find a straightforward solution that ~elevates UserClient's "privileges" in background like [Enable external communication]...

E.g. what ConsoleApp-logs to add/watch to reveal my potential bugs in DExt or in UserClient?

=5 option B) A way to pursue Apple-store to accept (without MFI) an UserClientApp with [Enable external communication].

Answered by DTS Engineer in 861705022

So, first off, there are two critical details I need to clarify:

Note in MAC OS everything (same DExt and UserClient-code) works fine in the background and in the foreground.

macOS doesn't implement any of the app suspension semantics iPadOS does, which means you can't really compare the behavior of the two systems.

That leads to here:

1 variation A) UserClientApp has attribute [Background modes][Enable external communications].

The "external-accessory" background mode is totally unrelated to DriverKit and isn't actually "doing" anything in your app. It allows apps using the ExternalAccessory framework to communicate with their accessory in the background, but that framework (iOS 3.0) and the background mode (iOS 4.0) are MUCH older than DriverKit.

Making this as explicit as possible:

B) A way to pursue Apple-store to accept (without MFI) an UserClientApp with [Enable external communication].

...adding "external-accessory" is not changing ANYTHING about how your app wakes/sleep in the background. You will not be allowed to use it, but that's because it's not relevant to your app.

I want to be clear on this point because I've seen many cases in the past where developers have convinced themselves that "something" was providing them some special power/capability that it simply did not provide. The system’s background support is complicated enough that it can be easy to misunderstand what's going on.

That leads to what's going on here:

after plug-in ConsoleApp logs "IOUsbUserInterface::init", "::start";

This is your DEXT loading, as expected.

UserClient never receives the respective callback from IOServiceAddMatchingNotification

Correct. Your app is suspended in the background, so it isn't notified of your accessory when it's initially attached. DriverKit doesn't have any "background app" support, so unless your app was already awake, it won't be notified at attach.

further IOKit APIs "teardown", "restart" and "re-enumeration of connected gadgets" doesn't reveal the zombie (while it sees another simultaneous gadget);

What did you actually try here? Do you find the accessory if you:

  1. Force quit the app (swipe "up" in the fast app switcher) and then relaunch?

  2. Use IOServiceGetMatchingServices() to do a search of the registry when your app wakes up?

  3. Tear down and recreate your device notification code and start a new monitoring session with IOServiceAddMatchingNotification:

While you may sometimes get service notifications when your app wakes from suspension, I'm not sure that mechanism can be relied on across an extended app suspension. However, you should get any existing accessories if you call IOServiceAddMatchingNotification again.

*Part of this is that I believe the original mach messages may time out and be discarded, but the bigger issue is that, after a long period of suspension, it's possible for the device state to be completely different than when you originally suspended. For example, "your accessory" may have been plugged/unplugged multiple times, meaning it's still "there" but is actually a totally different DEXT instance than when you started. Even worse, your own connections may prevent normal driver teardown, leaving you with a connection to a nonfunctional device.

The better approach is to disconnect from your accessory and stop IOKit monitoring before you suspend, then recreate it when your app wakes again.

unplug of the gadget logs "IOUsbUserInterface::stop".

This is your DEXT tearing down.

The key point to understand here is that your DEXT and app operate as independent components. Your DEXT loads/unloads when your hardware attaches and your app follows the normal system behavior for apps waking/sleeping.

All of which then leads to here:

  1. My wish/ask:

5 option A) A clue to resolve the "zombie-plugin-issue in background" in a technical manner.

Why? What are you trying to do? More specifically, why does your app need to be awake in the background?

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

So, first off, there are two critical details I need to clarify:

Note in MAC OS everything (same DExt and UserClient-code) works fine in the background and in the foreground.

macOS doesn't implement any of the app suspension semantics iPadOS does, which means you can't really compare the behavior of the two systems.

That leads to here:

1 variation A) UserClientApp has attribute [Background modes][Enable external communications].

The "external-accessory" background mode is totally unrelated to DriverKit and isn't actually "doing" anything in your app. It allows apps using the ExternalAccessory framework to communicate with their accessory in the background, but that framework (iOS 3.0) and the background mode (iOS 4.0) are MUCH older than DriverKit.

Making this as explicit as possible:

B) A way to pursue Apple-store to accept (without MFI) an UserClientApp with [Enable external communication].

...adding "external-accessory" is not changing ANYTHING about how your app wakes/sleep in the background. You will not be allowed to use it, but that's because it's not relevant to your app.

I want to be clear on this point because I've seen many cases in the past where developers have convinced themselves that "something" was providing them some special power/capability that it simply did not provide. The system’s background support is complicated enough that it can be easy to misunderstand what's going on.

That leads to what's going on here:

after plug-in ConsoleApp logs "IOUsbUserInterface::init", "::start";

This is your DEXT loading, as expected.

UserClient never receives the respective callback from IOServiceAddMatchingNotification

Correct. Your app is suspended in the background, so it isn't notified of your accessory when it's initially attached. DriverKit doesn't have any "background app" support, so unless your app was already awake, it won't be notified at attach.

further IOKit APIs "teardown", "restart" and "re-enumeration of connected gadgets" doesn't reveal the zombie (while it sees another simultaneous gadget);

What did you actually try here? Do you find the accessory if you:

  1. Force quit the app (swipe "up" in the fast app switcher) and then relaunch?

  2. Use IOServiceGetMatchingServices() to do a search of the registry when your app wakes up?

  3. Tear down and recreate your device notification code and start a new monitoring session with IOServiceAddMatchingNotification:

While you may sometimes get service notifications when your app wakes from suspension, I'm not sure that mechanism can be relied on across an extended app suspension. However, you should get any existing accessories if you call IOServiceAddMatchingNotification again.

*Part of this is that I believe the original mach messages may time out and be discarded, but the bigger issue is that, after a long period of suspension, it's possible for the device state to be completely different than when you originally suspended. For example, "your accessory" may have been plugged/unplugged multiple times, meaning it's still "there" but is actually a totally different DEXT instance than when you started. Even worse, your own connections may prevent normal driver teardown, leaving you with a connection to a nonfunctional device.

The better approach is to disconnect from your accessory and stop IOKit monitoring before you suspend, then recreate it when your app wakes again.

unplug of the gadget logs "IOUsbUserInterface::stop".

This is your DEXT tearing down.

The key point to understand here is that your DEXT and app operate as independent components. Your DEXT loads/unloads when your hardware attaches and your app follows the normal system behavior for apps waking/sleeping.

All of which then leads to here:

  1. My wish/ask:

5 option A) A clue to resolve the "zombie-plugin-issue in background" in a technical manner.

Why? What are you trying to do? More specifically, why does your app need to be awake in the background?

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks a lot!

"...adding "external-accessory" is not changing ANYTHING about how your app wakes/sleep in the background. You will not be allowed to use it, but that's because it's not relevant to your app."

In my tests 2 weeks ago I (might be by my mistake) saw a correspondence between "external-accessory"and the facts of callbacks from "IOServiceAddMatchingNotification". Or at least correlation with the moments of such callbacks at the time of application moves from background to foreground.

Having your unambiguous statement I tediously rechecked my environment now and I found 2 possible weak-points":

A) I conducted the tests above having 2 identical DExts (for 2 different but very similar UserClient-Apps) formally enabled in "Settings|Applications|Drivers". Though, of course, only one UserClient was active. I shall repeat the test (on near weeks) with single DExt physically installed. Another "Q&A in TestFlight" was being conducted in distant country. I shall reconfirm about the environment there.

B)To distinguish the situation when DExt is disabled (in [Settings][Apps][...]) I enlist connecting devices by ~"code from StackOverflow":

int Enlist() { // enlist connected devices . CFMutableDictionaryRef qMatchDict = IOServiceMatching( "IOUSBDevice" ); io_iterator_t qIter; IOServiceGetMatchingServices( kIOMainPortDefault, qMatchDict, &qIter ); // try enlist specific names in registry ... io_service_t qDevice; while((qDevice=IOIteratorNext(qIter))!=IO_OBJECT_NULL) > ... IOObjectRelease( qDevice ); // end of while IOObjectRelease( qIter ); // release the iterator

Then ~>0.1 sec later I call "IOServiceNameMatching"+"IOServiceAddMatchingNotification" following DExt-sample code from this developers.apple.com.

C) If you have a little time, then could you, please, comment whether MAC-specific API "IOServiceGetMatchingServices" and respective access to the names in ioregistry's entries could affect timing of consequent callbacks from "IOServiceAddMatchingNotification" ?

Thank you again. If I re-test and find than in a "strict code-line" there is no quirks with callbacks from IOServiceAddMatchingNotification and specific gadgets then I shall:

  • recognize your statement "there is no correspondence between external-accessory and processes' scheduling in IPADOS" as the solution :)

  • apologize for disturbance with my wrong interpretation of the outputs of my previous tests

Accepted Answer

First off, I need to repeat what I asked here:

Why? What are you trying to do? More specifically, why does your app need to be awake in the background?

The system has multiple APIs for managing background work and it's entirely possible there IS an API that would meet your needs. Notably, we just introduced BGContinuedProcessingTask which will allow your app to stay awake in the background for an extended period of time (several minutes). If you're trying to complete work on the user's behalf, that API is the solution.

In my tests 2 weeks ago, I (might be by my mistake) saw a correspondence between "external-accessory"and the facts of callbacks from "IOServiceAddMatchingNotification".

Were you testing using Xcode? The issue here is that in order to debug your app, Xcode disables normal app suspension, which ends up keeping your app awake in the background "forever". That then makes all sorts of things work with normally "wouldn't".

Having said that, the word "correlation" did raise a possibility I hadn't really considered:

Or at least correlation with the moments of such callbacks at the time of application moves from background to foreground.

One thing to understand about the way the system manages background work is that its focus is on managing whether or not the app was awake, NOT what the app actually does if/when it's awake. As a concrete example of this, music streaming is free to use the network while they're awake in the background, even though they're ONLY awake because they're "playing audio".

Did your app ONLY include "external-accessory" or did you also create an EAAccessoryManager? I don't think the background category itself would change anything, but it's theoretically possible also having an active EAAccessoryManager would have triggered additional wakeups that would not otherwise have occurred. Those extraneous wakeups would be bugs, but it is possible something like that is going on.

However, none of this changes my original answer. Your app should not be using "external-accessory".

B)To distinguish the situation when DExt is disabled (in [Settings][Apps][...]), I enlist connecting devices by ~"code from StackOverflow":

The correct way to check this is to use OSSystemExtensionsWorkspace.systemExtensionsForApplicationWithBundleID to retrieve OSSystemExtensionProperties, which will tell you everything you want to know.

C) If you have a little time, then could you, please, comment whether MAC-specific API "IOServiceGetMatchingServices" and

I don't know what you mean here. As far as I'm aware, IOKitLib is largely "identical" between iOS and macOS. Certainly the core discovery APIs (like IOServiceGetMatchingServices) are the same.

respective access to the names in ioregistry's entries could affect timing of consequent callbacks from "IOServiceAddMatchingNotification" ?

No, at least not in a way I'd consider meaningful. From the "kernel" perspective, a match notification will always happen "before" a service can be discovered for the simple reason that the code that sends the notification is exactly the same code that's doing the matching of the driver itself. That is, IOServiceGetMatchingServices doesn't "know" about a given device until matching "fully" returns, and the actual match process (at a VERY high level) looks something like this:

  1. Driver registerForService.
  2. Matching "starts"
  3. ...do work of matching...
  4. Post matching notification
  5. Matching "ends"

SO, IOServiceAddMatchingNotification was sent at #4 but "IOServiceGetMatchingServices" can't "work" until after #5.

HOWEVER, none of that is what matters as far as user space is concerned, as the general thread latency/noise is MUCH larger than the actual time gap between #4/#5. In user space, the main issue here is how threads and even delivery interact with each other. For example, if you set up a thread such that it is:

  1. The target for IOServiceAddMatchingNotification.

  2. Fires a "frequent" timer that polls IOServiceGetMatchingServices().

...There's a decent chance that you'll sometimes see the device "first" through IOServiceGetMatchingService, particularly if you have other work scheduled on that same thread. The kernel sent the match notification "immediately", but your callback can't be called until the thread finishes its existing work.

Note that the main thread (or any "busy" thread) will exacerbate these issues. Large "chunks" of work will both prevent immediate notification delivery and increase the likelihood that your timer poll timer will fire before the match notification (because the system is trying to fire your timer at the time it said it would).

That last point is key here- if you're seeing "meaningful" delays with IOServiceAddMatchingNotification, the solution is to either:

  • Optimize the work of that thread so it's not staying blocked.

OR

  • Move IOServiceAddMatchingNotification to a different thread so that it isn't being blocked.

...not calling IOServiceGetMatchingServices "more frequently".

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Dear Kevin, thanks a lot again!

  1. "What are you trying to do? More specifically, why does your app need to be awake in the background?"

= Supported USB gadgets might perform async BulkTransfer being in background. Some of gadgets need sync BulkTransfer (while in background).

2nd) great thanks for "HOWEVER, none of that is what matters as far as user space is concerned, as the general thread latency/noise is MUCH larger than the actual time gap between #4/#5."

= I should be able to comprehend this fact myself. But I didn't .

3rd) great thanks for "OSSystemExtensionProperties,"

= I can delete a lot of my non-robust creativity. I didn't discover this class at all when I tried to comprehend the documentation myself.

After your explanations now, seemingly I see a couple of logical ways to fix my logical bugs :)

"What are you trying to do? More specifically, why does your app need to be awake in the background?" Supported USB gadgets might perform async BulkTransfer being in the background. Some of the gadgets need sync BulkTransfer (while in the background).

OK. So, two points here:

  1. UIApplication.beginBackgroundTask(withName:expirationHandler:) will give you ~30s to "finish" whatever you need to. That isn't a lot of time, but it is enough time to warn the user (so they can bring you to the foreground) and/or shutdown your connection in a "clean" way.

  2. BGContinuedProcessingTask (new in iPadOS 26) gives your app an "extended" amount of time to continue running in the background. Exactly how long that is isn't formally defined and will depend on what the user is doing, but "many minutes" is a reasonable expectation. See the "Finish tasks in the background" from WWDC2025 for a full overview.

3rd) great thanks for "OSSystemExtensionProperties," = I can delete a lot of my non-robust creativity. I didn't discover this class at all when I tried to comprehend the documentation myself.

Yeah, I'm afraid it can be easy to overlook.

After your explanations now, seemingly I see a couple of logical ways to fix my logical bugs :)

You're welcome and good luck!

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

iPAD: achieve UserClient's privileges in background to have robust IOServiceAddMatchingNotification(...) as if ~[Background modes][Enable external communications].
 
 
Q