From: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
To: svt-core@virtuozzo.com
Cc: andrey.drobyshev@virtuozzo.com, den@openvz.org
Subject: [PATCH QEMU HCI-8.0 v6 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800
Date: Tue, 06 Oct 2026 13:34:29 +0300 [thread overview]
Message-ID: <20261006-usb-host_v3-v6-5-8688ec9576d1@virtuozzo.com> (raw)
In-Reply-To: <20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com>
URBs submitted by the old QEMU must not outlive it across a CPR
switchover: the kernel would complete them into buffers of an address
space that is gone after exec, and the new libusb context would reap URB
pointers it never submitted.
Register a vm_change_state handler that, when the VM stops for CPR
(RUN_STATE_FINISH_MIGRATE), cancels and reaps every pending transfer.
It runs before the vCPU/device snapshot, so a completion raised during
the drain lands in the still-live interrupt controller and is migrated
too. A transfer that already completed in the kernel is delivered
normally; one cancelled after moving some data is delivered as a short
transfer; one that moved nothing is suppressed, and the target replays
it from the start of the TD via usb_xhci_post_load(). A VM already
paused when migration starts never hits that transition, so .pre_save()
drains as a fallback, suppressing every URB for the target to replay.
Isochronous URBs live on their own rings; cancel and reap them too -- the
target reconstructs the stream from the guest's transfer ring.
Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
---
hw/usb/host-libusb.c | 220 ++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 193 insertions(+), 27 deletions(-)
diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c
index 5dc3308770e..d68fe0df03e 100644
--- a/hw/usb/host-libusb.c
+++ b/hw/usb/host-libusb.c
@@ -48,6 +48,7 @@
#include "qapi/error.h"
#include "migration/blocker.h"
#include "migration/cpr.h"
+#include "migration/misc.h"
#include "migration/vmstate.h"
#include "monitor/monitor.h"
#include "qemu/error-report.h"
@@ -122,6 +123,9 @@ struct USBHostDevice {
/* CPR migration */
Error *cpr_blocker;
+ VMChangeStateEntry *cpr_vmse;
+ bool cpr_drained;
+ bool cpr_replay_all;
/* request queues */
QTAILQ_HEAD(, USBHostRequest) requests;
@@ -405,7 +409,10 @@ static void usb_host_req_free(USBHostRequest *r)
#ifdef CONFIG_WIN32
request_count--;
#endif
- QTAILQ_REMOVE(&r->host->requests, r, next);
+ /* Skip if a timed-out reap already unlinked it (avoid a double remove) */
+ if (QTAILQ_IN_USE(r, next)) {
+ QTAILQ_REMOVE(&r->host->requests, r, next);
+ }
libusb_free_transfer(r->xfer);
g_free(r->buffer);
g_free(r);
@@ -423,6 +430,25 @@ static USBHostRequest *usb_host_req_find(USBHostDevice *s, USBPacket *p)
return NULL;
}
+/*
+ * Classify a reap during the CPR drain (r->p is still set). One that moved no
+ * data is suppressed for the target to replay; one that moved some is kept as a
+ * short transfer. Returns true if suppressed, so the caller frees the request.
+ */
+static bool usb_host_req_cpr_replay(USBHostRequest *r,
+ struct libusb_transfer *xfer)
+{
+ if (xfer->status != LIBUSB_TRANSFER_CANCELLED) {
+ return false;
+ }
+ if (xfer->actual_length != 0 && !r->host->cpr_replay_all) {
+ /* deliver as a short transfer */
+ xfer->status = LIBUSB_TRANSFER_COMPLETED;
+ return false;
+ }
+ return true;
+}
+
static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
{
USBHostRequest *r = xfer->user_data;
@@ -432,6 +458,9 @@ static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer)
if (r->p == NULL) {
goto out; /* request was canceled */
}
+ if (usb_host_req_cpr_replay(r, xfer)) {
+ goto out; /* CPR drain: the target will replay this packet */
+ }
r->p->status = status_map[xfer->status];
r->p->actual_length = xfer->actual_length;
@@ -482,6 +511,9 @@ static void LIBUSB_CALL usb_host_req_complete_data(struct libusb_transfer *xfer)
if (r->p == NULL) {
goto out; /* request was canceled */
}
+ if (usb_host_req_cpr_replay(r, xfer)) {
+ goto out; /* CPR drain: the target will replay this packet */
+ }
r->p->status = status_map[xfer->status];
if (r->in && xfer->actual_length) {
@@ -502,22 +534,29 @@ out:
}
}
-static void usb_host_req_abort(USBHostRequest *r)
+/* Fail r's in-flight packet with an error status and detach it from r. */
+static void usb_host_req_complete_err(USBHostRequest *r, int status)
{
- USBHostDevice *s = r->host;
- bool inflight = (r->p && r->p->state == USB_PACKET_ASYNC);
+ USBHostDevice *s = r->host;
- if (inflight) {
- r->p->status = USB_RET_NODEV;
- trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
- r->p->status, r->p->actual_length);
- if (r->p->ep->nr == 0) {
- usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
- } else {
- usb_packet_complete(USB_DEVICE(s), r->p);
- }
- r->p = NULL;
+ if (!r->p || r->p->state != USB_PACKET_ASYNC) {
+ return;
+ }
+ r->p->status = status;
+ trace_usb_host_req_complete(s->bus_num, s->addr, r->p,
+ r->p->status, r->p->actual_length);
+ if (r->p->ep->nr == 0) {
+ usb_generic_async_ctrl_complete(USB_DEVICE(s), r->p);
+ } else {
+ usb_packet_complete(USB_DEVICE(s), r->p);
+ }
+ r->p = NULL;
+}
+static void usb_host_req_abort(USBHostRequest *r)
+{
+ if (r->p && r->p->state == USB_PACKET_ASYNC) {
+ usb_host_req_complete_err(r, USB_RET_NODEV);
libusb_cancel_transfer(r->xfer);
}
}
@@ -1098,9 +1137,18 @@ fail:
return -1;
}
-static void usb_host_reap_xfers(USBHostDevice *s)
+/* libusb still owns cancelled-but-unreaped transfers: unlink, don't free. */
+static void usb_host_drop_requests(USBHostDevice *s)
{
USBHostRequest *r, *rtmp;
+
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ QTAILQ_REMOVE(&s->requests, r, next);
+ }
+}
+
+static void usb_host_reap_xfers(USBHostDevice *s)
+{
int limit = 100;
while (QTAILQ_FIRST(&s->requests) != NULL) {
@@ -1110,15 +1158,10 @@ static void usb_host_reap_xfers(USBHostDevice *s)
libusb_handle_events_timeout(ctx, &tv);
if (--limit == 0) {
/*
- * Don't wait forever for libusb calling the complete
- * callback (which will unlink and free the request).
- *
- * Leaking memory here, to make sure libusb will not
- * access memory which we have released already.
+ * Don't wait forever for libusb to call the complete callback
+ * (which unlinks and frees the request).
*/
- QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
- QTAILQ_REMOVE(&s->requests, r, next);
- }
+ usb_host_drop_requests(s);
return;
}
}
@@ -1134,6 +1177,105 @@ static void usb_host_abort_xfers(USBHostDevice *s)
usb_host_reap_xfers(s);
}
+static bool usb_host_iso_inflight(USBHostDevice *s)
+{
+ USBHostIsoRing *ring;
+
+ QTAILQ_FOREACH(ring, &s->isorings, next) {
+ if (!QTAILQ_EMPTY(&ring->inflight)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/*
+ * Quiesce the device for a CPR switchover. Cancel and reap all URBs,
+ * so that no URB submitted by this process can outlive it.
+ */
+static int usb_host_cpr_drain_xfers(USBHostDevice *s)
+{
+ USBHostRequest *r, *rtmp;
+ USBHostIsoRing *ring;
+ USBHostIsoXfer *xfer;
+ int limit = 100;
+
+ s->cpr_drained = true;
+
+ /*
+ * Cancel and reap every URB. Completing a reaped URB can make xHCI queue
+ * the next TD, so re-cancel whatever reappears until the queue stays empty.
+ */
+ while (!QTAILQ_EMPTY(&s->requests)) {
+ struct timeval tv = { .tv_usec = 2500 };
+
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ libusb_cancel_transfer(r->xfer);
+ }
+ libusb_handle_events_timeout(ctx, &tv);
+ if (--limit == 0) {
+ /*
+ * The device won't release these URBs. Error the packets out so
+ * the resumed guest retries them, leak the requests (libusb still
+ * owns their transfers) and abort the migration.
+ */
+ QTAILQ_FOREACH_SAFE(r, &s->requests, next, rtmp) {
+ usb_host_req_complete_err(r, USB_RET_IOERROR);
+ }
+ usb_host_drop_requests(s);
+ return -1;
+ }
+ }
+
+ /*
+ * Iso URBs are on the rings, not s->requests. Cancel them: one in
+ * flight when the guest stops may never complete on its own, and none
+ * may be left on the preserved fd. The target replays from the ring.
+ */
+ QTAILQ_FOREACH(ring, &s->isorings, next) {
+ QTAILQ_FOREACH(xfer, &ring->inflight, next) {
+ libusb_cancel_transfer(xfer->xfer);
+ }
+ }
+
+ /*
+ * Cap the reap at 2x a full ring:
+ * iso_urb_count URBs x iso_urb_frames packets each
+ */
+ limit = 2 * s->iso_urb_count * s->iso_urb_frames;
+ while (usb_host_iso_inflight(s)) {
+ struct timeval tv = { .tv_usec = 1000 };
+ libusb_handle_events_timeout(ctx, &tv);
+ if (limit-- == 0) {
+ return -1;
+ }
+ }
+ usb_host_iso_free_all(s);
+ return 0;
+}
+
+/*
+ * Quiesce the URB data plane around a CPR switchover: drain when the VM stops
+ * for CPR -- before the device/irqchip snapshot, so completions raised during
+ * the drain are migrated too.
+ */
+static void usb_host_cpr_vm_state(void *opaque, bool running, RunState state)
+{
+ USBHostDevice *s = opaque;
+ MigMode mode;
+
+ if (running || !s->dh || state != RUN_STATE_FINISH_MIGRATE) {
+ return;
+ }
+ mode = migrate_mode();
+ if (mode != MIG_MODE_CPR_EXEC && mode != MIG_MODE_CPR_TRANSFER) {
+ return;
+ }
+ if (usb_host_cpr_drain_xfers(s) < 0) {
+ migration_file_set_error(-EIO, NULL);
+ }
+}
+
static int usb_host_close(USBHostDevice *s)
{
USBDevice *udev = USB_DEVICE(s);
@@ -1301,6 +1443,8 @@ static void usb_host_realize(USBDevice *udev, Error **errp)
s->exit.notify = usb_host_exit_notifier;
qemu_add_exit_notifier(&s->exit);
+
+ s->cpr_vmse = qemu_add_vm_change_state_handler(usb_host_cpr_vm_state, s);
}
static void usb_host_instance_init(Object *obj)
@@ -1323,6 +1467,9 @@ static void usb_host_unrealize(USBDevice *udev)
}
usb_host_close(s);
migrate_del_blocker(&s->cpr_blocker);
+ if (s->cpr_vmse) {
+ qemu_del_vm_change_state_handler(s->cpr_vmse);
+ }
}
static void usb_host_cancel_packet(USBDevice *udev, USBPacket *p)
@@ -1791,10 +1938,8 @@ static int usb_host_post_load(void *opaque, int version_id)
USBHostDevice *dev = opaque;
/*
- * For CPR migration, device wasn't released/reset, and the guest
- * is unaware of the switchover. The detach/rescan performed in
- * usb_host_post_load_bh() only exists for cross-host migration.
- * Skip it for CPR.
+ * CPR keeps the device open and the guest unaware, so skip the detach and
+ * rescan in usb_host_post_load_bh() -- those are for cross-host migration.
*/
if (cpr_is_incoming()) {
return 0;
@@ -1809,10 +1954,31 @@ static int usb_host_post_load(void *opaque, int version_id)
return 0;
}
+static int usb_host_pre_save(void *opaque)
+{
+ USBHostDevice *s = opaque;
+ MigMode mode = migrate_mode();
+
+ /*
+ * The vm-state handler drains on the stop-for-CPR transition, but a VM
+ * already paused never hits it. Drain here instead; the stopped guest
+ * can't take completions, so suppress every URB and let the target replay.
+ */
+ if ((mode == MIG_MODE_CPR_EXEC || mode == MIG_MODE_CPR_TRANSFER) &&
+ s->dh && !s->cpr_drained) {
+ s->cpr_replay_all = true;
+ if (usb_host_cpr_drain_xfers(s) < 0) {
+ return -1;
+ }
+ }
+ return 0;
+}
+
static const VMStateDescription vmstate_usb_host = {
.name = "usb-host",
.version_id = 1,
.minimum_version_id = 1,
+ .pre_save = usb_host_pre_save,
.post_load = usb_host_post_load,
.fields = (const VMStateField[]) {
VMSTATE_USB_DEVICE(parent_obj, USBHostDevice),
--
2.47.1
next prev parent reply other threads:[~2026-10-06 10:34 UTC|newest]
Thread overview: 26+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-06 10:34 [PATCH QEMU HCI-8.0 v6 00/12] usb-host: support CPR migration Andrey Drobyshev
2026-10-06 10:34 ` [PATCH QEMU HCI-8.0 v6 01/12] usb-host: don't leak hostdev FD on open failure #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:37 ` no-reply
2026-10-06 10:34 ` [PATCH QEMU HCI-8.0 v6 02/12] usb-host: add migration blocker for CPR modes #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:36 ` no-reply
2026-10-06 10:34 ` [PATCH QEMU HCI-8.0 v6 03/12] usb-host: preserve hostdev FD during CPR migration #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:40 ` no-reply
2026-10-06 10:34 ` [PATCH QEMU HCI-8.0 v6 04/12] usb-host: factor out usb_host_reap_xfers() #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:38 ` no-reply
2026-10-06 10:34 ` Andrey Drobyshev [this message]
2026-10-06 10:48 ` [PATCH QEMU HCI-8.0 v6 05/12] usb-host: drain in-flight URBs across CPR #VSTOR-137800 no-reply
2026-10-06 10:34 ` [PATCH QEMU HCI-8.0 v6 06/12] usb-host: re-issue drained URBs if CPR is aborted #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:46 ` no-reply
2026-10-06 10:34 ` [PATCH QEMU HCI-8.0 v6 07/12] usb-host: skip product-string read on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:47 ` no-reply
2026-10-06 10:34 ` [PATCH QEMU HCI-8.0 v6 08/12] usb-host: hand off the device across cpr-transfer #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:54 ` no-reply
2026-10-06 10:34 ` [PATCH QEMU HCI-8.0 v6 09/12] usb: migrate the interface altsetting #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:52 ` no-reply
2026-10-06 10:34 ` [PATCH QEMU HCI-8.0 v6 10/12] usb-host: reconfigure endpoints on CPR incoming #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:57 ` no-reply
2026-10-06 10:34 ` [PATCH QEMU HCI-8.0 v6 11/12] hcd-xhci: resync isochronous endpoints after CPR #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:57 ` no-reply
2026-10-06 10:34 ` [PATCH QEMU HCI-8.0 v6 12/12] usb-host: make CPR migration blocker conditional #VSTOR-137800 Andrey Drobyshev
2026-10-06 10:58 ` no-reply
2026-10-06 11:49 ` [PATCH QEMU HCI-8.0 v6 00/12] usb-host: support CPR migration Andrey Drobyshev
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=20261006-usb-host_v3-v6-5-8688ec9576d1@virtuozzo.com \
--to=andrey.drobyshev@virtuozzo.com \
--cc=den@openvz.org \
--cc=svt-core@virtuozzo.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