* [PATCH] usbip: vhci_hcd: time out unanswered unlink requests
@ 2026-10-06 12:56 Hongyu Xie
2026-10-19 19:17 ` Shuah Khan
0 siblings, 1 reply; 2+ messages in thread
From: Hongyu Xie @ 2026-10-06 12:56 UTC (permalink / raw)
To: valentina.manea.m, shuah, gregkh
Cc: i, bgmerrell, linux-usb, linux-kernel, Hongyu Xie,
syzbot+b6f11035e572f08bc20f, syzbot+a7edecbf389d11a369d4,
syzbot+bfa320b67fcbe2583c6f
When the connection to the peer is alive, vhci_urb_dequeue() only queues
a CMD_UNLINK PDU and relies on the peer to send RET_UNLINK (or
RET_SUBMIT) before the unlinked URB is given back. A peer that keeps
the socket open but never answers therefore makes usb_kill_urb() wait
forever. The waiter is typically the hub work item, which runs with
the shared address0_mutex held, so every later task that needs that
mutex or the device lock chain - a second attach, usbfs open/release/
ioctl, udev - piles up behind it and the whole USB stack wedges.
syzbot reports this shape in three long-lived bugs:
INFO: task hung in hub_port_init
INFO: task hung in hub_event
INFO: task hung in usbdev_release
Give every queued unlink request a deadline of its own and arm a
per-device watchdog; when the watchdog fires, it tears the connection
down through the existing error path (VDEV_EVENT_ERROR_TCP) if any
request is still unanswered past its own deadline. The teardown
already knows how to give back every pending URB with -ENODEV, and a
RET_UNLINK that arrives late (or never) after that is handled by the
existing "already given back" paths.
The watchdog is cancelled whenever no request is outstanding. Queueing
a request re-arms it to that request's deadline, so the callback does
not trust the timer's expiry: it re-checks the outstanding requests
against their own deadlines under priv_lock and becomes a no-op when
none has run out. A peer that answers promptly is therefore unaffected.
Controller removal quiesces the device first - the event handler
stops the per-device rx/tx threads and drains the pending events -
and only then calls timer_shutdown_sync(), which also waits for a
watchdog callback that is already running, so the callback cannot
touch the controller after it is freed.
A similar fix was proposed as an RFC by the syzbot assisted-fix
pipeline; this version arms a single per-device watchdog instead of a
timer per unlink request.
Fixes: 04679b3489e0 ("Staging: USB/IP: add client driver")
Reported-by: syzbot+b6f11035e572f08bc20f@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?id=58aef18c1ad93d87652f24a0bc7a74b6d98ac710
Reported-by: syzbot+a7edecbf389d11a369d4@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?id=7ff8ae4abed51ff2354c1addb664143ec26ca588
Reported-by: syzbot+bfa320b67fcbe2583c6f@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?id=048da78aba1a3a6139509492b2be505b2d223762
Link: https://lore.kernel.org/all/1d6db53d-07ab-46ad-adc4-d09c95b2c83f@mail.kernel.org/
Signed-off-by: Hongyu Xie <xiehongyu1@kylinos.cn>
---
drivers/usb/usbip/vhci.h | 8 ++++
drivers/usb/usbip/vhci_hcd.c | 83 ++++++++++++++++++++++++++++++++++++
drivers/usb/usbip/vhci_rx.c | 2 +
3 files changed, 93 insertions(+)
diff --git a/drivers/usb/usbip/vhci.h b/drivers/usb/usbip/vhci.h
index 5659dce1526e..6222283d9551 100644
--- a/drivers/usb/usbip/vhci.h
+++ b/drivers/usb/usbip/vhci.h
@@ -11,6 +11,7 @@
#include <linux/list.h>
#include <linux/spinlock.h>
#include <linux/sysfs.h>
+#include <linux/timer.h>
#include <linux/types.h>
#include <linux/usb.h>
#include <linux/usb/hcd.h>
@@ -44,6 +45,9 @@ struct vhci_device {
struct list_head unlink_tx;
struct list_head unlink_rx;
+ /* fires when the peer leaves an unlink request unanswered */
+ struct timer_list unlink_timer;
+
/* vhci_tx thread sleeps for this queue */
wait_queue_head_t waitq_tx;
};
@@ -65,6 +69,9 @@ struct vhci_unlink {
/* seqnum of the unlink target */
unsigned long unlink_seqnum;
+
+ /* deadline enforced by the per-device unlink watchdog */
+ unsigned long expires;
};
enum hub_speed {
@@ -124,6 +131,7 @@ extern struct attribute_group vhci_attr_group;
/* vhci_hcd.c */
void rh_port_connect(struct vhci_device *vdev, enum usb_device_speed speed);
+void vhci_stop_unlink_timer(struct vhci_device *vdev);
/* vhci_sysfs.c */
int vhci_init_attr_group(void);
diff --git a/drivers/usb/usbip/vhci_hcd.c b/drivers/usb/usbip/vhci_hcd.c
index 39e8faf4c18c..220051339445 100644
--- a/drivers/usb/usbip/vhci_hcd.c
+++ b/drivers/usb/usbip/vhci_hcd.c
@@ -19,6 +19,15 @@
#define DRIVER_AUTHOR "Takahiro Hirofuchi"
#define DRIVER_DESC "USB/IP 'Virtual' Host Controller (VHCI) Driver"
+/*
+ * How long to wait for RET_UNLINK after sending CMD_UNLINK. A peer that
+ * cannot answer an unlink request within a control-transfer timeout
+ * (USB_CTRL_GET_TIMEOUT) worth of time is considered stuck, and the
+ * connection is torn down so that URBs waiting for their unlink reply can
+ * be given back instead of hanging their owners forever.
+ */
+#define VHCI_UNLINK_TIMEOUT_MS 5000
+
/*
* TODO
* - update root hub emulation
@@ -846,6 +855,67 @@ static int vhci_urb_enqueue(struct usb_hcd *hcd, struct urb *urb, gfp_t mem_flag
return ret;
}
+/*
+ * Is any queued unlink request past its own deadline? Each request
+ * carries the time by which the peer must have answered it, and the
+ * timer may have been re-armed by a newer request before that, so the
+ * callback decides by walking the lists rather than by the timer's own
+ * expiry.
+ */
+static bool vhci_unlink_expired(struct vhci_device *vdev, unsigned long now)
+{
+ struct vhci_unlink *unlink;
+
+ list_for_each_entry(unlink, &vdev->unlink_tx, list)
+ if (time_after_eq(now, unlink->expires))
+ return true;
+ list_for_each_entry(unlink, &vdev->unlink_rx, list)
+ if (time_after_eq(now, unlink->expires))
+ return true;
+
+ return false;
+}
+
+static void vhci_unlink_timeout(struct timer_list *t)
+{
+ struct vhci_device *vdev = timer_container_of(vdev, t, unlink_timer);
+ unsigned long flags;
+ bool expired;
+
+ spin_lock_irqsave(&vdev->priv_lock, flags);
+ expired = vhci_unlink_expired(vdev, jiffies);
+ spin_unlock_irqrestore(&vdev->priv_lock, flags);
+
+ if (!expired)
+ return;
+
+ dev_info(hcd_dev(vhci_hcd_to_hcd(vdev_to_vhci_hcd(vdev))),
+ "peer did not reply to an unlink request within %d msecs\n",
+ VHCI_UNLINK_TIMEOUT_MS);
+
+ /*
+ * Reuses the teardown path taken on other peer failures, which
+ * gives back all URBs waiting for their unlink reply.
+ */
+ usbip_event_add(&vdev->ud, VDEV_EVENT_ERROR_TCP);
+}
+
+/*
+ * Stop the unlink watchdog once every queued unlink request is answered
+ * or given back. Safe from any context: a concurrent watchdog expiry
+ * re-checks the per-request deadlines under vdev->priv_lock and becomes
+ * a no-op.
+ */
+void vhci_stop_unlink_timer(struct vhci_device *vdev)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&vdev->priv_lock, flags);
+ if (list_empty(&vdev->unlink_tx) && list_empty(&vdev->unlink_rx))
+ timer_delete(&vdev->unlink_timer);
+ spin_unlock_irqrestore(&vdev->priv_lock, flags);
+}
+
/*
* vhci_rx gives back the urb after receiving the reply of the urb. If an
* unlink pdu is sent or not, vhci_rx receives a normal return pdu and gives
@@ -968,6 +1038,14 @@ static int vhci_urb_dequeue(struct usb_hcd *hcd, struct urb *urb, int status)
/* send cmd_unlink and try to cancel the pending URB in the
* peer */
list_add_tail(&unlink->list, &vdev->unlink_tx);
+
+ /* if the peer does not answer this request within the
+ * timeout, tear the connection down
+ */
+ unlink->expires =
+ jiffies + msecs_to_jiffies(VHCI_UNLINK_TIMEOUT_MS);
+ mod_timer(&vdev->unlink_timer, unlink->expires);
+
wake_up(&vdev->waitq_tx);
spin_unlock(&vdev->priv_lock);
@@ -1029,6 +1107,8 @@ 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);
+
+ vhci_stop_unlink_timer(vdev);
}
/*
@@ -1143,6 +1223,8 @@ static void vhci_device_init(struct vhci_device *vdev)
init_waitqueue_head(&vdev->waitq_tx);
+ timer_setup(&vdev->unlink_timer, vhci_unlink_timeout, 0);
+
vdev->ud.eh_ops.shutdown = vhci_shutdown_connection;
vdev->ud.eh_ops.reset = vhci_device_reset;
vdev->ud.eh_ops.unusable = vhci_device_unusable;
@@ -1269,6 +1351,7 @@ static void vhci_stop(struct usb_hcd *hcd)
usbip_event_add(&vdev->ud, VDEV_EVENT_REMOVED);
usbip_stop_eh(&vdev->ud);
+ timer_shutdown_sync(&vdev->unlink_timer);
}
}
diff --git a/drivers/usb/usbip/vhci_rx.c b/drivers/usb/usbip/vhci_rx.c
index a678e7c89837..32b1550839d2 100644
--- a/drivers/usb/usbip/vhci_rx.c
+++ b/drivers/usb/usbip/vhci_rx.c
@@ -179,6 +179,8 @@ static void vhci_recv_ret_unlink(struct vhci_device *vdev,
}
kfree(unlink);
+
+ vhci_stop_unlink_timer(vdev);
}
static int vhci_priv_tx_empty(struct vhci_device *vdev)
--
2.32.0
^ permalink raw reply [flat|nested] 2+ messages in thread
* Re: [PATCH] usbip: vhci_hcd: time out unanswered unlink requests
2026-10-06 12:56 [PATCH] usbip: vhci_hcd: time out unanswered unlink requests Hongyu Xie
@ 2026-10-19 19:17 ` Shuah Khan
0 siblings, 0 replies; 2+ messages in thread
From: Shuah Khan @ 2026-10-19 19:17 UTC (permalink / raw)
To: Hongyu Xie, valentina.manea.m, shuah, gregkh
Cc: i, bgmerrell, linux-usb, linux-kernel,
syzbot+b6f11035e572f08bc20f, syzbot+a7edecbf389d11a369d4,
syzbot+bfa320b67fcbe2583c6f, Shuah Khan
On 10/6/26 06:56, Hongyu Xie wrote:
> When the connection to the peer is alive, vhci_urb_dequeue() only queues
> a CMD_UNLINK PDU and relies on the peer to send RET_UNLINK (or
> RET_SUBMIT) before the unlinked URB is given back. A peer that keeps
> the socket open but never answers therefore makes usb_kill_urb() wait
> forever. The waiter is typically the hub work item, which runs with
> the shared address0_mutex held, so every later task that needs that
> mutex or the device lock chain - a second attach, usbfs open/release/
> ioctl, udev - piles up behind it and the whole USB stack wedges.
>
> syzbot reports this shape in three long-lived bugs:
> INFO: task hung in hub_port_init
> INFO: task hung in hub_event
> INFO: task hung in usbdev_release
>
> Give every queued unlink request a deadline of its own and arm a
> per-device watchdog; when the watchdog fires, it tears the connection
> down through the existing error path (VDEV_EVENT_ERROR_TCP) if any
> request is still unanswered past its own deadline. The teardown
> already knows how to give back every pending URB with -ENODEV, and a
> RET_UNLINK that arrives late (or never) after that is handled by the
> existing "already given back" paths.
>
> The watchdog is cancelled whenever no request is outstanding. Queueing
> a request re-arms it to that request's deadline, so the callback does
> not trust the timer's expiry: it re-checks the outstanding requests
> against their own deadlines under priv_lock and becomes a no-op when
> none has run out. A peer that answers promptly is therefore unaffected.
> Controller removal quiesces the device first - the event handler
> stops the per-device rx/tx threads and drains the pending events -
> and only then calls timer_shutdown_sync(), which also waits for a
> watchdog callback that is already running, so the callback cannot
> touch the controller after it is freed.
>
> A similar fix was proposed as an RFC by the syzbot assisted-fix
> pipeline; this version arms a single per-device watchdog instead of a
> timer per unlink request.
Did you test this change with a usbip_host and client? How does this
reproduce in a regular setup?
thanks,
-- Shuah
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-19 19:17 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-06 12:56 [PATCH] usbip: vhci_hcd: time out unanswered unlink requests Hongyu Xie
2026-10-19 19:17 ` Shuah Khan
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®