[PATCH v6 2/2] usb: gadget: f_tcm: reject a short write data-out transfer

From: Yehyeong Lee

Date: Tue Oct 13 2026 - 02:28:01 EST


usbg_data_write_cmpl() copies se_cmd->data_length bytes out of the bounce
buffer into the command's scatter-gather list without checking how much the
host actually sent. A BOT host can declare a larger OUT transfer than it
delivers and end the data stage with a short packet: req->actual is then
shorter than req->length, but the completion handler still commits the full
declared length. The tail past req->actual is whatever was already in the
kmalloc'd bounce buffer, which is not zeroed, so uninitialised kernel heap
is written to the backing store and can be read back by the host.

Fail the command when req->actual does not match req->length instead of
committing the stale tail. By the time usbg_data_write_cmpl() runs the OUT
data request has completed, so mark the command and skip the ep_out drain
in bot_send_bad_status() - there is nothing left on the wire, and draining
it re-arms ep_out for data the host already finished sending and wedges the
bulk pipe. This mirrors how f_mass_storage stops draining once the host
ends the OUT stage. A command rejected before its data stage still drains,
since the host has yet to send it.

A short 8192/4096 WRITE then commits nothing to the backing store instead
of 4096 bytes of stale heap, and no OUT drain is queued.

Fixes: c52661d60f63 ("usb-gadget: Initial merge of target module for UASP + BOT")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Yehyeong Lee <yhlee@xxxxxxxxxxxxxxxxxx>
---
drivers/usb/gadget/function/f_tcm.c | 14 +++++++++++---
drivers/usb/gadget/function/tcm.h | 1 +
2 files changed, 12 insertions(+), 3 deletions(-)

diff --git a/drivers/usb/gadget/function/f_tcm.c b/drivers/usb/gadget/function/f_tcm.c
index 12e2b20e33b92..a37a3ec8e7a4d 100644
--- a/drivers/usb/gadget/function/f_tcm.c
+++ b/drivers/usb/gadget/function/f_tcm.c
@@ -120,7 +120,8 @@ static void bot_send_bad_status(struct usbg_cmd *cmd)

csw->Residue = cpu_to_le32(cmd->data_len);

- if (cmd->data_len) {
+ /* nothing to drain once the OUT data phase has completed */
+ if (cmd->data_len && !(cmd->flags & USBG_CMD_OUT_DATA_DONE)) {
if (cmd->is_read) {
ep = fu->ep_in;
req = fu->bot_req_in;
@@ -1090,8 +1091,15 @@ static void usbg_data_write_cmpl(struct usb_ep *ep, struct usb_request *req)
return;
}

- if (req->status) {
- pr_err("%s() state %d transfer failed\n", __func__, cmd->state);
+ /* the OUT data request completed; nothing is left to drain */
+ cmd->flags |= USBG_CMD_OUT_DATA_DONE;
+
+ /* a short data-out left the tail unwritten; don't commit stale data */
+ if (req->status || req->actual != req->length) {
+ struct usb_gadget *gadget = fuas_to_gadget(cmd->fu);
+
+ dev_err_ratelimited(&gadget->dev, "%s() state %d transfer failed\n",
+ __func__, cmd->state);
goto cleanup;
}

diff --git a/drivers/usb/gadget/function/tcm.h b/drivers/usb/gadget/function/tcm.h
index e1d5a93916127..50764a33cdf36 100644
--- a/drivers/usb/gadget/function/tcm.h
+++ b/drivers/usb/gadget/function/tcm.h
@@ -88,6 +88,7 @@ struct usbg_cmd {

u32 flags;
#define USBG_CMD_PENDING_DATA_WRITE BIT(0)
+#define USBG_CMD_OUT_DATA_DONE BIT(1)

/* UAS only */
u16 tag;
--
2.43.0