USB enumeration, URBs, drivers, and disconnect

Trace USB enumeration, URB completion and disconnect synchronization.

16 min readChapter 62 of 168Intermediate → advanced
In this chapter

Before you start#

Read character devices and TTYs and DMA ownership. Linux v6.12 is the historical source baseline. USB topology, descriptors, controller and runtime PM vary. The lab only reads already-published metadata; it sends no control transfers and does not unplug or reset anything.

Learning goals#

Follow USB enumeration and interface binding, distinguish endpoint transfer types, trace an asynchronous URB and USB skeleton's disconnect cleanup, and explain why submitted work and open-file lifetime survive beyond a single syscall.

Source walkthrough#

USB enumeration, URBs, drivers, and disconnect
  1. Enumeration and match

    USB core discovers a device and matches an interface.

  2. Probe and open

    Skeleton validates endpoints and obtains PM and kref ownership.

  3. URB submission

    Accepted request remains active until completion or kill.

  4. Disconnect

    Block new I/O, drain URBs, then release references.

Conceptual model. Conditional Linux v6.12 source model; illustrative state and ownership, not a captured hardware execution.

A USB host controller driver (HCD) operates a host controller and its root hub. Hubs report port changes; USB core enumerates a newly discovered device, assigns an address and reads descriptors to identify its capabilities and configurations. A usb_device represents the device, while a usb_interface represents a function within an active configuration. An interface can have alternate settings with different endpoints. A usb_driver normally matches and probes an interface using ID and class information; a composite device can have several interfaces bound to different drivers. Configuration changes, authorization, hub topology and errors can prevent or change binding. See drivers/usb/core/hub.c, drivers/usb/core/driver.c, and include/linux/usb.h.

An endpoint has a direction, number, transfer type and capacity described by its descriptors. Control transfers carry a setup request and associated stages; bulk transfers move nonperiodic data with no fixed latency guarantee; interrupt transfers are periodically scheduled USB transfers and are not the same thing as a CPU hardware interrupt; isochronous transfers reserve periodic service with different error/retry characteristics and timing expectations. Endpoint type and speed impose limits. A device descriptor or interface match is not proof that a particular endpoint is present or that a transfer succeeds.

The UAPI struct usb_endpoint_descriptor, Linux v6.12, GPL-2.0 WITH Linux-syscall-note (the audio-only fields are elided):

c
struct usb_endpoint_descriptor {
	__u8  bLength;
	__u8  bDescriptorType;

	__u8  bEndpointAddress;
	__u8  bmAttributes;
	__le16 wMaxPacketSize;
	__u8  bInterval;
	/* audio-only fields and comments omitted */
} __attribute__ ((packed));
A bulk-IN endpoint descriptor

Synthetic teaching data, not a device capture.

Select a field to connect its position, bytes, and meaning.

6 bytes in file order. Offsets start at zero.

Selected: bLength. Descriptor length: 7 bytes.

Read the byte table
OffsetFieldHex bytesInterpretation
0bLength07Descriptor length: 7 bytes
1bDescriptorType05USB_DT_ENDPOINT (0x05)
2bEndpointAddress81Direction bit set (IN); endpoint number 1
3bmAttributes02Low bits 2: bulk transfer type
4wMaxPacketSize4000Little-endian __le16: 0x0040 = 64 bytes
6bInterval00Polling interval; ignored for bulk endpoints
Synthetic teaching data. Synthetic 7-byte bulk-IN endpoint descriptor laid out from the Linux v6.12 UAPI struct; not captured from any device.

bEndpointAddress carries the direction and endpoint number, bmAttributes carries the transfer type, and wMaxPacketSize is a little-endian __le16. The structure describes what a descriptor contains, not which endpoints a device exposes or whether a transfer will succeed; those depend on the configuration the device reports.

An URB (struct urb) describes an asynchronous USB request: device, endpoint/pipe, transfer buffer, length, completion callback, context, status and actual length among other fields. The caller prepares the URB and submits it through USB core to the HCD. A successful usb_submit_urb means it was accepted for asynchronous processing, not that the device transferred all bytes. In usb_submit_urb, Linux v6.12, GPL-2.0, early validation includes:

