From: Raul E Rangel <rrangel@chromium.org>
To: Valentina Manea <valentina.manea.m@gmail.com>,
Shuah Khan <shuah@kernel.org>,
Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: Hongren Zheng <i@zenithal.me>,
linux-kernel@vger.kernel.org, linux-usb@vger.kernel.org,
Raul E Rangel <rrangel@chromium.org>,
stable@vger.kernel.org, Anirudh Rayabharam <mail@anirudhrb.com>,
"Brian G. Merrell" <bgmerrell@novell.com>
Subject: [PATCH 3/6] usbip: vhci_hcd: stop enqueue and drain all pending URBs on disconnect
Date: Wed, 21 Oct 2026 09:06:58 -0600 [thread overview]
Message-ID: <20261021150704.3129446-4-rrangel@chromium.org> (raw)
In-Reply-To: <20261021150704.3129446-1-rrangel@chromium.org>
Even with vhci_urb_dequeue() completing unsent URBs locally when
unlinked, several races in vhci_shutdown_connection() allow URBs to be
enqueued after shutdown begins or remain stranded on vdev->priv_tx and
vdev->priv_rx across disconnect and port reset:
1. vhci_urb_enqueue() only rejects URBs when vdev->ud.status is
VDEV_ST_NULL or VDEV_ST_ERROR. Because ud.status remains VDEV_ST_USED
after USBIP_EH_SHUTDOWN is set and until vhci_device_reset() runs
after vhci_shutdown_connection(), vhci_urb_enqueue() continues
accepting new URBs onto vdev->priv_tx even after tcp_tx has stopped.
2. vhci_shutdown_connection() clears vdev->ud.tcp_socket without holding
vhci->lock or vdev->ud.lock, racing with vhci_urb_enqueue() and
vhci_urb_dequeue().
3. vhci_device_unlink_cleanup() only processes vdev->unlink_tx and
vdev->unlink_rx, leaving non-unlinked URBs on vdev->priv_tx and
vdev->priv_rx until asynchronous rh_port_disconnect() work runs on
usb_hub_wq. If a task holds udev->dev.mutex while waiting on an URB,
usb_disconnect() blocks on udev->dev.mutex. Moreover, because
vhci_device_reset() immediately sets ud.status to VDEV_ST_NULL, any
entries left on vdev->priv_tx when the port is re-attached via
attach_store() will be transmitted by the new connection's tcp_tx
thread to the new server.
In a live deadlock captured with drgn on a hung host:
- rx thread logged "connection closed" and set USBIP_EH_SHUTDOWN, and
vhci_shutdown_connection() stopped tcp_rx and tcp_tx (29.6519 s)
before clearing tcp_socket (29.6716 s).
- In kernel memory (drgn), vdev->priv_tx still held never-transmitted
URB seqnum 17193284 with urb->unlinked = -ESHUTDOWN and
use_count = 1, while vdev->priv_rx held unanswered URB
seqnum 17193282 with urb->unlinked = 0 and vdev->unlink_tx/unlink_rx
were empty.
- A concurrent USBDEVFS_RESET ioctl holding udev->dev.mutex called
usb_hcd_flush_endpoint() -> usb_kill_urb(), blocking in D state on
seqnum 17193284 while usb_hub_wq blocked in usb_disconnect() waiting
for udev->dev.mutex, freezing the vhci root hub.
Fix this to guarantee all pending URBs are given back when the
connection is torn down (note that while a connection remains up,
usb_kill_urb() still waits for RET_UNLINK or connection teardown):
- Clear vdev->ud.tcp_socket under vhci->lock and vdev->ud.lock in
vhci_shutdown_connection(), and reject vhci_urb_enqueue() (with a
rate-limited error) when USBIP_EH_SHUTDOWN is set or tcp_socket is
NULL.
- Drain and give back all remaining URBs on vdev->priv_tx and
vdev->priv_rx in vhci_device_unlink_cleanup() via
vhci_cleanup_priv_list(), using list_first_entry() loops across lock
drops.
Fixes: 258c81b341c8 ("usbip: give back URBs for unsent unlink requests during cleanup")
Fixes: 04679b3489e0 ("Staging: USB/IP: add client driver")
Cc: stable@vger.kernel.org
Assisted-by: Gemini
Signed-off-by: Raul E Rangel <rrangel@chromium.org>
---
drivers/usb/usbip/vhci_hcd.c | 115 ++++++++++++++++++++++++-----------
1 file changed, 81 insertions(+), 34 deletions(-)
diff --git a/drivers/usb/usbip/vhci_hcd.c b/drivers/usb/usbip/vhci_hcd.c
index 7fadb41c01fc..78ea1fc7906e 100644
--- a/drivers/usb/usbip/vhci_hcd.c
+++ b/drivers/usb/usbip/vhci_hcd.c
@@ -727,8 +727,11 @@ static int vhci_urb_enqueue(struct usb_hcd *hcd, struct urb *urb, gfp_t mem_flag
/* refuse enqueue for dead connection */
spin_lock(&vdev->ud.lock);
if (vdev->ud.status == VDEV_ST_NULL ||
- vdev->ud.status == VDEV_ST_ERROR) {
- dev_err(dev, "enqueue for inactive port %d\n", vdev->rhport);
+ vdev->ud.status == VDEV_ST_ERROR ||
+ (vdev->ud.event & USBIP_EH_SHUTDOWN) ||
+ !vdev->ud.tcp_socket) {
+ dev_err_ratelimited(dev, "enqueue for inactive port %d\n",
+ vdev->rhport);
spin_unlock(&vdev->ud.lock);
spin_unlock_irqrestore(&vhci->lock, flags);
return -ENODEV;
@@ -982,18 +985,20 @@ static void vhci_cleanup_unlink_list(struct vhci_device *vdev,
struct vhci_hcd *vhci_hcd = vdev_to_vhci_hcd(vdev);
struct usb_hcd *hcd = vhci_hcd_to_hcd(vhci_hcd);
struct vhci *vhci = vhci_hcd->vhci;
- struct vhci_unlink *unlink, *tmp;
+ struct vhci_unlink *unlink;
unsigned long flags;
spin_lock_irqsave(&vhci->lock, flags);
spin_lock(&vdev->priv_lock);
- list_for_each_entry_safe(unlink, tmp, unlink_list, list) {
+ while (!list_empty(unlink_list)) {
struct urb *urb;
+ unlink = list_first_entry(unlink_list, struct vhci_unlink, list);
+ list_del(&unlink->list);
+
urb = pickup_urb_and_free_priv(vdev, unlink->unlink_seqnum);
if (!urb) {
- list_del(&unlink->list);
kfree(unlink);
continue;
}
@@ -1002,8 +1007,6 @@ static void vhci_cleanup_unlink_list(struct vhci_device *vdev,
usb_hcd_unlink_urb_from_ep(hcd, urb);
- list_del(&unlink->list);
-
spin_unlock(&vdev->priv_lock);
spin_unlock_irqrestore(&vhci->lock, flags);
@@ -1019,6 +1022,50 @@ static void vhci_cleanup_unlink_list(struct vhci_device *vdev,
spin_unlock_irqrestore(&vhci->lock, flags);
}
+/*
+ * Drain any remaining URBs still queued on priv_tx (e.g. enqueued before
+ * USBIP_EH_SHUTDOWN was set or after tcp_tx stopped) or waiting for a reply
+ * on priv_rx so no orphaned URBs remain after connection teardown.
+ */
+static void vhci_cleanup_priv_list(struct vhci_device *vdev,
+ struct list_head *priv_list)
+{
+ struct vhci_hcd *vhci_hcd = vdev_to_vhci_hcd(vdev);
+ struct usb_hcd *hcd = vhci_hcd_to_hcd(vhci_hcd);
+ struct vhci *vhci = vhci_hcd->vhci;
+ struct vhci_priv *priv;
+ unsigned long flags;
+
+ spin_lock_irqsave(&vhci->lock, flags);
+ spin_lock(&vdev->priv_lock);
+
+ while (!list_empty(priv_list)) {
+ struct urb *urb;
+
+ priv = list_first_entry(priv_list, struct vhci_priv, list);
+ urb = priv->urb;
+
+ list_del(&priv->list);
+ kfree(priv);
+ urb->hcpriv = NULL;
+
+ urb->status = -ENODEV;
+
+ usb_hcd_unlink_urb_from_ep(hcd, urb);
+
+ spin_unlock(&vdev->priv_lock);
+ spin_unlock_irqrestore(&vhci->lock, flags);
+
+ usb_hcd_giveback_urb(hcd, urb, urb->status);
+
+ spin_lock_irqsave(&vhci->lock, flags);
+ spin_lock(&vdev->priv_lock);
+ }
+
+ spin_unlock(&vdev->priv_lock);
+ spin_unlock_irqrestore(&vhci->lock, flags);
+}
+
static void vhci_device_unlink_cleanup(struct vhci_device *vdev)
{
/* give back URB of unsent unlink request */
@@ -1026,17 +1073,26 @@ static void vhci_device_unlink_cleanup(struct vhci_device *vdev)
/* give back URB of unanswered unlink request */
vhci_cleanup_unlink_list(vdev, &vdev->unlink_rx);
+
+ /* give back remaining unsent and unanswered URBs */
+ vhci_cleanup_priv_list(vdev, &vdev->priv_tx);
+ vhci_cleanup_priv_list(vdev, &vdev->priv_rx);
}
/*
- * The important thing is that only one context begins cleanup.
- * This is why error handling and cleanup become simple.
- * We do not want to consider race condition as possible.
+ * Tear down an active virtual port connection. After stopping tcp_rx and
+ * tcp_tx, tcp_socket is cleared under vhci->lock and ud->lock so concurrent
+ * vhci_urb_enqueue() and vhci_urb_dequeue() calls are serialized before
+ * vhci_device_unlink_cleanup() drains all pending URBs.
*/
static void vhci_shutdown_connection(struct usbip_device *ud)
{
struct vhci_device *vdev = container_of(ud, struct vhci_device, ud);
- struct usb_hcd *hcd = vhci_hcd_to_hcd(vdev_to_vhci_hcd(vdev));
+ struct vhci_hcd *vhci_hcd = vdev_to_vhci_hcd(vdev);
+ struct usb_hcd *hcd = vhci_hcd_to_hcd(vhci_hcd);
+ struct vhci *vhci = vhci_hcd->vhci;
+ struct socket *socket;
+ unsigned long flags;
/* need this? see stub_dev.c */
if (ud->tcp_socket) {
@@ -1056,34 +1112,25 @@ static void vhci_shutdown_connection(struct usbip_device *ud)
dev_info(hcd_dev(hcd), "stop threads\n");
/* active connection is closed */
- if (vdev->ud.tcp_socket) {
- sockfd_put(vdev->ud.tcp_socket);
- vdev->ud.tcp_socket = NULL;
- vdev->ud.sockfd = -1;
- }
+ spin_lock_irqsave(&vhci->lock, flags);
+ spin_lock(&vdev->ud.lock);
+ socket = vdev->ud.tcp_socket;
+ vdev->ud.tcp_socket = NULL;
+ vdev->ud.sockfd = -1;
+ spin_unlock(&vdev->ud.lock);
+ spin_unlock_irqrestore(&vhci->lock, flags);
+
+ if (socket)
+ sockfd_put(socket);
dev_info(hcd_dev(hcd), "release socket\n");
vhci_device_unlink_cleanup(vdev);
/*
- * rh_port_disconnect() is a trigger of ...
- * usb_disable_device():
- * disable all the endpoints for a USB device.
- * usb_disable_endpoint():
- * disable endpoints. pending urbs are unlinked(dequeued).
- *
- * NOTE: After calling rh_port_disconnect(), the USB device drivers of a
- * detached device should release used urbs in a cleanup function (i.e.
- * xxx_disconnect()). Therefore, vhci_hcd does not need to release
- * pushed urbs and their private data in this function.
- *
- * NOTE: vhci_dequeue() must be considered carefully. When shutting down
- * a connection, vhci_shutdown_connection() expects vhci_dequeue()
- * gives back pushed urbs and frees their private data by request of
- * the cleanup function of a USB driver. When unlinking a urb with an
- * active connection, vhci_dequeue() does not give back the urb which
- * is actually given back by vhci_rx after receiving its return pdu.
- *
+ * rh_port_disconnect() triggers asynchronous USB device disconnect on
+ * usb_hub_wq (usb_disconnect() -> usb_disable_device()). All pending
+ * URBs and unlink requests on vdev have already been unlinked and
+ * given back synchronously by vhci_device_unlink_cleanup() above.
*/
rh_port_disconnect(vdev);
--
2.56.0.212.g129ede20d9-goog
next prev parent reply other threads:[~2026-10-21 15:08 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-21 15:06 [PATCH 0/6] usbip: vhci_hcd: fix URB giveback leaks and disconnect races Raul E Rangel
2026-10-21 15:06 ` [PATCH 1/6] usbip: vhci_hcd: protect urb->hcpriv with priv_lock in vhci_urb_dequeue() Raul E Rangel
2026-10-21 15:06 ` [PATCH 2/6] usbip: vhci_hcd: complete unsent URBs locally " Raul E Rangel
2026-10-21 15:06 ` Raul E Rangel [this message]
2026-10-21 15:06 ` [PATCH 4/6] usbip: vhci_hcd: clear URB_DMA_MAP_SG on all giveback paths Raul E Rangel
2026-10-21 15:07 ` [PATCH 5/6] usbip: vhci_hcd: allocate vhci_priv outside lock and handle ENOMEM Raul E Rangel
2026-10-21 15:07 ` [PATCH 6/6] usbip: vhci_hcd: reject URBs from stale usb_device after port re-attach Raul E Rangel
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261021150704.3129446-4-rrangel@chromium.org \
--to=rrangel@chromium.org \
--cc=bgmerrell@novell.com \
--cc=gregkh@linuxfoundation.org \
--cc=i@zenithal.me \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-usb@vger.kernel.org \
--cc=mail@anirudhrb.com \
--cc=shuah@kernel.org \
--cc=stable@vger.kernel.org \
--cc=valentina.manea.m@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
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®