diff mbox series

USB: cdc-wdm: Fix use after free in service_outstanding_interrupt().

Message ID 620e2ee0-b9a3-dbda-a25b-a93e0ed03ec5@i-love.sakura.ne.jp (mailing list archive)
State Accepted
Commit 5e5ff0b4b6bcb4d17b7a26ec8bcfc7dd4651684f
Headers show
Series USB: cdc-wdm: Fix use after free in service_outstanding_interrupt(). | expand

Commit Message

Tetsuo Handa Dec. 19, 2020, 3:25 p.m. UTC
syzbot is reporting UAF at usb_submit_urb() [1], for
service_outstanding_interrupt() is not checking WDM_DISCONNECTING
before calling usb_submit_urb(). Close the race by doing same checks
wdm_read() does upon retry.

Also, while wdm_read() checks WDM_DISCONNECTING with desc->rlock held,
service_interrupt_work() does not hold desc->rlock. Thus, it is possible
that usb_submit_urb() is called from service_outstanding_interrupt() from
service_interrupt_work() after WDM_DISCONNECTING was set and kill_urbs()
 from wdm_disconnect() completed. Thus, move kill_urbs() in
wdm_disconnect() to after cancel_work_sync() (which makes sure that
service_interrupt_work() is no longer running) completed.

Although it seems to be safe to dereference desc->intf->dev in
service_outstanding_interrupt() even if WDM_DISCONNECTING was already set
because desc->rlock or cancel_work_sync() prevents wdm_disconnect() from
reaching list_del() before service_outstanding_interrupt() completes,
let's not emit error message if WDM_DISCONNECTING is set by
wdm_disconnect() while usb_submit_urb() is in progress.

[1] https://syzkaller.appspot.com/bug?extid=9e04e2df4a32fb661daf

Reported-by: syzbot <syzbot+9e04e2df4a32fb661daf@syzkaller.appspotmail.com>
Signed-off-by: Tetsuo Handa <penguin-kernel@I-love.SAKURA.ne.jp>
---
 drivers/usb/class/cdc-wdm.c | 16 +++++++++++++---
 1 file changed, 13 insertions(+), 3 deletions(-)

Comments

Oliver Neukum Dec. 28, 2020, 2:44 p.m. UTC | #1
Am Sonntag, den 20.12.2020, 00:25 +0900 schrieb Tetsuo Handa:
> syzbot is reporting UAF at usb_submit_urb() [1], for
> service_outstanding_interrupt() is not checking WDM_DISCONNECTING
> before calling usb_submit_urb(). Close the race by doing same checks
> wdm_read() does upon retry.

But wdm_read() does them with proper locking.

> Also, while wdm_read() checks WDM_DISCONNECTING with desc->rlock held,
> service_interrupt_work() does not hold desc->rlock. Thus, it is possible
> that usb_submit_urb() is called from service_outstanding_interrupt() from
> service_interrupt_work() after WDM_DISCONNECTING was set and kill_urbs()
>  from wdm_disconnect() completed. Thus, move kill_urbs() in
> wdm_disconnect() to after cancel_work_sync() (which makes sure that
> service_interrupt_work() is no longer running) completed.

That seems to be the right approach. You must prevent this helper
from being called in the first place.

	Regards
		Oliver
diff mbox series

Patch

diff --git a/drivers/usb/class/cdc-wdm.c b/drivers/usb/class/cdc-wdm.c
index 02d0cfd23bb2..508b1c3f8b73 100644
--- a/drivers/usb/class/cdc-wdm.c
+++ b/drivers/usb/class/cdc-wdm.c
@@ -465,13 +465,23 @@  static int service_outstanding_interrupt(struct wdm_device *desc)
 	if (!desc->resp_count || !--desc->resp_count)
 		goto out;
 
+	if (test_bit(WDM_DISCONNECTING, &desc->flags)) {
+		rv = -ENODEV;
+		goto out;
+	}
+	if (test_bit(WDM_RESETTING, &desc->flags)) {
+		rv = -EIO;
+		goto out;
+	}
+
 	set_bit(WDM_RESPONDING, &desc->flags);
 	spin_unlock_irq(&desc->iuspin);
 	rv = usb_submit_urb(desc->response, GFP_KERNEL);
 	spin_lock_irq(&desc->iuspin);
 	if (rv) {
-		dev_err(&desc->intf->dev,
-			"usb_submit_urb failed with result %d\n", rv);
+		if (!test_bit(WDM_DISCONNECTING, &desc->flags))
+			dev_err(&desc->intf->dev,
+				"usb_submit_urb failed with result %d\n", rv);
 
 		/* make sure the next notification trigger a submit */
 		clear_bit(WDM_RESPONDING, &desc->flags);
@@ -1027,9 +1037,9 @@  static void wdm_disconnect(struct usb_interface *intf)
 	wake_up_all(&desc->wait);
 	mutex_lock(&desc->rlock);
 	mutex_lock(&desc->wlock);
-	kill_urbs(desc);
 	cancel_work_sync(&desc->rxwork);
 	cancel_work_sync(&desc->service_outs_intr);
+	kill_urbs(desc);
 	mutex_unlock(&desc->wlock);
 	mutex_unlock(&desc->rlock);