Linux USB
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Jing Lin" <kayano050408@gmail.com>
Cc: linux-usb@vger.kernel.org
Subject: Re: [PATCH v6 1/3] usb: gadget: gadgetfs: fix ep0 request lifetime on disconnect
Date: Mon, 09 Nov 2026 07:32:04 +0000	[thread overview]
Message-ID: <sashiko-outbox-204544@kernel.org> (raw)
In-Reply-To: <20261109-b4-gadgetfs-ep0-v6-1-b410e3dced89@gmail.com>

Thank you for your contribution! Sashiko AI review found 8 potential
issues to consider:
- [High] usb: gadget: gadgetfs: unbind race with dev_req_complete() and
  ep0_complete()
- [High] usb: gadget: gadgetfs: unbounded hang in gadgetfs_unbind() when
  ep0 dequeue fails
- [Medium] usb: gadget: gadgetfs: ep0_poll() consumes setup_abort via
  ep0_check_abort()
- [Medium] usb: gadget: gadgetfs: early returns in gadgetfs_setup() skip
  setup_abort
- [Medium] usb: gadget: gadgetfs: ep0_read() wait misses state and
  generation changes
- [Medium] usb: gadget: gadgetfs: 0-length ep0_read() clobbers control
  OUT payload state
- [Medium] usb: gadget: gadgetfs: ep0_write() sets STATE_DEV_CONNECTED
  before user copy
- [Medium] usb: gadget: gadgetfs: ep0_read() overwrites
  STATE_DEV_UNBOUND on setup event
--

commit 143b1de0fb83afcb84b9c88be79a0db5944a14a3
Author: Jing Lin <kayano050408@gmail.com>

usb: gadget: gadgetfs: fix ep0 request lifetime on disconnect

This commit serializes ep0 request lifetime and buffer ownership across
control transfers, userspace ep0 reads and writes, and gadget unbind.

> diff --git a/drivers/usb/gadget/legacy/inode.c b/drivers/usb/gadget/legacy/inode.c
> index 7f6d8460152f9..1277982f194c2 100644
> --- a/drivers/usb/gadget/legacy/inode.c
> +++ b/drivers/usb/gadget/legacy/inode.c

[ ... ]

> @@ -984,54 +989,82 @@ static inline void ep0_readable (struct dev_data *dev)
>  	kill_fasync (&dev->fasync, SIGIO, POLL_IN);
>  }
>  
> -static void clean_req (struct usb_ep *ep, struct usb_request *req)
> +static void dev_req_complete(struct usb_ep *ep, struct usb_request *req);
> +
> +static void clean_req(struct usb_ep *ep, struct usb_request *req)
>  {
>  	struct dev_data		*dev = ep->driver_data;
>  
> -	if (req->buf != dev->rbuf) {
> -		kfree(req->buf);
> -		req->buf = dev->rbuf;
> -	}
> -	req->complete = epio_complete;
> -	dev->setup_out_ready = 0;
> +	/*
> +	 * req->buf may also point into dev->buf, which holds the
> +	 * descriptors; only the buffer allocated by setup_req() belongs
> +	 * to the request and may be freed here.
> +	 */
> +	kfree(dev->ep0_req_buf);
> +	dev->ep0_req_buf = NULL;
> +	req->buf = dev->rbuf;
> +	req->complete = dev_req_complete;
> +}
> +
> +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);
> +	clean_req(ep, req);
> +	dev->ep0_req_queued = false;
> +	spin_unlock_irqrestore(&dev->lock, flags);
> +
> +	/* gadgetfs_unbind() waits for this before freeing the request */
> +	wake_up(&dev->wait);
>  }
>  
>  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) {
> +	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);
> +	dev->ep0_req_queued = false;
>  	spin_unlock_irqrestore(&dev->lock, flags);
> +
> +	/* gadgetfs_unbind() waits for this before freeing the request */
> +	wake_up(&dev->wait);
>  }
>  
>  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;
>  	}

