[PATCH] usb: chipidea: udc: reject non-control requests while controller is suspended

From: Andreea.Popescu@xxxxxxxxxxx

Date: Tue Mar 31 2026 - 08:22:35 EST


When Linux runtime PM autosuspends a ChipIdea UDC that is still
enumerated by the host, the driver gates the PHY clocks and marks
the controller as suspended (ci->in_lpm = 1) but deliberately leaves
gadget.speed unchanged so upper-layer gadget drivers do not see a
spurious disconnect.

The problem is that those same drivers may continue to call
usb_ep_queue() during the autosuspend window. _hardware_enqueue()
silently adds the request to the endpoint queue and returns 0, but
hw_ep_prime() cannot succeed with gated clocks, so the completion
interrupt never fires. The request — and its backing buffer — is
permanently lost. The caller sees a successful return and never
frees the buffer.

The attached patch fixes this by adding an early -ESHUTDOWN return
in _ep_queue() for any non-control endpoint when ci->in_lpm is set
or when gadget.speed == USB_SPEED_UNKNOWN (cable gone / soft
disconnect). It also corrects ep_queue(), the public usb_ep_ops
wrapper, which was returning 0 instead of -ESHUTDOWN for the
USB_SPEED_UNKNOWN case, masking the same class of bug.

The guard follows the same pattern already used in ci_irq_handler(),
which exits early on ci->in_lpm, and has been verified against v6.19
ci.h (in_lpm field present) and core.c (ci_controller_suspend() sets
in_lpm without touching gadget.speed).

Please consider routing to stable; the bug has been present since
runtime PM autosuspend was introduced to this driver.

Thanks,
Andreea Popescu

---

diff --git a/drivers/usb/chipidea/udc.c b/drivers/usb/chipidea/udc.c
index 64a421ae0f05..09bdd78826b6 100644
--- a/drivers/usb/chipidea/udc.c
+++ b/drivers/usb/chipidea/udc.c
@@ -1100,6 +1100,27 @@ static int _ep_queue(struct usb_ep *ep, struct usb_request *req,
if (ep == NULL || req == NULL || hwep->ep.desc == NULL)
return -EINVAL;

+ /*
+ * Reject non-control submissions when the controller is suspended
+ * (ci->in_lpm set by ci_runtime_suspend) or the gadget is not yet
+ * enumerated (gadget.speed == USB_SPEED_UNKNOWN).
+ *
+ * ci_runtime_suspend gates the PHY clocks but intentionally leaves
+ * gadget.speed at its current value so that upper-layer drivers do
+ * not see a spurious disconnect. As a side-effect, callers have no
+ * way to distinguish an active gadget from a clock-gated one.
+ * _hardware_enqueue() adds the request to hwep->qh.queue and returns
+ * 0, but hw_ep_prime() is a no-op with gated clocks so the transfer
+ * completion interrupt never fires and the buffer is permanently lost.
+ *
+ * Return -ESHUTDOWN so callers can free the buffer immediately,
+ * matching the behaviour of a real hardware error completion.
+ */
+ if (hwep->type != USB_ENDPOINT_XFER_CONTROL &&
+ (ci->in_lpm || ci->gadget.speed == USB_SPEED_UNKNOWN))
+ return -ESHUTDOWN;
+
+
if (hwep->type == USB_ENDPOINT_XFER_CONTROL) {
if (req->length)
hwep = (ci->ep0_dir == RX) ?
@@ -1698,7 +1719,7 @@ static int ep_queue(struct usb_ep *ep, struct usb_request *req,
spin_lock_irqsave(hwep->lock, flags);
if (hwep->ci->gadget.speed == USB_SPEED_UNKNOWN) {
spin_unlock_irqrestore(hwep->lock, flags);
- return 0;
+ return -ESHUTDOWN;
}
retval = _ep_queue(ep, req, gfp_flags);
spin_unlock_irqrestore(hwep->lock, flags);