* [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
* 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
* [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
* 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
* [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
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