[ ... ]

> @@ -1073,13 +1123,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;

[Severity: Medium]
Can a zero-length ep0_read() during a delegated control OUT transfer queue
a bogus status-stage request and clobber dev->setup_out_len?

Previously, setup_req() rejected requests while dev->setup_out_ready was
set.  Now ep0_complete() clears dev->ep0_req_queued as soon as the OUT data
stage completes, and the len == 0 branch in ep0_read() does not check that
dev->setup_wLength == 0.  If userspace calls read(fd, buf, 0) after the OUT
data stage finishes, setup_req() succeeds and queues a 0-length request
whose ep0_complete() callback sees !dev->setup_in && dev->setup_wLength,
overwriting dev->setup_out_len with 0 and setting dev->setup_out_ready = 1
after dev->state has already returned to STATE_DEV_CONNECTED.

> @@ -1102,32 +1159,43 @@ ep0_read (struct file *fd, char __user *buf, size_t len, loff_t *ptr)
>  			}
>  			spin_unlock_irq (&dev->lock);
>  			retval = wait_event_interruptible (dev->wait,
> -					dev->setup_out_ready != 0);
> +					dev->setup_out_ready ||
> +					dev->setup_abort);
>  
>  			/* FIXME state could change from under us */
>  			spin_lock_irq (&dev->lock);
>  			if (retval)
>  				goto done;
>  
> -			if (dev->state != STATE_DEV_SETUP) {
> +			if (ep0_check_abort(dev)) {
> +				retval = -EIDRM;
> +				goto done;
> +			}
> +			if (dev->state != STATE_DEV_SETUP ||
> +			    dev->setup_in || !dev->setup_out_ready) {
>  				retval = -ECANCELED;
>  				goto done;
>  			}

[Severity: Medium]
Can wait_event_interruptible() in ep0_read() miss a disconnect, unbind, or
superseded control transfer?

The wait condition only checks dev->setup_out_ready || dev->setup_abort, so
if dev->state leaves STATE_DEV_SETUP without setting either flag (such as
in gadgetfs_disconnect() or gadgetfs_unbind()), ep0_read() stays blocked
instead of waking to return -ECANCELED.  In addition, ep0_read() does not
snapshot dev->setup_generation before dropping dev->lock, so if a new
delegated control OUT request is read and completes while the first reader
is asleep, the first ep0_read() can wake and consume the new transfer's
dev->setup_buf.

[ ... ]

> @@ -1267,29 +1334,45 @@ 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;
> +			unsigned int generation = dev->setup_generation;
> +
> +			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);
> +			/*
> +			 * A control request that arrived while dev->lock was
> +			 * dropped supersedes this data stage; do not queue the
> +			 * stale response for it.
> +			 */
> +			if (dev->setup_generation != generation)
> +				return -EIDRM;
> +			if (dev->state != STATE_DEV_CONNECTED)
> +				return -ECANCELED;

[Severity: Medium]
Does setting dev->state = STATE_DEV_CONNECTED before copy_from_user()
leave the state machine out of STATE_DEV_SETUP if the copy faults or races
with gadgetfs_setup()?

If copy_from_user() returns -EFAULT, ep0_write() returns without restoring
dev->state to STATE_DEV_SETUP even though no response was queued.  Also, if
a new SETUP packet arrives in gadgetfs_setup() while dev->lock is dropped
for copy_from_user(), gadgetfs_setup() sees dev->state ==
STATE_DEV_CONNECTED instead of STATE_DEV_SETUP and skips setting
dev->setup_abort = 1.

[ ... ]

> @@ -1360,8 +1443,7 @@ ep0_poll (struct file *fd, poll_table *wait)
>  	spin_lock_irq(&dev->lock);
>  
>  	/* report fd mode change before acting on it */
> -	if (dev->setup_abort) {
> -		dev->setup_abort = 0;
> +	if (ep0_check_abort(dev)) {
>  		mask = EPOLLHUP;
>  		goto out;
>  	}

[Severity: Medium]
Does calling ep0_check_abort() in ep0_poll() consume dev->setup_abort and
reset dev->state before ep0_read() or ep0_write() can see the abort?

ep0_check_abort() clears dev->setup_abort and transitions dev->state from
STATE_DEV_SETUP to STATE_DEV_CONNECTED.  If a process polls ep0 before
issuing read() or write(), or if a concurrent poll runs while ep0_read() is
waking from wait_event_interruptible(), ep0_poll() consumes the abort:
a waiting ep0_read() can miss dev->setup_abort and stay asleep, while a
subsequent control-data ep0_read() sees STATE_DEV_CONNECTED and falls
through into the event-reading path instead of returning -EIDRM.

> @@ -1470,6 +1552,10 @@ gadgetfs_setup (struct usb_gadget *gadget, const struct usb_ctrlrequest *ctrl)
>  	u16				w_value = le16_to_cpu(ctrl->wValue);
>  	u16				w_length = le16_to_cpu(ctrl->wLength);
>  
> +	spin_lock(&dev->lock);
> +	/* invalidate userspace copies started for an older control request */
> +	dev->setup_generation++;
> +
>  	if (w_length > RBUF_SIZE) {
>  		if (ctrl->bRequestType & USB_DIR_IN) {
>  			/* Cast away the const, we are going to overwrite on purpose. */
> @@ -1478,11 +1564,15 @@ gadgetfs_setup (struct usb_gadget *gadget, const struct usb_ctrlrequest *ctrl)
>  			*temp = cpu_to_le16(RBUF_SIZE);
>  			w_length = RBUF_SIZE;
>  		} else {
> +			spin_unlock(&dev->lock);
>  			return value;
>  		}
>  	}
>  
> -	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)
> @@ -1505,8 +1595,11 @@ gadgetfs_setup (struct usb_gadget *gadget, const struct usb_ctrlrequest *ctrl)
>  	 * then ep0_{read,write} will report the wrong status. controller
>  	 * driver will have aborted pending i/o.
>  	 */
> -	} else if (dev->state == STATE_DEV_SETUP)
> +	} else if (dev->state == STATE_DEV_SETUP) {
>  		dev->setup_abort = 1;
> +		/* wake an ep0_read() waiting for the aborted data stage */
> +		ep0_readable(dev);
> +	}

[Severity: Medium]
If gadgetfs_setup() returns early on w_length > RBUF_SIZE or
dev->ep0_req_queued while dev->state == STATE_DEV_SETUP, does it leave the
aborted transfer in STATE_DEV_SETUP without setting dev->setup_abort?

Both early returns happen after dev->setup_generation++ but before the
dev->state == STATE_DEV_SETUP check.  An ep0_read() waiting for the prior
transfer's OUT data stage is never woken, and a subsequent ep0_write()
called to answer the aborted IN transfer sees dev->setup_abort == 0,
snapshots the already-incremented dev->setup_generation, and queues its
stale response onto ep0.

[ ... ]

> @@ -1781,15 +1883,29 @@ 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);
> +			/*
> +			 * usb_ep_dequeue() may return before the completion
> +			 * routine runs.  Do not free the request until the
> +			 * controller has given it back.
> +			 */
> +			wait_event(dev->wait, !dev->ep0_req_queued);
> +		}
> +		dev->req->buf = dev->rbuf;
> +		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);
>  }

[Severity: High]
Can gadgetfs_unbind() race with dev_req_complete() or ep0_complete() on
dev->req and dev->wait?

CPU0                                    CPU1
dev_req_complete() / ep0_complete()     gadgetfs_unbind()
  spin_lock_irqsave(&dev->lock, flags)
  clean_req(ep, req)
  dev->ep0_req_queued = false
                                          wait_event(dev->wait,
                                                     !dev->ep0_req_queued)
                                          clean_req(gadget->ep0, dev->req)
                                          usb_ep_free_request(..., dev->req)
                                          ...
                                          put_dev(dev) -> kfree(dev)
  spin_unlock_irqrestore(&dev->lock, ...)
  wake_up(&dev->wait)  <-- use-after-free

wait_event() can observe !dev->ep0_req_queued and return before CPU0
releases dev->lock or calls wake_up(&dev->wait) (especially in
ep0_complete(), where ep0_readable() wakes dev->wait before clearing
dev->ep0_req_queued).  gadgetfs_unbind() then runs clean_req() and
usb_ep_free_request() without holding dev->lock while the completion
handler is still executing, and if the last reference on dev is dropped,
wake_up(&dev->wait) touches freed memory or triggers the
BUG_ON(waitqueue_active(&data->wait)) in put_dev().

[Severity: High]
Can wait_event(dev->wait, !dev->ep0_req_queued) hang forever in
gadgetfs_unbind() when usb_ep_dequeue() fails on ep0?

usb_ep_dequeue() only guarantees a completion callback when it returns 0,
and several UDC drivers (such as musb_g_ep0_dequeue(), mtu3_ep0_dequeue(),
pxa27x, pxa25x, and at91) do not support dequeuing ep0 requests and return
-EINVAL without giving back the request.  When pullup() on disconnect does
not nuke ep0 before unbind(), ignoring the usb_ep_dequeue() return value
leaves gadgetfs_unbind() sleeping in TASK_UNINTERRUPTIBLE forever.

[Severity: Medium]
Can setting dev->req = NULL and gadget->ep0->driver_data = NULL in
gadgetfs_unbind() lead to a NULL pointer dereference in setup_req() if a
GADGETFS_SETUP event was already queued?

gadgetfs_unbind() sets dev->state = STATE_DEV_UNBOUND but leaves queued
events in dev->event[].  If userspace calls ep0_read() after unbind, the
scan loop still transitions dev->state to STATE_DEV_SETUP when delivering a
queued GADGETFS_SETUP event:

drivers/usb/gadget/legacy/inode.c:ep0_read() {
    ...
		for (i = 0; i < n; i++) {
			if (dev->event [i].type == GADGETFS_SETUP) {
				dev->state = STATE_DEV_SETUP;
				n = i + 1;
				break;
			}
		}
    ...
}

A subsequent ep0_write() or 0-length ep0_read() then sees STATE_DEV_SETUP
and calls setup_req(dev->gadget->ep0, dev->req, ...), where ep->driver_data
and dev->req are now NULL.

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261109-b4-gadgetfs-ep0-v6-0-b410e3dced89@gmail.com?part=1

  reply	other threads:[~2026-11-09  7:32 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-09  6:50 [PATCH v6 0/3] usb: gadget: gadgetfs: fix ep0 request lifetime and event counter Jing Lin via B4 Relay
2026-11-09  6:50 ` [PATCH v6 1/3] usb: gadget: gadgetfs: fix ep0 request lifetime on disconnect Jing Lin via B4 Relay
2026-11-09  7:32   ` sashiko-bot [this message]
2026-11-09 16:51   ` Alan Stern
2026-11-09  6:50 ` [PATCH v6 2/3] usb: gadget: gadgetfs: prevent ep0 event counter underflow Jing Lin via B4 Relay
2026-11-09  7:12   ` sashiko-bot
2026-11-09  6:50 ` [PATCH v6 3/3] usb: gadget: gadgetfs: avoid writing through the const setup pointer Jing Lin via B4 Relay
2026-11-09  6:56   ` sashiko-bot
2026-11-09 16:22   ` Alan Stern

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-204544@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=kayano050408@gmail.com \
    --cc=linux-usb@vger.kernel.org \
    --cc=sashiko-reviews@lists.linux.dev \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox