AudioDriverKit: IOService::Stop ordering and IOOperationHandler callback drainage

I'm developing an iPadOS USB audio-MIDI driver for a hardware device. The audio side uses AudioDriverKit and USBDriverKit (DriverKit SDK 27.0, AudioDriverKit deployment target 21.0, iPadOS 18.0 minimum).

I need clarification about the ordering between IOUserAudioDriver::Stop, StopDevice/StopIO, and in-flight IOOperationHandler callbacks.

According to Apple's DriverKit documentation, IOService::Stop must quiesce outstanding operations before invoking its superclass implementation. AudioDriverKit documents host-controlled audio I/O and the StopDevice → device StopIO sequence.

However, I cannot find an explicit guarantee that the framework has stopped and drained all BeginRead/WriteEnd callbacks before invoking the driver's service Stop.

Relevant implementation (simplified):

// Audio driver service termination
kern_return_t result = StopUSBTransport();

if (result != kIOReturnSuccess) {
    return result;
}

// Release resources and close provider interfaces.
return Stop(in_provider, SUPERDISPATCH);

The transport retirement path currently refuses to release resources while framework I/O is marked active:

if (__atomic_load_n(&ivars->m_adk_running,
                    __ATOMIC_ACQUIRE)) {
    // Retain ownership; resources cannot be released safely.
    return kIOReturnBusy;
}

The local running state is cleared following successful device superclass StopIO, but this does not independently establish that already-entered framework audio callbacks have returned.

The USB teardown separately prevents new submissions, aborts outstanding transfers, waits for cancellation acknowledgements and native callback completion, and retains resources if ownership cannot be proven safe.

An offline regression exercising service termination while framework I/O is marked active reproduces the Busy result. We have not established that AudioDriverKit delivers this ordering on physical hardware.

Questions:

  1. Before invoking IOUserAudioDriver::Stop, does AudioDriverKit guarantee that StopDevice/StopIO has completed and that all outstanding IOOperationHandler invocations have returned?

  2. If not, what supported mechanism can a driver invoke during service Stop to prevent new BeginRead/WriteEnd callbacks and wait for already-entered callbacks to finish?

  3. Does calling superclass IOUserAudioDriver::Stop establish this callback-drain boundary? If so, how should hardware and USB resources be shut down when provider access is no longer permitted afterward?

  4. If termination cannot safely complete, is returning kIOReturnBusy a supported way to defer service termination? Is a subsequent retry guaranteed?

I'm looking for the documented lifecycle and synchronization contract, rather than a workaround based on assumed callback ordering.

Any clarification from Apple's DriverKit or Core Audio engineering team would be appreciated.

Answered by DTS Engineer in 911283022

Handler lifetime: Does AudioDriverKit guarantee that an invoked handler block and its captured context remain valid from before callback entry through return, even if the IOUserAudioDevice is concurrently released or reset?

Basically, yes. IOUserAudioDevice owns the block you pass into SetIOOperationHandler and frees it in free, so it can't/won't be freed until after I/O has definitively stopped.

Having said that...

Retirement and reclamation: Is there a public completion point establishing that no further handler invocations can begin and all existing invocations have returned?

What's your larger concern here? The basic API contract here is that your "Stop" won't be called until your children have stopped interacting with you and that you won't call "super::Stop" until after you've stopped your own activity and disconnected from your provider.

However, issues like memory management are less of an issue than they would be in the kernel, since any memory you've allocated will be reclaimed when your process is destroyed, regardless of whether or not you manually returned that memory to the system.

Can AudioDriverKit release its last reference to the handler block on a real-time audio thread, potentially destroying captured objects there? If no public retirement mechanism exists, what ownership pattern does Apple recommend for safe, non-real-time reclamation, including incomplete teardown or cancellation?

No. As noted above, this is tied to IOUserAudioDevice's lifetime, so it won't be destroyed until everything else is "done".

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

According to Apple's DriverKit documentation, IOService::Stop must quiesce outstanding operations.

Yes. Keep in mind that this is the base class (IOService) method, so "IOService::Stop" means "my driver has finished its work and is no longer operational.” By definition, that means any family-specific/higher-level work must be finished before this point.

However, the most critical detail is here:

before invoking its superclass implementation.

The "natural" C++ pattern is that you call a method, that method does its work, and then that method calls super. That's NOT (necessarily) how your class's implementation needs to work. In many cases, what your implementation will be doing is actually:

  1. System calls DEXT::Stop().

  2. Your DEXT returns from Stop() and finishes its own cleanup work.

  3. Your DEXT finishes that work and calls super::Stop().

Shifting to your specific questions:

Before invoking IOUserAudioDriver::Stop, does AudioDriverKit guarantee that StopDevice/StopIO has completed and that all outstanding IOOperationHandler invocations have returned?

I haven't looked closely, but the answer is almost certainly no. Your DEXT is the "bridge" between system (for example, USB and Audio), so the system can't really determine what "should" happen when a given driver is destroyed. For example, a driver that aggregates multiple hardware devices into a single audio interface might end up having Stop called all the time without ever passing that on to a different driver.

If not, what supported mechanism can a driver invoke during service Stop to prevent new BeginRead/WriteEnd callbacks and wait for already-entered callbacks to finish?

Does calling superclass IOUserAudioDriver::Stop establish this callback-drain boundary? If so, how should hardware and USB resources be shut down when provider access is no longer permitted afterward?

Until you call super::Stop(), IOKit level "stop" hasn't actually started. That means your DEXT can continue performing I/O and otherwise operating "normally"[1].

If termination cannot safely complete, is returning kIOReturnBusy a supported way to defer service termination?

Funny story there... You can return whatever you want, because I don't think anyone is really looking at what you return. The starting point for DriverKit Stop is IOService::Stop_async_Impl which, as you can see in its base implementation here, returns void, meaning the kernel itself is discarding the error code. Looking through our own implementation, I haven't actually found ANY driver that uses that error code for anything interesting, so I think the error code was included in case one of the families decided to use it, not because it had any immediate usefulness. I'd recommend returning kIOReturnSuccess, but that's only because I've seen a few drivers do assert(ret==kIOReturnSuccess), not because I think it'll actually change anything.

Is a subsequent retry guaranteed?

To be clear, there's no retry because there doesn't need to be. You tell the system when you're "done" by calling super::Stop().

[1] The fact that you can submit I/O to your provider doesn't mean your provider has to perform that I/O. When a device is unplugged, you’re allowed to submit your I/O to your provider, and your provider is free to fail that I/O until you call super::Stop.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks, Kevin. Understood—cleanup can continue after Stop() returns, and calling super::Stop() signals completion.

Could you clarify the AudioDriverKit-specific step needed during that cleanup?

Is it supported for the driver to initiate StopDevice directly during service termination? If so, does completion of the resulting StopIO sequence ensure that no further IOOperationHandler invocations can begin and that existing invocations have returned?

If that sequence does not establish this synchronization point, which AudioDriverKit operation should be used before the audio buffers and the handler’s supporting resources are released?

Thanks, Kevin. Understood—cleanup can continue after Stop() returns, and calling super::Stop() signals completion.

Could you clarify the AudioDriverKit-specific step needed during that cleanup?

I'm not sure there really is any. Your job is primarily to manage the device below you, not the audio stack "above" you. In terms of what our DEXTs do, most of them just close their provider and call "reset" on the IOUserAudioDevice they created (basically destroying it).

I think part of the confusion is a misunderstanding of what these methods are "for":

Is it supported for the driver to initiate StopDevice directly during service termination? If so, does completion of the resulting StopIO sequence ensure that no further IOOperationHandler invocations can begin and that existing invocations have returned?

Both of these methods are methods you can choose to implement IF you want to be involved when the audio system initiates a "stop". Note this description in both methods:

"The default implementation always returns kIOReturnSuccess. Subclass and override this method to handle hardware-specific stopping, then call the superclass to update the I/O state."

