[PATCH RFC] usb: raw-gadget: fix use-after-free during gadget unbind

"syzbot" <[email protected]> Wed, 29 Jul 2026 09:55:24 +0000 (UTC)
Newsgroups dev.linux.lists.syzbot
Message-ID <[email protected]>
A use-after-free vulnerability occurs in the raw-gadget driver when the
underlying USB Device Controller (UDC) is unbound while concurrent ioctls
are in progress.

When the UDC is unbound, gadget_unbind() is called. However, this function
fails to clear the dev->gadget pointer and update dev->state. It only
clears the driver data. After gadget_unbind() returns, the UDC core frees
the struct usb_gadget and its associated device name string.

If a concurrent ioctl (e.g., USB_RAW_IOCTL_EP0_READ) is executing, it may
pass the state checks because dev->state was not updated, and proceed to
use the dangling dev->gadget pointer. If usb_ep_queue() fails (e.g., due to
the UDC being disconnected), the error path attempts to print an error
message using dev_err(&dev->gadget->dev, ...), which dereferences the freed
device name string, resulting in a KASAN slab-use-after-free crash:

BUG: KASAN: slab-use-after-free in string_nocheck lib/vsprintf.c:648
[inline]
BUG: KASAN: slab-use-after-free in string+0x216/0x2d0 lib/vsprintf.c:730
Read of size 1 at addr ffff888195535600 by task syz.0.40/6395
...
Call Trace:
 <TASK>
...
 string_nocheck lib/vsprintf.c:648 [inline]
 string+0x216/0x2d0 lib/vsprintf.c:730
 vsnprintf+0x74a/0xef0 lib/vsprintf.c:2945
 snprintf+0xe8/0x140 lib/vsprintf.c:3043
 set_dev_info drivers/base/core.c:4984 [inline]
 dev_vprintk_emit+0x30f/0x400 drivers/base/core.c:4994
 dev_printk_emit+0xee/0x140 drivers/base/core.c:5007
 _dev_err+0x11e/0x180 drivers/base/core.c:5062
 raw_process_ep0_io+0x531/0x990 drivers/usb/gadget/legacy/raw_gadget.c:730
 raw_ioctl_ep0_read drivers/usb/gadget/legacy/raw_gadget.c:782 [inline]
 raw_ioctl+0x2721/0x40d0 drivers/usb/gadget/legacy/raw_gadget.c:1316
...

To fix this, gadget_unbind() must clear dev->gadget and update dev->state
to STATE_DEV_FAILED under dev->lock.

To prevent race conditions where an ioctl has already passed the checks but
hasn't yet called usb_ep_queue(), a synchronization mechanism is
introduced. A udc_usage counter tracks ongoing UDC calls. gadget_unbind()
waits for this counter to drop to zero before returning.

To avoid deadlocks where ioctls are waiting for I/O completion,
gadget_unbind() explicitly dequeues all pending requests before waiting for
udc_usage to drop. This forces the UDC core to complete the requests
immediately, waking up the waiting ioctls so they can decrement udc_usage
and signal the completion.

Additionally, if an ioctl is interrupted by a signal, it checks if
dev->state == STATE_DEV_FAILED. If so, it knows gadget_unbind() is handling
the dequeue and simply waits for the completion handler to finish. Finally,
proper cleanup is added in gadget_bind() if the connect event fails to
queue, and dev_err() calls are updated to use dev->dev instead of
dev->gadget->dev to avoid dereferencing the gadget pointer in error paths.

Fixes: f2c2e717642c ("usb: gadget: add raw-gadget interface")
Assisted-by: Gemini:gemini-3.5-flash Gemini:gemini-3.1-pro-preview syzbot
Reported-by: [email protected]
Closes: https://syzkaller.appspot.com/bug?extid=9aacea11bc70c3ddaff2
Link: https://syzkaller.appspot.com/ai_job?id=2b271df2-0caf-4de1-92ff-99534a3e1708
To: "Greg Kroah-Hartman" <[email protected]>
To: <[email protected]>
To: "Andrey Konovalov" <[email protected]>
Cc: "Andrey Konovalov" <[email protected]>
Cc: "Kees Cook" <[email protected]>
Cc: "Gopi Krishna Menon" <[email protected]>
Cc: <[email protected]>

