From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from GVXPR05CU001.outbound.protection.outlook.com (mail-swedencentralazon11023102.outbound.protection.outlook.com [52.101.83.102]) by lore.virtuozzo.com (Postfix) with ESMTPS id 55A69807D3 for ; Tue, 6 Oct 2026 16:28:02 +0000 (UTC) ARC-Seal: i=1; a=rsa-sha256; s=arcselector10001; d=microsoft.com; cv=none; b=RXwowfkDOszB9trw6c7Hn4uOPEl3caPun/0fxsgTrCpT0IXCdcalS4gCw6FLosGwo6GCBDlXeG0e/jADg0a5WcvGBwXAGmG3It+4liMueK/PEfptT5HYBNvZ4tuCYSwdFkPvDLlS0VL1djj4kzqyqXGE3JZrOlni/g1qAqG1nsedlubAIuEUGMdMI6H0XJh6w8HPsQ0DdbMHgjilAMPod05EBu8USeHA9SzSj0MPl1I042YEK4AONMtt2yL6Q3VhnwYKDWRnUJ04YuUbqQptn0fAZ45ZMi4ANy4afnQuQAApdMZ7Bz9jntW3ZhrGbGkrEXuxADyYsoTzq9s3Uc8zeQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; s=arcselector10001; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; bh=z1K3VVWT9P3QWQ/EUGRaCLGDODV5mxH5cFDmOfxpZNc=; b=iyT1eUMQ2acQ6PW4xs73BrICla0dTuCf8g6JCyFEPxE3aDkhMEDdIHHI7ZVYCpHcPEw37mvwQPBq56yeKnLsAaZg6KI5am7LgL08G96oaHw0e4iEbeUXG2gLC8h6Wp6icds8x4HYF/NvdCadyS5XFNdntCUpT07VYv8YNr0GYqQ5WTpbxpFl+fyZ0ubb9oF4ZGY0lhWrXLOLalTE5JPkZHy9MVZx6BKLf8RTe7aJZTxsFlXyP10cOu2KYCK+0FzozeyYUd3ue0HJJKKw0rqF4nT8TGHmbQq895W0FKM6dXjLBi8V09EyG1g8uNrxcQgBp0buHE5trc0ovbv3wm1IVg== ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=pass smtp.mailfrom=virtuozzo.com; dmarc=pass action=none header.from=virtuozzo.com; dkim=pass header.d=virtuozzo.com; arc=none DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=virtuozzo.com; s=selector2; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-SenderADCheck; bh=z1K3VVWT9P3QWQ/EUGRaCLGDODV5mxH5cFDmOfxpZNc=; b=wiQGwebLy5MnNkxYHpghJvFl+a4bYJTfbAO3mNW0PtbgWxZ+BGLZVK14NWZzDAPmzR7kxrP5Lv563/7xiqt0zQl5IXio8Z2Vf/FZfkpmVPGZ+0h5fDzz0o6Qai0tMEii29idsZRn6FYIZEkpGcMxnDKWLsC63I/qoAcyo+hZWithlQ2hBUNvsTHIKJXB/SMVKnk+sxF05FUd3NTs4OhZbWQTyL7OUwsOHb9ahNxfj69Uc6SdyOCtzyjtVb18BZd3iEJM7RJlpA/yRQK7doam289H2h1yd2728lRMZmuww2F3QR0dNidWRLYd4JUJcWqkCD4C9cMhdlTFxfgYuG7FHQ== Authentication-Results: mx.microsoft.com 1; dkim=none (message not signed) header.d=none;dmarc=none action=none header.from=virtuozzo.com; Received: from VI0PR08MB10656.eurprd08.prod.outlook.com (2603:10a6:800:20a::12) by AS8PR08MB8899.eurprd08.prod.outlook.com (2603:10a6:20b:5b6::19) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.21.496.14; Tue, 6 Oct 2026 16:27:59 +0000 Received: from VI0PR08MB10656.eurprd08.prod.outlook.com ([fe80::4e37:b189:ddcd:3dd8]) by VI0PR08MB10656.eurprd08.prod.outlook.com ([fe80::4e37:b189:ddcd:3dd8%7]) with mapi id 15.21.0496.010; Tue, 6 Oct 2026 16:27:59 +0000 From: Andrey Drobyshev Date: Tue, 06 Oct 2026 19:27:57 +0300 Subject: [PATCH QEMU HCI-8.0 v8 06/14] usb-host: drain in-flight URBs across CPR #VSTOR-137800 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 7bit Message-Id: <20261006-usb-host_v3-v8-6-72ae4fb8977b@virtuozzo.com> References: <20261006-usb-host_v3-v8-0-72ae4fb8977b@virtuozzo.com> In-Reply-To: <20261006-usb-host_v3-v8-0-72ae4fb8977b@virtuozzo.com> To: svt-core@virtuozzo.com Cc: andrey.drobyshev@virtuozzo.com, den@openvz.org X-Mailer: b4 0.16.0 X-ClientProxiedBy: VI1PR08CA0230.eurprd08.prod.outlook.com (2603:10a6:802:15::39) To VI0PR08MB10656.eurprd08.prod.outlook.com (2603:10a6:800:20a::12) MIME-Version: 1.0 X-MS-PublicTrafficType: Email X-MS-TrafficTypeDiagnostic: VI0PR08MB10656:EE_|AS8PR08MB8899:EE_ X-MS-Office365-Filtering-Correlation-Id: 5b6500ff-2e1f-472a-1519-08df23c6c78d X-LD-Processed: 0bc7f26d-0264-416e-a6fc-8352af79c58f,ExtAddr List-Id: svt-core@virtuozzo.com X-MS-Exchange-Transport-Rules-Loop: 1 X-MS-Exchange-SenderADCheck: 1 X-MS-Exchange-AntiSpam-Relay: 0 X-Microsoft-Antispam: BCL:0;ARA:13230040|1800799024|366016|52116014|376014|23010399003|5113699003|22082099003|18002099003|38350700014|56012099006|10067099003|5023799004; X-Microsoft-Antispam-Message-Info: rtAl2hjxG140kspbFPFvi0Ah9wKGZS4yqtUlqRciZ7XOfWNbLaA0twnGK0f1TahQlzh98JzhQ+51YmLsUzWhZkoY64XHgjzrkwvhSbxjlt78Xqzp+0ghZjsZgGvX7tr1RUKaSRHgOVU+U/dmLDDd9rNB7WHE3sz7VR8LcAEHGGNy0YdFi7UtxwQSGpc5r8WbolRQArxeF9RIzaKseCAzV8k5hads2DMfn5Kazpybd301sllnq7AifWat6TAWeVV7f+PSEMgnb4ll0FthDJzmS/hgSpAVsCtugRECifOHOY86RUWLCE9wILcb/hMSb8GlG3DNnEWxksMz/2mCC7W4wjvXFdk5w9ZBpYxRU00KrrU51SJ4wRTZNnDa8SpCaPF+x5P8t1iiXLNgUQzOTAN5RC4XNF+tM58Yr6EdLJoxJde7lWxmiGSKddHXYtercsuf4t+kjOMRkmm0W1p24jmwe42zwd39tj/r6I/9J727XV8hOzQnZtlepn55uoSTYtiZ+ou7K7XvXIf4tHpOAbFXFySNxE/TZwkY/MT1jcYHBJQGE3O8te1j+tPzF+A9CZD6/0Bl/XcWrynjJfwrIv+E/W1dgdpF/GV54AvWlPM+Nj2BbPGu2x8RnmkuvUx3nFsV/8hmxHHyFZCMnQCPJT9pqCfcbhvRylUjU33jxkkkkb1cp8EJ1OBCXofroWYRAYvtz0Wt7YE/LIgz9GuUd6pKiLMthf1Ww+qyCAChbAnlVZM= X-Forefront-Antispam-Report: CIP:255.255.255.255;CTRY:;LANG:en;SCL:1;SRV:;IPV:NLI;SFV:NSPM;H:VI0PR08MB10656.eurprd08.prod.outlook.com;PTR:;CAT:NONE;SFS:(13230040)(1800799024)(366016)(52116014)(376014)(23010399003)(5113699003)(22082099003)(18002099003)(38350700014)(56012099006)(10067099003)(5023799004);DIR:OUT;SFP:1102; X-MS-Exchange-AntiSpam-MessageData-ChunkCount: 1 X-MS-Exchange-AntiSpam-MessageData-0: =?utf-8?B?b1VnSHJZT0RZOUVmeFdZeFB5RVRkckxhLzY3S0pYYWE3QU1OcHB4R2hnMEUx?= =?utf-8?B?YzNBOEdac2hNUzl6MHJ0L2tLT0NPWmJuWHFzVUwzV1lzWnR1UVRoVFJ3UFlw?= =?utf-8?B?Q0NyRERkekQ0T3YvcnVQR1pNTUFsYktrSXkzSjRhZ0hGbm5selIzZXdKb2lY?= =?utf-8?B?K2cvb1hVZDVwZkZPcWlad2pqOXRxMzkrWWY2emthMFFjdzByM0dBK3BJdlpZ?= =?utf-8?B?TlFhcldTcEsvMWF5T0FycXk2UzZTcXROaVh0ZEtic3Z5WUpHd0lNSnNTNnQ0?= =?utf-8?B?aVI1bUEybTFWSU85NndqcXEzbGNGWWtSOXBCQW9veExrTGk2MTR0dnhiQ2Qx?= =?utf-8?B?ZmRkYWVYbWQ4UTRUMXl2c0Z1SFdsZDBNZ0NZai92QlpBNGttR0JnK2h1L2tX?= =?utf-8?B?cWN5dElib1BFN2xLWU5HUjVqWFNjbys4SUx1bkN2TXVyV1hYMGZ4b3dSdmV4?= =?utf-8?B?RU5HVXJ3WFFzcWRFbUpCTm1YZDNpaUhjUGRaVVlRdFJEYnBRc2MvWFJSUFEw?= =?utf-8?B?dWUrMmROa0o0MDhXWnh4TWx4M0srOXVhL0dTNlhqSEk0cGI4ekROTzZFeU9i?= =?utf-8?B?aFllTEYyVGgrcFlzTHIwVkJiOFRtR25HMDZsbTZtcGpIU1Q2djRCd3dDeG5v?= =?utf-8?B?ZllucE9xRVJHZnpqSFIrY3NoL0E5U2cwL25sNHI3VXhJMzlNV0RIU0JlUlNl?= =?utf-8?B?WERpK3BkY1ZYNDcvRUwrR1lVeWphUy9HZFUya0xxdW5qZ2hDZk9DdDlZOE05?= =?utf-8?B?ZHAxc0ZiSVBDTjFjaVl2S1NhY1lnMDhHWUhGQUxscHZUZDhZZEZwbUhaSWFF?= =?utf-8?B?ZlYrdk0wbnF0ZFpaR3RsYWdKcVJ0U2tsNDBIb1JYQzBZOVNZVStlOW04bjZG?= =?utf-8?B?a3hjUXpkaWUrdDZuVWdtNW9UclUvdzRTR2NQS0c0MVBFcllZOGxqWTg4cG1V?= =?utf-8?B?YWJmS0tVMkd0K0tOWURUVUNtdWNHUDhmeTMxRFJOQkJuMmFNYjA4M0JnbVk1?= =?utf-8?B?dDFrZGlYcWVGMUJ2TTZ5REwweU1ManhlS3hxOHRmNEVJdHFEcUN3QzJ5Mmw3?= =?utf-8?B?cWxVaEtIb05ubWFqT1hEaUVpN1Z4ZS94QmQ3bERqV3NKUG1EMDlWNzM0RnA0?= =?utf-8?B?Mk11K0VoQWpkamk2NndDKzdyaG45NVNxWE8yeU5vZFpXcjhGM1dQdkNlVTd3?= =?utf-8?B?S0d0T3RuakY4Q0JmRTN2Vzd6RWJwTWJlWi9MNTE3cG4xaTFsbzN1dWRzLzV2?= =?utf-8?B?dmhIaG5aMjJXU1FvM3BOenljTStGVER4NURMczZIS0VTRy8zVTdtTXZuQ3M4?= =?utf-8?B?Q004NEJCQ29OczlWaCs2Mnk5aHZxMG54TGtuazZNZnVWeXkzSFBIY081SEFP?= =?utf-8?B?V0Nvc050a2RxS3dYeVJvclFuRy9zN3pGemFSdkwydGM5YkplVklITG5jNGEr?= =?utf-8?B?bVVMbzVVNy9FWlQxbUpiUmVNbHZiT0twdHpROE1Mb04xYTBCalRxSGdrQTZV?= =?utf-8?B?VnhyU2l2WTd2SmpHUU10dXMzUlkzQ0ZPcGJhcUZJTzQrWGpZUEhPRHlCUW1H?= =?utf-8?B?V21DaEVSSW84eHRwbUhzdG9lK0tVSDQ1Vm1uTHpiRlQyaENKaGJKeDVNYXR6?= =?utf-8?B?aE00eFhXRFBKelJWOE41MmZCWjVoSi9KY2svYjJVS2c3N1FSUCs5aVBWUFJX?= =?utf-8?B?WUVNQXFiTXdYbFN0Q2wzdEVZM1dSQkdOaVBRSUFiRnRHVGtOekdoU3IyaW9t?= =?utf-8?B?MUw3YzBrSzlrM1AwVG5aZTludGx5Y2ZjU1E4TTJiSko1a2pDbmJxMi9CMUJ1?= =?utf-8?B?MWIyL1MzNjdkVmpKWXJnMlNYYTN0eCtrM1dQRDNNMW0yWmdRWHB6WlJYeEZm?= =?utf-8?B?QnFUallGOGlSd0NRcEpRa1daaTRmWm1vaEhXSVFHQTJ0S0lZei9WeGZkRXlt?= =?utf-8?B?YTJYbE1FN05SbVJMUWluL0JlVFQ0dTdpOVJkMjhjcis2ZXEyYTdackRpd0p4?= =?utf-8?B?NktUdXg3WGg3YUhBS2hIbDNhSWpCWGFBS1YzQTVJeUZlN0lhTmJ3d1RPRmZj?= =?utf-8?B?L2ZIQnQ3cUE1d3kwemRDdy9reU9YSVN5TWNrbFNVNWNnVXUwb1J1SjQ1VUtR?= =?utf-8?B?V0ZZZHowN2dFcllBTHAvRUtKbVdhT011cHN1bkNsNjhmZ3RxK2dDakc0TG01?= =?utf-8?B?ZFU1akdZZk8wVVgyTUFKL0Q2dWtTVVV3ejhJclRCR1NCMXg2VjNDM3lWSTVU?= =?utf-8?B?Umw5NElLTW5VeXB1blRabHJjcGRsbm1JV09FRCtlSys4WExHeEtFRXhTR2x0?= =?utf-8?B?Q3NwTmg1V0QxOHNGZnJKV20wdktIdjErdUVLMVM3dy9pVGRhZXh5YmRUc3cz?= =?utf-8?Q?jiw+WF0+PHRTLSqk=3D?= X-OriginatorOrg: virtuozzo.com X-MS-Exchange-CrossTenant-Network-Message-Id: 5b6500ff-2e1f-472a-1519-08df23c6c78d X-MS-Exchange-CrossTenant-AuthSource: VI0PR08MB10656.eurprd08.prod.outlook.com X-MS-Exchange-CrossTenant-AuthAs: Internal X-MS-Exchange-CrossTenant-OriginalArrivalTime: 06 Oct 2026 16:27:56.9613 (UTC) X-MS-Exchange-CrossTenant-FromEntityHeader: Hosted X-MS-Exchange-CrossTenant-Id: 0bc7f26d-0264-416e-a6fc-8352af79c58f X-MS-Exchange-CrossTenant-MailboxType: HOSTED X-MS-Exchange-CrossTenant-UserPrincipalName: /Alklbre2nczSq9DoRnoEM6bG16VNKQqBg2xUxgWb54JbrfOUwz7XALffPJMqcMlBf0/HEDgBYyaRNv/6pGqbJSxEy9rsU8H7IpxRfMHGbQ= X-MS-Exchange-Transport-CrossTenantHeadersStamped: AS8PR08MB8899 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 --- hw/usb/host-libusb.c | 249 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 222 insertions(+), 27 deletions(-) diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c index 1a79cdf9e4d..413ddc12ee4 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,41 @@ static USBHostRequest *usb_host_req_find(USBHostDevice *s, USBPacket *p) return NULL; } +/* + * Classify a reap during the CPR drain (r->p is still set). The paused-VM + * fallback (cpr_replay_all) can't deliver completions, so it suppresses every + * reap for the target to replay; otherwise one that moved no data is suppressed + * and 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 (!r->host->cpr_drained) { + /* no CPR drain in progress: normal completion */ + return false; + } + if (!r->host->cpr_replay_all) { + if (xfer->status != LIBUSB_TRANSFER_CANCELLED) { + return false; + } + if (xfer->actual_length != 0) { + /* deliver as a short transfer */ + xfer->status = LIBUSB_TRANSFER_COMPLETED; + if (!r->in) { + /* + * complete_data() only trims the length for IN; for OUT + * report what the device actually consumed rather than + * the full submit-time size. + */ + r->p->actual_length = xfer->actual_length; + } + return false; + } + } + return true; +} + static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer) { USBHostRequest *r = xfer->user_data; @@ -432,6 +474,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 +527,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 +550,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 +1153,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 +1174,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 +1193,118 @@ 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. + * Reaping an iso IN URB wakes the endpoint, which can make xHCI submit + * fresh iso URBs mid-drain, so re-cancel here on every iteration too. + * + * 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 }; + + QTAILQ_FOREACH(ring, &s->isorings, next) { + QTAILQ_FOREACH(xfer, &ring->inflight, next) { + libusb_cancel_transfer(xfer->xfer); + } + } + 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 the in-flight + * transfers before the device/irqchip snapshot, so completions raised during + * the drain are migrated too. + */ +static int usb_host_cpr_quiesce(USBHostDevice *s, MigMode mode) +{ + if (usb_host_cpr_drain_xfers(s) < 0) { + return -1; + } + return 0; +} + +/* + * Run the CPR quiesce from the vm-state handler, on the stop-for-CPR + * transition before the device/irqchip snapshot. + */ +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_quiesce(s, mode) < 0) { + migration_file_set_error(-EIO, NULL); + } +} + static int usb_host_close(USBHostDevice *s) { USBDevice *udev = USB_DEVICE(s); @@ -1301,6 +1472,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 +1496,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) @@ -1792,10 +1968,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; @@ -1810,10 +1984,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_quiesce(s, mode) < 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