From b47c26fa561c63d91dd1fc836a2787144d3742a0 Mon Sep 17 00:00:00 2001 From: Sebastien Boeuf Date: Wed, 22 Jul 2026 06:35:59 -0700 Subject: [PATCH] virtio-devices: Resume vhost-user devices after snapshot For vhost-user devices, a snapshot involved the vrings to be stopped, but they couldn't be resumed. This commit aims at fixing this by saving the vrings base so that it can be safely reset after the snapshot has succeeded. This will allow VMs where snapshots are taken from internal CH implementation and from an offload daemon to be resumed once the snapshot is complete. Signed-off-by: Sebastien Boeuf Assisted-by: Claude:claude-opus-4-8 --- virtio-devices/src/lib.rs | 2 + virtio-devices/src/vhost_user/mod.rs | 58 +++++++++++++------ .../src/vhost_user/vu_common_ctrl.rs | 38 +++++++++++- 3 files changed, 80 insertions(+), 18 deletions(-) diff --git a/virtio-devices/src/lib.rs b/virtio-devices/src/lib.rs index 7b5374b9d..4d2db1129 100644 --- a/virtio-devices/src/lib.rs +++ b/virtio-devices/src/lib.rs @@ -111,6 +111,8 @@ pub enum ActivateError { BadActivate, #[error("Failed to clone EventFd")] CloneEventFd(#[source] io::Error), + #[error("Failed to create EventFd")] + CreateEventFd(#[source] io::Error), #[error("Failed to spawn thread")] ThreadSpawn(#[source] io::Error), #[error("Failed to setup vhost-user-fs daemon")] diff --git a/virtio-devices/src/vhost_user/mod.rs b/virtio-devices/src/vhost_user/mod.rs index d9eb9f73f..693f3033f 100644 --- a/virtio-devices/src/vhost_user/mod.rs +++ b/virtio-devices/src/vhost_user/mod.rs @@ -265,6 +265,7 @@ pub const DEFAULT_VIRTIO_FEATURES: u64 = (1 << VIRTIO_F_RING_INDIRECT_DESC) const HUP_CONNECTION_EVENT: u16 = EPOLL_HELPER_EVENT_LAST + 1; const BACKEND_REQ_EVENT: u16 = EPOLL_HELPER_EVENT_LAST + 2; +const RESUME_EVENT: u16 = EPOLL_HELPER_EVENT_LAST + 3; #[derive(Default)] pub struct Inflight { @@ -287,6 +288,7 @@ pub struct VhostUserEpollHandler { pub inflight: Option, /// Flag set by the worker when the vhost-user backend is no longer reachable. pub disconnected: Arc, + pub resume_evt: EventFd, } impl VhostUserEpollHandler { @@ -306,6 +308,8 @@ impl VhostUserEpollHandler { helper.add_event(backend_req_handler.as_raw_fd(), BACKEND_REQ_EVENT)?; } + helper.add_event(self.resume_evt.as_raw_fd(), RESUME_EVENT)?; + helper.run(paused, paused_sync, self)?; Ok(()) @@ -415,6 +419,26 @@ impl EpollHelperHandler for VhostUserEpollHandle Ok(()) } } + RESUME_EVENT => { + let _ = self.resume_evt.read(); + self.vu + .lock() + .unwrap() + .resume( + &self.mem.memory(), + &self.queues, + self.virtio_interrupt.as_ref(), + self.acked_features, + &self.backend_req_handler, + self.inflight.as_mut(), + ) + .map_err(|e| { + EpollHelperError::HandleEvent(anyhow!( + "failed to resume vhost-user backend for socket {}: {e:?}", + self.socket_path + )) + }) + } _ => Err(EpollHelperError::HandleEvent(anyhow!( "Unknown event for vhost-user thread" ))), @@ -472,6 +496,7 @@ pub struct VhostUserCommon { pub disconnected: Arc, saved_dirty_log: Option, dirty_logging: bool, + resume_evt: Option, } impl VhostUserCommon { @@ -527,6 +552,13 @@ impl VhostUserCommon { ) .map_err(ActivateError::VhostUserSetup)?; + let resume_evt = EventFd::new(libc::EFD_NONBLOCK).map_err(ActivateError::CreateEventFd)?; + self.resume_evt = Some( + resume_evt + .try_clone() + .map_err(ActivateError::CloneEventFd)?, + ); + Ok(VhostUserEpollHandler { vu: vu.clone(), mem, @@ -541,6 +573,7 @@ impl VhostUserCommon { backend_req_handler, inflight, disconnected: self.disconnected.clone(), + resume_evt, }) } @@ -706,31 +739,22 @@ impl VhostUserCommon { } fn resume_internal(&mut self) -> result::Result<(), MigratableError> { - // Skip the resume_vhost_user call if the backend is disconnected. Process the queue - // interrupts to kick any paused workers. if self.disconnected.load(Ordering::Relaxed) { return Err(MigratableError::DeviceDisconnected( self.socket_path.clone(), )); } - if let Some(vu) = &self.vu - && let Err(e) = vu.lock().unwrap().resume_vhost_user() - { - if e.is_transport_lost() { - self.disconnected.store(true, Ordering::Relaxed); - return Err(MigratableError::DeviceDisconnected( - self.socket_path.clone(), - )); - } + let Some(evt) = &self.resume_evt else { + return Ok(()); + }; - return Err(MigratableError::Resume(anyhow!( - "Error resuming vhost-user backend for socket {}: {e:?}", + evt.write(1).map_err(|e| { + MigratableError::Resume(anyhow!( + "Error signaling vhost-user resume for socket {}: {e}", self.socket_path - ))); - } - - Ok(()) + )) + }) } pub fn resume(&mut self) -> result::Result<(), MigratableError> { diff --git a/virtio-devices/src/vhost_user/vu_common_ctrl.rs b/virtio-devices/src/vhost_user/vu_common_ctrl.rs index 6e8f4d9d6..e2f7b5572 100644 --- a/virtio-devices/src/vhost_user/vu_common_ctrl.rs +++ b/virtio-devices/src/vhost_user/vu_common_ctrl.rs @@ -67,6 +67,7 @@ pub struct VhostUserHandle { acked_features: u64, vrings_info: Option>, queue_indexes: Vec, + vring_bases: Option>, } impl VhostUserHandle { @@ -172,6 +173,9 @@ impl VhostUserHandle { return Err(Error::VringBasesCountMismatch(bases.len(), queues.len())); } + // May run more than once on the same handle (resume after snapshot) + self.queue_indexes.clear(); + self.vu .set_features(acked_features) .map_err(Error::VhostUserSetFeatures)?; @@ -427,6 +431,7 @@ impl VhostUserHandle { acked_features: 0, vrings_info: None, queue_indexes: Vec::new(), + vring_bases: None, }; vhost_user .vu @@ -485,6 +490,7 @@ impl VhostUserHandle { acked_features: 0, vrings_info: None, queue_indexes: Vec::new(), + vring_bases: None, }) .map_err(Error::VhostUserConnect); @@ -580,7 +586,7 @@ impl VhostUserHandle { Ok(()) } - pub fn resume_vhost_user(&mut self) -> Result<()> { + fn resume_vhost_user(&mut self) -> Result<()> { if self.ready { self.enable_vhost_user_vrings(self.queue_indexes.clone(), true)?; } @@ -641,9 +647,39 @@ impl VhostUserHandle { .check_device_state() .map_err(Error::VhostUserCheckDeviceState)?; + // Store the bases in case we need to resume after snapshot + self.vring_bases = Some(vring_bases.clone()); + Ok((state, vring_bases)) } + /// Resume the vhost-user backend from the device thread. + /// Initialize the vrings from the bases captured earlier, or simply + /// enable them if they haven't been stopped. + pub fn resume( + &mut self, + mem: &GuestMemoryMmap, + queues: &[(u16, Queue, EventFd)], + virtio_interrupt: &dyn VirtioInterrupt, + acked_features: u64, + backend_req_handler: &Option>, + inflight: Option<&mut Inflight>, + ) -> Result<()> { + let Some(vring_bases) = self.vring_bases.take() else { + return self.resume_vhost_user(); + }; + + self.setup_vhost_user( + mem, + queues, + virtio_interrupt, + acked_features, + backend_req_handler, + inflight, + Some(&vring_bases), + ) + } + pub fn restore_state(&mut self, state: &VhostUserState) -> Result<()> { state.validate()?; if let Some(backend_state) = &state.backend_state {