In other words, these are about the audio system manipulating your driver, not DriverKit/IOKit manipulating the audio system.

That leads to here:

If that sequence does not establish this synchronization point, which AudioDriverKit operation should be used before the audio buffers and the handler’s supporting resources are released?

One thing to understand here is that the "Stop" process actually starts at your DEXT, so until you call super::Stop(), the audio system doesn't actually "know" that the process is occurring. That could be confusing things a bit, since there will naturally be activity that continues between the time you receive "Stop" and the time you call "super::Stop".

Is it supported for the driver to initiate StopDevice directly during service termination? If so, does completion of the resulting StopIO sequence ensure that no further IOOperationHandler invocations can begin and that existing invocations have returned?

You could, but I'm not sure it's necessary (see above). One other note— I believe StopIO is called inside StopDevice, so calling it directly isn't necessary, and many drivers just implement StopDevice.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

The related Feedback Assistant report is FB25067968.

So, based on the system log archive in the sysdiagnose, I think something is going wrong on your side. I think your main driver's logging has been redacted, but it looks like theres a problem with you MIDI driver and I suspect the same issue is happening in both DEXT. Looking at that logging, it appears to be initializing normally:

2026-10-05 20:31:36.230049+1100 kernel: (<dext id>) <private>: Starting IOUserMIDIDriver

Then eventually gets this error:

2026-10-05 20:31:36.433129+1100 kernel: (<dext id>) <prod name> MIDI RX - transfer stopped result=0xe00002ed

Followed by a whole lot of this error:

2026-10-05 20:31:36.449016+1100 kernel: (<dext id>) <private>: Failed to wait on host eventlink e00002d7

Until you eventually teardown here:

2026-10-05 20:31:36.454745+1100 kernel: (<dext id>) <prod name> MIDI - USB action cancellation completed remaining=0
2026-10-05 20:31:36.454830+1100 kernel: (<dext id>) <prod name> MIDI - Stop USB provider closed before superclass result=0x00000000
2026-10-05 20:31:36.455023+1100 kernel: (<dext id>) <private>: Calling StopIO on device 2
2026-10-05 20:31:36.455133+1100 kernel: (<dext id>) <prod name> MIDI - Stop complete after USB cancellation result=0x00000000
2026-10-05 20:31:36.455366+1100 kernel: (<dext id>) <private>: IOUserMIDIDriverUserClient disconnected
2026-10-05 20:31:38.961360+1100 kernel: Driver <bundle id> has crashed 0 time(s)

I dont know exactly what's going on, but I do have an educated guess which which would explain both the error why USB fails but thunderbolt does not.

Does your DEXT include "IOMatchCategory" in it's IOKitPersonalities dictionary?

If so, then you need to read through my "Basic introduction to DEXT Matching and Loading". Its all worth reading, but the relavant detail is the part that starts with:

"Following on from that point, most DEXT should NOT include "IOMatchCategory". Quoting IOKit fundamentals:"

Leading back to here:

...advertising the audio device's correct USB transport type prevents iPadOS from making the device available to audio applications, whereas advertising Thunderbolt allows it to function.

If you meant that the accessory works on Thunderbolt but fails on USB, the difference is all of the USB class drivers. On USB, including IOMatchCategory would allow other drivers to load "beside" yours, which means they'll end up opening the device first, preventing you from loading. That would also be true on PCI, except the lack of class drivers meant that there wasn't any other driver trying to load "beside" you, masking the problem.

Putting that another way, including IOMatchCategory is a mistake on PCI and USB, but it's a lot more likely to immediately cause problems on USB.

In any case, please let me know if I guessed right.

Covering a few other things:

The original .logarchive could not be located, but the filtered console export from that capture has been preserved.

One thing I'm always VERY careful about is being overly narrow about analyzing the console log. The system logs an enormous amount of data, which inevitably means that you have filter out a lot of it to find anything meaningful. However, the trap in that process is that we always start with an assumption about what's happening, so the focus the becomes narrowing the data set looking for THAT issue instead of trying to simply understand what happened. In the worst case, this can end up wasting hours/days poking at something that's totally irrelevant to the issue.