c
	if (!urb || !urb->complete)
		return -EINVAL;
	if (urb->hcpriv) {
		WARN_ONCE(1, "URB %pK submitted while active\n", urb);
		return -EBUSY;
	}

The first test requires an URB and completion callback; the second rejects an already-active URB. Later checks validate device state, endpoint and transfer type, then set status to in-progress and actual_length to zero before further submission work. A return error means the caller must follow that API's failure cleanup rather than wait for a completion that was never successfully submitted. The HCD and DMA mapping handle the transfer under USB core rules; buffer and URB memory must remain valid until completion or a synchronous cancellation protocol says otherwise.

USB skeleton: open, I/O, and disconnect#

drivers/usb/usb-skeleton.c is an illustrative example driver, not a production template or a specification for every USB device. skel_probe allocates a private usb_skel, initializes references, locks and anchors, locates suitable bulk endpoints, allocates its read URB/buffer and registers a minor-device interface. A missing endpoint or allocation/registration failure follows cleanup instead of exposing a fully functional file. Endpoint descriptors and sizes come from the actual interface; a driver must validate them rather than assume every USB device has the skeleton's layout.

skel_open finds an interface from the minor number and obtains its private state. It calls usb_autopm_get_interface before taking an additional kref for the open file and storing the private pointer in file->private_data. A failed lookup or PM get returns an error and does not establish that file ownership. skel_release drops the PM usage reference and the file's kref under its path. A USB runtime-PM reference helps coordinate power management; it is not proof that a physical device cannot disconnect. The kref keeps the private object alive, not the hardware cable attached.

In skel_release, Linux v6.12, GPL-2.0, the close path releases only what open acquired (the function is shown in full):

c
static int skel_release(struct inode *inode, struct file *file)
{
	struct usb_skel *dev;

	dev = file->private_data;
	if (dev == NULL)
		return -ENODEV;

	/* allow the device to be autosuspended */
	usb_autopm_put_interface(dev->interface);

	/* decrement the count on our device */
	kref_put(&dev->kref, skel_delete);
	return 0;
}

It drops the autopm reference and the file's kref. The object itself is freed in skel_delete only when the last kref is released. The excerpt does not show skel_open error paths, so it does not establish which references a failed open has already dropped.

skel_disconnect, v6.12, GPL-2.0, contains:

c
	mutex_lock(&dev->io_mutex);
	dev->disconnected = 1;
	mutex_unlock(&dev->io_mutex);

	usb_kill_urb(dev->bulk_in_urb);
	usb_kill_anchored_urbs(&dev->submitted);

The mutex protects setting the disconnected state against I/O start paths that use the same lock. It then synchronously kills the read URB and anchored submitted URBs. Before this excerpt the driver deregisters its minor; afterward it drops the interface's kref. An already-open file may still hold a kref, so private state can survive until the final release even though new hardware I/O is no longer permitted. Killing URBs and marking disconnected are separate steps. A callback and a concurrent file operation must obey the driver's lock, error and reference rules.

Skeleton disconnect ordering
  1. Deregister minor

    usb_deregister_dev gives back the minor and stops new opens.

  2. Set disconnected

    Under io_mutex, so protected I/O start paths see the flag.

  3. Kill read URB

    usb_kill_urb waits for the read URB to finish.

  4. Kill anchored URBs

    usb_kill_anchored_urbs drains tracked writes.

  5. Drop kref

    The object is freed only when the last kref is released.

Conceptual model. Teaching model of skel_disconnect ordering in the Linux v6.12 USB skeleton, an illustrative example driver; not a production template.

Completion, anchors, and cancellation#

A completion callback runs asynchronously in a context that generally must not sleep; code should use the appropriate locking and defer sleepable work. It inspects urb->status and urb->actual_length: success can still have a short transfer, and an error can be reported after submission returned zero. The buffer must not be reused or freed while the HCD owns the active request. Coherent buffers, streaming DMA mappings and USB core mapping flags have distinct rules; an arbitrary CPU pointer is not automatically a device DMA address. USB core/HCD performs applicable mapping work, but the driver must use the correct allocation and flags and retain the buffer for the full transfer.

