[PATCH v2 2/3] usb: core: Close anchor capture-to-increment race in URB giveback
From: Hongyu Xie
Date: Mon Nov 02 2026 - 21:21:36 EST
__usb_hcd_giveback_urb() captures urb->anchor into a local
variable at the top of the function, but increments the anchor's
suspend_wakeups counter only after unmapping the URB for DMA and
running the usbmon hooks. Nothing pins the anchor during that
window. If the URB is concurrently unanchored by another thread --
a driver calling usb_unanchor_urb() on its own in-flight URB, or
usb_get_from_anchor() or usb_scuttle_anchored_urbs() taking it --
the anchor list can become empty while suspend_wakeups is still
zero. A usb_wait_anchor_empty_timeout() waiter then observes the
condition true, returns, and the driver frees the anchor (anchors
are typically embedded in driver-private structures); the giveback's
atomic_inc() on the captured pointer, and the later decrement and
wake_up() in usb_anchor_resume_wakeups(), operate on freed memory.
The previous fix ("usb: core: Fix use-after-free in anchor wakeup
protocol") made both the waiter's condition check and the wakeup
side hold anchor->lock, but its guarantee was deliberately scoped
to givebacks that had already incremented suspend_wakeups: the
capture-to-increment gap remained open.
Fold the pointer capture, the increment and the unanchor into a
single anchor->lock critical section which re-verifies that
urb->anchor still names the captured anchor, so a giveback that
lost the race to a concurrent unanchor performs no anchor work at
all. From the verified increment onward, a non-zero
suspend_wakeups keeps waiters from freeing the anchor until
usb_anchor_resume_wakeups() has finished with it.
What remains open is the few instructions between reading
urb->anchor and acquiring anchor->lock inside the new helper: a
concurrent unanchor plus waiter return plus anchor free would all
have to fit into that gap for the lock acquisition itself to touch
freed memory. This is the same residual exposure
usb_unanchor_urb() has had since usb_anchor was introduced; closing
it entirely would require a lifetime protocol change (reference
count or RCU) for anchors, which is out of scope for a bugfix.
No syzbot report has been attributed to this interleaving; it was
found by extending the analysis of the previous fix.
Fixes: 6ec4147e7bdb ("usb-anchor: Delay usb_wait_anchor_empty_timeout wake up till completion is done")
Cc: stable@xxxxxxxxxxxxxxx
Cc: Alan Stern <stern@xxxxxxxxxxxxxxxxxxx>
Cc: Oliver Neukum <oliver@xxxxxxxxxx>
Cc: Hans de Goede <hansg@xxxxxxxxxx>
Signed-off-by: Hongyu Xie <xiehongyu1@xxxxxxxxxx>
---
drivers/usb/core/hcd.c | 10 ++++++---
drivers/usb/core/urb.c | 48 ++++++++++++++++++++++++++++++++++++++++++
drivers/usb/core/usb.h | 1 +
3 files changed, 56 insertions(+), 3 deletions(-)
diff --git a/drivers/usb/core/hcd.c b/drivers/usb/core/hcd.c
index 8e8a5f59b319..f221425fec0a 100644
--- a/drivers/usb/core/hcd.c
+++ b/drivers/usb/core/hcd.c
@@ -1630,7 +1630,7 @@ int usb_hcd_unlink_urb (struct urb *urb, int status)
static void __usb_hcd_giveback_urb(struct urb *urb)
{
struct usb_hcd *hcd = bus_to_hcd(urb->dev->bus);
- struct usb_anchor *anchor = urb->anchor;
+ struct usb_anchor *anchor;
int status = urb->unlinked;
urb->hcpriv = NULL;
@@ -1641,8 +1641,12 @@ static void __usb_hcd_giveback_urb(struct urb *urb)
unmap_urb_for_dma(hcd, urb);
usbmon_urb_complete(&hcd->self, urb, status);
- usb_anchor_suspend_wakeups(anchor);
- usb_unanchor_urb(urb);
+ /*
+ * Capturing the anchor, suspending its wakeups and unanchoring
+ * must be atomic with each other; see
+ * usb_unanchor_urb_and_suspend_wakeups().
+ */
+ anchor = usb_unanchor_urb_and_suspend_wakeups(urb);
if (likely(status == 0))
usb_led_activity(USB_LED_EVENT_HOST);
diff --git a/drivers/usb/core/urb.c b/drivers/usb/core/urb.c
index 3afd184c9b32..22312892e528 100644
--- a/drivers/usb/core/urb.c
+++ b/drivers/usb/core/urb.c
@@ -14,6 +14,8 @@
#include <linux/usb/hcd.h>
#include <linux/scatterlist.h>
+#include "usb.h"
+
#define to_urb(d) container_of(d, struct urb, kref)
@@ -205,6 +207,52 @@ void usb_unanchor_urb(struct urb *urb)
}
EXPORT_SYMBOL_GPL(usb_unanchor_urb);
+/*
+ * usb_unanchor_urb_and_suspend_wakeups - atomically unanchor @urb and
+ * increment the suspend_wakeups counter of its anchor.
+ * @urb: the URB to unanchor
+ *
+ * Used by the URB giveback path, which must capture urb->anchor and
+ * increment anchor->suspend_wakeups atomically with the removal of
+ * @urb from anchor->urb_list. Otherwise a concurrent unanchor of
+ * @urb (a driver calling usb_unanchor_urb() on its own in-flight URB,
+ * or usb_get_from_anchor() or usb_scuttle_anchored_urbs() taking it)
+ * can leave the anchor with an empty urb_list while suspend_wakeups
+ * is still zero, so a usb_wait_anchor_empty_timeout() waiter observes
+ * the wait condition true, returns, and the driver may free the
+ * anchor before the giveback gets to touch it.
+ *
+ * The verification of urb->anchor under anchor->lock makes this safe
+ * against racing re-anchoring, because "urb->anchor == anchor" and
+ * "@urb is on anchor->urb_list" are changed together only while
+ * holding anchor->lock.
+ *
+ * Return: the anchor, which the caller must later pass to
+ * usb_anchor_resume_wakeups(), or NULL if @urb is not anchored
+ * anymore (a concurrent unanchor won the race).
+ */
+struct usb_anchor *usb_unanchor_urb_and_suspend_wakeups(struct urb *urb)
+{
+ struct usb_anchor *anchor;
+ unsigned long flags;
+
+ anchor = urb->anchor;
+ if (!anchor)
+ return NULL;
+
+ spin_lock_irqsave(&anchor->lock, flags);
+ if (likely(anchor == urb->anchor)) {
+ atomic_inc(&anchor->suspend_wakeups);
+ __usb_unanchor_urb(urb, anchor);
+ spin_unlock_irqrestore(&anchor->lock, flags);
+ } else {
+ spin_unlock_irqrestore(&anchor->lock, flags);
+ anchor = NULL;
+ }
+
+ return anchor;
+}
+
/*-------------------------------------------------------------------*/
static const int pipetypes[4] = {
diff --git a/drivers/usb/core/usb.h b/drivers/usb/core/usb.h
index a9b37aeb515b..362bfda497cc 100644
--- a/drivers/usb/core/usb.h
+++ b/drivers/usb/core/usb.h
@@ -20,6 +20,7 @@ extern int usb_create_ep_devs(struct device *parent,
struct usb_host_endpoint *endpoint,
struct usb_device *udev);
extern void usb_remove_ep_devs(struct usb_host_endpoint *endpoint);
+extern struct usb_anchor *usb_unanchor_urb_and_suspend_wakeups(struct urb *urb);
extern void usb_enable_endpoint(struct usb_device *dev,
struct usb_host_endpoint *ep, bool reset_toggle);
--
2.32.0