[PATCH] usb: gadget: fix race condition in UDC class

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



This commit fixes a possible race condition in IDC class when
usb_del_udc() is called concurrently to
usb_gadget_register_driver() or usb_gadget_unregister_driver().

* NOT FOR MERGING, NOT TESTED, NOT EVEN COMPILED *

Signed-off-by: Michal Nazarewicz <m.nazarewicz@xxxxxxxxxxx>
---
 drivers/usb/gadget/udc-core.c |   96 +++++++++++++++++++++++++---------------
 include/linux/usb/udc.h       |    3 -
 2 files changed, 60 insertions(+), 39 deletions(-)

On Wed, 22 Sep 2010 11:24:59 +0200, MichaƂ Nazarewicz wrote:
> Also, the more I think about it, the more I'm convinced that this needs
> some synchronisation.  If usb_del_udc() is called at the same time as
> usb_gadget_unregister_driver() funky stuff can happen.  Funky stuff will
> also happen if usb_del_udc() is called at the same time as
> usb_gadget_register_driver().
>
> I think, spin lock should be replaced with a mutex and the all of the
> operations need to be performed when holding the mutex.  Alternatively,
> mutex can be used in addition to spin lock and then usb_add_udc() could
> work without locking the mutex.  Or, per-UDC mutex needs to be created
> and registering, unregistering and deleting would be performed while
> holding this mutex.

After more thought I came up with a scheme where usage of atomic
operations (xchg and cmpxchg) is enough to guarantee correctness.

The idea is that the driver field can have additional value:
&udc_claimed.  This value means that usb_del_udc(),
usb_gadget_register_driver() or usb_gadget_unregister_driver() does
something with the UDC.  So basically, UDC can be in either of three
states: free, busy or claimed where claimed means the above.

usb_gadget_register_driver() treats claimed UDCs as busy,
usb_gadget_unregister_driver() as ones of no interest, and
usb_del_udc() waits till the UDC is unclaimed.

To make it possible for usb_del_udc() to wait till
usb_*register_gadget_driver() finishes work on UDC a wait queue is
added.

The busy flag has been removed.

diff --git a/drivers/usb/gadget/udc-core.c b/drivers/usb/gadget/udc-core.c
index 9334a91..6e40005 100644
--- a/drivers/usb/gadget/udc-core.c
+++ b/drivers/usb/gadget/udc-core.c
@@ -31,6 +31,8 @@ static struct class *udc_class;
 static struct device_type udc_device_type;
 static LIST_HEAD(udc_list);
 static spinlock_t udc_lock;
+static DECLARE_WAIT_QUEUE_HEAD(udc_wq);
+static struct usb_gadget_driver udc_claimed;
 
 /* ------------------------------------------------------------------------- */
 
@@ -98,8 +100,7 @@ int usb_add_udc(struct device *parent, struct usb_udc *udc)
 	unsigned long		flags;
 	int			ret;
 
-	if (!udc->gadget || !udc->gadget->ops ||
-			!udc->gadget->ops->start) {
+	if (!udc->gadget || !udc->gadget->ops || !udc->gadget->ops->start) {
 		dev_dbg(parent, "unitializaed gadget\n");
 		return -EINVAL;
 	}
@@ -158,6 +159,34 @@ err1:
 }
 EXPORT_SYMBOL_GPL(usb_add_udc);
 
