cdc-wdm: Fix more races on the read path
[pandora-kernel.git] / drivers / usb / class / cdc-wdm.c
index 23cf9d3..7ca54d4 100644 (file)
@@ -70,6 +70,7 @@ MODULE_DEVICE_TABLE (usb, wdm_ids);
 #define WDM_POLL_RUNNING       6
 #define WDM_RESPONDING         7
 #define WDM_SUSPENDING         8
+#define WDM_RESETTING          9
 
 #define WDM_MAX                        16
 
@@ -105,7 +106,8 @@ struct wdm_device {
        int                     count;
        dma_addr_t              shandle;
        dma_addr_t              ihandle;
-       struct mutex            lock;
+       struct mutex            wlock;
+       struct mutex            rlock;
        wait_queue_head_t       wait;
        struct work_struct      rxwork;
        int                     werr;
@@ -314,7 +316,7 @@ static ssize_t wdm_write
        }
 
        /* concurrent writes and disconnect */
-       r = mutex_lock_interruptible(&desc->lock);
+       r = mutex_lock_interruptible(&desc->wlock);
        rv = -ERESTARTSYS;
        if (r) {
                kfree(buf);
@@ -339,6 +341,10 @@ static ssize_t wdm_write
        else
                if (test_bit(WDM_IN_USE, &desc->flags))
                        r = -EAGAIN;
+
+       if (test_bit(WDM_RESETTING, &desc->flags))
+               r = -EIO;
+
        if (r < 0) {
                kfree(buf);
                goto out;
@@ -377,7 +383,7 @@ static ssize_t wdm_write
 out:
        usb_autopm_put_interface(desc->intf);
 outnp:
-       mutex_unlock(&desc->lock);
+       mutex_unlock(&desc->wlock);
 outnl:
        return rv < 0 ? rv : count;
 }
@@ -385,16 +391,17 @@ outnl:
 static ssize_t wdm_read
 (struct file *file, char __user *buffer, size_t count, loff_t *ppos)
 {
-       int rv, cntr = 0;
+       int rv, cntr;
        int i = 0;
        struct wdm_device *desc = file->private_data;
 
 
-       rv = mutex_lock_interruptible(&desc->lock); /*concurrent reads */
+       rv = mutex_lock_interruptible(&desc->rlock); /*concurrent reads */
        if (rv < 0)
                return -ERESTARTSYS;
 
-       if (desc->length == 0) {
+       cntr = ACCESS_ONCE(desc->length);
+       if (cntr == 0) {
                desc->read = 0;
 retry:
                if (test_bit(WDM_DISCONNECTING, &desc->flags)) {
@@ -418,6 +425,10 @@ retry:
                        rv = -ENODEV;
                        goto err;
                }
+               if (test_bit(WDM_RESETTING, &desc->flags)) {
+                       rv = -EIO;
+                       goto err;
+               }
                usb_mark_last_busy(interface_to_usbdev(desc->intf));
                if (rv < 0) {
                        rv = -ERESTARTSYS;
@@ -445,16 +456,20 @@ retry:
                        goto retry;
                }
                clear_bit(WDM_READ, &desc->flags);
+               cntr = desc->length;
                spin_unlock_irq(&desc->iuspin);
        }
 
-       cntr = count > desc->length ? desc->length : count;
+       if (cntr > count)
+               cntr = count;
        rv = copy_to_user(buffer, desc->ubuf, cntr);
        if (rv > 0) {
                rv = -EFAULT;
                goto err;
        }
 
+       spin_lock_irq(&desc->iuspin);
+
        for (i = 0; i < desc->length - cntr; i++)
                desc->ubuf[i] = desc->ubuf[i + cntr];
 
@@ -462,10 +477,13 @@ retry:
        /* in case we had outstanding data */
        if (!desc->length)
                clear_bit(WDM_READ, &desc->flags);
+
+       spin_unlock_irq(&desc->iuspin);
+
        rv = cntr;
 
 err:
-       mutex_unlock(&desc->lock);
+       mutex_unlock(&desc->rlock);
        return rv;
 }
 
@@ -531,7 +549,8 @@ static int wdm_open(struct inode *inode, struct file *file)
        }
        intf->needs_remote_wakeup = 1;
 
-       mutex_lock(&desc->lock);
+       /* using write lock to protect desc->count */
+       mutex_lock(&desc->wlock);
        if (!desc->count++) {
                desc->werr = 0;
                desc->rerr = 0;
@@ -544,7 +563,7 @@ static int wdm_open(struct inode *inode, struct file *file)
        } else {
                rv = 0;
        }
-       mutex_unlock(&desc->lock);
+       mutex_unlock(&desc->wlock);
        usb_autopm_put_interface(desc->intf);
 out:
        mutex_unlock(&wdm_mutex);
@@ -556,9 +575,11 @@ static int wdm_release(struct inode *inode, struct file *file)
        struct wdm_device *desc = file->private_data;
 
        mutex_lock(&wdm_mutex);
-       mutex_lock(&desc->lock);
+
+       /* using write lock to protect desc->count */
+       mutex_lock(&desc->wlock);
        desc->count--;
-       mutex_unlock(&desc->lock);
+       mutex_unlock(&desc->wlock);
 
        if (!desc->count) {
                dev_dbg(&desc->intf->dev, "wdm_release: cleanup");
@@ -655,7 +676,8 @@ next_desc:
        desc = kzalloc(sizeof(struct wdm_device), GFP_KERNEL);
        if (!desc)
                goto out;
-       mutex_init(&desc->lock);
+       mutex_init(&desc->rlock);
+       mutex_init(&desc->wlock);
        spin_lock_init(&desc->iuspin);
        init_waitqueue_head(&desc->wait);
        desc->wMaxCommand = maxcom;
@@ -771,11 +793,13 @@ static void wdm_disconnect(struct usb_interface *intf)
        /* to terminate pending flushes */
        clear_bit(WDM_IN_USE, &desc->flags);
        spin_unlock_irqrestore(&desc->iuspin, flags);
-       mutex_lock(&desc->lock);
+       wake_up_all(&desc->wait);
+       mutex_lock(&desc->rlock);
+       mutex_lock(&desc->wlock);
        kill_urbs(desc);
        cancel_work_sync(&desc->rxwork);
-       mutex_unlock(&desc->lock);
-       wake_up_all(&desc->wait);
+       mutex_unlock(&desc->wlock);
+       mutex_unlock(&desc->rlock);
        if (!desc->count)
                cleanup(desc);
        mutex_unlock(&wdm_mutex);
@@ -790,8 +814,10 @@ static int wdm_suspend(struct usb_interface *intf, pm_message_t message)
        dev_dbg(&desc->intf->dev, "wdm%d_suspend\n", intf->minor);
 
        /* if this is an autosuspend the caller does the locking */
-       if (!PMSG_IS_AUTO(message))
-               mutex_lock(&desc->lock);
+       if (!PMSG_IS_AUTO(message)) {
+               mutex_lock(&desc->rlock);
+               mutex_lock(&desc->wlock);
+       }
        spin_lock_irq(&desc->iuspin);
 
        if (PMSG_IS_AUTO(message) &&
@@ -807,8 +833,10 @@ static int wdm_suspend(struct usb_interface *intf, pm_message_t message)
                kill_urbs(desc);
                cancel_work_sync(&desc->rxwork);
        }
-       if (!PMSG_IS_AUTO(message))
-               mutex_unlock(&desc->lock);
+       if (!PMSG_IS_AUTO(message)) {
+               mutex_unlock(&desc->wlock);
+               mutex_unlock(&desc->rlock);
+       }
 
        return rv;
 }
@@ -846,9 +874,6 @@ static int wdm_pre_reset(struct usb_interface *intf)
 {
        struct wdm_device *desc = usb_get_intfdata(intf);
 
-       mutex_lock(&desc->lock);
-       kill_urbs(desc);
-
        /*
         * we notify everybody using poll of
         * an exceptional situation
@@ -856,9 +881,16 @@ static int wdm_pre_reset(struct usb_interface *intf)
         * message from the device is lost
         */
        spin_lock_irq(&desc->iuspin);
+       set_bit(WDM_RESETTING, &desc->flags);   /* inform read/write */
+       set_bit(WDM_READ, &desc->flags);        /* unblock read */
+       clear_bit(WDM_IN_USE, &desc->flags);    /* unblock write */
        desc->rerr = -EINTR;
        spin_unlock_irq(&desc->iuspin);
        wake_up_all(&desc->wait);
+       mutex_lock(&desc->rlock);
+       mutex_lock(&desc->wlock);
+       kill_urbs(desc);
+       cancel_work_sync(&desc->rxwork);
        return 0;
 }
 
@@ -867,8 +899,10 @@ static int wdm_post_reset(struct usb_interface *intf)
        struct wdm_device *desc = usb_get_intfdata(intf);
        int rv;
 
+       clear_bit(WDM_RESETTING, &desc->flags);
        rv = recover_from_urb_loss(desc);
-       mutex_unlock(&desc->lock);
+       mutex_unlock(&desc->wlock);
+       mutex_unlock(&desc->rlock);
        return 0;
 }