* [PATCH v5 0/3] usb: gadget: gadgetfs: fix ep0 request lifetime and event counter
@ 2026-11-09 5:55 Jing Lin via B4 Relay
2026-11-09 5:55 ` [PATCH v5 1/3] usb: gadget: gadgetfs: fix ep0 request lifetime on disconnect Jing Lin via B4 Relay
` (2 more replies)
0 siblings, 3 replies; 7+ messages in thread
From: Jing Lin via B4 Relay @ 2026-11-09 5:55 UTC (permalink / raw)
To: Greg Kroah-Hartman, Neill Kapron
Cc: Jing Lin, syzbot+c73e1942197af9f54ca3, Alan Stern, linux-kernel,
stable, linux-usb
This series fixes the ep0 request lifetime and the shared request buffer
races in gadgetfs, and the pre-existing event counter underflow.
The first patch covers the request lifetime and buffer ownership
problems, the second patch the event counter underflow, and the third
removes the const cast in gadgetfs_setup().
Changes in v5:
- Expand the first patch's changelog with the concrete interleavings of
the ep0_read()/ep0_write() races and explain why a mutex cannot be
used, as requested by Alan Stern.
- Add a patch that clamps a local copy of the control request instead of
casting away the constness and writing into the caller's setup packet,
also suggested by Alan Stern.
- Restore STATE_DEV_CONNECTED when a delegated data read is aborted by a
new setup, so the following event read is not misdirected into the
control data path.
- Link to v4: https://lore.kernel.org/all/20261107-b4-gadgetfs-ep0-v4-0-86bdfd684b54@gmail.com/
On the submit/unbind window: every ep0 submission increments udc_usage
under dev->lock before the lock is dropped, and gadgetfs_unbind() sets
STATE_DEV_UNBOUND and waits for udc_usage to reach zero before it samples
ep0_req_queued. A thread that has set ep0_req_queued but has not yet
called usb_ep_queue() therefore keeps unbind waiting; when unbind
resumes, the queue call has completed and the flag and the endpoint queue
agree. The request that is still queued at unbind is then dequeued
before it is freed. With a test-only delay between setting the flag and
calling usb_ep_queue(), close() blocked for the remaining delay (610 ms,
versus 3 ms without the delay), confirming that teardown waits for the
submission to finish.
The series was tested with the syzbot reproducer and with targeted
lifecycle, request-reuse, wedge, and event-counter tests on arm64 QEMU
with dummy_hcd, KASAN, and lockdep.
Signed-off-by: Jing Lin <kayano050408@gmail.com>
---
Jing Lin (3):
usb: gadget: gadgetfs: fix ep0 request lifetime on disconnect
usb: gadget: gadgetfs: prevent ep0 event counter underflow
usb: gadget: gadgetfs: avoid writing through the const setup pointer
drivers/usb/gadget/legacy/inode.c | 181 ++++++++++++++++++++++++++------------
1 file changed, 123 insertions(+), 58 deletions(-)
---
base-commit: f66469d9905b39d2c260a15d7d6db450f4db67c1
change-id: 20261106-b4-gadgetfs-ep0-f814c14717a9
Best regards,
--
Jing Lin <kayano050408@gmail.com>
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v5 1/3] usb: gadget: gadgetfs: fix ep0 request lifetime on disconnect
2026-11-09 5:55 [PATCH v5 0/3] usb: gadget: gadgetfs: fix ep0 request lifetime and event counter Jing Lin via B4 Relay
@ 2026-11-09 5:55 ` Jing Lin via B4 Relay
2026-11-09 6:16 ` sashiko-bot
2026-11-09 5:55 ` [PATCH v5 2/3] usb: gadget: gadgetfs: prevent ep0 event counter underflow Jing Lin via B4 Relay
2026-11-09 5:55 ` [PATCH v5 3/3] usb: gadget: gadgetfs: avoid writing through the const setup pointer Jing Lin via B4 Relay
2 siblings, 1 reply; 7+ messages in thread
From: Jing Lin via B4 Relay @ 2026-11-09 5:55 UTC (permalink / raw)
To: Greg Kroah-Hartman, Neill Kapron
Cc: Jing Lin, syzbot+c73e1942197af9f54ca3, Alan Stern, linux-kernel,
stable, linux-usb
From: Jing Lin <kayano050408@gmail.com>
gadgetfs_unbind() frees the preallocated ep0 request after disconnecting
the gadget, assuming that no I/O remains active.
With dummy_hcd, disconnecting does not necessarily drain ep0, so a delayed
userspace response can leave dev->req queued when it is freed. This
triggers the warning in dummy_free_request() and a use-after-free when
dummy_udc_stop() subsequently drains the endpoint queue.
gadgetfs_setup() and the userspace copy helpers also race on dev->req and
req->buf:
1. ep0_read() versus gadgetfs_setup(). A delegated control OUT request
is queued by gadgetfs_setup() and completed by ep0_complete(), which
leaves the payload in dev->req->buf and sets setup_out_ready.
ep0_read() copies that payload with copy_to_user() after dropping
dev->lock, so the copy can fault and sleep. If a new control transfer
arrives during that window, gadgetfs_setup() reuses dev->req for it;
for example, GET_DESCRIPTOR(config) points req->buf at dev->config.
When ep0_read() resumes, clean_req() frees whatever req->buf points to
at that moment, which is now dev->config. The following
GET_DESCRIPTOR(config) dereferences the freed descriptor.
2. ep0_write() versus gadgetfs_setup(). For a delegated control IN
request, ep0_write() prepares dev->req and copies the response from
userspace with copy_from_user() after dropping dev->lock. A control
request that arrives during that copy can point req->buf at dev->dev
or dev->config, so the userspace copy overwrites a descriptor buffer,
or it can reset req->buf to dev->rbuf, so the request is queued with
the wrong buffer. Completion then calls clean_req() on the wrong
object.
Copy completed control OUT payloads into a separate setup_buf in
ep0_complete(), copy control IN payloads from userspace into a stack
buffer before preparing dev->req in ep0_write(), and reset req->buf when
control transfers finish. Track whether dev->req is queued, reject reuse
while it is queued, and dequeue it without dev->lock before freeing it in
gadgetfs_unbind().
Return to STATE_DEV_CONNECTED when a delegated data read is aborted by a
new setup, so that the following event read is not misdirected into the
control data path.
The setup() callback runs in interrupt context and may not sleep, so a
mutex cannot be used to serialize it against ep0_read() and ep0_write().
The shared request state is protected by dev->lock and the payloads are
handed off through private buffers instead.
Fixes: 1da177e4c3f41 ("Linux-2.6.12-rc2")
Reported-by: syzbot+c73e1942197af9f54ca3@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=c73e1942197af9f54ca3
Cc: stable@vger.kernel.org
Signed-off-by: Jing Lin <kayano050408@gmail.com>
---
drivers/usb/gadget/legacy/inode.c | 146 ++++++++++++++++++++++++++++----------
1 file changed, 108 insertions(+), 38 deletions(-)
diff --git a/drivers/usb/gadget/legacy/inode.c b/drivers/usb/gadget/legacy/inode.c
index 7f6d84601..e9380538e 100644
--- a/drivers/usb/gadget/legacy/inode.c
+++ b/drivers/usb/gadget/legacy/inode.c
@@ -140,6 +140,8 @@ struct dev_data {
setup_abort : 1,
gadget_registered : 1;
unsigned setup_wLength;
+ unsigned int setup_out_len;
+ bool ep0_req_queued;
/* the rest is basically write-once */
struct usb_config_descriptor *config, *hs_config;
@@ -153,6 +155,7 @@ struct dev_data {
/* except this scratch i/o buffer for ep0 */
u8 rbuf[RBUF_SIZE];
+ u8 setup_buf[RBUF_SIZE];
};
static inline void get_dev (struct dev_data *data)
@@ -984,6 +987,17 @@ static inline void ep0_readable (struct dev_data *dev)
kill_fasync (&dev->fasync, SIGIO, POLL_IN);
}
+static void dev_req_complete(struct usb_ep *ep, struct usb_request *req)
+{
+ struct dev_data *dev = ep->driver_data;
+ unsigned long flags;
+
+ spin_lock_irqsave(&dev->lock, flags);
+ req->buf = dev->rbuf;
+ dev->ep0_req_queued = false;
+ spin_unlock_irqrestore(&dev->lock, flags);
+}
+
static void clean_req (struct usb_ep *ep, struct usb_request *req)
{
struct dev_data *dev = ep->driver_data;
@@ -992,30 +1006,31 @@ static void clean_req (struct usb_ep *ep, struct usb_request *req)
kfree(req->buf);
req->buf = dev->rbuf;
}
- req->complete = epio_complete;
- dev->setup_out_ready = 0;
+ req->complete = dev_req_complete;
}
static void ep0_complete (struct usb_ep *ep, struct usb_request *req)
{
struct dev_data *dev = ep->driver_data;
unsigned long flags;
- int free = 1;
/* for control OUT, data must still get to userspace */
spin_lock_irqsave(&dev->lock, flags);
- if (!dev->setup_in) {
+ dev->ep0_req_queued = false;
+ if (!dev->setup_in && dev->setup_wLength) {
dev->setup_out_error = (req->status != 0);
- if (!dev->setup_out_error)
- free = 0;
+ if (dev->setup_out_error) {
+ dev->setup_out_len = 0;
+ } else {
+ dev->setup_out_len = min_t(unsigned int, req->actual,
+ sizeof(dev->setup_buf));
+ memcpy(dev->setup_buf, req->buf, dev->setup_out_len);
+ }
dev->setup_out_ready = 1;
- ep0_readable (dev);
+ ep0_readable(dev);
}
- /* clean up as appropriate */
- if (free && req->buf != &dev->rbuf)
- clean_req (ep, req);
- req->complete = epio_complete;
+ clean_req(ep, req);
spin_unlock_irqrestore(&dev->lock, flags);
}
@@ -1023,12 +1038,14 @@ static int setup_req (struct usb_ep *ep, struct usb_request *req, u16 len)
{
struct dev_data *dev = ep->driver_data;
- if (dev->setup_out_ready) {
+ if (dev->ep0_req_queued) {
DBG (dev, "ep0 request busy!\n");
return -EBUSY;
}
if (len > sizeof (dev->rbuf))
req->buf = kmalloc(len, GFP_ATOMIC);
+ else
+ req->buf = dev->rbuf;
if (req->buf == NULL) {
req->buf = dev->rbuf;
return -ENOMEM;
@@ -1045,6 +1062,7 @@ ep0_read (struct file *fd, char __user *buf, size_t len, loff_t *ptr)
struct dev_data *dev = fd->private_data;
ssize_t retval;
enum ep0_state state;
+ u8 tmp[RBUF_SIZE];
spin_lock_irq (&dev->lock);
if (dev->state <= STATE_DEV_OPENED) {
@@ -1073,13 +1091,20 @@ ep0_read (struct file *fd, char __user *buf, size_t len, loff_t *ptr)
struct usb_request *req = dev->req;
if ((retval = setup_req (ep, req, 0)) == 0) {
+ dev->state = STATE_DEV_CONNECTED;
++dev->udc_usage;
+ dev->ep0_req_queued = true;
spin_unlock_irq (&dev->lock);
retval = usb_ep_queue (ep, req, GFP_KERNEL);
spin_lock_irq (&dev->lock);
--dev->udc_usage;
+ if (retval < 0) {
+ dev->ep0_req_queued = false;
+ clean_req(ep, req);
+ }
+ } else {
+ dev->state = STATE_DEV_CONNECTED;
}
- dev->state = STATE_DEV_CONNECTED;
/* assume that was SET_CONFIGURATION */
if (dev->current_config) {
@@ -1109,25 +1134,37 @@ ep0_read (struct file *fd, char __user *buf, size_t len, loff_t *ptr)
if (retval)
goto done;
- if (dev->state != STATE_DEV_SETUP) {
+ if (dev->setup_abort) {
+ dev->setup_abort = 0;
+ dev->state = STATE_DEV_CONNECTED;
+ retval = -EIDRM;
+ goto done;
+ }
+ if (dev->state != STATE_DEV_SETUP ||
+ dev->setup_in || !dev->setup_out_ready) {
retval = -ECANCELED;
goto done;
}
dev->state = STATE_DEV_CONNECTED;
- if (dev->setup_out_error)
+ if (dev->setup_out_error) {
+ dev->setup_out_ready = 0;
+ dev->setup_out_error = 0;
+ dev->setup_out_len = 0;
retval = -EIO;
- else {
- len = min (len, (size_t)dev->req->actual);
+ } else {
+ len = min_t(size_t, len, dev->setup_out_len);
+ memcpy(tmp, dev->setup_buf, len);
+ dev->setup_out_ready = 0;
+ dev->setup_out_len = 0;
++dev->udc_usage;
spin_unlock_irq(&dev->lock);
- if (copy_to_user (buf, dev->req->buf, len))
+ if (copy_to_user(buf, tmp, len))
retval = -EFAULT;
else
retval = len;
spin_lock_irq(&dev->lock);
--dev->udc_usage;
- clean_req (dev->gadget->ep0, dev->req);
/* NOTE userspace can't yet choose to stall */
}
}
@@ -1267,29 +1304,41 @@ ep0_write (struct file *fd, const char __user *buf, size_t len, loff_t *ptr)
len = min_t(size_t, len, dev->setup_wLength);
if (dev->setup_in) {
- retval = setup_req (dev->gadget->ep0, dev->req, len);
+ u8 tmp[RBUF_SIZE];
+ bool zero = len < dev->setup_wLength;
+
+ dev->state = STATE_DEV_CONNECTED;
+ spin_unlock_irq(&dev->lock);
+ if (copy_from_user(tmp, buf, len)) {
+ spin_lock_irq(&dev->lock);
+ return -EFAULT;
+ }
+ spin_lock_irq(&dev->lock);
+ if (dev->setup_abort) {
+ dev->setup_abort = 0;
+ return -EIDRM;
+ }
+ if (dev->state != STATE_DEV_CONNECTED)
+ return -ECANCELED;
+ retval = setup_req(dev->gadget->ep0, dev->req, len);
if (retval == 0) {
- dev->state = STATE_DEV_CONNECTED;
+ memcpy(dev->req->buf, tmp, len);
+ dev->req->zero = zero;
++dev->udc_usage;
- spin_unlock_irq (&dev->lock);
- if (copy_from_user (dev->req->buf, buf, len))
- retval = -EFAULT;
- else {
- if (len < dev->setup_wLength)
- dev->req->zero = 1;
- retval = usb_ep_queue (
- dev->gadget->ep0, dev->req,
- GFP_KERNEL);
- }
+ dev->ep0_req_queued = true;
+ spin_unlock_irq(&dev->lock);
+ retval = usb_ep_queue(
+ dev->gadget->ep0, dev->req,
+ GFP_KERNEL);
spin_lock_irq(&dev->lock);
--dev->udc_usage;
if (retval < 0) {
+ dev->ep0_req_queued = false;
clean_req (dev->gadget->ep0, dev->req);
} else
retval = len;
-
- return retval;
}
+ return retval;
/* can stall some OUT transfers */
} else if (dev->setup_can_stall) {
@@ -1483,6 +1532,10 @@ gadgetfs_setup (struct usb_gadget *gadget, const struct usb_ctrlrequest *ctrl)
}
spin_lock (&dev->lock);
+ if (dev->ep0_req_queued) {
+ spin_unlock(&dev->lock);
+ return -EBUSY;
+ }
dev->setup_abort = 0;
if (dev->state == STATE_DEV_UNCONNECTED) {
if (gadget_is_dualspeed(gadget)
@@ -1618,6 +1671,7 @@ gadgetfs_setup (struct usb_gadget *gadget, const struct usb_ctrlrequest *ctrl)
dev->setup_wLength = w_length;
dev->setup_out_ready = 0;
dev->setup_out_error = 0;
+ dev->setup_out_len = 0;
/* read DATA stage for OUT right away */
if (unlikely (!dev->setup_in && w_length)) {
@@ -1627,12 +1681,14 @@ gadgetfs_setup (struct usb_gadget *gadget, const struct usb_ctrlrequest *ctrl)
break;
++dev->udc_usage;
+ dev->ep0_req_queued = true;
spin_unlock (&dev->lock);
value = usb_ep_queue (gadget->ep0, dev->req,
GFP_KERNEL);
spin_lock (&dev->lock);
--dev->udc_usage;
if (value < 0) {
+ dev->ep0_req_queued = false;
clean_req (gadget->ep0, dev->req);
break;
}
@@ -1663,21 +1719,26 @@ gadgetfs_setup (struct usb_gadget *gadget, const struct usb_ctrlrequest *ctrl)
if (value >= 0 && dev->state != STATE_DEV_SETUP) {
req->length = value;
req->zero = value < w_length;
+ req->complete = dev_req_complete;
++dev->udc_usage;
+ dev->ep0_req_queued = true;
spin_unlock (&dev->lock);
value = usb_ep_queue (gadget->ep0, req, GFP_KERNEL);
spin_lock(&dev->lock);
--dev->udc_usage;
- spin_unlock(&dev->lock);
if (value < 0) {
+ dev->ep0_req_queued = false;
+ req->buf = dev->rbuf;
DBG (dev, "ep_queue --> %d\n", value);
req->status = 0;
}
+ spin_unlock(&dev->lock);
return value;
}
/* device stalls when value < 0 */
+ req->buf = dev->rbuf;
spin_unlock (&dev->lock);
return value;
}
@@ -1771,6 +1832,7 @@ static void
gadgetfs_unbind (struct usb_gadget *gadget)
{
struct dev_data *dev = get_gadget_data (gadget);
+ bool queued;
DBG (dev, "%s\n", __func__);
@@ -1781,15 +1843,21 @@ gadgetfs_unbind (struct usb_gadget *gadget)
usleep_range(1000, 2000);
spin_lock_irq(&dev->lock);
}
+ queued = dev->ep0_req_queued;
spin_unlock_irq (&dev->lock);
+ if (dev->req) {
+ if (queued)
+ usb_ep_dequeue(gadget->ep0, dev->req);
+ clean_req(gadget->ep0, dev->req);
+ usb_ep_free_request(gadget->ep0, dev->req);
+ dev->req = NULL;
+ }
+
destroy_ep_files (dev);
gadget->ep0->driver_data = NULL;
set_gadget_data (gadget, NULL);
- /* we've already been disconnected ... no i/o is active */
- if (dev->req)
- usb_ep_free_request (gadget->ep0, dev->req);
DBG (dev, "%s done\n", __func__);
put_dev (dev);
}
@@ -1816,11 +1884,13 @@ static int gadgetfs_bind(struct usb_gadget *gadget,
get_dev (dev);
/* preallocate control response and buffer */
+ dev->ep0_req_queued = false;
dev->req = usb_ep_alloc_request (gadget->ep0, GFP_KERNEL);
if (!dev->req)
goto enomem;
dev->req->context = NULL;
- dev->req->complete = epio_complete;
+ dev->req->complete = dev_req_complete;
+ dev->req->buf = dev->rbuf;
if (activate_ep_files (dev) < 0)
goto enomem;
--
Git-155)
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v5 2/3] usb: gadget: gadgetfs: prevent ep0 event counter underflow
2026-11-09 5:55 [PATCH v5 0/3] usb: gadget: gadgetfs: fix ep0 request lifetime and event counter Jing Lin via B4 Relay
2026-11-09 5:55 ` [PATCH v5 1/3] usb: gadget: gadgetfs: fix ep0 request lifetime on disconnect Jing Lin via B4 Relay
@ 2026-11-09 5:55 ` Jing Lin via B4 Relay
2026-11-09 6:08 ` sashiko-bot
2026-11-09 5:55 ` [PATCH v5 3/3] usb: gadget: gadgetfs: avoid writing through the const setup pointer Jing Lin via B4 Relay
2 siblings, 1 reply; 7+ messages in thread
From: Jing Lin via B4 Relay @ 2026-11-09 5:55 UTC (permalink / raw)
To: Greg Kroah-Hartman, Neill Kapron
Cc: Jing Lin, syzbot+c73e1942197af9f54ca3, Alan Stern, linux-kernel,
stable, linux-usb
From: Jing Lin <kayano050408@gmail.com>
ep0_read() snapshots dev->ev_next, drops dev->lock while copy_to_user()
runs directly from dev->event[], and subtracts the snapshot afterwards.
A concurrent disconnect, connect, setup, or another reader can reset,
deduplicate, or consume the queue in that window. This races on
dev->event[], can drop newly arrived GADGETFS_SETUP events, and wraps
dev->ev_next if the queue shrinks below the snapshot.
Dequeue the events into the local buffer and update dev->ev_next before
dropping dev->lock for copy_to_user().
Fixes: 1da177e4c3f41 ("Linux-2.6.12-rc2")
Cc: stable@vger.kernel.org
Signed-off-by: Jing Lin <kayano050408@gmail.com>
---
drivers/usb/gadget/legacy/inode.c | 26 ++++++++++----------------
1 file changed, 10 insertions(+), 16 deletions(-)
diff --git a/drivers/usb/gadget/legacy/inode.c b/drivers/usb/gadget/legacy/inode.c
index e9380538e..4635a603e 100644
--- a/drivers/usb/gadget/legacy/inode.c
+++ b/drivers/usb/gadget/legacy/inode.c
@@ -1196,25 +1196,19 @@ ep0_read (struct file *fd, char __user *buf, size_t len, loff_t *ptr)
break;
}
}
- spin_unlock_irq (&dev->lock);
- len = n * sizeof (struct usb_gadgetfs_event);
- if (copy_to_user (buf, &dev->event, len))
+ len = n * sizeof(struct usb_gadgetfs_event);
+ memcpy(tmp, &dev->event, len);
+ if (dev->ev_next > n) {
+ memmove(&dev->event[0], &dev->event[n],
+ sizeof(struct usb_gadgetfs_event)
+ * (dev->ev_next - n));
+ }
+ dev->ev_next -= n;
+ spin_unlock_irq(&dev->lock);
+ if (copy_to_user(buf, tmp, len))
retval = -EFAULT;
else
retval = len;
- if (len > 0) {
- /* NOTE this doesn't guard against broken drivers;
- * concurrent ep0 readers may lose events.
- */
- spin_lock_irq (&dev->lock);
- if (dev->ev_next > n) {
- memmove(&dev->event[0], &dev->event[n],
- sizeof (struct usb_gadgetfs_event)
- * (dev->ev_next - n));
- }
- dev->ev_next -= n;
- spin_unlock_irq (&dev->lock);
- }
return retval;
}
if (fd->f_flags & O_NONBLOCK) {
--
Git-155)
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH v5 3/3] usb: gadget: gadgetfs: avoid writing through the const setup pointer
2026-11-09 5:55 [PATCH v5 0/3] usb: gadget: gadgetfs: fix ep0 request lifetime and event counter Jing Lin via B4 Relay
2026-11-09 5:55 ` [PATCH v5 1/3] usb: gadget: gadgetfs: fix ep0 request lifetime on disconnect Jing Lin via B4 Relay
2026-11-09 5:55 ` [PATCH v5 2/3] usb: gadget: gadgetfs: prevent ep0 event counter underflow Jing Lin via B4 Relay
@ 2026-11-09 5:55 ` Jing Lin via B4 Relay
2026-11-09 6:00 ` sashiko-bot
2 siblings, 1 reply; 7+ messages in thread
From: Jing Lin via B4 Relay @ 2026-11-09 5:55 UTC (permalink / raw)
To: Greg Kroah-Hartman, Neill Kapron
Cc: Jing Lin, syzbot+c73e1942197af9f54ca3, Alan Stern, linux-kernel,
stable, linux-usb
From: Jing Lin <kayano050408@gmail.com>
gadgetfs_setup() receives the setup packet as const, but clamps an
oversized control IN request by casting away the constness and writing
the truncated length back into the caller's wLength field.
The setup() callback has no business modifying the setup packet. Use a
local copy of the request for the clamped length instead. Userspace
still observes the truncated wLength in the GADGETFS_SETUP event, and
the queued request length is unchanged.
Suggested-by: Alan Stern <stern@rowland.harvard.edu>
Signed-off-by: Jing Lin <kayano050408@gmail.com>
---
drivers/usb/gadget/legacy/inode.c | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/drivers/usb/gadget/legacy/inode.c b/drivers/usb/gadget/legacy/inode.c
index 4635a603e..23a36d556 100644
--- a/drivers/usb/gadget/legacy/inode.c
+++ b/drivers/usb/gadget/legacy/inode.c
@@ -1510,15 +1510,16 @@ gadgetfs_setup (struct usb_gadget *gadget, const struct usb_ctrlrequest *ctrl)
struct usb_request *req = dev->req;
int value = -EOPNOTSUPP;
struct usb_gadgetfs_event *event;
+ struct usb_ctrlrequest ctrl_buf;
u16 w_value = le16_to_cpu(ctrl->wValue);
u16 w_length = le16_to_cpu(ctrl->wLength);
if (w_length > RBUF_SIZE) {
if (ctrl->bRequestType & USB_DIR_IN) {
- /* Cast away the const, we are going to overwrite on purpose. */
- __le16 *temp = (__le16 *)&ctrl->wLength;
-
- *temp = cpu_to_le16(RBUF_SIZE);
+ /* Report the length we can actually handle to userspace. */
+ ctrl_buf = *ctrl;
+ ctrl_buf.wLength = cpu_to_le16(RBUF_SIZE);
+ ctrl = &ctrl_buf;
w_length = RBUF_SIZE;
} else {
return value;
--
Git-155)
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH v5 3/3] usb: gadget: gadgetfs: avoid writing through the const setup pointer
2026-11-09 5:55 ` [PATCH v5 3/3] usb: gadget: gadgetfs: avoid writing through the const setup pointer Jing Lin via B4 Relay
@ 2026-11-09 6:00 ` sashiko-bot
0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-11-09 6:00 UTC (permalink / raw)
To: Jing Lin; +Cc: linux-usb
> From: Jing Lin <kayano050408@gmail.com>
>
> gadgetfs_setup() receives the setup packet as const, but clamps an
> oversized control IN request by casting away the constness and writing
> the truncated length back into the caller's wLength field.
>
> The setup() callback has no business modifying the setup packet. Use a
> local copy of the request for the clamped length instead. Userspace
> still observes the truncated wLength in the GADGETFS_SETUP event, and
> the queued request length is unchanged.
>
> Suggested-by: Alan Stern <stern@rowland.harvard.edu>
> Signed-off-by: Jing Lin <kayano050408@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261109-b4-gadgetfs-ep0-v5-0-f16d0b648bc2@gmail.com?part=3
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v5 2/3] usb: gadget: gadgetfs: prevent ep0 event counter underflow
2026-11-09 5:55 ` [PATCH v5 2/3] usb: gadget: gadgetfs: prevent ep0 event counter underflow Jing Lin via B4 Relay
@ 2026-11-09 6:08 ` sashiko-bot
0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-11-09 6:08 UTC (permalink / raw)
To: Jing Lin; +Cc: linux-usb
> From: Jing Lin <kayano050408@gmail.com>
>
> ep0_read() snapshots dev->ev_next, drops dev->lock while copy_to_user()
> runs directly from dev->event[], and subtracts the snapshot afterwards.
> A concurrent disconnect, connect, setup, or another reader can reset,
> deduplicate, or consume the queue in that window. This races on
> dev->event[], can drop newly arrived GADGETFS_SETUP events, and wraps
> dev->ev_next if the queue shrinks below the snapshot.
>
> Dequeue the events into the local buffer and update dev->ev_next before
> dropping dev->lock for copy_to_user().
>
> Fixes: 1da177e4c3f41 ("Linux-2.6.12-rc2")
> Cc: stable@vger.kernel.org
> Signed-off-by: Jing Lin <kayano050408@gmail.com>
Sashiko has reviewed this patch and found no issues.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261109-b4-gadgetfs-ep0-v5-0-f16d0b648bc2@gmail.com?part=2
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v5 1/3] usb: gadget: gadgetfs: fix ep0 request lifetime on disconnect
2026-11-09 5:55 ` [PATCH v5 1/3] usb: gadget: gadgetfs: fix ep0 request lifetime on disconnect Jing Lin via B4 Relay
@ 2026-11-09 6:16 ` sashiko-bot
0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-11-09 6:16 UTC (permalink / raw)
To: Jing Lin; +Cc: linux-usb
Thank you for your contribution! Sashiko AI review found 4 potential
issues to consider:
- [High] usb: gadget: gadgetfs: invalid kfree of descriptors in
gadgetfs_unbind()
- [Medium] usb: gadget: gadgetfs: premature state change in ep0_write()
hides abort
- [Medium] usb: gadget: gadgetfs: incomplete setup_abort handling in
ep0_read()
- [Medium] usb: gadget: gadgetfs: setup_abort in ep0_read() clobbers
unbound state
--
commit 3d2d9c4891365d79ece2e8243c8a799fb412498e
Author: Jing Lin <kayano050408@gmail.com>
usb: gadget: gadgetfs: fix ep0 request lifetime on disconnect
This commit tracks whether the preallocated ep0 request is queued, stages
delegated control payloads through private buffers so copy_to_user() and
copy_from_user() do not race with gadgetfs_setup() on dev->req->buf, and
dequeues dev->req in gadgetfs_unbind() before freeing it.
> diff --git a/drivers/usb/gadget/legacy/inode.c b/drivers/usb/gadget/legacy/inode.c
> index 7f6d8460152f9..e9380538e0b98 100644
> --- a/drivers/usb/gadget/legacy/inode.c
> +++ b/drivers/usb/gadget/legacy/inode.c
[ ... ]
> @@ -1109,25 +1134,37 @@ ep0_read (struct file *fd, char __user *buf, size_t len, loff_t *ptr)
> if (retval)
> goto done;
>
> - if (dev->state != STATE_DEV_SETUP) {
> + if (dev->setup_abort) {
> + dev->setup_abort = 0;
> + dev->state = STATE_DEV_CONNECTED;
> + retval = -EIDRM;
> + goto done;
> + }
[Severity: Medium]
Can ep0_read() ever reach this dev->setup_abort check when a delegated
control OUT transfer is aborted before its data stage completes?
Immediately above this hunk, ep0_read() sleeps in:
drivers/usb/gadget/legacy/inode.c:ep0_read() {
...
retval = wait_event_interruptible (dev->wait,
dev->setup_out_ready != 0);
...
}
When a new SETUP packet aborts the pending OUT transfer in
gadgetfs_setup(), dev->setup_abort is set to 1 while dev->setup_out_ready
remains 0 (or is reset to 0 for a new delegated setup), so the wait
condition is still false and ep0_read() stays asleep instead of returning
-EIDRM. If the earlier OUT transfer had already set dev->setup_out_ready
before an in-kernel setup aborted it, this branch also returns -EIDRM
without clearing dev->setup_out_ready, dev->setup_out_error, or
dev->setup_out_len.
In addition, if the abort arrives before userspace calls ep0_read(),
ep0_write(), or ep0_poll(), the entry checks in those functions clear
dev->setup_abort = 0 without resetting dev->state from STATE_DEV_SETUP to
STATE_DEV_CONNECTED, so the next event read is still misdirected into the
control data stage path.
> + if (dev->state != STATE_DEV_SETUP ||
> + dev->setup_in || !dev->setup_out_ready) {
> retval = -ECANCELED;
> goto done;
> }
[Severity: Medium]
Does handling dev->setup_abort before checking dev->state overwrite
STATE_DEV_UNBOUND if gadgetfs_unbind() ran while ep0_read() was sleeping?
CPU0 CPU1
ep0_read()
spin_unlock_irq(&dev->lock)
wait_event_interruptible(...)
gadgetfs_setup()
dev->setup_abort = 1
...
ep0_complete()
dev->setup_out_ready = 1
ep0_readable(dev)
gadgetfs_unbind()
dev->state = STATE_DEV_UNBOUND
usb_ep_free_request(..., dev->req)
dev->req = NULL
gadget->ep0->driver_data = NULL
spin_lock_irq(&dev->lock)
if (dev->setup_abort)
dev->state = STATE_DEV_CONNECTED <-- clobbers STATE_DEV_UNBOUND
Because wait_event_interruptible() runs with dev->lock dropped and without
incrementing dev->udc_usage, gadgetfs_unbind() can finish tearing down
dev->req and gadget->ep0->driver_data before ep0_read() reacquires
dev->lock. Setting dev->state = STATE_DEV_CONNECTED before verifying that
dev->state is still STATE_DEV_SETUP allows subsequent ep0_read() and
ep0_write() calls to pass the dev->state <= STATE_DEV_OPENED check and
dereference NULL ep->driver_data and dev->req in setup_req().
[ ... ]
> @@ -1267,29 +1304,41 @@ ep0_write (struct file *fd, const char __user *buf, size_t len, loff_t *ptr)
>
> len = min_t(size_t, len, dev->setup_wLength);
> if (dev->setup_in) {
> - retval = setup_req (dev->gadget->ep0, dev->req, len);
> + u8 tmp[RBUF_SIZE];
> + bool zero = len < dev->setup_wLength;
> +
> + dev->state = STATE_DEV_CONNECTED;
> + spin_unlock_irq(&dev->lock);
> + if (copy_from_user(tmp, buf, len)) {
> + spin_lock_irq(&dev->lock);
> + return -EFAULT;
> + }
> + spin_lock_irq(&dev->lock);
> + if (dev->setup_abort) {
> + dev->setup_abort = 0;
> + return -EIDRM;
> + }
> + if (dev->state != STATE_DEV_CONNECTED)
> + return -ECANCELED;
> + retval = setup_req(dev->gadget->ep0, dev->req, len);
[Severity: Medium]
Does setting dev->state = STATE_DEV_CONNECTED before dropping dev->lock
prevent ep0_write() from detecting an abort during copy_from_user()?
CPU0 CPU1
ep0_write()
dev->state = STATE_DEV_CONNECTED
spin_unlock_irq(&dev->lock)
copy_from_user(tmp, buf, len)
gadgetfs_setup()
dev->setup_abort = 0
if (dev->state == STATE_DEV_SETUP)
dev->setup_abort = 1 <-- skipped
/* new delegated setup queued */
spin_lock_irq(&dev->lock)
if (dev->setup_abort) <-- false
if (dev->state != STATE_DEV_CONNECTED) <-- false
setup_req(dev->gadget->ep0, dev->req, len)
usb_ep_queue(dev->gadget->ep0, dev->req, ...) <-- queues stale response
gadgetfs_setup() and next_event() only set dev->setup_abort = 1 when
dev->state == STATE_DEV_SETUP, and gadgetfs_setup() clears
dev->setup_abort = 0 at entry. Because ep0_write() has already changed
dev->state to STATE_DEV_CONNECTED before sleeping in copy_from_user(), a
new SETUP packet arriving in that window leaves dev->setup_abort == 0 and
dev->state == STATE_DEV_CONNECTED, so ep0_write() queues the stale IN
payload onto ep0 for the new control transfer (and leaves dev->state as
STATE_DEV_CONNECTED even if copy_from_user() fails with -EFAULT).
[ ... ]
> @@ -1781,15 +1843,21 @@ gadgetfs_unbind (struct usb_gadget *gadget)
> usleep_range(1000, 2000);
> spin_lock_irq(&dev->lock);
> }
> + queued = dev->ep0_req_queued;
> spin_unlock_irq (&dev->lock);
>
> + if (dev->req) {
> + if (queued)
> + usb_ep_dequeue(gadget->ep0, dev->req);
> + clean_req(gadget->ep0, dev->req);
> + usb_ep_free_request(gadget->ep0, dev->req);
> + dev->req = NULL;
> + }
[Severity: High]
Can clean_req() here call kfree() on dev->config, dev->hs_config, or
dev->dev when an in-kernel GET_DESCRIPTOR request is active during unbind?
In gadgetfs_setup(), GET_DESCRIPTOR points dev->req->buf at dev->dev,
dev->hs_config, or dev->config (which all live inside the single dev->buf
allocation freed in dev_release()), while clean_req() frees any buffer
other than dev->rbuf:
drivers/usb/gadget/legacy/inode.c:clean_req() {
...
if (req->buf != dev->rbuf) {
kfree(req->buf);
req->buf = dev->rbuf;
}
...
}
If usb_ep_dequeue() on ep0 returns an error without invoking
dev_req_complete() (for example on UDCs whose ep0 does not support
dequeue, or when the controller is already completing the request on
another CPU) or completes asynchronously, dev->req->buf is not reset to
dev->rbuf before clean_req() runs. clean_req() then passes dev->config
(causing a double free when dev_release() frees dev->buf) or interior
pointers dev->hs_config / dev->dev to kfree(), and usb_ep_free_request()
frees dev->req while the UDC may still complete it.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261109-b4-gadgetfs-ep0-v5-0-f16d0b648bc2@gmail.com?part=1
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-11-09 6:16 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-09 5:55 [PATCH v5 0/3] usb: gadget: gadgetfs: fix ep0 request lifetime and event counter Jing Lin via B4 Relay
2026-11-09 5:55 ` [PATCH v5 1/3] usb: gadget: gadgetfs: fix ep0 request lifetime on disconnect Jing Lin via B4 Relay
2026-11-09 6:16 ` sashiko-bot
2026-11-09 5:55 ` [PATCH v5 2/3] usb: gadget: gadgetfs: prevent ep0 event counter underflow Jing Lin via B4 Relay
2026-11-09 6:08 ` sashiko-bot
2026-11-09 5:55 ` [PATCH v5 3/3] usb: gadget: gadgetfs: avoid writing through the const setup pointer Jing Lin via B4 Relay
2026-11-09 6:00 ` sashiko-bot
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox