Skip to content

examples: ask the guest for its snapshot, and keep a VM whose spawn is unconfirmed (SL5.1a follow-up) - #1508

Merged
Pedro Henrique Penna (ppenna) merged 2 commits into
feature/nvx-refactorfrom
agents/slim-sl5-1a-examples
Nov 12, 2026
Merged

Pedro Henrique Penna (ppenna) merged 2 commits into
feature/nvx-refactorfrom
agents/slim-sl5-1a-examples

Conversation

@ppenna

@ppenna Pedro Henrique Penna (ppenna) commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Part of #1118 (SL5.1a follow-up). Parent #1086, umbrella #1076.

What and why

SL5.1a (#1488) added the compile-only examples nvx-rs/examples/host-workflows.rs and guest-workflows.rs, which CI compiles and nothing runs. Copilot's review of #1500 raised two findings on host-workflows.rs, in code that is now on feature/nvx-refactor: r4544426592 and r4544426638. Both hold against the library's contracts. This PR fixes them in the example alone, which stays compile-only and uses the public API alone.

1. capture() never asked for the capture

Symptom. capture() spawned the capture's VM and waited for its exit, saying "the guest captures itself, as the guest example shows". No guest that the examples describe captures without a request, so the capture's VM ran until the deadline and was stopped without a snapshot.

Evidence.

  • guest-workflows.rs's capture "Captures the VM once the host asked for a snapshot and the init answered the request".
  • doc/guest-agent-guidelines.md, "Keep the transport silent": "The init answers the host's capture request, then sends nothing on its transport until capture returns". The rustdoc of guest::snapshot says the same.
  • nvx-init, NVX's own init, captures only on NVXC's capture request (nvx-rs slimming 3.2.1: NVXC asks for a capture and activates a restore, and a blocking driver restores #1113): a Fresh capture boot (nvx_snapshot_tier=platform) reports NVX-INIT-CAPTURE: waiting and serves its control session (guest/init.rs, AgentConfig::serve_capture), whose dispatcher queues a capture only for APP_CAPTURE (guest/managed_agent/dispatch.rs). Boot::capture says "what a capture of the VM adds, if the VM may be captured"; the guest's own guest::snapshot::capture requests the capture.
  • The CLI's snapshot capture (host/cli/snapshots/capture.rs, request_and_wait) connects to the VM's verified endpoint, attaches with the capability, sends the request with the crate-private ControlSession::capture, and only then waits for the VM's exit.

Fix. capture() takes a request: FnOnce(&mut ControlSession, Instant) -> Result<(), SessionError> callback, as restore() takes activate: the caller asks its guest for the snapshot in its own protocol, since NVXC's capture request isn't public. Once the VM runs, capture() connects with session::connect, which accepts only an endpoint that the VM's own process serves, attaches with ControlSession::attach, calls the callback, and closes the session, as the CLI does; then it waits for the exit. A failed connection, attachment or request ends the VM through end(), as an expired deadline does. The doc names what the configuration needs, a boot with a capture, rather than "a boot that asks for a capture". It also says that a failed request or an expired deadline finishes no snapshot, where it said that the VM captures nothing, since a guest that got the request may already capture (from the code review).

2. A spawn that couldn't confirm its exit escaped Unstopped

Symptom. restore() said "A spawn that fails stops the child that it started" and returned vm::spawn's error with ?. When the spawn could not confirm the exit of the process that it started, that error neither ended the VM nor came as an Unstopped error, the two outcomes that restore()'s 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. capture() did the same with its VM directory.

Evidence.

  • vm::spawn documents: "A failure after child creation terminates only that child and waits for its confirmed exit. ... An unconfirmed exit has kind ErrorKind::StillRunning, with the original error." It fails so when the cleanup of a child whose identity it cannot verify fails (spawn_verified_with_cleanup), or when Windows creates the process without a handle (platform/windows/spawn.rs), and it keeps no handle to the process either way.
  • vm::create requires the caller's snapshot lock "through a peer-verified restore-ready event or the VM's verified exit".
  • The CLI tells this case apart too. Its foreground runs keep their temporary VM directory after a spawn's StillRunning (host/cli/run.rs, host/cli/sandbox/lifecycle/start.rs), and a sandbox start that restores leaves its VM to the launch journal's recovery, which ends it before the snapshot's lock goes (launch_locked, tested by a_restore_whose_spawn_cannot_confirm_that_its_vm_ended_recovers_it_before_its_snapshot_goes_free). That recovery finds the VM through its endpoint and its log, by means that the library doesn't make public.

Fix. capture() and restore() start their VM through a new start(), which turns a spawn's StillRunning into an Unstopped error without the VM. Unstopped::launched is now an Option<Launched>, 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 of the snapshot, until the host restarts. The comment that said that a failed spawn stops its child is gone, and restore()'s doc says that the call ends the VM when the restore fails, or fails with an Unstopped error, after which the caller keeps holding the lock.

cold_boot() doesn't change: it returns vm::spawn's own nvx::Error, promises no Unstopped error, holds no lock or view, and does nothing fallible after its spawn, so the error's kind, StillRunning, tells its caller that the VM may still run.

Size

One file, nvx-rs/examples/host-workflows.rs: +49 −18 lines (+33 −8 of code, +15 −10 of comments). Examples count as documentation for the size rules, so 0 core lines in 0 counted files.

Commits

  1. examples: ask the guest for its snapshot before a capture waits
  2. examples: keep a VM whose spawn could not confirm its exit

Not in this PR

A test that runs either example or a VM; any change to the library or its public API (the snapshots don't change); guest-workflows.rs, whose capture already follows the host's request; and external-launch.rs, which launches through its own launcher rather than vm::spawn.

Validation

On the head 82ebf5d63df (base 64ad98afacc), with this branch's check-nvx-rs commands, in nvx-rs/, with NVX_BUNDLE=skip and Rust 1.95.0:

Check Windows (local) Linux (nvx-kvm-test, KVM, virtual machine)
cargo fmt --all --check passed passed
cargo clippy --locked --all-targets --all-features -- -D warnings passed passed, with both examples
cargo clippy --locked --example host-workflows --features host -- -D warnings passed, on each commit (git rebase --exec) passed
cargo clippy --locked --target x86_64-unknown-linux-musl --no-default-features --features guest --all-targets -- -D warnings, the Clippy run of nvx.py check-guest-binaries, which compiles guest-workflows Linux only passed
cargo build --locked --example host-workflows --features host passed
cargo +1.89 check --locked --all-targets --all-features passed passed
cargo check --locked --all-targets --all-features --target aarch64-apple-darwin passed
python scripts/check_test_layout.py, python scripts/check_markdown_links.py passed
python scripts/check_test_changes.py --base origin/feature/nvx-refactor 0 changes to existing tests, 0 added

local_gates.py (dev's copy, --files nvx-rs/examples/host-workflows.rs) also selects gates that this change cannot affect, which I leave to CI: the Test step of check-nvx-rs, whose cargo test builds the examples as the build above does; artifact bundling; rustdoc, which documents the library and not the examples; the public API check, with no library change; the guest binaries' build and musl tests, with no guest change; the runtime adapter's checks; and the Linux CLI validation. No VM ran: both examples stay compile-only.

The case matrix, doc/acceptance-case-matrix.md, needs no change: it lists tests rather than examples, its rows that name the examples as coverage stay true, and no test changes.

Nor do the README and the docs that link the examples. nvx-rs/README.md lists the host example's capture without saying how it starts, and its sections on nvx::guest::snapshot and snapshot capture already have the guest answer the host's capture request, as doc/guest-agent-guidelines.md does. No rustdoc links the examples.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The review identifies no active issues in the host-workflow example updates.

No open issues 🎉

What changed in this PR

Aligns NVX’s compile-only host workflow examples with the public API’s snapshot and process-lifecycle contracts, following up on SL5.1a without changing the library.

Changes:

  • Connects to the verified VM endpoint and invokes a caller-provided capture request before waiting for snapshot completion.
  • Wraps unconfirmed spawn failures in Unstopped, allowing for a missing process handle, and documents retention of VM directories, restore views, and snapshot locks.
File Description
nvx-rs/​examples/​host-workflows.rs Adds request-driven capture and shared handling of unconfirmed spawn failures, with updated resource-retention guidance.

Review effort: Balanced

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>
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<Launched>, 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants