All of lore.kernel.org
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Igor Putko" <igorpetindev@gmail.com>
Cc: linux-usb@vger.kernel.org
Subject: Re: [PATCH] usb: raw-gadget: fix double-free race during UDC unbind and rebind
Date: Sat, 24 Oct 2026 18:48:20 +0000	[thread overview]
Message-ID: <sashiko-outbox-183840@kernel.org> (raw)
In-Reply-To: <20261024183727.42339-1-igorpetindev@gmail.com>

Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Critical] usb: raw-gadget: deadlock and use-after-free in gadget_unbind() ep0 cleanup
- [Critical] usb: raw-gadget: race between gadget_unbind() and endpoint ioctls
- [Low] usb: raw-gadget: unreachable dev->req cleanup left in dev_free()
--

commit a0840cd47b22c347c5350236b13bd1cde724c788
Author: Igor Putko <igorpetindev@gmail.com>

usb: raw-gadget: fix double-free race during UDC unbind and rebind

This patch moves endpoint disabling, request freeing, and descriptor cleanup
from dev_free() into gadget_unbind() so that UDC endpoints are torn down
synchronously before the UDC can be rebound. It also stores the endpoint
descriptor pointer in struct raw_ep and frees the ep0 request on error in
gadget_bind().

> diff --git a/drivers/usb/gadget/legacy/raw_gadget.c b/drivers/usb/gadget/legacy/raw_gadget.c
> index 4febf8dac7ca5..379351a010f82 100644
> --- a/drivers/usb/gadget/legacy/raw_gadget.c
> +++ b/drivers/usb/gadget/legacy/raw_gadget.c
[ ... ]
> @@ -218,14 +218,6 @@ static void dev_free(struct kref *kref)
>  		usb_ep_free_request(dev->gadget->ep0, dev->req);
>  	}

[Severity: Low]
Is the dev->req cleanup in dev_free() still reachable after this change?

dev->req is only allocated in gadget_bind(). If gadget_bind() fails after
setting dev->req, the new error path frees the request and sets dev->req to
NULL. If gadget_bind() succeeds, it takes a reference via
kref_get(&dev->count) that is only dropped in gadget_unbind(), which now also
frees dev->req and sets both dev->req and dev->gadget to NULL before calling
kref_put().

Should the remaining if (dev->req) block in dev_free() be removed as well?

>  	raw_event_queue_destroy(&dev->queue);
> -	for (i = 0; i < dev->eps_num; i++) {
> -		if (dev->eps[i].state == STATE_EP_DISABLED)
> -			continue;
> -		usb_ep_disable(dev->eps[i].ep);
> -		usb_ep_free_request(dev->eps[i].ep, dev->eps[i].req);
> -		kfree(dev->eps[i].ep->desc);
> -		dev->eps[i].state = STATE_EP_DISABLED;
> -	}
>  	kfree(dev);
>  }
[ ... ]
> @@ -328,6 +326,43 @@ static int gadget_bind(struct usb_gadget *gadget,
>  static void gadget_unbind(struct usb_gadget *gadget)
>  {
>  	struct raw_dev *dev = get_gadget_data(gadget);
> +	int i;
> +	unsigned long flags;
> +
> +	if (!dev)
> +		return;
> +
> +	for (i = 0; i < dev->eps_num; i++) {
> +		struct raw_ep *ep = &dev->eps[i];
> +
> +		spin_lock_irqsave(&dev->lock, flags);
> +		if (ep->state == STATE_EP_DISABLED) {
> +			spin_unlock_irqrestore(&dev->lock, flags);
> +			continue;
> +		}
> +		ep->state = STATE_EP_DISABLED;
> +		spin_unlock_irqrestore(&dev->lock, flags);
> +
> +		usb_ep_disable(ep->ep);
> +
> +		spin_lock_irqsave(&dev->lock, flags);
> +		usb_ep_free_request(ep->ep, ep->req);
> +		kfree(ep->desc);
> +		ep->desc = NULL;
> +		ep->ep->desc = NULL;
> +		spin_unlock_irqrestore(&dev->lock, flags);
> +	}

[Severity: Critical]
Can moving endpoint teardown into gadget_unbind() race with concurrent
endpoint ioctls when the UDC is unbound while the raw-gadget file descriptor
is still open (for example via sysfs unbind or usb_del_gadget())?

There appear to be three races with active ioctl calls:

1. In raw_ioctl_ep_disable(), dev->lock is dropped with state still set to
STATE_EP_ENABLED and disabling set to true:

drivers/usb/gadget/legacy/raw_gadget.c:raw_ioctl_ep_disable() {
	...
	dev->eps[i].disabling = true;
	spin_unlock_irqrestore(&dev->lock, flags);

	usb_ep_disable(dev->eps[i].ep);

	spin_lock_irqsave(&dev->lock, flags);
	usb_ep_free_request(dev->eps[i].ep, dev->eps[i].req);
	...
}

Because gadget_unbind() only checks ep->state == STATE_EP_DISABLED and does
not check ep->disabling, won't both gadget_unbind() and
raw_ioctl_ep_disable() call usb_ep_free_request(ep->ep, ep->req) on the same
request?

2. In raw_process_ep_io(), dev->lock is dropped with ep->urb_queued set to
true before queueing or waiting on ep->req:

drivers/usb/gadget/legacy/raw_gadget.c:raw_process_ep_io() {
	...
	ep->urb_queued = true;
	spin_unlock_irqrestore(&dev->lock, flags);

	ret = usb_ep_queue(ep->ep, ep->req, GFP_KERNEL);
	if (ret) {
		dev_err(&dev->gadget->dev,
				"fail, usb_ep_queue returned %d\n", ret);
		...
	}

	ret = wait_for_completion_interruptible(&done);
	if (ret) {
		dev_dbg(&dev->gadget->dev, "wait interrupted\n");
		usb_ep_dequeue(ep->ep, ep->req);
		...
}

Because gadget_unbind() does not check or wait for ep->urb_queued before
freeing ep->req and clearing dev->gadget, can raw_process_ep_io() pass a
freed ep->req to usb_ep_queue() or usb_ep_dequeue(), and dereference a NULL
dev->gadget in dev_err() or dev_dbg()?

3. While gadget_unbind() drops dev->lock around usb_ep_disable(ep->ep),
dev->state is still STATE_DEV_RUNNING, dev->gadget is still non-NULL, and
ep->state has just been set to STATE_EP_DISABLED. If raw_ioctl_ep_enable()
runs during that window, it can re-enable the current endpoint (causing
gadget_unbind() to immediately free the newly allocated ep->req and ep->desc
while leaving ep->state as STATE_EP_ENABLED) or enable an earlier endpoint i
that gadget_unbind() already passed (leaking the new ep->req and ep->desc and
leaving a dangling ep->ep->desc on the UDC endpoint once dev->eps_num is set
to 0).

> +
> +	spin_lock_irqsave(&dev->lock, flags);
> +	if (dev->req) {
> +		if (dev->ep0_urb_queued)
> +			usb_ep_dequeue(dev->gadget->ep0, dev->req);
> +		usb_ep_free_request(dev->gadget->ep0, dev->req);
> +		dev->req = NULL;
> +	}
> +	dev->gadget = NULL;
> +	dev->eps_num = 0;
> +	spin_unlock_irqrestore(&dev->lock, flags);

[Severity: Critical]
Does this ep0 teardown in gadget_unbind() risk a deadlock and use-after-free
when an ep0 transfer is in flight in raw_process_ep0_io()?

First, gadget_unbind() calls usb_ep_dequeue(dev->gadget->ep0, dev->req) while
holding dev->lock. For UDC drivers that give back dequeued requests
synchronously inside their dequeue callback, such as
dummy_dequeue()->dummy_giveback()->usb_gadget_giveback_request(), the
completion handler gadget_ep0_complete() is invoked on the same CPU:

drivers/usb/gadget/legacy/raw_gadget.c:gadget_ep0_complete() {
	struct raw_dev *dev = req->context;
	unsigned long flags;

	spin_lock_irqsave(&dev->lock, flags);
	...
}

Won't gadget_ep0_complete() deadlock trying to acquire dev->lock while
gadget_unbind() already holds it?

Second, for UDC drivers where usb_ep_dequeue() completes asynchronously,
gadget_unbind() immediately calls usb_ep_free_request(dev->gadget->ep0,
dev->req) without waiting for dev->ep0_done, which frees dev->req while the
UDC still owns the request.

Third, dev->ep0_urb_queued is only set while raw_process_ep0_io() has dropped
dev->lock:

drivers/usb/gadget/legacy/raw_gadget.c:raw_process_ep0_io() {
	...
	dev->ep0_urb_queued = true;
	spin_unlock_irqrestore(&dev->lock, flags);

	ret = usb_ep_queue(dev->gadget->ep0, dev->req, GFP_KERNEL);
	if (ret) {
		dev_err(&dev->gadget->dev,
				"fail, usb_ep_queue returned %d\n", ret);
		spin_lock_irqsave(&dev->lock, flags);
		goto out_queue_failed;
	}

	ret = wait_for_completion_interruptible(&dev->ep0_done);
	if (ret) {
		dev_dbg(&dev->gadget->dev, "wait interrupted\n");
		usb_ep_dequeue(dev->gadget->ep0, dev->req);
		wait_for_completion(&dev->ep0_done);
	...
}

If gadget_unbind() runs concurrently, frees dev->req, and sets dev->req and
dev->gadget to NULL, won't raw_process_ep0_io() dereference a NULL
dev->gadget and pass a freed or NULL dev->req to usb_ep_queue() or
usb_ep_dequeue()?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261024183727.42339-1-igorpetindev@gmail.com?part=1

  reply	other threads:[~2026-10-24 18:48 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-24 18:37 [PATCH] usb: raw-gadget: fix double-free race during UDC unbind and rebind Igor Putko
2026-10-24 18:48 ` sashiko-bot [this message]
2026-10-24 19:07 ` [PATCH v2] " Igor Putko
2026-10-24 19:28   ` sashiko-bot

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-183840@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=igorpetindev@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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.