USB: fix race between root-hub wakeup & controller suspend
authorAlan Stern <stern@rowland.harvard.edu>
Fri, 25 Jun 2010 18:02:35 +0000 (14:02 -0400)
committerGreg Kroah-Hartman <gregkh@suse.de>
Tue, 10 Aug 2010 21:35:38 +0000 (14:35 -0700)
This patch (as1395) adds code to hcd_pci_suspend() for handling wakeup
races.  This is another general race pattern, similar to the "open
vs. unregister" race we're all familiar with.  Here, the race is
between suspending a device and receiving a wakeup request from one of
the device's suspended children.

In particular, if a root-hub wakeup is requested at about the same
time as the corresponding USB controller is suspended, and if the
controller is enabled for wakeup, then the controller should either
fail to suspend or else wake right back up again.

During system sleep this won't happen very much, especially since host
controllers generally aren't enabled for wakeup during sleep.  However
it is definitely an issue for runtime PM.  Something like this will be
needed to prevent the controller from autosuspending while waiting for
a root-hub resume to take place.  (That is, in fact, the common case,
for which there is an extra test.)

Signed-off-by: Alan Stern <stern@rowland.harvard.edu>
Signed-off-by: Greg Kroah-Hartman <gregkh@suse.de>
drivers/usb/core/hcd-pci.c
drivers/usb/core/hcd.c
include/linux/usb/hcd.h

index e387e394f8762af6f4c85177379537ced658caa6..352577baa53d8fb7e5a6cde6c6ca9ac1bfe83748 100644 (file)
@@ -388,8 +388,20 @@ static int hcd_pci_suspend(struct device *dev)
        if (hcd->driver->pci_suspend) {
                bool    do_wakeup = device_may_wakeup(dev);
 
+               /* Optimization: Don't suspend if a root-hub wakeup is
+                * pending and it would cause the HCD to wake up anyway.
+                */
+               if (do_wakeup && HCD_WAKEUP_PENDING(hcd))
+                       return -EBUSY;
                retval = hcd->driver->pci_suspend(hcd, do_wakeup);
                suspend_report_result(hcd->driver->pci_suspend, retval);
+
+               /* Check again in case wakeup raced with pci_suspend */
+               if (retval == 0 && do_wakeup && HCD_WAKEUP_PENDING(hcd)) {
+                       if (hcd->driver->pci_resume)
+                               hcd->driver->pci_resume(hcd, false);
+                       retval = -EBUSY;
+               }
                if (retval)
                        return retval;
        }
index f2fe7c8e991d77798b0c2ed3f56dcae0040f5766..0358c05e6e8a1c2dee98a75ee593096fa4e0d0e3 100644 (file)
@@ -1940,6 +1940,7 @@ int hcd_bus_resume(struct usb_device *rhdev, pm_message_t msg)
 
        dev_dbg(&rhdev->dev, "usb %s%s\n",
                        (msg.event & PM_EVENT_AUTO ? "auto-" : ""), "resume");
+       clear_bit(HCD_FLAG_WAKEUP_PENDING, &hcd->flags);
        if (!hcd->driver->bus_resume)
                return -ENOENT;
        if (hcd->state == HC_STATE_RUNNING)
@@ -1993,8 +1994,10 @@ void usb_hcd_resume_root_hub (struct usb_hcd *hcd)
        unsigned long flags;
 
        spin_lock_irqsave (&hcd_root_hub_lock, flags);
-       if (hcd->rh_registered)
+       if (hcd->rh_registered) {
+               set_bit(HCD_FLAG_WAKEUP_PENDING, &hcd->flags);
                queue_work(pm_wq, &hcd->wakeup_work);
+       }
        spin_unlock_irqrestore (&hcd_root_hub_lock, flags);
 }
 EXPORT_SYMBOL_GPL(usb_hcd_resume_root_hub);
index ae10020b4023fdfd2b81f298a5ebb992ba20d4bb..3b571f1ffbb3aee4b80881076d6ca6b0042840d9 100644 (file)
@@ -98,6 +98,7 @@ struct usb_hcd {
 #define HCD_FLAG_SAW_IRQ               1
 #define HCD_FLAG_POLL_RH               2       /* poll for rh status? */
 #define HCD_FLAG_POLL_PENDING          3       /* status has changed? */
+#define HCD_FLAG_WAKEUP_PENDING                4       /* root hub is resuming? */
 
        /* The flags can be tested using these macros; they are likely to
         * be slightly faster than test_bit().
@@ -106,6 +107,7 @@ struct usb_hcd {
 #define HCD_SAW_IRQ(hcd)       ((hcd)->flags & (1U << HCD_FLAG_SAW_IRQ))
 #define HCD_POLL_RH(hcd)       ((hcd)->flags & (1U << HCD_FLAG_POLL_RH))
 #define HCD_POLL_PENDING(hcd)  ((hcd)->flags & (1U << HCD_FLAG_POLL_PENDING))
+#define HCD_WAKEUP_PENDING(hcd)        ((hcd)->flags & (1U << HCD_FLAG_WAKEUP_PENDING))
 
        /* Flags that get set only during HCD registration or removal. */
        unsigned                rh_registered:1;/* is root hub registered? */