Also, what is the recommended way to capture an I/O Registry snapshot from the iPad?

Collect a sysdiagnose, look for ioreg/IOService.txt. That file contains a text dump of the full registry.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks, Kevin.

First, to answer your IOMatchCategory question: yes, both DEXT personalities originally included private matching categories. I've removed them, and subsequent controlled physical tests confirmed that both drivers attach to their intended USB providers with exclusive ownership. However, changing the advertised audio transport type from Thunderbolt to USB still prevents the device from appearing in an audio host/DAW. I've posted the detailed results in the USB transport thread.

Regarding service termination, I'm planning to restructure Stop() so cleanup proceeds asynchronously after the subclass method returns, with superclass Stop() called exactly once after cleanup. I understand that returning Busy does not request a retry, and that StopDevice/StopIO should not be assumed to drain outstanding audio callbacks.

One AudioDriverKit ownership issue remains before I can implement this safely.

Our IOUserAudioDevice registers an IOOperationHandler that accesses cached audio mappings and driver-owned state. I'm considering an independently owned context captured by the handler, with atomic admission control and an in-flight callback count. However, even callbacks rejected by admission control must first access that context, so counting admitted callbacks alone cannot establish its safe lifetime.

Could you clarify two points?

  1. Handler lifetime: Does AudioDriverKit guarantee that an invoked handler block and its captured context remain valid from before callback entry through return, even if the IOUserAudioDevice is concurrently released or reset?

  2. Retirement and reclamation: Is there a public completion point establishing that no further handler invocations can begin and all existing invocations have returned? Can AudioDriverKit release its last reference to the handler block on a real-time audio thread, potentially destroying captured objects there? If no public retirement mechanism exists, what ownership pattern does Apple recommend for safe, non-real-time reclamation, including incomplete teardown or cancellation?

I'm not assuming that StopIO, RemoveObject or superclass Stop() establishes callback retirement.

A supported ownership pattern or reference implementation would be extremely helpful. If this requires a more detailed code-level DTS investigation, please let me know.

Thanks,

Luke

Accepted Answer

Handler lifetime: Does AudioDriverKit guarantee that an invoked handler block and its captured context remain valid from before callback entry through return, even if the IOUserAudioDevice is concurrently released or reset?

Basically, yes. IOUserAudioDevice owns the block you pass into SetIOOperationHandler and frees it in free, so it can't/won't be freed until after I/O has definitively stopped.

Having said that...

Retirement and reclamation: Is there a public completion point establishing that no further handler invocations can begin and all existing invocations have returned?

What's your larger concern here? The basic API contract here is that your "Stop" won't be called until your children have stopped interacting with you and that you won't call "super::Stop" until after you've stopped your own activity and disconnected from your provider.

However, issues like memory management are less of an issue than they would be in the kernel, since any memory you've allocated will be reclaimed when your process is destroyed, regardless of whether or not you manually returned that memory to the system.

Can AudioDriverKit release its last reference to the handler block on a real-time audio thread, potentially destroying captured objects there? If no public retirement mechanism exists, what ownership pattern does Apple recommend for safe, non-real-time reclamation, including incomplete teardown or cancellation?

No. As noted above, this is tied to IOUserAudioDevice's lifetime, so it won't be destroyed until everything else is "done".

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

My larger concern was the lifetime of the driver-owned resources accessed by the audio callbacks, rather than the lifetime of the handler block itself.

The callbacks access shared driver state and audio buffers, while the USB transport has separate asynchronous completion and cancellation requirements. I wanted to ensure that teardown could not release those resources while an audio callback was still accessing them, without introducing blocking operations on the real-time thread.

Your explanation that IOUserAudioDevice retains the handler until I/O has definitively stopped, together with the service termination ordering you’ve described, clarifies the framework’s ownership responsibilities.

That should help ensure the driver’s own USB resources and state are retired safely before completing service termination.

Thanks.