+
+static struct usb_gadget_driver *udc_claim_driver_wait(struct usb_udc *udc)
+{
+	struct usb_gadget_driver *driver;
+	wait_event(udc_wq, (driver = xchg(&udc->driver, &udc_claimed))
+			   != &udc_claimed);
+	return driver;
+}
+
+static int udc_claim_driver_if(struct usb_udc *udc,
+			       struct usb_gadget_driver *old)
+{
+	return cmpxchg(&udc->driver, old, &udc_claimed) == old
+		? 0 : -EBUSY;
+}
+
+static void udc_unclaim_driver(struct usb_udc *udc,
+			       struct usb_gadget_driver *driver)
+{
+	smp_wmb();
+	udc->driver = driver;
+	wake_up_all(&udc_wq);
+}
+
+static void __usb_gadget_unregister_driver(struct usb_udc *udc,
+					   struct usb_gadget_driver *driver);
+
+
 /**
  * usb_del_udc - deletes @udc from udc_list
  * @udc: the udc to be removed.
@@ -168,6 +197,7 @@ EXPORT_SYMBOL_GPL(usb_add_udc);
 void usb_del_udc(struct usb_udc *udc)
 {
 	unsigned long		flags;
+	struct usb_gadget_driver *driver;
 
 	dev_vdbg(udc->dev->parent, "unregistering UDC\n");
 
@@ -175,8 +205,9 @@ void usb_del_udc(struct usb_udc *udc)
 	list_del(&udc->list);
 	spin_unlock_irqrestore(&udc_lock, flags);
 
-	if (udc->busy)
-		usb_gadget_unregister_driver(udc->driver);
+	driver = udc_claim_driver_wait(udc);
+	if (driver)
+		__usb_gatget_unregister_driver(udc, driver);
 
 	device_unregister(udc->dev);
 	device_unregister(&udc->gadget->dev);
@@ -190,33 +221,30 @@ int usb_gadget_register_driver(struct usb_gadget_driver *driver)
 {
 	struct usb_udc		*udc = NULL;
 	unsigned long		flags;
-	int			ret;
+	int			ret = -ENODEV;
 
 	if (!driver || !driver->bind || !driver->setup)
 		return -EINVAL;
 
 	spin_lock_irqsave(&udc_lock, flags);
-	list_for_each_entry(udc, &udc_list, list) {
-		if (!udc->busy) {
-			pr_debug("using UDC '%s'\n", udc->name);
+	list_for_each_entry(udc, &udc_list, list)
+		if (!udc_claim_driver_if(udc, NULL)) {
+			ret = 0;
 			break;
 		}
 
+	spin_unlock_irqrestore(&udc_lock, flags);
+
+	if (ret) {
 		pr_debug("couldn't find an available UDC\n");
-		ret = -ENODEV;
-		spin_unlock_irqrestore(&udc_lock, flags);
-		goto err0;
+		return ret;
 	}
 
 	dev_dbg(udc->dev, "registering UDC driver [%s]\n",
 			driver->function);
 
-	udc->busy = true;
-	udc->driver = driver;
 	udc->dev->driver = &driver->driver;
 
-	spin_unlock_irqrestore(&udc_lock, flags);
-
 	ret = usb_gadget_start(udc->gadget);
 	if (ret) {
 		dev_err(udc->dev, "failed to start %s --> %d\n",
@@ -239,6 +267,7 @@ int usb_gadget_register_driver(struct usb_gadget_driver *driver)
 	}
 
 	kobject_uevent(&udc->dev->kobj, KOBJ_ADD);
+	udc_unclaim_driver(udc, driver);
 
 	return 0;
 
@@ -249,12 +278,9 @@ err2:
 	usb_gadget_stop(udc->gadget);
 
 err1:
-	udc->driver = NULL;
 	udc->dev->driver = NULL;
-	wmb();
-	udc->busy = false;
+	udc_unclaim_driver(udc, NULL);
 
-err0:
 	return ret;
 }
 EXPORT_SYMBOL_GPL(usb_gadget_register_driver);
@@ -263,26 +289,31 @@ int usb_gadget_unregister_driver(struct usb_gadget_driver *driver)
 {
 	struct usb_udc		*udc = NULL;
 	unsigned long		flags;
-	int			ret = 0;
+	int			ret = -ENODEV;
 
 	if (!driver || !driver->unbind)
 		return -EINVAL;
 
 	spin_lock_irqsave(&udc_lock, flags);
-	list_for_each_entry(udc, &udc_list, list) {
-		if (udc->driver == driver)
+	list_for_each_entry(udc, &udc_list, list)
+		if (!udc_claim_driver_if(udc, driver)) {
+			ret = 0;
 			break;
+		}
+	spin_unlock_irqrestore(&udc_lock, flags);
 
-		if (udc->driver != driver || !udc->busy)
-			continue;
-
-		ret = -ENODEV;
-		spin_unlock_irqrestore(&udc_lock, flags);
-		goto out;
+	if (!ret) {
+		__usb_gatget_unregister_driver(udc, driver);
+		udc_unclaim_driver(udc, NULL);
 	}
 
-	spin_unlock_irqrestore(&udc_lock, flags);
+	return ret;
+}
+EXPORT_SYMBOL_GPL(usb_gadget_unregister_driver);
 
+static void __usb_gadget_unregister_driver(struct usb_udc *udc,
+					   struct usb_gadget_driver *driver)
+{
 	(void) usb_gadget_vbus_draw(udc->gadget, 0);
 
 	dev_dbg(udc->dev, "unregistering UDC driver [%s]\n", driver->function);
@@ -292,15 +323,8 @@ int usb_gadget_unregister_driver(struct usb_gadget_driver *driver)
 	usb_gadget_stop(udc->gadget);
 	usb_gadget_disconnect(udc->gadget);
 
-	udc->driver = NULL;
 	udc->dev->driver = NULL;
-	wmb();
-	udc->busy = false;
-
-out:
-	return ret;
 }
-EXPORT_SYMBOL_GPL(usb_gadget_unregister_driver);
 
 /* ------------------------------------------------------------------------- */
 
diff --git a/include/linux/usb/udc.h b/include/linux/usb/udc.h
index 6332df0..ce44468 100644
--- a/include/linux/usb/udc.h
+++ b/include/linux/usb/udc.h
@@ -32,7 +32,6 @@
  * @driver - the gadget driver pointer. For use by the class code
  * @dev - the child device to the actual controller
  * @list - for use by the udc class driver
- * @busy - true when this udc already has @driver and @gadget
  * @name - a human friendly name representation
  *
  * a pointer to this structure should be passed to udc class
@@ -46,8 +45,6 @@ struct usb_udc {
 
 	struct list_head		list;
 
-	unsigned			busy:1;
-
 	const char			*name;
 };
 
-- 
1.7.1

--
To unsubscribe from this list: send the line "unsubscribe linux-usb" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html


[Index of Archives]     [Linux Media]     [Linux Input]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]     [Old Linux USB Devel Archive]

  Powered by Linux