From 3038703cbe56b6326e31aea569d1d5230c0a239d Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Wed, 11 Nov 2026 16:44:25 -0800 Subject: [PATCH 1/2] examples: ask the guest for its snapshot before a capture waits host-workflows' capture spawned the capture's VM and waited for its exit, saying that the guest captures itself, as the guest example shows. But the guest example's init captures once the host asked for a snapshot and the init answered the request, as the guest-agent guidelines have every init do, and nvx-init, NVX's own init, waits in a capture boot for NVXC's capture request before it captures. Neither captured without a request, so the capture's VM ran until the deadline and was stopped without a snapshot. capture now takes a request callback, as restore takes activate. Once the VM runs, capture connects to its control endpoint, which only the VM's own process may serve, attaches with the capability, and has the callback ask the guest for its snapshot in the caller's own protocol; only then does it wait for the VM's exit. A failed connection, attachment or request ends the VM through end, as an expired deadline does. The CLI's snapshot capture drives NVX's own capture in the same order: it connects to the VM's verified endpoint, attaches, sends the capture request of ControlSession::capture, which the library keeps private, and waits for the exit. The doc now names what the configuration needs, a boot with a capture, rather than a boot that asks for one. Part of #1118 (SL5.1a follow-up). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nvx-rs/examples/host-workflows.rs | 33 ++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/nvx-rs/examples/host-workflows.rs b/nvx-rs/examples/host-workflows.rs index 9a7fd64c4..248bfdcf3 100644 --- a/nvx-rs/examples/host-workflows.rs +++ b/nvx-rs/examples/host-workflows.rs @@ -100,21 +100,28 @@ pub fn cold_boot( Ok((launched, saved)) } -/// Captures a snapshot of `config`, whose boot asks for a capture, in `directory`, a new private +/// Captures a snapshot of `config`, whose boot has a `capture`, in `directory`, a new private /// directory, moves it to `snapshot_dir`, another, which must be empty, and opens it there. /// -/// The guest captures itself, as the guest example shows, and its VM exits once the capture +/// `request` asks the guest for a snapshot, in the caller's own protocol with its guest, on a +/// control session that only the VM's own process may serve, and returns once the guest answered. +/// The guest then captures the VM, as the guest example shows, and the VM exits once the capture /// commits. The caller finishes the capture with the proof of that exit, then moves the /// snapshot's three entries, and nothing else, out of the VM directory. It keeps them private -/// and immutable while it restores from them. A VM that doesn't exit by `deadline` captures -/// nothing, and the caller ends it, or keeps it in an [`Unstopped`] error. -pub fn capture( +/// and immutable while it restores from them. When the request fails, or the VM doesn't exit by +/// `deadline`, the caller finishes no snapshot: it ends the VM, or keeps it in an [`Unstopped`] +/// error. +pub fn capture( runtime: &Runtime, config: &VmConfig, directory: &Path, snapshot_dir: &Path, + request: R, deadline: Instant, -) -> Result> { +) -> Result> +where + R: FnOnce(&mut ControlSession, Instant) -> Result<(), SessionError>, +{ vm::create_private_dir(snapshot_dir)?; if fs::read_dir(snapshot_dir)?.next().is_some() { return Err("the snapshot's directory is not empty".into()); @@ -126,8 +133,20 @@ pub fn capture( let context = prepared .capture_context() .cloned() - .ok_or("the configuration's boot asks for no capture")?; + .ok_or("the configuration's boot has no capture")?; let launched = vm::spawn(prepared)?; + let process = launched.process.reference(); + let requested = session::connect( + &launched.outputs.endpoint, + process.pid, + process.start_time, + deadline, + ) + .and_then(|stream| ControlSession::attach(stream, launched.capability.as_bytes(), deadline)) + .and_then(|mut session| request(&mut session, deadline)); + if let Err(error) = requested { + return Err(end(launched, error.into())); + } let exited = match launched.process.wait(deadline) { Ok(Some(exited)) => exited, Ok(None) => { From 23289a75ee3d7cc2413bdb837e9a79dc691bf304 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Wed, 11 Nov 2026 16:45:01 -0800 Subject: [PATCH 2/2] examples: keep a VM whose spawn could not confirm its exit vm::spawn ends the process that it started when it fails after creating it, but when it cannot confirm that exit it fails with ErrorKind::StillRunning, and the process may still run. host-workflows' restore said that a failed spawn stops its child, and returned that error with ?, so it neither ended the VM nor returned an Unstopped error, the two outcomes that its doc gives the caller. A caller that followed the doc could release the snapshot's lock, or remove the view, while OpenVMM still used them, although vm::create requires the lock through the restore-ready event or the VM's verified exit. capture did the same with its VM directory. capture and restore now start their VM through start, which turns a spawn's StillRunning into an Unstopped error without the VM. Unstopped holds an Option, None when the spawn kept no process, which the caller can then neither stop nor wait for: it keeps the VM's directory, and a restore's view and lock, until the host restarts. The CLI tells the same case apart: its foreground runs keep their temporary VM directory after a spawn's StillRunning, and a sandbox start that restores leaves its VM to the launch journal's recovery, which ends it before the snapshot's lock goes, through means that the library doesn't make public. cold_boot keeps returning vm::spawn's error: it promises no end of its VM and no Unstopped error, and the error's kind tells its caller that the VM may still run. Part of #1118 (SL5.1a follow-up). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nvx-rs/examples/host-workflows.rs | 34 +++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/nvx-rs/examples/host-workflows.rs b/nvx-rs/examples/host-workflows.rs index 248bfdcf3..8aedcf940 100644 --- a/nvx-rs/examples/host-workflows.rs +++ b/nvx-rs/examples/host-workflows.rs @@ -28,7 +28,7 @@ use nvx::host::restore::{ use nvx::host::runtime::Runtime; use nvx::host::session::{self, ControlSession, SessionError}; use nvx::host::snapshot::{self, Snapshot}; -use nvx::host::vm::{self, Launched, Reattached, SavedVm, Stopped, VmDir, VmOutputs}; +use nvx::host::vm::{self, Launched, Prepared, Reattached, SavedVm, Stopped, VmDir, VmOutputs}; /// One byte past OpenVMM's 25-byte restore-ready event, so that a longer event fails to parse. const READY_LIMIT: usize = 26; @@ -51,13 +51,14 @@ pub struct Saved { } /// A capture or restore that failed once its VM started, and whose VM didn't show its exit when -/// the caller ended it. The caller takes it from the error with `downcast`, and keeps the VM, to -/// stop it again, with its directory, and a restore's view and lock of the snapshot, until the -/// VM exits. +/// the spawn or the caller ended it. The caller takes it from the error with `downcast`, and +/// keeps the VM, to stop it again, with its directory, and a restore's view and lock of the +/// snapshot, until the VM exits, or, without the VM, until the host restarts. #[derive(Debug)] pub struct Unstopped { - /// The VM, whose process may still run. - pub launched: Launched, + /// The VM, whose process may still run, or `None` after a spawn that could not confirm the + /// exit of the process that it started, which the caller can then neither stop nor wait for. + pub launched: Option, /// Why the capture or restore failed, and why its VM didn't stop. pub reason: String, } @@ -134,7 +135,7 @@ where .capture_context() .cloned() .ok_or("the configuration's boot has no capture")?; - let launched = vm::spawn(prepared)?; + let launched = start(prepared)?; let process = launched.process.reference(); let requested = session::connect( &launched.outputs.endpoint, @@ -174,7 +175,7 @@ where /// which every caller of the snapshot takes before it opens it, through the restore-ready event /// or the VM's verified exit, and around the view's removal. NVX takes no lock, and Windows /// needs none. The caller holds it through this call, which ends the VM when the restore fails, -/// or returns the VM in an [`Unstopped`] error, which the caller keeps. +/// or fails with an [`Unstopped`] error, after which the caller keeps holding it. /// /// `activate` sends the activation record's bytes as the first request of the caller's own /// protocol with its guest, and returns the activation that the guest's Ready reports. The @@ -195,8 +196,7 @@ where let dir = VmDir::open(directory)?; let prepared = vm::create(runtime, config, Some(snapshot), &dir)?; let record = prepared.activation_record(transaction)?; - // A spawn that fails stops the child that it started. - let mut launched = vm::spawn(prepared)?; + let mut launched = start(prepared)?; match drive(&mut launched, &record, activate, deadline) { Ok(session) => Ok((launched, session)), Err(error) => Err(end(launched, error)), @@ -398,6 +398,18 @@ pub fn stop( vm::stop(process, outputs, request, Instant::now() + grace, deadline) } +/// Starts a capture's or restore's VM. A spawn that fails ends the process that it started; one +/// that cannot confirm that exit fails here with an [`Unstopped`] error without the VM. +fn start(prepared: Prepared) -> Result> { + vm::spawn(prepared).map_err(|error| match error.kind() { + ErrorKind::StillRunning => Box::new(Unstopped { + reason: format!("{error}, and the process that it started may still run"), + launched: None, + }) as Box, + _ => error.into(), + }) +} + /// Ends a VM that its caller gives up on after `error`, at once, and returns `error` once the /// VM's exit is verified, or an [`Unstopped`] error that returns the VM to the caller. fn end(launched: Launched, error: Box) -> Box { @@ -412,7 +424,7 @@ fn end(launched: Launched, error: Box) -> Box { Ok(_) => error, Err(failure) => Box::new(Unstopped { reason: format!("{error}, and its VM did not stop: {failure}"), - launched, + launched: Some(launched), }), } }