[PATCH] usb: dwc2: truncate PIO RX FIFO reads to the request's remaining space

Haofeng Li posted 1 patch 1 month ago
drivers/usb/dwc2/gadget.c | 37 +++++++++++++++++++++++++++----------
1 file changed, 27 insertions(+), 10 deletions(-)
[PATCH] usb: dwc2: truncate PIO RX FIFO reads to the request's remaining space
Posted by Haofeng Li 1 month ago
dwc2_hsotg_rx_data() reads the OUT packet length from the RX FIFO status
(GRXSTS.BYTECNT, host-controlled) and, when it exceeds the remaining
space of the active gadget request (max_req = req.length - req.actual),
merely fires WARN_ON_ONCE(1) and then stores the whole packet into
hs_req->req.buf + req->req.actual via dwc2_readl_rep():

    req.actual += size;
    dwc2_readl_rep(hsotg, EPFIFO(ep_idx), req.buf + actual,
                   DIV_ROUND_UP(size, 4));

A packet larger than the request buffer therefore over-writes up to
maxpacket bytes past it.  The rounded-up word read additionally emits up
to 3 bytes past the logical end even when the size does not exceed the
remaining space (short-request boundary).

Attack chain (USB peripheral/gadget mode, PIO only; the attacker is the
USB host):

    malicious host -> OUT packet on epN -> RX FIFO interrupt
      -> dwc2_hsotg_handle_rx() (GRXSTS_PKTSTS_OUTRX / SETUPRX)
      -> dwc2_hsotg_rx_data(hsotg, epnum, BYTECNT=64) with the queued
         request having req.length=8, req.actual=7 (1 byte left)
      -> "to_read(64) > max_req(1)" -> WARN_ON_ONCE(1) only
      -> dwc2_readl_rep() writes 16 words at req.buf+7
      -> 63 bytes past the 8-byte request buffer

Reproduced (kernel 7.2.0+, KASAN/SLUB debug): calling the real
dwc2_hsotg_rx_data() with the above state (request buffer 8 bytes,
actual 7, size 64) triggers the WARN and leaves req.actual = 71 (the
64 bytes are accrued regardless), and the SLUB redzone immediately
after the 8-byte object plus neighbouring slab objects are overwritten
with FIFO content (byte 7 changes from a marker to FIFO data) - the
out-of-bounds write is visible byte-for-byte.  The FIFO read is done by
raw 32-bit I/O words, which generic KASAN does not instrument, so the
redzone/neighbour clobber is the forensic evidence.

Signed-off-by: Haofeng Li <lihaofeng@kylinos.cn>
Assisted-by: opencode:deepseek-v4-flash-free
---
 drivers/usb/dwc2/gadget.c | 37 +++++++++++++++++++++++++++----------
 1 file changed, 27 insertions(+), 10 deletions(-)

diff --git a/drivers/usb/dwc2/gadget.c b/drivers/usb/dwc2/gadget.c
index c8b02c27d27d..61bf42b04d83 100644
--- a/drivers/usb/dwc2/gadget.c
+++ b/drivers/usb/dwc2/gadget.c
@@ -2267,6 +2267,7 @@ static void dwc2_hsotg_rx_data(struct dwc2_hsotg *hsotg, int ep_idx, int size)
 	int to_read;
 	int max_req;
 	int read_ptr;
+	u32 drain;
 
 	if (!hs_req) {
 		u32 epctl = dwc2_readl(hsotg, DOEPCTL(ep_idx));
@@ -2286,30 +2287,46 @@ static void dwc2_hsotg_rx_data(struct dwc2_hsotg *hsotg, int ep_idx, int size)
 	to_read = size;
 	read_ptr = hs_req->req.actual;
 	max_req = hs_req->req.length - read_ptr;
+	drain = 0;
 
 	dev_dbg(hsotg->dev, "%s: read %d/%d, done %d/%d\n",
 		__func__, to_read, max_req, read_ptr, hs_req->req.length);
 
 	if (to_read > max_req) {
 		/*
-		 * more data appeared than we where willing
-		 * to deal with in this request.
+		 * More data appeared than we were willing to deal with in
+		 * this request.  Keep only what fits in the request buffer
+		 * and discard the rest from the FIFO, instead of overwriting
+		 * bytes past the request buffer (a 64-byte packet into a
+		 * request with one byte left used to over-write 63 bytes past
+		 * its end).
 		 */
-
-		/* currently we don't deal this */
-		WARN_ON_ONCE(1);
+		dev_dbg(hsotg->dev, "%s: packet %d > request space %d, dropping %d\n",
+			__func__, to_read, max_req, to_read - max_req);
+		drain = DIV_ROUND_UP(to_read - max_req, 4);
+		to_read = max_req;
 	}
 
 	hs_ep->total_data += to_read;
 	hs_req->req.actual += to_read;
-	to_read = DIV_ROUND_UP(to_read, 4);
 
 	/*
-	 * note, we might over-write the buffer end by 3 bytes depending on
-	 * alignment of the data.
+	 * Copy word-at-a-time so the request buffer is exactly filled and
+	 * never over-run by the 4-byte rounding of the previous FIFO bulk
+	 * read (which could also emit up to 3 bytes past the buffer end).
 	 */
-	dwc2_readl_rep(hsotg, EPFIFO(ep_idx),
-		       hs_req->req.buf + read_ptr, to_read);
+	while (to_read > 0) {
+		u32 word = dwc2_readl(hsotg, EPFIFO(ep_idx));
+		unsigned int chunk = min_t(unsigned int, to_read, 4);
+
+		memcpy(hs_req->req.buf + read_ptr, &word, chunk);
+		read_ptr += chunk;
+		to_read -= chunk;
+	}
+
+	/* drain the discarded bytes so the next FIFO event is a fresh packet */
+	while (drain-- > 0)
+		(void)dwc2_readl(hsotg, EPFIFO(ep_idx));
 }
 
 /**
-- 
2.25.1