What is the supported DriverKit Stop/drain sequence for an IOUserClient operation queue?

Environment:

  • macOS 26.6.2 (25G83), Apple silicon
  • Xcode 26.6 (17F113)
  • DriverKit SDK 25.5

I am implementing a DriverKit IOService with an IOUserClient. This is a lifecycle and object-ownership question independent of the device protocol.

The intended design admits at most one user client during a provider lifetime. Lifecycle methods run on the provider’s default queue, while IOUserClient ExternalMethod requests run on a separate serial IODispatchQueue. At most one device request may be in flight.

The shutdown invariant we need is:

  1. Stop accepting new requests.
  2. Allow every accepted request to complete exactly once, or cancel it.
  3. Observe completion of the operation queue’s cancellation handler.
  4. Call the inherited Stop implementation last.
  5. Perform no provider access afterward.

The relevant public documentation is:

IOService::Stop: https://developer.apple.com/documentation/driverkit/ioservice/stop

IODispatchQueue::Cancel: https://developer.apple.com/documentation/driverkit/iodispatchqueue/cancel

IOService::SetDispatchQueue: https://developer.apple.com/documentation/driverkit/ioservice/setdispatchqueue

For the normal path, the proposed sequence is conceptually:

Stop(provider): close request admission operationQueue->Cancel(cancellationHandler) wait for the cancellation handler from the separate queue super::Stop(provider)

I need clarification of the complete supported public API contract:

  1. If IODispatchQueue::Cancel returns a non-success result, is its cancellation handler still guaranteed to execute? If it is not, what supported action lets Stop keep the provider and user client valid until previously accepted work is no longer capable of accessing them?

  2. Is it supported for the provider and its one user client to share the provider-owned serial operation queue? If the IOUserClient stops independently, must it own and cancel a separate queue, or is there a supported per-client drain mechanism that does not cancel provider-owned work?

  3. Is the driver’s public IOService::Stop override guaranteed to run on every termination path where accepted user-client work must be drained, including when the provider is already inactive or the DriverKit server has slept? If not, which public lifecycle callback supplies that drain point?

  4. Is blocking the provider’s default queue inside Stop while awaiting the cancellation handler from a separate operation queue the supported interpretation of “wait for your cancellation handlers”? If not, what public continuation mechanism should be used before calling inherited Stop?

We also observed one power-management panic after sleep/wake:

HiMDScsiDriver::setPowerState(..., 0 -> 4) timed out after 20342 ms

The DEXT does not currently override SetPowerState. This panic motivates the lifecycle review, but I am not treating it as proof that the Stop/drain design caused the timeout.

I am looking specifically for a supported public DriverKit sequence. I do not want to rely on private framework entry points or infer object-lifetime guarantees from a successful build or experiment.

First, I'd strongly recommend you take a look at the "Managing Device Removal" section of "IOKit Fundamentals", particularly "The Phases of Device Removal". That's the foundation of how this works, and I'll be referring back to it below.

So, with that context, the first thing to understand is that (kernel) IOService:stop and (DriverKit) IOService:Stop are NOT direct equivalents. Within the kernel, "stop" is actually the first step in final object destruction and, most critically, it won't be called until normal activity has stopped and I/O cleared. In practical terms, the reaching "stop" means that your driver has ALREADY stopped "working" and is functionally "dead".

That's NOT true of (DriverKit) IOService:Stop. More specifically, "Stop" is actually called during phase 2 as part of termination. Note these comments in the class reference:

"Before terminating the object in the provider, the system calls this method to stop the service associated with that object."

and

"After you call super, it is a programmer error to access the provider object."

Flipping those statements around, until your DEXT calls "super:Stop", its provider is still valid and fully usable. Similarly:

"If your driver has any in-progress asynchronous tasks, cancel those tasks and wait for DriverKit to call the associated cancellation handler before calling the super version of this method."

Meaning, until your DEXT calls "super::Stop"... it hasn't actually "stopped".

Understanding that last point is critical. Just like IOKit, device termination is a process your DEXT is part of, not something that's "done" to your DEXT. Indeed, the most common way device termination fails is that your DEXT doesn't tear down, leaving it "live" indefinitely.

Shifting into specifics:

Is blocking the provider’s default queue inside Stop while awaiting the cancellation handler from a separate operation queue the supported interpretation of “wait for your cancellation handlers”? If not, what public continuation mechanism should be used before calling inherited Stop?

So, I think the documentation was somewhat poorly phrased when it said:

"Use your implementation of this method to stop all activity and put your driver in a quiescent state.... wait for DriverKit… calling the super version of this method"

That natural reading of that is that you should block inside "Stop()", but there's actually no reason to do so. What's more typical, assuming the implementation isn't trivial, is to do what our IOUserClient sample code does, which is to fire off its cleanup work, then call "super::Stop()" when a callback determines that work is done.

If IODispatchQueue::Cancel returns a non-success result, is its cancellation handler still guaranteed to execute?

As far as I can tell, our "::Cancel" methods never actually fail. More specifically, the majority of them are hard-coded to return "kIOReturnSuccess". Most of them don't have a failure path at all, and the few exceptions I've found assert on any failure instead of returning. Somewhat amusingly, it looks like the author of IOInterruptDispatchSource() shared your concern, as its only failure path is an assert checking that a different cancel method didn't fail.

Honestly, I think I'd copy that approach and use an assert to confirm "kIOReturnSuccess". If cancellation fails, that’s a change/bug you need to investigate and resolve, not a normal behavior you can anticipate.

Is it supported for the provider and its one-user client to share the provider-owned serial operation queue?

This depends entirely on the driver and its user client. It's fairly typical for the provider to own all "work", with the user client simply passing commands to it. There are other cases where the user client is doing substantial tracking alongside its provider. It really just depends on what you're trying to do.

If the IOUserClient stops independently,

Keep in mind that user client termination is relatively common, since it can be triggered by things like the connecting process termination.

must it own and cancel a separate queue,

Most user clients are doing some amount of work to track the work they're managing on behalf of their client; however, there's a lot of variation in how complex that actually is.

or is there a supported per-client drain mechanism that does not cancel provider-owned work?

We don't have a specific API for this, as the details vary too much between use cases and implementations.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thank you. This resolves the normal Stop path, asynchronous cleanup pattern, Cancel-success expectation, and provider-owned work model.

We have a corrected lifecycle, but one blocker remains with one remaining clarification from question 3:

Does the public IOService::Stop phase-2/provider-validity guidance also apply when the provider is already inactive or the DriverKit user server has slept?

In those cases, is the driver’s public Stop override still guaranteed to be invoked, with the provider remaining valid until the driver’s asynchronous cancellation callback calls super::Stop?

I am asking only which public lifecycle guarantee the driver may rely on, not how any private framework entry point is implemented.

What is the supported DriverKit Stop/drain sequence for an IOUserClient operation queue?
 
 
Q