Skip to contentSkip to conversation

usb: host: xhci-sideband: fix deadlock in unregister path

RFC PATCH In review Guan-Yu Lin 3 participants 6 messages Jan 30 – Feb 4

Subsystems (3)
lore
Share & tools
lore
#0 lore raw

RFC PATCH 1/1 usb: host: xhci-sideband: fix deadlock in unregister path

When a USB device is disconnected or a driver is unbound, the USB core invokes the driver's disconnect callback while holding the udev device lock. If the driver calls xhci_sideband_unregister(), it eventually reaches usb_offload_put(), which attempts to acquire the same udev lock, resulting in a self-deadlock.

Introduce lockless variants __usb_offload_get() and __usb_offload_put() to allow modifying the offload usage count when the device lock is already held. These helpers use device_lock_assert() to ensure callers meet the locking requirements.

Update the xHCI sideband implementation to use these lockless variants in the unregister and interrupter removal paths. For public sideband APIs that can be called from other contexts, wrap the calls with explicit udev locking to maintain synchronization.

Fixes ef82a4803aab ("xhci: sideband: add api to trace sideband usage")

---
Hi Maintainers,

In the current implementation, xhci_sideband_unregister() is only invoked when a driver manages a USB interface via snd_usb_platform_ops callbacks (.connect_cb and .disconnect_cb). In these contexts, the parent USB device lock is already held by the USB core. However, the original design of usb_offload_put() attempts to re-acquire the same USB device lock, leading to a recursive locking scenario.

This patch resolves the issue by:
1. Refactoring Lock Ownership: Shifting lock acquisition responsibility to the upper-level USB driver. This ensures usb_offload_put() can be safely called from contexts where the lock is already held.
2. Standardizing the Lock Hierarchy: To prevent potential ABBA deadlocks, the locking sequence is unified across the driver. The functions xhci_sideband_create_interrupter() and xhci_sideband_remove_interrupter() have been updated to strictly follow the same hierarchy.

---
 drivers/usb/core/offload.c       | 92 +++++++++++++++++++++-----------
 drivers/usb/host/xhci-sideband.c | 48 +++++++++++------
 include/linux/usb.h              |  6 +++
 3 files changed, 100 insertions(+), 46 deletions(-)
drivers/usb/core/offload.c +61 -31
index 7c699f1b8d2b..bc5f337fb355 100644
@@ -13,44 +13,59 @@
 #include "usb.h"
 
 /**
- * usb_offload_get - increment the offload_usage of a USB device
+ * __usb_offload_get - increment the offload_usage of a USB device
  * @udev: the USB device to increment its offload_usage
  *
- * Incrementing the offload_usage of a usb_device indicates that offload is
- * enabled on this usb_device; that is, another entity is actively handling USB
- * transfers. This information allows the USB driver to adjust its power
- * management policy based on offload activity.
+ * This is the lockless version of usb_offload_get. The caller must hold
+ * @udev's device lock.
  *
  * Return: 0 on success. A negative error code otherwise.
  */
-int usb_offload_get(struct usb_device *udev)
+int __usb_offload_get(struct usb_device *udev)
 {
 	int ret;
 
-	usb_lock_device(udev);
-	if (udev->state == USB_STATE_NOTATTACHED) {
-		usb_unlock_device(udev);
+	device_lock_assert(&udev->dev);
+
+	if (udev->state == USB_STATE_NOTATTACHED)
 		return -ENODEV;
-	}
 
 	if (udev->state == USB_STATE_SUSPENDED ||
-		   udev->offload_at_suspend) {
-		usb_unlock_device(udev);
+	    udev->offload_at_suspend)
 		return -EBUSY;
-	}
 
 	/*
 	 * offload_usage could only be modified when the device is active, since
 	 * it will alter the suspend flow of the device.
 	 */
 	ret = usb_autoresume_device(udev);
-	if (ret < 0) {
-		usb_unlock_device(udev);
+	if (ret < 0)
 		return ret;
-	}
 
 	udev->offload_usage++;
 	usb_autosuspend_device(udev);
+
+	return ret;
+}
+EXPORT_SYMBOL_GPL(__usb_offload_get);
+
+/**
+ * usb_offload_get - increment the offload_usage of a USB device
+ * @udev: the USB device to increment its offload_usage
+ *
+ * Incrementing the offload_usage of a usb_device indicates that offload is
+ * enabled on this usb_device; that is, another entity is actively handling USB
+ * transfers. This information allows the USB driver to adjust its power
+ * management policy based on offload activity.
+ *
+ * Return: 0 on success. A negative error code otherwise.
+ */
+int usb_offload_get(struct usb_device *udev)
+{
+	int ret;
+
+	usb_lock_device(udev);
+	ret = __usb_offload_get(udev);
 	usb_unlock_device(udev);
 
 	return ret;
@@ -58,45 +73,60 @@ int usb_offload_get(struct usb_device *udev)
 EXPORT_SYMBOL_GPL(usb_offload_get);
 
 /**
- * usb_offload_put - drop the offload_usage of a USB device
+ * __usb_offload_put - drop the offload_usage of a USB device
  * @udev: the USB device to drop its offload_usage
  *
- * The inverse operation of usb_offload_get, which drops the offload_usage of
- * a USB device. This information allows the USB driver to adjust its power
- * management policy based on offload activity.
+ * This is the lockless version of usb_offload_put. The caller must hold
+ * @udev's device lock.
  *
  * Return: 0 on success. A negative error code otherwise.
  */
-int usb_offload_put(struct usb_device *udev)
+int __usb_offload_put(struct usb_device *udev)
 {
 	int ret;
 
-	usb_lock_device(udev);
-	if (udev->state == USB_STATE_NOTATTACHED) {
-		usb_unlock_device(udev);
+	device_lock_assert(&udev->dev);
+
+	if (udev->state == USB_STATE_NOTATTACHED)
 		return -ENODEV;
-	}
 
 	if (udev->state == USB_STATE_SUSPENDED ||
-		   udev->offload_at_suspend) {
-		usb_unlock_device(udev);
+	    udev->offload_at_suspend)
 		return -EBUSY;
-	}
 
 	/*
 	 * offload_usage could only be modified when the device is active, since
 	 * it will alter the suspend flow of the device.
 	 */
 	ret = usb_autoresume_device(udev);
-	if (ret < 0) {
-		usb_unlock_device(udev);
+	if (ret < 0)
 		return ret;
-	}
 
 	/* Drop the count when it wasn't 0, ignore the operation otherwise. */
 	if (udev->offload_usage)
 		udev->offload_usage--;
 	usb_autosuspend_device(udev);
+
+	return ret;
+}
+EXPORT_SYMBOL_GPL(__usb_offload_put);
+
+/**
+ * usb_offload_put - drop the offload_usage of a USB device
+ * @udev: the USB device to drop its offload_usage
+ *
+ * The inverse operation of usb_offload_get, which drops the offload_usage of
+ * a USB device. This information allows the USB driver to adjust its power
+ * management policy based on offload activity.
+ *
+ * Return: 0 on success. A negative error code otherwise.
+ */
+int usb_offload_put(struct usb_device *udev)
+{
+	int ret;
+
+	usb_lock_device(udev);
+	ret = __usb_offload_put(udev);
 	usb_unlock_device(udev);
 
 	return ret;
drivers/usb/host/xhci-sideband.c +33 -15

87 lines not on this page · raw message

include/linux/usb.h +6 -0
index fbfcc70b07fb..19c84c05f376 100644
@@ -846,11 +846,17 @@ static inline void usb_mark_last_busy(struct usb_device *udev)
 #endif
 
 #if IS_ENABLED(CONFIG_USB_XHCI_SIDEBAND)
+int __usb_offload_get(struct usb_device *udev);
+int __usb_offload_put(struct usb_device *udev);
 int usb_offload_get(struct usb_device *udev);
 int usb_offload_put(struct usb_device *udev);
 bool usb_offload_check(struct usb_device *udev);
 #else
 
+static inline int __usb_offload_get(struct usb_device *udev)
+{ return 0; }
+static inline int __usb_offload_put(struct usb_device *udev)
+{ return 0; }
 static inline int usb_offload_get(struct usb_device *udev)
 { return 0; }
 static inline int usb_offload_put(struct usb_device *udev)
Signature · 1 line
2.53.0.rc1.225.gd81095ad13-goog
maintainer
#1 lore raw
Quoted 10 lines · Guan-Yu Lin

When a USB device is disconnected or a driver is unbound, the USB core invokes the driver's disconnect callback while holding the udev device lock. If the driver calls xhci_sideband_unregister(), it eventually reaches usb_offload_put(), which attempts to acquire the same udev lock, resulting in a self-deadlock.

Introduce lockless variants __usb_offload_get() and __usb_offload_put() to allow modifying the offload usage count when the device lock is already held. These helpers use device_lock_assert() to ensure callers meet the locking requirements.

Ugh. Didn't I warn about this when the original functions were added?

Adding functions with __ is a mess, please make these names, if you _REALLY_ need them, obvious that this is a no lock function.

And now that you added the lockless functions, are there any in-kernel users of the locked versions? At a quick glance I didn't see them, did I miss it somewhere?

thanks,

greg k-h

#2 lore raw
Quoted 18 lines · Greg KH
Quoted 10 lines · Guan-Yu Lin

When a USB device is disconnected or a driver is unbound, the USB core invokes the driver's disconnect callback while holding the udev device lock. If the driver calls xhci_sideband_unregister(), it eventually reaches usb_offload_put(), which attempts to acquire the same udev lock, resulting in a self-deadlock.

Introduce lockless variants __usb_offload_get() and __usb_offload_put() to allow modifying the offload usage count when the device lock is already held. These helpers use device_lock_assert() to ensure callers meet the locking requirements.

Ugh. Didn't I warn about this when the original functions were added?

Adding functions with __ is a mess, please make these names, if you _REALLY_ need them, obvious that this is a no lock function.

Quoted 7 lines

And now that you added the lockless functions, are there any in-kernel users of the locked versions? At a quick glance I didn't see them, did I miss it somewhere?

thanks,

greg k-h

Hi Greg,

You are right; xhci-sideband.c is the only in-kernel user of the locked versions. I will rename the __ functions to usb_offload_* and remove the locked variants in the next version to clean up the API.

Regarding the deadlock fix itself, we have analyzed two potential solutions. The core issue is that xhci_sideband_unregister() (and xhci_sideband_remove_interrupter()) needs to decrement the offload usage count (which requires the USB device lock), but it is called from the disconnect path where that lock is already held.

Option A: Fix the Callers of xhci_sideband functions
In this approach, we keep the usb_offload calls inside the xhci_sideband functions but replace the internal usb_lock_device() with device_lock_assert(). We then update callers in qc_audio_offload.c to explicitly acquire the lock.
This ensures the offload count remains tightly coupled with the interrupter's lifecycle, though it effectively changes the API
contract: calling xHCI sideband functions now requires holding the USB device lock.

Option B: Decouple usb_offload functions from xhci_sideband functions In this approach, we strip the usb_offload calls out of xhci_sideband functions entirely. The client driver (qc_audio_offload) becomes responsible for the full transaction: acquiring the lock, managing usb_offload_get/put(), and creating/removing the interrupter. This restores clean encapsulation (xHCI functions only handle hardware), but it places the burden on the client driver to correctly balance the usb_offload calls.

I lean towards Option A to ensure the offload count implies an active interrupter by design, but please let me know if you prefer the cleaner separation of Option B.

Regards,
Guan-Yu

#3 lore raw
Quoted 58 lines · Guan-Yu Lin
Quoted 18 lines · Greg KH
Quoted 10 lines · Guan-Yu Lin

When a USB device is disconnected or a driver is unbound, the USB core invokes the driver's disconnect callback while holding the udev device lock. If the driver calls xhci_sideband_unregister(), it eventually reaches usb_offload_put(), which attempts to acquire the same udev lock, resulting in a self-deadlock.

Introduce lockless variants __usb_offload_get() and __usb_offload_put() to allow modifying the offload usage count when the device lock is already held. These helpers use device_lock_assert() to ensure callers meet the locking requirements.

Ugh. Didn't I warn about this when the original functions were added?

Adding functions with __ is a mess, please make these names, if you _REALLY_ need them, obvious that this is a no lock function.

Quoted 7 lines

And now that you added the lockless functions, are there any in-kernel users of the locked versions? At a quick glance I didn't see them, did I miss it somewhere?

thanks,

greg k-h

Hi Greg,

You are right; xhci-sideband.c is the only in-kernel user of the locked versions. I will rename the __ functions to usb_offload_* and remove the locked variants in the next version to clean up the API.

Regarding the deadlock fix itself, we have analyzed two potential solutions. The core issue is that xhci_sideband_unregister() (and xhci_sideband_remove_interrupter()) needs to decrement the offload usage count (which requires the USB device lock), but it is called from the disconnect path where that lock is already held.

Option A: Fix the Callers of xhci_sideband functions
In this approach, we keep the usb_offload calls inside the xhci_sideband functions but replace the internal usb_lock_device() with device_lock_assert(). We then update callers in qc_audio_offload.c to explicitly acquire the lock.
This ensures the offload count remains tightly coupled with the interrupter's lifecycle, though it effectively changes the API
contract: calling xHCI sideband functions now requires holding the USB device lock.

Option B: Decouple usb_offload functions from xhci_sideband functions In this approach, we strip the usb_offload calls out of xhci_sideband functions entirely. The client driver (qc_audio_offload) becomes responsible for the full transaction: acquiring the lock, managing usb_offload_get/put(), and creating/removing the interrupter. This restores clean encapsulation (xHCI functions only handle hardware), but it places the burden on the client driver to correctly balance the usb_offload calls.

Quoted 3 lines

I lean towards Option A to ensure the offload count implies an active interrupter by design, but please let me know if you prefer the cleaner separation of Option B.

I would prefer option B
Decouple the offload from sideband.

The secondary interrupter in sideband was specifically createad for qc_audio_offload.

Vendors using the xHCI hardware Audio sideband Capability (xHCI 7.9) won't use a secondary interrupter, but might sill want to prevent suspending the device. So it shuold be better to make this decision in the class driver.

The offload count shoudn't be that complicated. Isn't it binary at the moment? We don't allow more than one sideband per device, and it can only have one interrupter.

I unfortunately couldn't participate in the review and development of drivers/usb/core/offload.c, but my original idea before it was implemented was to keep usb core out of sideband as much as possible as its not really a part of usb specification.

This is also why I added the sideband pointer to struct xhci_virt_device. It allows us to figure out if a device is dedicated for sideband use.

so xhci_sideband_check() could be something like

bool xhci_sideband_check(struct xhci_hcd *xhci)

{
	guard(spinlock_irqsave)(&xhci->lock);

	for (int i = 1; i < HC_MAX_SLOTS; i++) {
	if (xhci->devs[i] && xhci->devs[i]->sideband)
		return true;
	}
	return false;
}

Thanks
Mathias

#4 lore raw
Quoted 99 lines · Mathias Nyman
Quoted 58 lines · Guan-Yu Lin
Quoted 18 lines · Greg KH
Quoted 10 lines · Guan-Yu Lin

When a USB device is disconnected or a driver is unbound, the USB core invokes the driver's disconnect callback while holding the udev device lock. If the driver calls xhci_sideband_unregister(), it eventually reaches usb_offload_put(), which attempts to acquire the same udev lock, resulting in a self-deadlock.

Introduce lockless variants __usb_offload_get() and __usb_offload_put() to allow modifying the offload usage count when the device lock is already held. These helpers use device_lock_assert() to ensure callers meet the locking requirements.

Ugh. Didn't I warn about this when the original functions were added?

Adding functions with __ is a mess, please make these names, if you _REALLY_ need them, obvious that this is a no lock function.

Quoted 7 lines

And now that you added the lockless functions, are there any in-kernel users of the locked versions? At a quick glance I didn't see them, did I miss it somewhere?

thanks,

greg k-h

Hi Greg,

You are right; xhci-sideband.c is the only in-kernel user of the locked versions. I will rename the __ functions to usb_offload_* and remove the locked variants in the next version to clean up the API.

Regarding the deadlock fix itself, we have analyzed two potential solutions. The core issue is that xhci_sideband_unregister() (and xhci_sideband_remove_interrupter()) needs to decrement the offload usage count (which requires the USB device lock), but it is called from the disconnect path where that lock is already held.

Option A: Fix the Callers of xhci_sideband functions
In this approach, we keep the usb_offload calls inside the xhci_sideband functions but replace the internal usb_lock_device() with device_lock_assert(). We then update callers in qc_audio_offload.c to explicitly acquire the lock.
This ensures the offload count remains tightly coupled with the interrupter's lifecycle, though it effectively changes the API
contract: calling xHCI sideband functions now requires holding the USB device lock.

Option B: Decouple usb_offload functions from xhci_sideband functions In this approach, we strip the usb_offload calls out of xhci_sideband functions entirely. The client driver (qc_audio_offload) becomes responsible for the full transaction: acquiring the lock, managing usb_offload_get/put(), and creating/removing the interrupter. This restores clean encapsulation (xHCI functions only handle hardware), but it places the burden on the client driver to correctly balance the usb_offload calls.

Quoted 3 lines

I lean towards Option A to ensure the offload count implies an active interrupter by design, but please let me know if you prefer the cleaner separation of Option B.

I would prefer option B
Decouple the offload from sideband.

The secondary interrupter in sideband was specifically createad for qc_audio_offload.

Vendors using the xHCI hardware Audio sideband Capability (xHCI 7.9) won't use a secondary interrupter, but might sill want to prevent suspending the device. So it shuold be better to make this decision in the class driver.

The offload count shoudn't be that complicated. Isn't it binary at the moment? We don't allow more than one sideband per device, and it can only have one interrupter.

I unfortunately couldn't participate in the review and development of drivers/usb/core/offload.c, but my original idea before it was implemented was to keep usb core out of sideband as much as possible as its not really a part of usb specification.

This is also why I added the sideband pointer to struct xhci_virt_device. It allows us to figure out if a device is dedicated for sideband use.

so xhci_sideband_check() could be something like

bool xhci_sideband_check(struct xhci_hcd *xhci)

{
        guard(spinlock_irqsave)(&xhci->lock);

        for (int i = 1; i < HC_MAX_SLOTS; i++) {
        if (xhci->devs[i] && xhci->devs[i]->sideband)
                return true;
        }
        return false;
}
Quoted 2 lines

Thanks
Mathias

Hi Mathias,

Thanks for the feedback.
I will proceed with Option B as you suggested. Decoupling the offload logic from the sideband mechanism seems cleaner and places the responsibility correctly on the class driver (qc_audio_offload) to manage the offload state.

I will implement the following changes in v2:
1. API Cleanup: As Greg requested, I will rename __usb_offload_* to usb_offload_* and remove the locked variants. These functions will use device_lock_assert() to ensure the caller holds the lock.
2. Class Driver Logic: qc_audio_offload will handle locking udev and calling usb_offload_get/put() directly.

Regarding xhci_sideband_check():
I have a concern regarding power management with the proposed check:

if (xhci->devs[i] && xhci->devs[i]->sideband)

        return true;

vdev->sideband is assigned during xhci_sideband_register(), which happens when the class driver probes (device connection), and it persists until disconnect. If we use this check, the xHCI controller will be prevented from PM suspending (system suspend) as long as the device is connected, even if it is idle (not playing audio). For mobile power optimization, we need to allow the controller to suspend when the sideband is registered but idle.

Since we are proceeding with Option B, the class driver will be maintaining the udev->offload_usage count via usb_offload_get/put(). I propose we still rely on usb_offload_check() (or check offload_usage) within the xHCI sideband check. This ensures we only block or adjust the PM suspend flow only when there is active data transfer.

Regards,
Guan-Yu

#5 lore raw
Quoted 14 lines · Guan-Yu Lin

Regarding xhci_sideband_check():
I have a concern regarding power management with the proposed check:

if (xhci->devs[i] && xhci->devs[i]->sideband)

         return true;

vdev->sideband is assigned during xhci_sideband_register(), which happens when the class driver probes (device connection), and it persists until disconnect. If we use this check, the xHCI controller will be prevented from PM suspending (system suspend) as long as the device is connected, even if it is idle (not playing audio). For mobile power optimization, we need to allow the controller to suspend when the sideband is registered but idle.

Quoted 5 lines

Since we are proceeding with Option B, the class driver will be maintaining the udev->offload_usage count via usb_offload_get/put(). I propose we still rely on usb_offload_check() (or check offload_usage) within the xHCI sideband check. This ensures we only block or adjust the PM suspend flow only when there is active data transfer.

Sounds reasonable to me

Thanks
Mathias