From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from MRWPR03CU001.outbound.protection.outlook.com (mail-francesouthazon11021082.outbound.protection.outlook.com [40.107.130.82]) by lore.virtuozzo.com (Postfix) with ESMTPS id 84CF2807A7 for ; Tue, 6 Oct 2026 10:34:42 +0000 (UTC) ARC-Seal: i=1; a=rsa-sha256; s=arcselector10001; d=microsoft.com; cv=none; b=K/8U5Il92idbtY2Vz5NTqlm3XNt+Avo3RceYEck+oBaNzrq+QQd8KdPgQL0Y2YojPsR0TMOfwcJu00JdUPoHDhXs6rUgdzZyvo+cmxDzHbrDXHKso9O4RIKfAH2450Ni3lNw0L8fwX16BQy0DQFjhPzU5htlGq1F7EgQ7rTR+nRoEzQ4apNbeMQrxbl9XS3d9JrACWvyA++JXhFf+UEPphu+naeiENfML6KNMxTnar5ReIFQHaGzPq6bjO8taP954k8zQwfJqR7KznIu2vnE5K0zbZPedXft9BBH7k0DV2+LsJW41DQrI3cph25yEKeXTDVWMM/vMMXYs7MFgjYcIw== 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=uM3w19LR33m+fL0qLEwj+YfDJK/2bJuKvjiUJZQdxyw=; b=nZAftiGGn8vWhooHohhZfrBYRFWzqHvfKifyHgAELi3q38DiAyFI33SIgnyy59rI+eSD1FRt2hXqYKuTR0xzhGFXN2EOJn8DdaSDZ+IRBrbMdfLaVVzwl/U6Z1eff+jP8qc9Pc3G3TM0hTxeysRoCizXKGnifhM+9Mes6g05HvnUCeK8b3zQ3kFKUIaiVUQXNNCEoZiydfcATBJqk1V9sFZ0k2TrQreOjfrgEiV6/+eV7qZ/ZHP0eAmvnyx2MqhIPfDbcWyG8u+e75o5adf7IIHO8WMl2aQ3Z5cTQUikOlqlgaQX/TFA8EJNWm6UrjWX/BuaGyscZquESr4jGSAi8g== 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=uM3w19LR33m+fL0qLEwj+YfDJK/2bJuKvjiUJZQdxyw=; b=vJsJO6kGXX4cuwYXrtjobMRXOFqPXmsbBn5RHqnVb0fSdRDuUWnULiYHW1LefP9oGiAqanuAF2LDlkPoB496aBd92lbpFcoe5mTKmDY+OxRlPicqAEOp+uXSYHpFowyLauZlwNbkHRB9kfw7BPd/dlLybH8v3gHkg34qz5dxtCmLhSYDu7jNTMQveJNk/rtDYkNu5VoCSMevGjEDfzNlqpxnCRQU18uORhblA4ERSYEaRejNpeOrZhHoIdoxXEeu5P2IjgamNDbEwnVoblc+f3xiP0rIhpfHrfo5gDCUpekeTnUtgDTBE8bHmRdD7RGoKp2G0TRjfcdvCCAp2Ff0yg== 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 GV1PR08MB8692.eurprd08.prod.outlook.com (2603:10a6:150:86::16) 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 10:34:36 +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 10:34:36 +0000 From: Andrey Drobyshev Date: Tue, 06 Oct 2026 13:34:29 +0300 Subject: [PATCH QEMU HCI-8.0 v6 05/12] 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-v6-5-8688ec9576d1@virtuozzo.com> References: <20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com> In-Reply-To: <20261006-usb-host_v3-v6-0-8688ec9576d1@virtuozzo.com> To: svt-core@virtuozzo.com Cc: andrey.drobyshev@virtuozzo.com, den@openvz.org X-Mailer: b4 0.16.0 X-ClientProxiedBy: VI1PR09CA0115.eurprd09.prod.outlook.com (2603:10a6:803:78::38) To VI0PR08MB10656.eurprd08.prod.outlook.com (2603:10a6:800:20a::12) MIME-Version: 1.0 X-MS-PublicTrafficType: Email X-MS-TrafficTypeDiagnostic: VI0PR08MB10656:EE_|GV1PR08MB8692:EE_ X-MS-Office365-Filtering-Correlation-Id: d679ff42-025e-478f-8b61-08df23956932 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|52116014|376014|23010399003|1800799024|366016|10067099003|56012099006|5113699003|5023799004|22082099003|18002099003|38350700014; X-Microsoft-Antispam-Message-Info: AIu2BWoa6Xan+smGBJxSeLhE7fyuVyRY5RUsj4nTitGRSrAmLNUy+VA/gQw9MtRFugX6iQkwt51uO0gDXJFelLpakJiIue/viGqxfoSEjUZGwI1d6uBtic7HC5oQunueZJVqxVA/eDblRpQOkwe7PUkNTJSKB8QVzHtIkYEAHaT4mSQ+w9/KnFHaqHzvmxe3d725hv7GHaqq7ZklMCWZ9iYzUAZt2gsylppfIKClgdGw14cCOiwcWMJE1n2wHB/epnxafmL97+2h0WsA4o83g/+a/FODUOf2aYnkZhfzukATEa+LrHsoKwA2DYsdUw5jukMNBzDiM22zw9LSNAyTtf823DGhmDtXs6TQtmBRA9hJay5GY94JG6K8D/vmaH/L6f/rKxRL9uUv7sXyOAMt8cNANJCYzfwgVHMPfKi7gRb22vKA7MS4/7v7YrQTyFOVceNGabLteT1bciixbq3qsW6ITX/zqKhFkNFhG4s4F9WnmbZ4zGID0T8dyLKSqwVhtCVO6vpApRL1h7SAcoXAas6/jtqs1N6+13gsNO5wm184CTv/WVFALUaNsC2A+7uIkJdENZjeMGh1HMMeJJGdDttUh0oBAAoeGb6FtwDe/zn5803kJZHqOKswHQHlfth6LUGiPhAtRIZVMDoPokh6QtMp6sgG4npJ0Z88cNwUDkR0gt8s2J0aloD/M7Sxh0WhbulQC+EjQQvC24+jHERGAg+x/GAbAUpsF18OrnNPxVo= 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)(52116014)(376014)(23010399003)(1800799024)(366016)(10067099003)(56012099006)(5113699003)(5023799004)(22082099003)(18002099003)(38350700014);DIR:OUT;SFP:1102; X-MS-Exchange-AntiSpam-MessageData-ChunkCount: 1 X-MS-Exchange-AntiSpam-MessageData-0: =?utf-8?B?enJiQmE0UDM3UHltdG9oUjExL0htNXY3a3ppVGNMRnVTTEl1NGpRRjJVcmRN?= =?utf-8?B?NGI5Q2lCbGJYaTVUQVRHam9rWFFwdHR5cUVBZjJkSEIySzFFZXhuT0tZWUZ0?= =?utf-8?B?V2pobUVld0ZNSmtpdmZBblZiV3NhT0hTNmdBMDF5Tk5tMERJV214Ym5ONkpt?= =?utf-8?B?eFVpM3lETVBvQjF6UHBYMTZ2TVNCNzRhNVl0YUFMU0FYK2R5bithK2RWQlB6?= =?utf-8?B?cGVkcENTZHIyem9abE1UT25JTnpucXZsVGxFVkxmbmpNSmNyb3ZVQ0FCL1ZE?= =?utf-8?B?UUtDMVczL3FRWThEVGdtS2s2SGxnS00rYnp2UjhQM2EwakNQVERKKzJzRTJH?= =?utf-8?B?L3AyemdhTTJVbFJEcWhHMDE3MXFRUVRnL3Z6SkZIb01RY1I1cHhtanFUQ2ZX?= =?utf-8?B?ekJ2VkJOa0ZJUmtnT0JLSVY3dVlUL3lFeW1JRVJMazY5N2RZcWRETWtRRE9F?= =?utf-8?B?OUlUemhWWGJMZk4xSmMxUVVoaVNsU3R5TjN5QXlnc2FEZzg3SUxQNTNBUFI4?= =?utf-8?B?WnFpYndNcVN4TndHRlhDdFpjM1V6WEpVendDRjZiMHZBYUNvTXMwMGZCNHlS?= =?utf-8?B?S1JXZG1XU0RFelFWUU1yM21Nb01kSWNlaHl4emNieStmM3hQTmhPMTRyVVlr?= =?utf-8?B?UWFvUnBSMXY5b09TOUZxUnVRVDNUWUdsSmxyS21Ma2NrK2lDdFF0c29SY2Rj?= =?utf-8?B?cThRMmRHQkxMbXNqR2d1U2d1dlVJVW40eE9EMGpJMHFiVUd1SDhqbENxN0R3?= =?utf-8?B?blYrdkdZUFZWa242S0RZUDJzcldHS1RUaUQvMGs5TVhCRDgvRE5wcldIQmFN?= =?utf-8?B?ejg5RzVxV1dSYko1UG1RbWRTMU12amxZZlFLSFVmT3dCZXBmb3RUcXU3UEZk?= =?utf-8?B?eGhubkMyM3R2eXJ2N2FRNFIvZytuR0VFbTlJbWYwUVgySThpVXZ1T2t6ZGhH?= =?utf-8?B?bmp2WE9Pa1hWc1ZVbHFFeVpwbWRVVjVMaXpaelYvOWx3LzhrKzMzZThBekw5?= =?utf-8?B?NHVOcnpHTENIdVVYbHMwQ1dTMWYvUHFjTTVOeE91TU1zalRuTzR4dk4wYWxF?= =?utf-8?B?OTh0TkYxR1I1RTFCSmtub1p6akgwaStMV1F3ZnIyUHFHMzJpUUNKM1A5QmM4?= =?utf-8?B?WHlzSmRUZWZiS0xKaUxPdU95dlROdjVTbW85NFkyL2JuYWZKKy9JTXJFQy9M?= =?utf-8?B?UlNYZjRFOHJuMFBwRXo4aldGQWY2emh2TDYrdDdHR2podmZHZEN2RXNPUzRz?= =?utf-8?B?dWdCTTJwbVlLT25MWVgrOU9helhzQ0NQbGwvUzZjMTBEdlJQaTBZYndpVDlM?= =?utf-8?B?UUVwZDRnbWRpV0k3VFZLM2JtU0w2dC9Yc05EclNIZjJES2dTSzF4WWU4WGVX?= =?utf-8?B?ODAyT3pEQ2FXV3FYR0ZjbmpxL3RjSWxDcm82ZHFLYU1LT3NmTDJJbW9OUmhw?= =?utf-8?B?VFhKME01M3l0MnpiSndkVVdDY0dQd2RVNk1ZdUtzSVk2aERnaUJlR0RtRVFC?= =?utf-8?B?blFuZ2VOUnF1Smtib2RmSDBsY29XMHdKVjhXUjZqR3R0Nmx6YlZZNTVpbHd2?= =?utf-8?B?V0Mwa3VHN1NkS1BaWXFJSDRJODJkSkh0cWYvVTVUbVgrTkRuR1ljOHN5QXFr?= =?utf-8?B?UVZpMkhOYnBTQkZFcTYzUXZIVVdHa1N4eVM0Q2xoemhhYkVqNkt6cHJ1UXpD?= =?utf-8?B?VWZDVzlmYy8vWDVTN2lwVlBVRzBHemNHU1lyYXhuVytWVVhZSjh2K2lSMzV1?= =?utf-8?B?aHZNOTI0QURpOHovQW1ucFVEUmVnSnEzd0hHRmgvRHNqU1VTdFFSb1V2dFZC?= =?utf-8?B?RmlzdjNJRmozNmJwSXN2V3o0V2liUmcwRzFDSk5lT05oRVRCZll5ZlZESGh1?= =?utf-8?B?WW5zNng1MklMMENVUDdqV25pa3lEbUkwMjByM2huUnhkc21aZ01mZU0zODFh?= =?utf-8?B?SEtvRWR3ek1LZGZCeFFwNS93eFdJSTFWeEt3OE1zbnBIZi9lWEpGeDdiUnhj?= =?utf-8?B?YjA2bkpyZ3YvWDVpOEFEUzNtd2R1OWd6ak00RVQ1VWZWcDYyaDZPWkJuMnZn?= =?utf-8?B?S1NGc2xJZy9tL2tRaXZnVVJNRHE2MXUwNDNJNHU4RmQrcjQzdlVKY0JwM1Bv?= =?utf-8?B?Rzc0L0lXTk40d21nUUpxTnFWQjQyTDlPeXlWTDV1ZXFUTzEvWVMwdVB6Zm5S?= =?utf-8?B?cWtDcWhFeXB5QmE1N1RSRm5NeGRiWmhLT284VzZMVlhDNlVtSERKYWcvMktj?= =?utf-8?B?Smc2cGRmM3pLNkw1SjNDeTNBYkVMSFJJMmQxUm1iQUQzSi9CVzU4MWRkRE9I?= =?utf-8?B?elpSdnArdU9aTE5QcndhWVBydFFEWnhRVXNYM1I1ZDc0akY1V1RVSzRiTzR1?= =?utf-8?Q?C3jBzkJ/at0E3Ce4=3D?= X-OriginatorOrg: virtuozzo.com X-MS-Exchange-CrossTenant-Network-Message-Id: d679ff42-025e-478f-8b61-08df23956932 X-MS-Exchange-CrossTenant-AuthSource: VI0PR08MB10656.eurprd08.prod.outlook.com X-MS-Exchange-CrossTenant-AuthAs: Internal X-MS-Exchange-CrossTenant-OriginalArrivalTime: 06 Oct 2026 10:34:33.3190 (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: QAhZUTu64ULC+uXoGlnG+SzWgDkN5oaVmdxgKZlzuw3hBho7tGUS/e+DcF+f/S2P1lwOPGIcOGdjbp+NqbOXyDyZ8+V6RTxqx9SRmutafzs= X-MS-Exchange-Transport-CrossTenantHeadersStamped: GV1PR08MB8692 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 | 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