---
diff --git a/drivers/usb/gadget/legacy/raw_gadget.c b/drivers/usb/gadget/legacy/raw_gadget.c
index 4febf8dac..bce4f8d3f 100644
--- a/drivers/usb/gadget/legacy/raw_gadget.c
+++ b/drivers/usb/gadget/legacy/raw_gadget.c
@@ -175,6 +175,8 @@ struct raw_dev {
 	bool				gadget_registered;
 	struct usb_gadget		*gadget;
 	struct usb_request		*req;
+	int				udc_usage;
+	struct completion		*unbind_completion;
 	bool				ep0_in_pending;
 	bool				ep0_out_pending;
 	bool				ep0_urb_queued;
@@ -205,27 +207,13 @@ static struct raw_dev *dev_new(void)
 static void dev_free(struct kref *kref)
 {
 	struct raw_dev *dev = container_of(kref, struct raw_dev, count);
-	int i;
 
 	kfree(dev->udc_name);
 	kfree(dev->driver.udc_name);
 	kfree(dev->driver.driver.name);
 	if (dev->driver_id_number >= 0)
 		ida_free(&driver_id_numbers, dev->driver_id_number);
-	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);
-	}
 	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);
 }
 
@@ -316,6 +304,11 @@ static int gadget_bind(struct usb_gadget *gadget,
 	ret = raw_queue_event(dev, USB_RAW_EVENT_CONNECT, 0, NULL);
 	if (ret < 0) {
 		dev_err(&gadget->dev, "failed to queue connect event\n");
+		spin_lock_irqsave(&dev->lock, flags);
+		dev->gadget = NULL;
+		dev->req = NULL;
+		spin_unlock_irqrestore(&dev->lock, flags);
+		usb_ep_free_request(gadget->ep0, req);
 		set_gadget_data(gadget, NULL);
 		return ret;
 	}
@@ -328,6 +321,47 @@ static int gadget_bind(struct usb_gadget *gadget,
 static void gadget_unbind(struct usb_gadget *gadget)
 {
 	struct raw_dev *dev = get_gadget_data(gadget);
+	unsigned long flags;
+	DECLARE_COMPLETION_ONSTACK(done);
+	int i;
+
+	spin_lock_irqsave(&dev->lock, flags);
+	dev->state = STATE_DEV_FAILED;
+
+	if (dev->ep0_urb_queued) {
+		spin_unlock_irqrestore(&dev->lock, flags);
+		usb_ep_dequeue(gadget->ep0, dev->req);
+		spin_lock_irqsave(&dev->lock, flags);
+	}
+	for (i = 0; i < dev->eps_num; i++) {
+		if (dev->eps[i].state == STATE_EP_ENABLED && dev->eps[i].urb_queued) {
+			spin_unlock_irqrestore(&dev->lock, flags);
+			usb_ep_dequeue(dev->eps[i].ep, dev->eps[i].req);
+			spin_lock_irqsave(&dev->lock, flags);
+		}
+	}
+
+	if (dev->udc_usage > 0) {
+		dev->unbind_completion = &done;
+		spin_unlock_irqrestore(&dev->lock, flags);
+		wait_for_completion(&done);
+		spin_lock_irqsave(&dev->lock, flags);
+	}
+	dev->gadget = NULL;
+	spin_unlock_irqrestore(&dev->lock, flags);
+
+	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;
+	}
+	if (dev->req) {
+		usb_ep_free_request(gadget->ep0, dev->req);
+		dev->req = NULL;
+	}
 
 	set_gadget_data(gadget, NULL);
 	/* Matches kref_get() in gadget_bind(). */
@@ -637,11 +671,11 @@ static int raw_ioctl_event_fetch(struct raw_dev *dev, unsigned long value)
 
 	event = raw_event_queue_fetch(&dev->queue);
 	if (PTR_ERR(event) == -EINTR) {
-		dev_dbg(&dev->gadget->dev, "event fetching interrupted\n");
+		dev_dbg(dev->dev, "event fetching interrupted\n");
 		return -EINTR;
 	}
 	if (IS_ERR(event)) {
-		dev_err(&dev->gadget->dev, "failed to fetch event\n");
+		dev_err(dev->dev, "failed to fetch event\n");
 		spin_lock_irqsave(&dev->lock, flags);
 		dev->state = STATE_DEV_FAILED;
 		spin_unlock_irqrestore(&dev->lock, flags);
@@ -723,22 +757,37 @@ static int raw_process_ep0_io(struct raw_dev *dev, struct usb_raw_ep_io *io,
 	dev->req->length = io->length;
 	dev->req->zero = usb_raw_io_flags_zero(io->flags);
 	dev->ep0_urb_queued = true;
+	dev->udc_usage++;
 	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);
+		dev_err(dev->dev, "fail, usb_ep_queue returned %d\n", ret);
 		spin_lock_irqsave(&dev->lock, flags);
+		dev->udc_usage--;
+		if (!dev->udc_usage && dev->unbind_completion)
+			complete(dev->unbind_completion);
 		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);
+		dev_dbg(dev->dev, "wait interrupted\n");
 		spin_lock_irqsave(&dev->lock, flags);
+		if (dev->state != STATE_DEV_FAILED && dev->gadget) {
+			struct usb_ep *ep0 = dev->gadget->ep0;
+
+			spin_unlock_irqrestore(&dev->lock, flags);
+
+			usb_ep_dequeue(ep0, dev->req);
+			wait_for_completion(&dev->ep0_done);
+
+			spin_lock_irqsave(&dev->lock, flags);
+		} else {
+			spin_unlock_irqrestore(&dev->lock, flags);
+			wait_for_completion(&dev->ep0_done);
+			spin_lock_irqsave(&dev->lock, flags);
+		}
 		if (dev->ep0_status == -ECONNRESET)
 			dev->ep0_status = -EINTR;
 		goto out_interrupted;
@@ -750,6 +799,9 @@ static int raw_process_ep0_io(struct raw_dev *dev, struct usb_raw_ep_io *io,
 	ret = dev->ep0_status;
 out_queue_failed:
 	dev->ep0_urb_queued = false;
+	dev->udc_usage--;
+	if (!dev->udc_usage && dev->unbind_completion)
+		complete(dev->unbind_completion);
 out_unlock:
 	spin_unlock_irqrestore(&dev->lock, flags);
 	return ret;
@@ -956,11 +1008,15 @@ static int raw_ioctl_ep_disable(struct raw_dev *dev, unsigned long value)
 		goto out_unlock;
 	}
 	dev->eps[i].disabling = true;
+	dev->udc_usage++;
 	spin_unlock_irqrestore(&dev->lock, flags);
 
 	usb_ep_disable(dev->eps[i].ep);
 
 	spin_lock_irqsave(&dev->lock, flags);
+	dev->udc_usage--;
+	if (!dev->udc_usage && dev->unbind_completion)
+		complete(dev->unbind_completion);
 	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;
@@ -1109,22 +1165,37 @@ static int raw_process_ep_io(struct raw_dev *dev, struct usb_raw_ep_io *io,
 	ep->req->length = io->length;
 	ep->req->zero = usb_raw_io_flags_zero(io->flags);
 	ep->urb_queued = true;
+	dev->udc_usage++;
 	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);
+		dev_err(dev->dev, "fail, usb_ep_queue returned %d\n", ret);
 		spin_lock_irqsave(&dev->lock, flags);
+		dev->udc_usage--;
+		if (!dev->udc_usage && dev->unbind_completion)
+			complete(dev->unbind_completion);
 		goto out_queue_failed;
 	}
 
 	ret = wait_for_completion_interruptible(&done);
 	if (ret) {
-		dev_dbg(&dev->gadget->dev, "wait interrupted\n");
-		usb_ep_dequeue(ep->ep, ep->req);
-		wait_for_completion(&done);
+		dev_dbg(dev->dev, "wait interrupted\n");
 		spin_lock_irqsave(&dev->lock, flags);
+		if (dev->state != STATE_DEV_FAILED && dev->gadget) {
+			struct usb_ep *usb_ep = ep->ep;
+
+			spin_unlock_irqrestore(&dev->lock, flags);
+
+			usb_ep_dequeue(usb_ep, ep->req);
+			wait_for_completion(&done);
+
+			spin_lock_irqsave(&dev->lock, flags);
+		} else {
+			spin_unlock_irqrestore(&dev->lock, flags);
+			wait_for_completion(&done);
+			spin_lock_irqsave(&dev->lock, flags);
+		}
 		if (ep->status == -ECONNRESET)
 			ep->status = -EINTR;
 		goto out_interrupted;
@@ -1136,6 +1207,9 @@ static int raw_process_ep_io(struct raw_dev *dev, struct usb_raw_ep_io *io,
 	ret = ep->status;
 out_queue_failed:
 	ep->urb_queued = false;
+	dev->udc_usage--;
+	if (!dev->udc_usage && dev->unbind_completion)
+		complete(dev->unbind_completion);
 out_unlock:
 	spin_unlock_irqrestore(&dev->lock, flags);
 	return ret;


base-commit: f5098b6bae761e346ebcd9da7f95622c04733cff
-- 
This is an AI-generated patch subject to moderation.
Reply with '#syz upstream' to Sign-off the patch as a human author
and send it to the upstream kernel mailing lists.
Reply with '#syz reject' to reject it ('#syz unreject' to undo).

See https://goo.gle/syzbot-ai-patches for information about AI-generated patches.
You can comment on the patch as usual, syzbot will try to address
the comments and send a new version of the patch if necessary.
syzbot engineers can be reached at [email protected].