An anchor tracks a group of URBs for management; it is not a packet queue and does not itself make a callback sleepable. usb_unlink_urb requests asynchronous unlink under its contract, while usb_kill_urb synchronously waits for the URB and its completion to be finished and prevents resubmission during the kill operation. It can sleep and is not an operation to invoke while holding a lock needed by the completion callback. usb_kill_anchored_urbs handles URBs in an anchor. The driver must also stop new submissions before draining; otherwise another thread can submit work after the drain. The skeleton's io_mutex and disconnected flag form part of its particular protocol.

Consider a hypothetical bulk read URB U with a 64-byte buffer. Submission returns zero, then unplug begins before completion. Disconnect deregisters the minor and sets disconnected = 1 under io_mutex, preventing a subsequent I/O start in the protected path. It calls usb_kill_urb(U) and waits for its completion/cancellation. The callback observes the actual status and length, which may be an unlink/shutdown error or another result depending on the race; do not assert one universal status. Only after the relevant callback and references are finished can its buffer be freed. An open fd can retain the skeleton object through its kref, but cannot turn the disconnected device back into a valid transfer target.

URB U from submit to completion
URB U from submit to completion1. Driver to USB core: usb_submit_urb(U). 2. USB core to Driver (return): 0: accepted, not finished. 3. USB core to HCD: queue bulk transfer. 4. Driver to USB core: unplug: usb_kill_urb(U). 5. USB core to HCD: unlink U. 6. HCD to Completion: callback: status, actual_length. 7. Completion to Driver (return): callback returns; kill completes.DriverUSB coreHCDCompletionusb_submit_urb(U)0: accepted, notfinishedqueue bulk transferunplug:usb_kill_urb(U)unlink Ucallback: status,actual_lengthcallback returns; kill completes
  1. DriverUSB core

    usb_submit_urb(U)

  2. USB coreDriver

    Return. 0: accepted, not finished

  3. USB coreHCD

    queue bulk transfer

  4. DriverUSB core

    unplug: usb_kill_urb(U)

  5. USB coreHCD

    unlink U

  6. HCDCompletion

    callback: status, actual_length

  7. CompletionDriver

    Return. callback returns; kill completes

Time runs downward. Dashed arrows show returns.

Read the text version
  1. Driver → USB core: usb_submit_urb(U)
  2. USB core → Driver (return): 0: accepted, not finished
  3. USB core → HCD: queue bulk transfer
  4. Driver → USB core: unplug: usb_kill_urb(U)
  5. USB core → HCD: unlink U
  6. HCD → Completion: callback: status, actual_length
  7. Completion → Driver (return): callback returns; kill completes
Conceptual model. Conceptual model of the chapter hypothetical URB U in Linux v6.12 USB core; the final status depends on the race. Not a captured trace.

Enumeration failure and object lifetime#

Enumeration is a sequence of control exchanges and state transitions performed by USB core/HCD and hub logic. A port can disconnect during descriptor reads, a descriptor can be malformed, address assignment can fail, or no authorized configuration can be selected. A driver can match an interface then fail probe because an endpoint or memory allocation is missing. Each stage must release the references and allocations it actually acquired. A read-only sysfs entry or lsusb topology snapshot does not prove that an interface is currently bound or that a transfer was tested.

A usb_device reference and a usb_interface reference protect kernel objects under their APIs. The driver must match the reference acquisition and release; holding a pointer without a reference across disconnect risks use after free. Conversely, a reference does not guarantee that the device remains connected. The skeleton keeps an interface and device association in its private structure, uses a kref to coordinate open and disconnect, and has a delete callback for final release. A production driver may need additional state and more complex ordering for multiple endpoints, reset, autosuspend, userspace mappings or firmware protocols.

Transfer types and timing#

A control request has a setup packet and can have data and status stages, but its device-specific request semantics must be documented by the device. Bulk is appropriate for reliable nonperiodic transfers at the USB protocol level; a bulk application still must handle short transfers, stalls, disconnection and errors. Interrupt endpoints offer periodic service scheduling and commonly carry small reports; the word "interrupt" describes the USB transfer type, not a direct CPU interrupt for each report. Isochronous transfers trade retry behavior for timing, with per-packet status and length relevant to audio/video-like workloads. Poll interval, bandwidth reservation and maximum transfer size depend on speed and descriptors.

