* [PATCH v2] usb: xhci: Fix use-after-free race in xhci_stop_device()
@ 2026-10-26 12:36 YuanZhe Liu
2026-10-30 6:01 ` YuanZhe Liu
2026-11-03 14:56 ` Mathias Nyman
0 siblings, 2 replies; 6+ messages in thread
From: YuanZhe Liu @ 2026-10-26 12:36 UTC (permalink / raw)
To: Mathias Nyman, Greg Kroah-Hartman
Cc: linux-usb, linux-kernel, stable, Bin Yu, Cheng Wen
A KASAN slab-use-after-free read was observed in xhci_stop_device():
BUG: KASAN: slab-use-after-free in xhci_stop_device.constprop.0.isra.0+0x395/0x430
drivers/usb/host/xhci-hub.c:479
Read of size 8 at addr ffff88804e85d2f0
Freed by task 585700:
kfree+0xf8/0x390 mm/slub.c:6566
xhci_free_virt_device+0x3f/0x50 drivers/usb/host/xhci-mem.c:881
xhci_free_dev+0x286/0x350 drivers/usb/host/xhci.c:4131
usb_disconnect+0x6b6/0x910 drivers/usb/core/hub.c:2391
...
usbdev_ioctl+0x6f05/0x86d0 drivers/usb/core/devio.c:2826
Allocated by task 572283:
xhci_alloc_virt_device+0x171/0xb70 drivers/usb/host/xhci-mem.c:986
xhci_alloc_dev+0x475/0x840 drivers/usb/host/xhci.c:4271
usb_alloc_dev+0xe8/0xdc0 drivers/usb/core/usb.c:661
hub_event+0x1c82/0x4050 drivers/usb/core/hub.c:5953
xhci_stop_device() reads xhci->devs[slot_id] without xhci->lock and
keeps using the pointer across xhci_alloc_command(GFP_NOIO), which may
sleep. All callers (the port suspend and bus suspend paths in
xhci_hub_control() and xhci_bus_suspend()) drop xhci->lock before
calling it, so a concurrent usb_disconnect() -> xhci_free_dev() can
clear xhci->devs[slot_id] and free virt_dev under xhci->lock in that
window. The endpoint loop then dereferences the freed virt_dev.
commit 88716a93766b ("usb: xhci: Fix panic if disconnect") added a
NULL check for this case, but that only helps when the device was
already freed before the check; it does not close the window.
Re-check xhci->devs[slot_id] against the pointer fetched earlier after
taking xhci->lock and bail out if it no longer matches, or if the
device is already being torn down. The tracepoint reloads
virt_dev->udev, so it moves under the lock and only runs while the
device is still attached.
xhci_free_dev() clears virt_dev->udev without xhci->lock, and only
later detaches the device from xhci->devs[] in xhci_free_virt_device(),
also under the lock. Take xhci->lock when clearing ->udev so that
xhci_stop_device() can rely on it while holding the same lock.
xhci_free_virt_device() frees the device under xhci->lock, so a
matching pointer while holding the lock keeps virt_dev alive until
xhci_stop_device() drops the lock, after which virt_dev is not used
anymore.
Fixes: be88fe4f4dda93e3 ("USB: xHCI: port power management implementation")
Cc: stable@vger.kernel.org
Co-developed-by: Bin Yu <byu@xidian.edu.cn>
Signed-off-by: Bin Yu <byu@xidian.edu.cn>
Co-developed-by: Cheng Wen <wencheng@xidian.edu.cn>
Signed-off-by: Cheng Wen <wencheng@xidian.edu.cn>
Signed-off-by: YuanZhe Liu <25031212351@stu.xidian.edu.cn>
---
Changes since v1:
- xhci_free_dev(): clear virt_dev->udev under xhci->lock, so that
checking it under the same lock actually synchronizes with the
store.
- xhci_stop_device(): bail out with -ENODEV once ->udev has been
cleared, instead of queueing stop-endpoint commands for a slot
that is already being disabled.
drivers/usb/host/xhci-hub.c | 19 +++++++++++++++++--
drivers/usb/host/xhci.c | 3 +++
2 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/drivers/usb/host/xhci-hub.c b/drivers/usb/host/xhci-hub.c
index b0264bd8577a..f4401f2fcea0 100644
--- a/drivers/usb/host/xhci-hub.c
+++ b/drivers/usb/host/xhci-hub.c
@@ -467,13 +467,28 @@ static int xhci_stop_device(struct xhci_hcd *xhci, int slot_id, int suspend)
if (!virt_dev)
return -ENODEV;
- trace_xhci_stop_device(virt_dev);
-
cmd = xhci_alloc_command(xhci, true, GFP_NOIO);
if (!cmd)
return -ENOMEM;
spin_lock_irqsave(&xhci->lock, flags);
+
+ /*
+ * Callers drop xhci->lock before calling us and allocating cmd
+ * above can sleep, so xhci_free_dev() may have detached and freed
+ * the device in the meantime. It removes the device from
+ * xhci->devs[slot_id] and clears ->udev under xhci->lock, so while
+ * the lock is held a matching, still-attached device stays alive
+ * and keeps its udev.
+ */
+ if (xhci->devs[slot_id] != virt_dev || !virt_dev->udev) {
+ spin_unlock_irqrestore(&xhci->lock, flags);
+ xhci_free_command(xhci, cmd);
+ return -ENODEV;
+ }
+
+ trace_xhci_stop_device(virt_dev);
+
for (i = LAST_EP_INDEX; i > 0; i--) {
if (virt_dev->eps[i].ring && virt_dev->eps[i].ring->dequeue) {
struct xhci_ep_ctx *ep_ctx;
diff --git a/drivers/usb/host/xhci.c b/drivers/usb/host/xhci.c
index 5c759ad28199..9e375eb48f38 100644
--- a/drivers/usb/host/xhci.c
+++ b/drivers/usb/host/xhci.c
@@ -4128,9 +4128,12 @@ static void xhci_free_dev(struct usb_hcd *hcd, struct usb_device *udev)
trace_xhci_free_dev(slot_ctx);
/* Stop any wayward timer functions (which may grab the lock) */
+ spin_lock_irqsave(&xhci->lock, flags);
for (i = 0; i < 31; i++)
virt_dev->eps[i].ep_state &= ~EP_STOP_CMD_PENDING;
virt_dev->udev = NULL;
+ spin_unlock_irqrestore(&xhci->lock, flags);
+
xhci_disable_slot(xhci, udev->slot_id);
spin_lock_irqsave(&xhci->lock, flags);
--
2.34.1
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH v2] usb: xhci: Fix use-after-free race in xhci_stop_device()
2026-10-26 12:36 [PATCH v2] usb: xhci: Fix use-after-free race in xhci_stop_device() YuanZhe Liu
@ 2026-10-30 6:01 ` YuanZhe Liu
2026-11-03 14:56 ` Mathias Nyman
1 sibling, 0 replies; 6+ messages in thread
From: YuanZhe Liu @ 2026-10-30 6:01 UTC (permalink / raw)
To: Mathias Nyman, Greg Kroah-Hartman, Mathias Nyman
Cc: linux-usb, linux-kernel, stable, Bin Yu, Cheng Wen
Hi Mathias,
Could you please review this patch?
[PATCH v2] usb: xhci: Fix use-after-free race in xhci_stop_device()
It synchronizes access to the virtual device and its USB device pointer
with xhci->lock to avoid a disconnect race in xhci_stop_device().
The patch is unchanged. I am copying this address for visibility.
Original submission:
https://lore.kernel.org/r/20261026123628.7493-1-25031212351@stu.xidian.edu.cn
Thanks,
YuanZhe
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH v2] usb: xhci: Fix use-after-free race in xhci_stop_device()
2026-10-26 12:36 [PATCH v2] usb: xhci: Fix use-after-free race in xhci_stop_device() YuanZhe Liu
2026-10-30 6:01 ` YuanZhe Liu
@ 2026-11-03 14:56 ` Mathias Nyman
2026-11-03 16:01 ` [PATCH v3] " YuanZhe Liu
1 sibling, 1 reply; 6+ messages in thread
From: Mathias Nyman @ 2026-11-03 14:56 UTC (permalink / raw)
To: YuanZhe Liu, Mathias Nyman, Greg Kroah-Hartman
Cc: linux-usb, linux-kernel, stable, Bin Yu, Cheng Wen
Hi
On 10/26/26 14:36, YuanZhe Liu wrote:
> A KASAN slab-use-after-free read was observed in xhci_stop_device():
>
> BUG: KASAN: slab-use-after-free in xhci_stop_device.constprop.0.isra.0+0x395/0x430
> drivers/usb/host/xhci-hub.c:479
> Read of size 8 at addr ffff88804e85d2f0
>
> Freed by task 585700:
> kfree+0xf8/0x390 mm/slub.c:6566
> xhci_free_virt_device+0x3f/0x50 drivers/usb/host/xhci-mem.c:881
> xhci_free_dev+0x286/0x350 drivers/usb/host/xhci.c:4131
> usb_disconnect+0x6b6/0x910 drivers/usb/core/hub.c:2391
> ...
> usbdev_ioctl+0x6f05/0x86d0 drivers/usb/core/devio.c:2826
>
> Allocated by task 572283:
> xhci_alloc_virt_device+0x171/0xb70 drivers/usb/host/xhci-mem.c:986
> xhci_alloc_dev+0x475/0x840 drivers/usb/host/xhci.c:4271
> usb_alloc_dev+0xe8/0xdc0 drivers/usb/core/usb.c:661
> hub_event+0x1c82/0x4050 drivers/usb/core/hub.c:5953
>
> xhci_stop_device() reads xhci->devs[slot_id] without xhci->lock and
> keeps using the pointer across xhci_alloc_command(GFP_NOIO), which may
> sleep. All callers (the port suspend and bus suspend paths in
> xhci_hub_control() and xhci_bus_suspend()) drop xhci->lock before
> calling it, so a concurrent usb_disconnect() -> xhci_free_dev() can
> clear xhci->devs[slot_id] and free virt_dev under xhci->lock in that
> window. The endpoint loop then dereferences the freed virt_dev.
>
> commit 88716a93766b ("usb: xhci: Fix panic if disconnect") added a
> NULL check for this case, but that only helps when the device was
> already freed before the check; it does not close the window.
>
> Re-check xhci->devs[slot_id] against the pointer fetched earlier after
> taking xhci->lock and bail out if it no longer matches, or if the
> device is already being torn down. The tracepoint reloads
> virt_dev->udev, so it moves under the lock and only runs while the
> device is still attached.
>
> xhci_free_dev() clears virt_dev->udev without xhci->lock, and only
> later detaches the device from xhci->devs[] in xhci_free_virt_device(),
> also under the lock. Take xhci->lock when clearing ->udev so that
> xhci_stop_device() can rely on it while holding the same lock.
> xhci_free_virt_device() frees the device under xhci->lock, so a
> matching pointer while holding the lock keeps virt_dev alive until
> xhci_stop_device() drops the lock, after which virt_dev is not used
> anymore.
>
> Fixes: be88fe4f4dda93e3 ("USB: xHCI: port power management implementation")
> Cc: stable@vger.kernel.org
> Co-developed-by: Bin Yu <byu@xidian.edu.cn>
> Signed-off-by: Bin Yu <byu@xidian.edu.cn>
> Co-developed-by: Cheng Wen <wencheng@xidian.edu.cn>
> Signed-off-by: Cheng Wen <wencheng@xidian.edu.cn>
> Signed-off-by: YuanZhe Liu <25031212351@stu.xidian.edu.cn>
Thanks for the patch, good catch.
I have some small minor adjustments that would make final code a bit cleaner
> ---
> Changes since v1:
> - xhci_free_dev(): clear virt_dev->udev under xhci->lock, so that
> checking it under the same lock actually synchronizes with the
> store.
> - xhci_stop_device(): bail out with -ENODEV once ->udev has been
> cleared, instead of queueing stop-endpoint commands for a slot
> that is already being disabled.
>
> drivers/usb/host/xhci-hub.c | 19 +++++++++++++++++--
> drivers/usb/host/xhci.c | 3 +++
> 2 files changed, 20 insertions(+), 2 deletions(-)
>
> diff --git a/drivers/usb/host/xhci-hub.c b/drivers/usb/host/xhci-hub.c
> index b0264bd8577a..f4401f2fcea0 100644
> --- a/drivers/usb/host/xhci-hub.c
> +++ b/drivers/usb/host/xhci-hub.c
> @@ -467,13 +467,28 @@ static int xhci_stop_device(struct xhci_hcd *xhci, int slot_id, int suspend)
> if (!virt_dev)
> return -ENODEV;
>
> - trace_xhci_stop_device(virt_dev);
> -
> cmd = xhci_alloc_command(xhci, true, GFP_NOIO);
> if (!cmd)
> return -ENOMEM;
>
> spin_lock_irqsave(&xhci->lock, flags);
> +
> + /*
> + * Callers drop xhci->lock before calling us and allocating cmd
> + * above can sleep, so xhci_free_dev() may have detached and freed
> + * the device in the meantime. It removes the device from
> + * xhci->devs[slot_id] and clears ->udev under xhci->lock, so while
> + * the lock is held a matching, still-attached device stays alive
> + * and keeps its udev.
> + */
Comment above is not needed, can be removed
> + if (xhci->devs[slot_id] != virt_dev || !virt_dev->udev) {
> + spin_unlock_irqrestore(&xhci->lock, flags);
> + xhci_free_command(xhci, cmd);
> + return -ENODEV;
> + }
No point in keeping the 'virt_dev = xhci->devs[slot_id]' before taking the lock.
Just move that part inside the lock as well:
virt_dev = xhci->devs[slot_id];
if (!virt_dev || !virt_dev->udev) {
unlock
free command
return -ENODEV
}
> +
> + trace_xhci_stop_device(virt_dev);
> +
> for (i = LAST_EP_INDEX; i > 0; i--) {
> if (virt_dev->eps[i].ring && virt_dev->eps[i].ring->dequeue) {
> struct xhci_ep_ctx *ep_ctx;
> diff --git a/drivers/usb/host/xhci.c b/drivers/usb/host/xhci.c
> index 5c759ad28199..9e375eb48f38 100644
> --- a/drivers/usb/host/xhci.c
> +++ b/drivers/usb/host/xhci.c
> @@ -4128,9 +4128,12 @@ static void xhci_free_dev(struct usb_hcd *hcd, struct usb_device *udev)
> trace_xhci_free_dev(slot_ctx);
>
> /* Stop any wayward timer functions (which may grab the lock) */
> + spin_lock_irqsave(&xhci->lock, flags);
> for (i = 0; i < 31; i++)
> virt_dev->eps[i].ep_state &= ~EP_STOP_CMD_PENDING;
> virt_dev->udev = NULL;
> + spin_unlock_irqrestore(&xhci->lock, flags);
> +
Should work, we are now using virt_dev->udev as a flag to tell xhci_stop_device() to
not queue any stop endpoint commands as we know a 'disable slot' command will be
queued shortly.
Another option would be to add an actual 'disable_slot_pending' flag to struct
xhci_virt_device, and set it in xhci_disable_slot() when we really are queuing the
disable slot command with lock held.
While looking at this I realize that xhci_disable_and_free_slot() might also race
with xhci_stop_device(), but that change doesn't need to be part of this patch.
Thanks
Mathias
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH v3] usb: xhci: Fix use-after-free race in xhci_stop_device()
2026-11-03 14:56 ` Mathias Nyman
@ 2026-11-03 16:01 ` YuanZhe Liu
2026-11-03 23:00 ` Michal Pecio
0 siblings, 1 reply; 6+ messages in thread
From: YuanZhe Liu @ 2026-11-03 16:01 UTC (permalink / raw)
To: Mathias Nyman, Greg Kroah-Hartman
Cc: linux-usb, linux-kernel, stable, Bin Yu, Cheng Wen
A KASAN slab-use-after-free read was observed in xhci_stop_device():
BUG: KASAN: slab-use-after-free in xhci_stop_device.constprop.0.isra.0+0x395/0x430
drivers/usb/host/xhci-hub.c:479
Read of size 8 at addr ffff88804e85d2f0
Freed by task 585700:
kfree+0xf8/0x390 mm/slub.c:6566
xhci_free_virt_device+0x3f/0x50 drivers/usb/host/xhci-mem.c:881
xhci_free_dev+0x286/0x350 drivers/usb/host/xhci.c:4131
usb_disconnect+0x6b6/0x910 drivers/usb/core/hub.c:2391
...
usbdev_ioctl+0x6f05/0x86d0 drivers/usb/core/devio.c:2826
Allocated by task 572283:
xhci_alloc_virt_device+0x171/0xb70 drivers/usb/host/xhci-mem.c:986
xhci_alloc_dev+0x475/0x840 drivers/usb/host/xhci.c:4271
usb_alloc_dev+0xe8/0xdc0 drivers/usb/core/usb.c:661
hub_event+0x1c82/0x4050 drivers/usb/core/hub.c:5953
xhci_stop_device() reads xhci->devs[slot_id] without xhci->lock and
keeps using the pointer across xhci_alloc_command(GFP_NOIO), which may
sleep. All callers (the port suspend and bus suspend paths in
xhci_hub_control() and xhci_bus_suspend()) drop xhci->lock before
calling it, so a concurrent usb_disconnect() -> xhci_free_dev() can
clear xhci->devs[slot_id] and free virt_dev under xhci->lock in that
window. The endpoint loop then dereferences the freed virt_dev.
commit 88716a93766b ("usb: xhci: Fix panic if disconnect") added a
NULL check for this case, but that only helps when the device was
already freed before the check; it does not close the window.
Look up xhci->devs[slot_id] only after taking xhci->lock, and bail out
if it is NULL or if the device is already being torn down. The
tracepoint reloads virt_dev->udev, so it moves under the lock and only
runs while the device is still attached.
xhci_free_dev() clears virt_dev->udev without xhci->lock, and only
later detaches the device from xhci->devs[] in xhci_free_virt_device(),
also under the lock. Take xhci->lock when clearing ->udev so that
xhci_stop_device() can rely on it while holding the same lock.
xhci_free_virt_device() clears xhci->devs[slot_id] and frees the device
under xhci->lock, so a device found there while holding the lock stays
alive until xhci_stop_device() drops the lock, after which virt_dev is
not used anymore.
Fixes: be88fe4f4dda93e3 ("USB: xHCI: port power management implementation")
Cc: stable@vger.kernel.org
Co-developed-by: Bin Yu <byu@xidian.edu.cn>
Signed-off-by: Bin Yu <byu@xidian.edu.cn>
Co-developed-by: Cheng Wen <wencheng@xidian.edu.cn>
Signed-off-by: Cheng Wen <wencheng@xidian.edu.cn>
Signed-off-by: YuanZhe Liu <25031212351@stu.xidian.edu.cn>
---
Hi Mathias,
Thanks for the review. Both points are addressed:
Changes since v2:
- Moved the xhci->devs[slot_id] lookup inside xhci->lock and dropped
the separate pointer comparison, so the unlocked read before the
lock is gone.
- Dropped the explanatory comment.
I kept virt_dev->udev as the marker for "teardown already started"
instead of adding a disable_slot_pending flag. It is now written
under xhci->lock, so reading it there is enough to tell
xhci_stop_device() not to queue stop endpoint commands, and it keeps
the change small.
The xhci_disable_and_free_slot() race is left for a separate patch.
drivers/usb/host/xhci-hub.c | 14 +++++++++-----
drivers/usb/host/xhci.c | 3 +++
2 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/drivers/usb/host/xhci-hub.c b/drivers/usb/host/xhci-hub.c
index b0264bd8577a..9a3437248e04 100644
--- a/drivers/usb/host/xhci-hub.c
+++ b/drivers/usb/host/xhci-hub.c
@@ -463,17 +463,21 @@ static int xhci_stop_device(struct xhci_hcd *xhci, int slot_id, int suspend)
int i;
ret = 0;
- virt_dev = xhci->devs[slot_id];
- if (!virt_dev)
- return -ENODEV;
-
- trace_xhci_stop_device(virt_dev);
cmd = xhci_alloc_command(xhci, true, GFP_NOIO);
if (!cmd)
return -ENOMEM;
spin_lock_irqsave(&xhci->lock, flags);
+ virt_dev = xhci->devs[slot_id];
+ if (!virt_dev || !virt_dev->udev) {
+ spin_unlock_irqrestore(&xhci->lock, flags);
+ xhci_free_command(xhci, cmd);
+ return -ENODEV;
+ }
+
+ trace_xhci_stop_device(virt_dev);
+
for (i = LAST_EP_INDEX; i > 0; i--) {
if (virt_dev->eps[i].ring && virt_dev->eps[i].ring->dequeue) {
struct xhci_ep_ctx *ep_ctx;
diff --git a/drivers/usb/host/xhci.c b/drivers/usb/host/xhci.c
index 5c759ad28199..9e375eb48f38 100644
--- a/drivers/usb/host/xhci.c
+++ b/drivers/usb/host/xhci.c
@@ -4128,9 +4128,12 @@ static void xhci_free_dev(struct usb_hcd *hcd, struct usb_device *udev)
trace_xhci_free_dev(slot_ctx);
/* Stop any wayward timer functions (which may grab the lock) */
+ spin_lock_irqsave(&xhci->lock, flags);
for (i = 0; i < 31; i++)
virt_dev->eps[i].ep_state &= ~EP_STOP_CMD_PENDING;
virt_dev->udev = NULL;
+ spin_unlock_irqrestore(&xhci->lock, flags);
+
xhci_disable_slot(xhci, udev->slot_id);
spin_lock_irqsave(&xhci->lock, flags);
--
2.34.1
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH v3] usb: xhci: Fix use-after-free race in xhci_stop_device()
2026-11-03 16:01 ` [PATCH v3] " YuanZhe Liu
@ 2026-11-03 23:00 ` Michal Pecio
2026-11-04 1:32 ` [PATCH v4] " YuanZhe Liu
0 siblings, 1 reply; 6+ messages in thread
From: Michal Pecio @ 2026-11-03 23:00 UTC (permalink / raw)
To: YuanZhe Liu
Cc: Mathias Nyman, Greg Kroah-Hartman, linux-usb, linux-kernel,
stable, Bin Yu, Cheng Wen
On Wed, 4 Nov 2026 00:01:00 +0800, YuanZhe Liu wrote:
> A KASAN slab-use-after-free read was observed in xhci_stop_device():
>
> BUG: KASAN: slab-use-after-free in xhci_stop_device.constprop.0.isra.0+0x395/0x430
> drivers/usb/host/xhci-hub.c:479
> Read of size 8 at addr ffff88804e85d2f0
>
> Freed by task 585700:
> kfree+0xf8/0x390 mm/slub.c:6566
> xhci_free_virt_device+0x3f/0x50 drivers/usb/host/xhci-mem.c:881
> xhci_free_dev+0x286/0x350 drivers/usb/host/xhci.c:4131
> usb_disconnect+0x6b6/0x910 drivers/usb/core/hub.c:2391
> ...
> usbdev_ioctl+0x6f05/0x86d0 drivers/usb/core/devio.c:2826
>
> Allocated by task 572283:
> xhci_alloc_virt_device+0x171/0xb70 drivers/usb/host/xhci-mem.c:986
> xhci_alloc_dev+0x475/0x840 drivers/usb/host/xhci.c:4271
> usb_alloc_dev+0xe8/0xdc0 drivers/usb/core/usb.c:661
> hub_event+0x1c82/0x4050 drivers/usb/core/hub.c:5953
>
> xhci_stop_device() reads xhci->devs[slot_id] without xhci->lock and
> keeps using the pointer across xhci_alloc_command(GFP_NOIO), which may
> sleep. All callers (the port suspend and bus suspend paths in
> xhci_hub_control() and xhci_bus_suspend()) drop xhci->lock before
> calling it, so a concurrent usb_disconnect() -> xhci_free_dev() can
> clear xhci->devs[slot_id] and free virt_dev under xhci->lock in that
> window. The endpoint loop then dereferences the freed virt_dev.
Hi,
Good job crashing a real driver and not just dummy-hcd as usual.
But is it not a bug in the first place that we are suspending a device
which is being deleted or deleting one which is being suspended?
Would you mind revealing your secret sauce and posting complete stack
traces of how you got there, including xhci_stop_device()? What kernel
version was that? Those line numbers don't match current code.
As you are .edu, perhaps you have a reproducer for this bug?
> commit 88716a93766b ("usb: xhci: Fix panic if disconnect") added a
> NULL check for this case, but that only helps when the device was
> already freed before the check; it does not close the window.
That's an old bug, not sure how such things can still be happening?
It seems that xhci_free_dev() doesn't return until everything is
cleared, and I would naively expect USB core not to suspend the bus
until all devices are suspended or finished disconnecting.
In testing, I see xhci_bus_suspend() 100 ms after disconnection, so
this race seems tricky to trigger. How have you done it?
Regards,
Michal
> Look up xhci->devs[slot_id] only after taking xhci->lock, and bail out
> if it is NULL or if the device is already being torn down. The
> tracepoint reloads virt_dev->udev, so it moves under the lock and only
> runs while the device is still attached.
>
> xhci_free_dev() clears virt_dev->udev without xhci->lock, and only
> later detaches the device from xhci->devs[] in xhci_free_virt_device(),
> also under the lock. Take xhci->lock when clearing ->udev so that
> xhci_stop_device() can rely on it while holding the same lock.
> xhci_free_virt_device() clears xhci->devs[slot_id] and frees the device
> under xhci->lock, so a device found there while holding the lock stays
> alive until xhci_stop_device() drops the lock, after which virt_dev is
> not used anymore.
>
> Fixes: be88fe4f4dda93e3 ("USB: xHCI: port power management implementation")
> Cc: stable@vger.kernel.org
> Co-developed-by: Bin Yu <byu@xidian.edu.cn>
> Signed-off-by: Bin Yu <byu@xidian.edu.cn>
> Co-developed-by: Cheng Wen <wencheng@xidian.edu.cn>
> Signed-off-by: Cheng Wen <wencheng@xidian.edu.cn>
> Signed-off-by: YuanZhe Liu <25031212351@stu.xidian.edu.cn>
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH v4] usb: xhci: Fix use-after-free race in xhci_stop_device()
2026-11-03 23:00 ` Michal Pecio
@ 2026-11-04 1:32 ` YuanZhe Liu
0 siblings, 0 replies; 6+ messages in thread
From: YuanZhe Liu @ 2026-11-04 1:32 UTC (permalink / raw)
To: Michal Pecio
Cc: Mathias Nyman, Greg Kroah-Hartman, linux-usb, linux-kernel,
stable, Bin Yu, Cheng Wen
A KASAN slab-use-after-free read was observed in xhci_stop_device():
BUG: KASAN: slab-use-after-free in xhci_stop_device.constprop.0.isra.0+0x395/0x430
drivers/usb/host/xhci-hub.c:479
Read of size 8 at addr ffff88804e85d2f0
Freed by task 585700:
kfree+0xf8/0x390 mm/slub.c:6566
xhci_free_virt_device+0x3f/0x50 drivers/usb/host/xhci-mem.c:881
xhci_free_dev+0x286/0x350 drivers/usb/host/xhci.c:4131
usb_disconnect+0x6b6/0x910 drivers/usb/core/hub.c:2391
...
usbdev_ioctl+0x6f05/0x86d0 drivers/usb/core/devio.c:2826
Allocated by task 572283:
xhci_alloc_virt_device+0x171/0xb70 drivers/usb/host/xhci-mem.c:986
xhci_alloc_dev+0x475/0x840 drivers/usb/host/xhci.c:4271
usb_alloc_dev+0xe8/0xdc0 drivers/usb/core/usb.c:661
hub_event+0x1c82/0x4050 drivers/usb/core/hub.c:5953
xhci_stop_device() reads xhci->devs[slot_id] without xhci->lock and
keeps using the pointer across xhci_alloc_command(GFP_NOIO), which may
sleep. All callers drop xhci->lock before calling it. Both sides of
the race can come from userspace through usbfs: a hub class
CLEAR_FEATURE(PORT_SUSPEND) request to the root hub reaches
xhci_stop_device() through xhci_hub_control(), while releasing the hub
driver interface through USBDEVFS_IOCTL() quiesces the hub and
disconnects the devices below it, ending in xhci_free_dev(). USB core
does not serialize those two, and xhci_free_dev() frees virt_dev under
xhci->lock, so the endpoint loop can dereference a freed device.
commit 88716a93766b ("usb: xhci: Fix panic if disconnect") added a
NULL check for this case, but that only helps when the device was
already freed before the check; it does not close the window.
Look up xhci->devs[slot_id] only after taking xhci->lock, and bail out
if it is NULL or if the device is already being torn down. The
tracepoint reloads virt_dev->udev, so it moves under the lock and only
runs while the device is still attached.
xhci_free_dev() clears virt_dev->udev without xhci->lock, and only
later detaches the device from xhci->devs[] in xhci_free_virt_device(),
also under the lock. Take xhci->lock when clearing ->udev so that
xhci_stop_device() can rely on it while holding the same lock.
xhci_free_virt_device() clears xhci->devs[slot_id] and frees the device
under xhci->lock, so a device found there while holding the lock stays
alive until xhci_stop_device() drops the lock, after which virt_dev is
not used anymore.
Fixes: be88fe4f4dda93e3 ("USB: xHCI: port power management implementation")
Cc: stable@vger.kernel.org
Co-developed-by: Bin Yu <byu@xidian.edu.cn>
Signed-off-by: Bin Yu <byu@xidian.edu.cn>
Co-developed-by: Cheng Wen <wencheng@xidian.edu.cn>
Signed-off-by: Cheng Wen <wencheng@xidian.edu.cn>
Signed-off-by: YuanZhe Liu <25031212351@stu.xidian.edu.cn>
---
Hi Michal,
Thanks for looking at this. Full answers below. The code is unchanged
since v3; what changed is the commit message, which now says how the two
sides of the race reach each other.
> But is it not a bug in the first place that we are suspending a device
> which is being deleted or deleting one which is being suspended?
Yes, and that is exactly the interleaving. What makes it possible is
that the suspend side does not come from the USB core suspend path at
all:
CPU0 (control request to the root hub) CPU1 (disconnect_claim, hub)
-------------------------------------- ---------------------------
usbdev_ioctl(USBDEVFS_CONTROL) usbdev_ioctl(USBDEVFS_IOCTL)
do_proc_control proc_ioctl
usbfs_start_wait_urb usb_driver_release_interface
usb_submit_urb usb_unbind_interface
rh_call_control hub_disconnect
xhci_hub_control hub_quiesce
CLEAR_FEATURE(PORT_SUSPEND) usb_disconnect(child)
xhci_stop_device(slot, 1) hub_free_dev
xhci_free_dev
xhci_free_virt_device
kfree(virt_dev)
The port suspend is a hub class CLEAR_FEATURE(PORT_SUSPEND) sent from
userspace through usbfs, and the teardown is userspace releasing the
hub driver interface, which quiesces the hub and disconnects every
child under it. Neither side takes a per-device reference, and the
usbfs control request is not serialized against the hub driver being
unbound. That is also why xhci_bus_suspend() 100 ms after a disconnect
looks fine: this is a different entry into xhci_stop_device().
> Would you mind revealing your secret sauce and posting complete stack
> traces of how you got there, including xhci_stop_device()? What kernel
> version was that? Those line numbers don't match current code.
Fair question, and I should have been upfront about it in the commit
message. This was found by syzkaller running against the xHCI on QEMU,
on 7.1.2; the patch itself is against 7.2.10, which is why the line
numbers in the trace do not line up with current code.
The report, trimmed to the stacks and the dmesg lines just above them:
xhci_hcd 0000:00:04.0: USB core suspending port 1-3 not in U0/U1/U2
usb usb2: usbfs: process 585700 (syz.6.213219) did not claim interface 0 before use
usb 1-1: USB disconnect, device number 108
BUG: KASAN: slab-use-after-free in xhci_stop_device.constprop.0.isra.0+0x395/0x430
Read of size 8 at addr ffff88804e85d2f0 by task syz.4.213206/585661
Call Trace:
<TASK>
__dump_stack lib/dump_stack.c:94 [inline]
dump_stack_lvl+0xa7/0xd0 lib/dump_stack.c:120
print_address_description mm/kasan/report.c:378 [inline]
print_report+0xcb/0x5d0 mm/kasan/report.c:482
? _raw_write_unlock_irq+0x60/0x60
? xhci_stop_device.constprop.0.isra.0+0x395/0x430 drivers/usb/host/xhci-hub.c:479
kasan_report+0xaf/0xe0 mm/kasan/report.c:595
? xhci_stop_device.constprop.0.isra.0+0x395/0x430 drivers/usb/host/xhci-hub.c:479
xhci_stop_device.constprop.0.isra.0+0x395/0x430 drivers/usb/host/xhci-hub.c:479
? instrument_read_write include/linux/instrumented.h:55 [inline]
? atomic_try_cmpxchg_acquire include/linux/atomic/atomic-instrumented.h:1301 [inline]
? queued_spin_lock include/asm-generic/qspinlock.h:111 [inline]
? do_raw_spin_lock include/linux/spinlock.h:187 [inline]
? __raw_spin_lock_irqsave include/linux/spinlock_api_smp.h:133 [inline]
? _raw_spin_lock_irqsave+0x72/0xb0 kernel/locking/spinlock.c:166
xhci_hub_control+0x1b6b/0x5590 drivers/usb/host/xhci-hub.c:1315
? dma_coherent_ok+0xa2/0x280 kernel/dma/direct.c:77
? xhci_set_link_state+0x30/0x30 drivers/usb/host/xhci-hub.c:805
? unpoison_slab_object mm/kasan/common.c:336 [inline]
? __kasan_slab_alloc+0x2c/0x60 mm/kasan/common.c:366
? kasan_save_track+0x10/0x30 mm/kasan/common.c:78
rh_call_control drivers/usb/core/hcd.c:650 [inline]
rh_urb_enqueue drivers/usb/core/hcd.c:815 [inline]
usb_hcd_submit_urb+0x90d/0x1c20 drivers/usb/core/hcd.c:1536
usb_submit_urb+0x86c/0x1740 drivers/usb/core/urb.c:586
usbfs_start_wait_urb+0x10a/0x3a0 drivers/usb/core/devio.c:1149
? kasan_unpoison+0x23/0x50 mm/kasan/shadow.c:178
? usbfs_blocking_completion+0x40/0x40 drivers/usb/core/devio.c:1132
? kasan_unpoison+0x23/0x50 mm/kasan/shadow.c:178
? __mutex_unlock_slowpath.constprop.0+0x360/0x360
do_proc_control drivers/usb/core/devio.c:1249 [inline]
proc_control drivers/usb/core/devio.c:1280 [inline]
usbdev_do_ioctl drivers/usb/core/devio.c:2645 [inline]
usbdev_ioctl+0x33e1/0x86d0 drivers/usb/core/devio.c:2826
? instrument_atomic_write include/linux/instrumented.h:97 [inline]
? set_bit include/asm-generic/bitops/instrumented-atomic.h:28 [inline]
? set_ti_thread_flag include/linux/thread_info.h:104 [inline]
? __resched_curr+0x1e2/0x270 kernel/sched/core.c:1192
? __css_rstat_updated+0x15e/0x470 kernel/cgroup/rstat.c:97
? cpu_cgroup_cancel_attach+0x10/0x10 include/linux/workqueue.h:759
? root_cgroup_cputime+0x3d0/0x3d0 include/linux/find.h:67
? instrument_atomic_read include/linux/instrumented.h:82 [inline]
? _test_bit include/asm-generic/bitops/instrumented-non-atomic.h:141 [inline]
? test_ti_thread_flag include/linux/thread_info.h:133 [inline]
? test_tsk_thread_flag include/linux/sched.h:2069 [inline]
? test_tsk_need_resched include/linux/sched.h:2088 [inline]
? wakeup_preempt+0x1cd/0x330 kernel/sched/core.c:2260
? usbdev_mmap+0xa00/0xa00 drivers/usb/core/devio.c:286
? update_load_avg+0x176/0x1880 kernel/sched/fair.c:4983
? update_load_avg+0x176/0x1880 kernel/sched/fair.c:4983
? set_next_entity+0x1db/0x630 kernel/sched/fair.c:5743
? pick_next_task_fair+0x10da/0x2320 kernel/sched/fair.c:9283
? futex_wake+0x1aa/0x500 kernel/futex/futex.h:240
? futex_wake_mark+0x230/0x230 kernel/futex/waitwake.c:112
? __fget_files+0x1a0/0x250 fs/file.c:1103
? usbdev_mmap+0xa00/0xa00 drivers/usb/core/devio.c:286
vfs_ioctl fs/ioctl.c:51 [inline]
__do_sys_ioctl fs/ioctl.c:597 [inline]
__se_sys_ioctl fs/ioctl.c:583 [inline]
__x64_sys_ioctl+0x877/0x1640 fs/ioctl.c:583
? ioctl_file_clone+0xf0/0xf0 fs/ioctl.c:238
? io_schedule_timeout+0x150/0x150 kernel/sched/core.c:8079
? do_sys_open fs/open.c:1370 [inline]
? __do_sys_openat fs/open.c:1386 [inline]
? __se_sys_openat fs/open.c:1381 [inline]
? __x64_sys_openat+0x12c/0x1d0 fs/open.c:1381
? __schedule_loop kernel/sched/core.c:7268 [inline]
? schedule+0x7e/0x2f0 kernel/sched/core.c:7283
? restore_fpregs_from_fpstate+0x7e/0x100 arch/x86/kernel/fpu/core.c:210
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0xad/0x480 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x4b/0x53
Allocated by task 572283:
kasan_save_stack+0x20/0x40 mm/kasan/common.c:57
kasan_save_track+0x10/0x30 mm/kasan/common.c:78
poison_kmalloc_redzone mm/kasan/common.c:398 [inline]
__kasan_kmalloc+0x77/0x90 mm/kasan/common.c:415
kmalloc_noprof include/linux/slab.h:950 [inline]
kzalloc_noprof include/linux/slab.h:1188 [inline]
xhci_alloc_virt_device+0x171/0xb70 drivers/usb/host/xhci-mem.c:986
xhci_alloc_dev+0x475/0x840 drivers/usb/host/xhci.c:4271
usb_alloc_dev+0xe8/0xdc0 drivers/usb/core/usb.c:661
hub_port_connect drivers/usb/core/hub.c:5470 [inline]
hub_port_connect_change drivers/usb/core/hub.c:5707 [inline]
port_event drivers/usb/core/hub.c:5871 [inline]
hub_event+0x1c82/0x4050 drivers/usb/core/hub.c:5953
process_one_work+0x662/0xdb0 kernel/workqueue.c:3314
process_scheduled_works kernel/workqueue.c:3397 [inline]
worker_thread+0x58b/0xc70 kernel/workqueue.c:3478
kthread+0x394/0x4b0 kernel/kthread.c:436
ret_from_fork+0x37f/0x7b0 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x11/0x20 arch/x86/entry/entry_64.S:245
Freed by task 585700:
kasan_save_stack+0x20/0x40 mm/kasan/common.c:57
kasan_save_track+0x10/0x30 mm/kasan/common.c:78
kasan_save_free_info+0x37/0x50 mm/kasan/generic.c:584
poison_slab_object mm/kasan/common.c:253 [inline]
__kasan_slab_free+0x3b/0x60 mm/kasan/common.c:285
kasan_slab_free include/linux/kasan.h:235 [inline]
slab_free_hook mm/slub.c:2689 [inline]
slab_free mm/slub.c:6251 [inline]
kfree+0xf8/0x390 mm/slub.c:6566
xhci_free_virt_device+0x3f/0x50 drivers/usb/host/xhci-mem.c:881
xhci_free_dev+0x286/0x350 drivers/usb/host/xhci.c:4131
hub_free_dev drivers/usb/core/hub.c:2283 [inline]
usb_disconnect+0x6b6/0x910 drivers/usb/core/hub.c:2391
hub_quiesce+0x159/0x310 drivers/usb/core/hub.c:1405
hub_disconnect+0xd1/0x460 drivers/usb/core/hub.c:1811
usb_unbind_interface+0x188/0x770 drivers/usb/core/driver.c:458
device_remove drivers/base/dd.c:621 [inline]
device_remove+0x11f/0x170 drivers/base/dd.c:613
__device_release_driver drivers/base/dd.c:1352 [inline]
device_release_driver_internal+0x4b6/0x660 drivers/base/dd.c:1375
usb_driver_release_interface+0x105/0x180 drivers/usb/core/driver.c:640
proc_ioctl drivers/usb/core/devio.c:2359 [inline]
proc_ioctl_default drivers/usb/core/devio.c:2402 [inline]
usbdev_do_ioctl drivers/usb/core/devio.c:2766 [inline]
usbdev_ioctl+0x6f05/0x86d0 drivers/usb/core/devio.c:2826
vfs_ioctl fs/ioctl.c:51 [inline]
__do_sys_ioctl fs/ioctl.c:597 [inline]
__se_sys_ioctl fs/ioctl.c:583 [inline]
__x64_sys_ioctl+0x877/0x1640 fs/ioctl.c:583
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0xad/0x480 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x4b/0x53
> As you are .edu, perhaps you have a reproducer for this bug?
No, unfortunately. syzkaller reported the crash but failed to
reproduce it, so there is only the one trace. The new part seems to be
that a usbfs control request to the root hub can reach
xhci_stop_device() while another thread is tearing that hub down.
> That's an old bug, not sure how such things can still be happening?
> It seems that xhci_free_dev() doesn't return until everything is
> cleared, and I would naively expect USB core not to suspend the bus
> until all devices are suspended or finished disconnecting.
>
> In testing, I see xhci_bus_suspend() 100 ms after disconnection, so
> this race seems tricky to trigger. How have you done it?
Agreed for the bus suspend path: usb_disconnect() is long done by then.
xhci_stop_device() is also the per-port suspend helper for the root
hub, and that path is reachable from userspace without any of that
ordering, which is the window above.
The patch makes xhci_stop_device() re-read xhci->devs[] and the "device
is going away" marker under xhci->lock, and give up with -ENODEV
instead of touching a device that xhci_free_dev() has already detached.
For that check to mean anything, xhci_free_dev() has to clear
virt_dev->udev under the same lock, which is the second hunk.
If you think the mutual exclusion belongs higher up instead - do not
let usbfs suspend a port while the hub is being unbound - I am happy to
work on that; from your message it sounds like you would prefer the fix
there.
Thanks,
YuanZhe
Changes since v3:
- Commit message only, no code change: the v3 text described the race
without saying how either side is reached, which made it look like it
had to collide with the system bus suspend.
drivers/usb/host/xhci-hub.c | 14 +++++++++-----
drivers/usb/host/xhci.c | 3 +++
2 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/drivers/usb/host/xhci-hub.c b/drivers/usb/host/xhci-hub.c
index b0264bd8577a..9a3437248e04 100644
--- a/drivers/usb/host/xhci-hub.c
+++ b/drivers/usb/host/xhci-hub.c
@@ -463,17 +463,21 @@ static int xhci_stop_device(struct xhci_hcd *xhci, int slot_id, int suspend)
int i;
ret = 0;
- virt_dev = xhci->devs[slot_id];
- if (!virt_dev)
- return -ENODEV;
-
- trace_xhci_stop_device(virt_dev);
cmd = xhci_alloc_command(xhci, true, GFP_NOIO);
if (!cmd)
return -ENOMEM;
spin_lock_irqsave(&xhci->lock, flags);
+ virt_dev = xhci->devs[slot_id];
+ if (!virt_dev || !virt_dev->udev) {
+ spin_unlock_irqrestore(&xhci->lock, flags);
+ xhci_free_command(xhci, cmd);
+ return -ENODEV;
+ }
+
+ trace_xhci_stop_device(virt_dev);
+
for (i = LAST_EP_INDEX; i > 0; i--) {
if (virt_dev->eps[i].ring && virt_dev->eps[i].ring->dequeue) {
struct xhci_ep_ctx *ep_ctx;
diff --git a/drivers/usb/host/xhci.c b/drivers/usb/host/xhci.c
index 5c759ad28199..9e375eb48f38 100644
--- a/drivers/usb/host/xhci.c
+++ b/drivers/usb/host/xhci.c
@@ -4128,9 +4128,12 @@ static void xhci_free_dev(struct usb_hcd *hcd, struct usb_device *udev)
trace_xhci_free_dev(slot_ctx);
/* Stop any wayward timer functions (which may grab the lock) */
+ spin_lock_irqsave(&xhci->lock, flags);
for (i = 0; i < 31; i++)
virt_dev->eps[i].ep_state &= ~EP_STOP_CMD_PENDING;
virt_dev->udev = NULL;
+ spin_unlock_irqrestore(&xhci->lock, flags);
+
xhci_disable_slot(xhci, udev->slot_id);
spin_lock_irqsave(&xhci->lock, flags);
--
2.34.1
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-11-04 1:32 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-26 12:36 [PATCH v2] usb: xhci: Fix use-after-free race in xhci_stop_device() YuanZhe Liu
2026-10-30 6:01 ` YuanZhe Liu
2026-11-03 14:56 ` Mathias Nyman
2026-11-03 16:01 ` [PATCH v3] " YuanZhe Liu
2026-11-03 23:00 ` Michal Pecio
2026-11-04 1:32 ` [PATCH v4] " YuanZhe Liu
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®