USB Device Disconnect During I/O: Drain Callbacks Before Releasing Driver State

USB disconnect must close the submission path, finish or cancel outstanding requests, and drain their callbacks before releasing the state those callbacks use. Cancelling a transfer is not the same as waiting for its completion handler. An open file or queued work item can also outlive the physical device.

Applicability: this guide targets Linux v6.12 USB host-interface drivers using URBs. It uses usb-skeleton.c as a source-level example, not a production driver template. Gadget-side requests, userspace USB libraries, and device-specific reset protocols have different contracts and are outside this scope.

Four stages show closed submission, drained URBs, remaining users and final reference release, with asynchronous unlink contrasted with synchronous kill.
The exact workqueue and URB drain ordering must follow the driver’s dependency graph. This is a proposed lifetime model.

1. Inventory every path that can submit or retain state

List file operations, completion callbacks, workqueues, timers, and error-recovery paths that can submit another URB. For each, identify the object it references and the synchronization that protects admission. A disconnected flag checked without coordination with submission leaves a race between “still connected” and the actual submit.

Track requests that belong to an anchor separately from dedicated URBs stored directly in the device object. Killing one category does not prove the other has stopped. Also identify synchronous control helpers whose internal request handle is not directly available to your driver.

Draw the reference graph before drawing a cleanup sequence: interface binding, open files, URB callback context, and deferred work may each retain access. Mark whether each relationship owns a reference or only borrows a pointer. A borrowed pointer is safe only while another proven lifetime covers its use.

2. Distinguish unlink from kill

The v6.12 USB host API distinguishes asynchronous unlinking from usb_kill_urb(), which waits for completion handlers and leaves the URB idle on return. Killing cannot be performed from a completion handler, interrupt context, or while holding a spinlock because it can sleep.

Choose the primitive from the required postcondition. If cleanup will immediately free callback state, an asynchronous cancellation request alone is insufficient. If cancellation is initiated from atomic context, hand the waiting stage to an appropriate context while retaining all required references.

Do not let the completion callback free an URB while another thread is inside usb_kill_urb() on it. The API explicitly requires the URB itself to remain valid throughout the call. Separate ownership of the URB allocation from ownership of its transfer buffer and callback context; they need not share one release point.

3. Close admission before draining transfers

The v6.12 USB skeleton disconnect path deregisters its device node, marks disconnected under io_mutex, kills its dedicated input URB and anchored requests, then drops its binding reference. Read this together with its submission and file-operation paths, not as an isolated list of cleanup calls.

A proposed integration sequence is: stop new entry, publish the disconnected state under the admission protocol, release locks that callbacks need, drain submitted transfers, stop deferred producers, and retire references. The exact order between workqueue shutdown and URB draining depends on which can create the other; derive it from the reference graph.

A common failure is holding a mutex while waiting for a callback that needs the same mutex to finish. Another is draining an anchor while a worker can immediately add a new request. Establish a gate that covers every producer, then perform waits without introducing a dependency cycle.

4. Keep completion behavior valid during unplug

Completion may run while disconnect is in progress. It should classify cancellation or removal as a terminal event for that request, release the request’s resources exactly once, and avoid restarting traffic after admission has closed. Preserve enough status for a waiting caller to distinguish removal from successful I/O.

Do not assume that a callback’s return means all work it scheduled has finished. A completion that queues a processing job creates another lifetime edge. Either the queued job owns a suitable reference or shutdown must drain it before the referenced object can disappear.

For a hypothetical driver, request 42 completes while disconnect is draining request 43. Request 42’s callback queues a parser job holding the device pointer. If disconnect frees the device immediately after the last URB finishes, the parser job can still use freed memory. The repair is an explicit deferred-work lifetime, not an extra arbitrary delay after cancellation.

5. Retire open-file references separately

Physical unplug need not coincide with the application’s final close. The kernel kref guidance requires references to be acquired before handing an object to another user and released when that use ends. The final release callback performs destruction when the owned references are gone.

After disconnect, existing file operations should follow a defined unavailable-device path without attempting fresh USB I/O. Releasing the binding reference must not free state still legitimately retained by an open file. Conversely, leaving the entire object permanently allocated to avoid a race turns unplug into a leak.

Keep device generation identity separate from a pathname or minor number. Replug can create a new instance while an old file remains open. Old callbacks and file handles must not silently target the replacement device just because its user-visible name resembles the previous one.

6. Test the race boundaries deliberately

  1. Submission race: pause a submitter at the admission boundary, trigger disconnect, and verify that no request enters after the gate closes.
  2. Completion race: disconnect with callbacks active and check that destruction follows their final access.
  3. Deferred work: delay a callback-created job and verify its reference or draining contract.
  4. Open file: unplug while a reader waits; confirm it wakes with the documented outcome and later close releases its reference.
  5. Replug: create a new device instance before the old file closes; require strict separation between generations.

Use an isolated development device and instrument submission, gate closure, callback entry/exit, work completion, and final release. Kernel memory diagnostics can help expose invalid accesses, but a clean run alone is weaker than an explicit ordering trace. Include failed submissions and partially completed transfers in the record.

No USB driver was built or unplugged for this article. The deliverable is a submission-and-reference graph plus a disconnect/callback trace specification. Acceptance requires all producers to be closed, all borrowed accesses to be finished, and each owned reference to be released before final destruction.

Similar Posts