A stalled endpoint, canceled URB or unplug can yield different status codes. Blindly resubmitting in a completion callback without checking disconnected state can race removal; sleeping in that callback can violate context rules. A driver using an anchor must know whether a submitted URB is anchored before a disconnect drain, and a failure after allocation but before submission requires different cleanup from an active URB. usb_kill_urb waits for an active callback to finish; it does not automatically cancel unrelated timers or workqueues that the callback scheduled.

Probe and endpoint selection in the example#

The skeleton driver's skel_probe is called for an interface that matched its ID table. It allocates a private object, initializes the kref, locks, wait queue and write anchor, and obtains references needed to identify the device and interface. It iterates endpoint descriptors looking for bulk-in and bulk-out endpoints, records the relevant endpoint addresses and packet size, and allocates a read buffer and read URB. If either required endpoint is absent, it returns an error instead of publishing an interface that cannot perform its intended transfers. A later registration failure also reaches cleanup. The example's device IDs and endpoint expectations are illustrative; a different USB driver must validate its own descriptor layout and protocol.

In skel_probe, Linux v6.12, GPL-2.0, the bulk endpoint lookup runs before the read buffer and URB are allocated (the earlier setup lines are omitted):

c
	/* set up the endpoint information */
	/* use only the first bulk-in and bulk-out endpoints */
	retval = usb_find_common_endpoints(interface->cur_altsetting,
			&bulk_in, &bulk_out, NULL, NULL);
	if (retval) {
		dev_err(&interface->dev,
			"Could not find both bulk-in and bulk-out endpoints\n");
		goto error;
	}

The helper keeps the first bulk-in and bulk-out endpoints, as the comment says. A missing endpoint jumps to the error label before any read buffer or URB is allocated. The excerpt does not show that label cleanup, or whether another alternate setting could have supplied the endpoints.

After registration, an open resolves the minor to an interface, obtains its private state, takes the interface's runtime-PM usage reference and then increments the kref. The file's private pointer is useful only while that reference and the driver's disconnect protocol remain valid. A failed PM get returns before the kref and file-private assignment shown in the source. skel_release balances the PM and object references on its own path. A usb_device object can remain referenced after unplug, but transfers to the disconnected physical device will not magically work.

URB buffer and completion example#

Suppose a bulk-out write creates URB W with a 32-byte transfer buffer. After the driver anchors W and successfully submits it, the HCD can use the buffer until the completion callback or synchronous cancellation establishes that access has ended. The callback inspects status, records an error where appropriate, frees the coherent buffer in the skeleton's write path and releases its throttling semaphore. A return from write can precede that callback. If usb_submit_urb fails immediately, the error path must unanchor and release resources according to the code; it cannot rely on a callback for a request that was never accepted.

Anchoring gives the disconnect path a way to find a set of in-flight writes. It does not eliminate a race with a new writer, so skeleton checks disconnected while holding io_mutex in its submission path and sets that state under the same mutex on disconnect. Once new submissions are blocked, killing the anchor waits for the relevant callbacks. The completion callback and the thread killing URBs must not deadlock on a lock held across the kill. Other workqueues or timers started by a different driver require separate cancellation; they are not necessarily members of the URB anchor.

actual_length is meaningful together with status and transfer type. A bulk read that returns fewer bytes than requested can be valid for a device protocol, while another protocol may treat it as an incomplete record. Isochronous URBs can contain multiple packet descriptors whose individual status and length matter; a single aggregate assumption can hide partial failure. A control request's setup buffer, direction and data stages have distinct validation. None of these examples sends a host control request or assumes a keyboard can safely be unplugged for testing.

Runtime power and physical removal#

USB runtime PM can autosuspend an interface when its usage count and policy permit. An open path that needs the interface active can acquire an autopm reference, and must release it when finished. A PM get can fail or race a disconnect; a reference is not a guarantee that the cable remains attached. System suspend and reset add other callbacks and ordering requirements. The skeleton's draw-down path waits for anchored writes for a bounded period and kills them if necessary, then kills its read URB. This illustrates one policy, not a promise that every device can suspend or reset the same way.

