From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from AM0PR83CU005.outbound.protection.outlook.com (mail-westeuropeazon11020085.outbound.protection.outlook.com [52.101.69.85]) by lore.virtuozzo.com (Postfix) with ESMTPS id 5CDE9802A7 for ; Tue, 6 Oct 2026 15:36:01 +0000 (UTC) ARC-Seal: i=1; a=rsa-sha256; s=arcselector10001; d=microsoft.com; cv=none; b=T1Wfcot5x0ebHWdZHyrADs3irQ368Dq1Nv8Lci/lQRPUEVeK1iYq5fAcL78p19HZvMNNYRI4SL9rScv2nY7NB1bwBxsBj3WzhpPw5bLsXALI1TCPJYPaSOspCdrcQd580uQUhU2yJ+/qlVRMfFDU8YAeuq5Qed9uHeH7hpaxzNJbeLFHgIS0/LQAd1ejNU/dYg6hn5EJ61ZSid+VBZ7OIZaNzraTZUpdmSZMmBkhi2OlQzt6vRHmnwpgdT0YksSm2TgFUZSZRfbPFHCUynpvIb3GW+IGJnjGVXGc/WdbTQKP2FriaiPPUshJJk5q6XRXoOcNabaj819NACtCV9+/Jw== 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=3ygabraS1h5mllcP5dQk60MI4kdut2VPwhOcDtpc/bc=; b=TyWb35Z3VgFeIDokaSkHvbE0o4JWtGhETLsvSWXaxsK4JhEfIfuedtMxg9D0q6XIGI1x7olZoAOBEdmZZO7US97Dzu5mNHWH2LwEnXd4YAQ3Z+YznnGfeQNf+BhZZwVAv2U6SpiA29/gcLTTpc93TJAzd/SsWC1xqPYllSEGQ5VnU3skVKcGqFuO92nI1imRKk0wDCHuSxfc5jEA6IU8W23JO5u9JrjK9kXWn8T3mrSscm3cRMqh5NeacK57uQzG9m3sZ8Pg+dxeOdHvaubGbmBOi95VPV27/zFrzUnnuQJ1ocIbvrQxMsvLXmczP+zVi+RN7uf5oiPQbm8HKO1SFQ== 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=3ygabraS1h5mllcP5dQk60MI4kdut2VPwhOcDtpc/bc=; b=coEj5ufxIDJAY6Pq4cVuNr6dcDSpE4gX+anuq/tV1sUyUxjJijVoZR2PcjD8gSaI7lt+6v4I3734QvRBOAf//l45MKgcm3K9nLD33zIytBKLthsmc7HUz8GnY5efjaidHTYz2371U7aDI2ar/+sCRBU9W6d4Cd9Em229fyRtDbq95k8b0DPHSfUN2c9LmreoShzRqfGfvWFM8Gp1/DNmZtTIn5vZMgcK+da0PoqtTSu0ZETn39dCANlN0V/6qlcIkTIvvku99GJaxPGB+xINEsXd8pzII5MlrIBsb1VNgOs0q78f48YZZiebI9Dt8p8r1lNVexpJ6tAtuW0CKUY9Ag== 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 DU0PR08MB9774.eurprd08.prod.outlook.com (2603:10a6:10:445::18) 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 15:35:58 +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 15:35:58 +0000 From: Andrey Drobyshev Date: Tue, 06 Oct 2026 18:35:57 +0300 Subject: [PATCH QEMU HCI-8.0 v7 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-v7-6-044f668789a1@virtuozzo.com> References: <20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com> In-Reply-To: <20261006-usb-host_v3-v7-0-044f668789a1@virtuozzo.com> To: svt-core@virtuozzo.com Cc: den@openvz.org, andrey.drobyshev@virtuozzo.com X-Mailer: b4 0.16.0 X-ClientProxiedBy: VI5PEPF00000985.AUTP296.PROD.OUTLOOK.COM (2603:10a6:808:1::81a) To VI0PR08MB10656.eurprd08.prod.outlook.com (2603:10a6:800:20a::12) MIME-Version: 1.0 X-MS-PublicTrafficType: Email X-MS-TrafficTypeDiagnostic: VI0PR08MB10656:EE_|DU0PR08MB9774:EE_ X-MS-Office365-Filtering-Correlation-Id: 622c42da-e735-4759-0eb2-08df23bf8498 X-LD-Processed: 0bc7f26d-0264-416e-a6fc-8352af79c58f,ExtAddr List-Id: svt-core@virtuozzo.com X-MS-Exchange-SenderADCheck: 1 X-MS-Exchange-AntiSpam-Relay: 0 X-Microsoft-Antispam: BCL:0;ARA:13230040|23010399003|1800799024|366016|376014|52116014|5023799004|5113699003|56012099006|10067099003|22082099003|18002099003|38350700014; X-Microsoft-Antispam-Message-Info: Wt55CPsYYP4lvGzdyHLOc4iKTFNWrece+WbMcLbuy+RxB5jvTLhblefJcFN46TZE6AyJCTl9MpsJzZ3RxpXO9sxetXdH2FWnlxBX1oXsK74HEhnnV/w9qA+oG+cRISjgxaPGI15ZjWQ2CiAdA2zveqXeijcnF4mrji3O60sGaidl7Vp1HQI9XIkJNTYWEYkqrmhzFjp0VxkdBLlKY2B+4/Q3I2Jty+peMAIXroYcUJQI8sMqy4FQD2iG4mDiqkL0uF2RSw/VZUFHd4/smbb9hIGu/9TOa0v3so9VMTGBSuRDkhsxa5V5uaTK6dRKC6yGGCzgsSvEaUP0ngeIdw76IorfkukenW3t0yzEyDV+HWHKrzXU9hNwbh2Vg2HMDy3AW3HvqV1A1noH0Tq7QHtfUGKhOeZzY6NlRQXVI6u3fDe1ExxDX+4q6bVKtLZepRyMxEVuodWroGBdV9YOoSZnOdU8LF47kZs28bQRt4NEaKT8VG5VVeW83S8qtID9zuKfYBVN6nRxyWAAIiUv2O06zvNzpchp0pENyi7UuQ/74GrXnWXxUVh3CnXV+T/h0nyEuOk+tuE0dAWed7uWA2NnJJLdl+SNJR2rGrH2e7Sv0gqbkz+cJNvk0c8vrC1qXzl9MOpnnAfI9BMqfl71VB9SDHRJXP3b94ZqVlkRw6sw6AzerYsBCT4Ue08Lz/H9WLgi/AipvEof59gFBAZyww8z+TW+lj/ANNgvkAI79LQS7V0= 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)(23010399003)(1800799024)(366016)(376014)(52116014)(5023799004)(5113699003)(56012099006)(10067099003)(22082099003)(18002099003)(38350700014);DIR:OUT;SFP:1102; X-MS-Exchange-AntiSpam-MessageData-ChunkCount: 1 X-MS-Exchange-AntiSpam-MessageData-0: =?utf-8?B?d1VldlBTamExYnRIejkyeUJqYVAzRlpoUEdKUVJ3RHpqdFBTMlZ6amtCNGg3?= =?utf-8?B?cUQ2T3lJUFVydjlBVXVNM3VSclNzb1I0RUJrbitHYTdVa1VOQWVXSGcwMnBC?= =?utf-8?B?RVAwSU52YmFzaFRzVUMwM2l0cmN5NFBzQzA4elZLNkUraERmNWZzanpxMElv?= =?utf-8?B?dGhaRmVhMUlpRk5WbU9HWVV3Q0ZJeFFzSXljajRZb2FvR0RwdkNBalhtVUk1?= =?utf-8?B?bFdKQ2NMbW4xN3M0KzlVSENTZjFXMHZ2RS9BbWVUSjFBdTk1cXN0QkFCZUli?= =?utf-8?B?N1dpQlBOaFJsYTZuK21KOUJjZndJc0FFQXVFOGZ5cm1pNHp0TkxlSVVOQjZW?= =?utf-8?B?ZllsUGN3NFdMOGpKdm83VDhWdFVhWGgvV0xERUVXd3lzaHdHamZTUWN5eDRR?= =?utf-8?B?YWlyS3haeHcvTDQrRlBCOFVNNmZUaDh1ckFCL1ZuZFdvajZSdFN3bTBEWnY4?= =?utf-8?B?RnNPSGhLTGtwbVRlSHlUZ3MwSWFxMTNtRVg2Vk1zbW9GUnpiSDYvQ3ZhdzR1?= =?utf-8?B?WXF1OFFvNHVZSGpxTDFLaEZMcW1GditZSVJxRnBBNUtNb3NjeGtpVXBid2xk?= =?utf-8?B?VXlLQm5WMXk5RVVRY0NCL0ZiNk1oRmlvSUcrbUUyMkdOcEZJaHMxanRQclBn?= =?utf-8?B?UnN6MHZDUW16c3NZblAzT3BScmtwQWpIU28vc1dSQlpJTXNBSmdyVE5LTE96?= =?utf-8?B?NHplS253ZTJyeUZOL2VtR3gwa0c3eFM4L0RFdjluVlh4c3FXRmNlcFFoazV3?= =?utf-8?B?bGQ1bUVKeFRzUkVQbGV0T0hHTzBjRUlxWjdKYTNYNFJFclpxUjg5OHVrZFp0?= =?utf-8?B?REcwSXF1TTByYXRMTGF1ZVNrbnArbEdpeWdHeXhYNGppbVRyV0dkTHBXeTk1?= =?utf-8?B?YlNqS09pYnBTQzhlOGRnZHVZUnFsQTRUam9GZ0xrL0lQdjNTTnpVd0xGY0JD?= =?utf-8?B?eHNnL2VSRjlCK1YrK2JGMEdWaEZrOVVaM1hMRnNHTHI2Ulh6Zm1tRDNKWXV3?= =?utf-8?B?cVpOK2dCb0NtbFJ0aTJFcWhCamYwWGh5NnA2RXRUSGJ3UW56Tm5zQ3FObDBG?= =?utf-8?B?ZHRLOXYwdlNrU3ZKeU4yWnFVdEhFZzd0UlVsNnM5blVXcDBFQ3BkTk12U3Mz?= =?utf-8?B?cmZLeCtlb0NLR3RiOEp4ZzUzdWRYRS9EUXpUM3FtcE5NQ3RVNldiNjRsa0Yr?= =?utf-8?B?NHgxaDZuMTcwY2RvRDlBT2R0dklpZ0lDRm41V3UxbVpuSWxVSHFDN0JDRHV0?= =?utf-8?B?RzlZMnVTeGhrRlRhV21Oa0VRMnZTUVhxMjdpQnIzcTh5VXRzK0V3VmJ2cytm?= =?utf-8?B?YnBFbGtLSEdMb3gyei9YRmNmUGZGYTlwWFMwQXdUV2cycExsYzlZTXJqQnBs?= =?utf-8?B?aStvK1ZibUpBVld1cUE3WmFLV0pPV3hjaHR2TjFwYnVOVDh6U2VQcWs0QzFR?= =?utf-8?B?aXBtSEJLM0M2R1cxKythZ2dTdFhLUUg3SW9mN2NIYXhUQmdFbzlXYkIwL3Fz?= =?utf-8?B?eUVFWHF6S2hzTmN3L3hsV1J4THNUZC9uTUszTmMrTlV1ZUFTVEdsODY3WVky?= =?utf-8?B?NTQ3VjMrTmVGRmU4eEU1eHFXMzQzRmJQR3kxVFZlbndwKzlHVk9OVW9mOC9J?= =?utf-8?B?TEZCNVRiVzd5MzF6NGxEdzFGNXM5bzBqK3V1VlIyczkrSzdjNlUyNFZrc2ho?= =?utf-8?B?R2k3OVJtOGg0Y1ZBQmlSNXJVSnpmVDd2bDBzRkIvU2RYendYYTg5cUNBNkhI?= =?utf-8?B?YUsxQnR2MHhERGxqWnFqOEd1ZGpzYVpiQ0pVcXhlcEJWNDVkd3BYdWx0Q0tu?= =?utf-8?B?Ly92YklIYlZuaHNqSVJOSVJWaTZ4blF3YlJ0b1V6RkE1aFFJZWhDMVcyRDlB?= =?utf-8?B?ZGNrQ0NPL2RGTXRBdjkrcEtWQUp0MmxTS0pSZDNqdTRGdU9CcVRLOS9xYnhp?= =?utf-8?B?UzVLSWluYVJRWnBCdXY1Wm1ML3VQWTdDVHVaZW1aMUEwZUFNSmtOMEw1b1da?= =?utf-8?B?cVdZaVQxYXltakMyS1hXRGpkalhUazFkeEdqS1BXclUyYUtpT254OG9vOFFP?= =?utf-8?B?YnhzdXhOVVN6dEF4akNoY0JBWHprQm9aMnVwbE5VRHN6ZVhYSE1NazNzYzBx?= =?utf-8?B?WU5vR3NOQVhPWGFEcDFobCtKTUhuS2w1dzlrY1B2YnNxNE5nUitwam5CREhl?= =?utf-8?B?UGRWRld1ZzFsU2dZUTl6V0w2Wld4YUxqV3I5S1NJQ3hlUGZKbEY5QUEvaDB0?= =?utf-8?B?dDRxS1BrZjhBU2VQRGNJQWFENVBJc3RBdjJQSE9sSmhFZkVoaS95NjllOUJw?= =?utf-8?B?S2JDWVBQdzh2M2psOU9uUWpvQkc3enBOay9UcEMyWHNxb1o1Qm5ZS3JRczBh?= =?utf-8?Q?yzLWBBC22Dm77Kbg=3D?= X-Auto-Response-Suppress: DR, OOF, AutoReply X-OriginatorOrg: virtuozzo.com X-MS-Exchange-CrossTenant-Network-Message-Id: 622c42da-e735-4759-0eb2-08df23bf8498 X-MS-Exchange-CrossTenant-AuthSource: VI0PR08MB10656.eurprd08.prod.outlook.com X-MS-Exchange-CrossTenant-AuthAs: Internal X-MS-Exchange-CrossTenant-OriginalArrivalTime: 06 Oct 2026 15:35:58.1660 (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: lYkr+U0fkOq/IY/OODQI4+nZz5DonbLqQlRqlePMhn11kS/4/Brc7JuGOJefDcm7drVCwADHHZAEfUDazGXPgg97OmITUhuGSLQEGdgKpQM= X-MS-Exchange-Transport-CrossTenantHeadersStamped: DU0PR08MB9774 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 | 236 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 209 insertions(+), 27 deletions(-) diff --git a/hw/usb/host-libusb.c b/hw/usb/host-libusb.c index 1a79cdf9e4d..e631b683ed1 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,29 @@ 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_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; + return false; + } + } + return true; +} + static void LIBUSB_CALL usb_host_req_complete_ctrl(struct libusb_transfer *xfer) { USBHostRequest *r = xfer->user_data; @@ -432,6 +462,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 +515,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 +538,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 +1141,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 +1162,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 +1181,117 @@ 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 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 +1459,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 +1483,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 +1955,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 +1971,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