[PATCH v2] usb: gadget: dummy_hcd: fix premature URB completion when ZLP follows partial transfer

From: Sebastian Urban

Date: Sun Mar 15 2026 - 11:11:28 EST


When a gadget request is only partially transferred in transfer()
because the per-frame bandwidth budget is exhausted, the loop advances
to the next queued request. If that next request is a zero-length
packet (ZLP), len evaluates to zero and the code takes the
unlikely(len == 0) path, which sets is_short = 1. This bypasses the
bandwidth guard ("limit < ep->ep.maxpacket && limit < len") that
lives in the else branch and would otherwise break out of the loop for
non-zero requests. The is_short path then completes the URB before all
data from the first request has been transferred.

Reproducer (bulk IN, high speed):

Device side (FunctionFS with Linux AIO):
1. Queue a 65024-byte write via io_submit (127 * 512, i.e. a
multiple of the HS bulk max packet size).
2. Immediately queue a zero-length write (ZLP) via io_submit.

Host side:
3. Submit a 65536-byte bulk IN URB.

Expected: URB completes with actual_length = 65024.
Actual: URB completes with actual_length = 53248, losing 11776
bytes that leak into subsequent URBs.

At high speed the per-frame budget is 53248 bytes (512 * 13 * 8).
The 65024-byte request exhausts this budget after 53248 bytes, leaving
the request incomplete (req->req.actual < req->req.length). Neither
the request nor the URB is finished, and rescan is 0, so the loop
advances to the ZLP. For the ZLP, dev_len = 0, so len = min(12288, 0)
= 0, taking the unlikely(len == 0) path and setting is_short = 1.
The is_short handler then sets *status = 0, completing the URB with
only 53248 of the expected 65024 bytes.

Fix this by breaking out of the loop when the current request has
remaining data (req->req.actual < req->req.length). The request
resumes on the next timer tick, preserving correct data ordering.

Signed-off-by: Sebastian Urban <surban@xxxxxxxxxx>
---
Changes in v2:
- Rewrote commit message to describe the specific ZLP-after-partial-transfer
scenario rather than overstating the general case (Alan Stern).
- Added reproducer and step-by-step code path walkthrough.

drivers/usb/gadget/udc/dummy_hcd.c | 6 ++++++
1 file changed, 6 insertions(+)

diff --git a/drivers/usb/gadget/udc/dummy_hcd.c b/drivers/usb/gadget/udc/dummy_hcd.c
index 1cefca660..0eead4a85 100644
--- a/drivers/usb/gadget/udc/dummy_hcd.c
+++ b/drivers/usb/gadget/udc/dummy_hcd.c
@@ -1534,6 +1534,12 @@ static int transfer(struct dummy_hcd *dum_hcd, struct urb *urb,
/* rescan to continue with any other queued i/o */
if (rescan)
goto top;
+
+ /* request not fully transferred; stop iterating to
+ * preserve data ordering across queued requests.
+ */
+ if (req->req.actual < req->req.length)
+ break;
}
return sent;
}
--
2.53.0