A hub can report a disconnect while userspace still holds an fd. The USB core, interface driver, URB callbacks and file release each own different references. Deregistering the minor blocks the driver's new open path, setting the disconnected flag blocks protected new I/O, and draining URBs handles active transfers. Only the last kref release calls the private delete callback. If a driver skips any of those steps, a late callback can access freed memory or an open fd can attempt a transfer against absent hardware. The read-only metadata lab does not create this race; the source timeline is the evidence available without a disposable USB test device.

Interface references and disconnect ordering#

A USB device can expose several interfaces and endpoints, each with separate driver binding. Disconnect of the physical device eventually affects those interfaces, but one interface driver's state and open-file references are not interchangeable with another's. The skeleton's minor lookup finds the interface associated with its own driver; a missing interface or missing private data makes open fail. A driver must retain the device/interface references needed by its later callbacks and release them at the matching lifecycle point. A device object reference protects the kernel object under its contract, not the physical link.

When a completion callback records an error, the next userspace operation can observe it later than the submission that caused it. The callback must not assume that userspace is still waiting in the originating write. Conversely, an open file can remain after disconnect and must not access a buffer that the disconnect path has already freed. The skeleton's kref, io_mutex, disconnected flag and URB kills address different parts of that problem. The source example is useful precisely because those mechanisms are separate; production devices can require additional locks, protocol recovery and resource ownership.

Practice#

Source-only route:

sh
rg -n -A55 'static int skel_open\(' "$KERNEL_SRC/drivers/usb/usb-skeleton.c"
rg -n -A80 'static int skel_probe\(' "$KERNEL_SRC/drivers/usb/usb-skeleton.c"
rg -n -A32 'static void skel_disconnect\(' "$KERNEL_SRC/drivers/usb/usb-skeleton.c"
rg -n -A38 'int usb_submit_urb\(' "$KERNEL_SRC/drivers/usb/core/urb.c"

Verify v6.12. Mark the PM reference, file kref, endpoint validation, disconnected flag, kill calls and final put. Draw the outstanding-U timeline and label submission acceptance separately from completion.

Read-only topology/metadata observation:

sh
if command -v lsusb >/dev/null 2>&1; then lsusb -t || echo 'USB topology unavailable'; else echo 'lsusb unavailable'; fi
python3 - <<'PY'
from pathlib import Path
p = Path('/sys/bus/usb/devices')
try:
    entries = list(p.iterdir()) if p.is_dir() else []
    print('visible USB sysfs entries:', len(entries))
    print('with driver link:', sum((x / 'driver').exists() for x in entries))
except OSError as e: print('USB metadata unavailable:', e.strerror)
PY

No visible USB controller, missing lsusb, restricted permissions or no driver link are valid results. The count includes different USB object types and does not measure physical peripherals or completed transfers. The commands issue no explicit device control request; do not use reset, unbind, power writes or unplug a keyboard. Deliver the observed count or limitation and the U disconnect timeline.

Knowledge check#

  1. Why is a USB interrupt endpoint not the same as a CPU interrupt handler?
  2. What does usb_submit_urb returning zero establish?
  3. Why does skeleton take a kref during open and drop one during disconnect?
  4. What does usb_kill_urb contribute, and why must new submissions also be blocked?
  5. Why must completion inspect both status and actual length?

Answers#

  1. It is a periodically scheduled USB transfer type; the host controller and CPU notification mechanism are separate.
  2. The request was accepted for asynchronous processing; completion status and length remain to be observed later.
  3. The reference lets private state outlive disconnect while an open file still needs it; it does not keep hardware connected.
  4. It synchronously drains the specified URB and completion under its contract; a separate disconnected/start protocol prevents new work racing the drain.
  5. A transfer can fail or complete short; submission success is not a full-length data result.

Checkpoint#

Save the source timeline and metadata result. Pass when you distinguish device from interface, transfer type from CPU interrupt, URB acceptance from completion, and kref lifetime from hardware presence. If USB metadata is absent, use the source-only route and record that limitation. This is practice, not certification.

Sources#

A little further beneath the surface.

Check your understanding with the checkpoint. Marking a chapter as read saves a bookmark, not a proficiency assessment.