From 98bb4a43863d321cdc696ce498ed476d88322107 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Thu, 5 Nov 2026 11:24:38 -0800 Subject: [PATCH] host: make what every launch shares one value in host::runtime Move RuntimeIdentity, ArtifactIdentity and IdentityPart from the store's identity module into host::runtime, preserving their validation, serialization and comparison semantics. Keep the existing store exports. Move the public hypervisor probe beside them without changing its platform implementations or failure classification. Add Runtime for the executable, kernel and initramfs paths, their identity, the hypervisor, optional CPU profile, Windows job breakaway and LaunchOptions. The launch options hold the guest shutdown action, Exit by default, and how long OpenVMM waits for the host to authenticate. The boot console remains a per-VM setting. Classify the existing runtime-mismatch code as ErrorKind::RuntimeMismatch instead of Other, retaining its spelling, messages and CLI status. Clarify Unsupported's existing malformed-value semantics and remove the error inventory's stale ordering promise without changing its order. Move the identity tests unchanged, cover the launch defaults and foreign hypervisor probe, extend the error-kind assertions, and update the documentation and public API snapshots. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: abef22e3-3caa-4e45-837b-652018e4ced0 --- nvx-rs/README.md | 38 ++++- nvx-rs/public-api/linux.txt | 44 +++++- nvx-rs/public-api/windows.txt | 44 +++++- nvx-rs/src/contract.rs | 8 +- nvx-rs/src/error.rs | 16 +- nvx-rs/src/error/tests.rs | 12 ++ nvx-rs/src/host/artifacts/mod.rs | 2 +- nvx-rs/src/host/mod.rs | 5 + nvx-rs/src/host/platform/mod.rs | 13 +- .../{store/identity.rs => runtime/mod.rs} | 84 ++++++++-- nvx-rs/src/host/runtime/tests.rs | 148 ++++++++++++++++++ nvx-rs/src/host/store/mod.rs | 19 ++- nvx-rs/src/host/store/tests.rs | 91 +---------- 13 files changed, 391 insertions(+), 133 deletions(-) rename nvx-rs/src/host/{store/identity.rs => runtime/mod.rs} (75%) create mode 100644 nvx-rs/src/host/runtime/tests.rs diff --git a/nvx-rs/README.md b/nvx-rs/README.md index ff16eb8df..7236ca3b0 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -45,12 +45,12 @@ nvx-rs/ │ ├── error.rs nvx::Error, its ErrorKind, and its stable ErrorCode │ ├── exec/ the execution engine: the streams, credit, and outcome of an execution's two │ │ sides, without I/O, threads, or features, for any framing and host -│ ├── host/ feature `host` (default): artifacts, launch, network and egress policy files, -│ │ filesystem, NVXS connection, session, process, platform, VM instances, -│ │ sandbox stores, templates' manifests, compatibility keys, store, and leases, -│ │ outcome reports, file seals, and the sandbox launch profile of -│ │ `nvx-rs sandbox`, its distro and scratch bindings, and its prepared launches; -│ │ with feature `cli`, the `nvx-rs` CLI +│ ├── host/ feature `host` (default): the runtime that every launch shares, artifacts, +│ │ launch, network and egress policy files, filesystem, NVXS connection, session, +│ │ process, platform, VM instances, sandbox stores, templates' manifests, +│ │ compatibility keys, store, and leases, outcome reports, file seals, and the +│ │ sandbox launch profile of `nvx-rs sandbox`, its distro and scratch bindings, +│ │ and its prepared launches; with feature `cli`, the `nvx-rs` CLI │ ├── testing/ feature `testing`: the fake OpenVMM │ ├── guest/ feature `guest`: the programs that run in Linux guests, the guest's NVXS │ │ connection, the libraries through which a guest init or agent opens the @@ -120,6 +120,7 @@ changes only when the module's purpose does. | [`nvx::uuid`](#nvxuuid) | UUIDs, such as those of GPT disks and filesystems, in their canonical text and in GPT's layout | | [`nvx::Hypervisor`](#nvxhypervisor) | KVM, MSHV, or WHP, with its serde form behind the `serde` feature | | [`nvx::exec`](#nvxexec) | The execution engine: the streams, credit, and outcome of an execution's two sides, without I/O, for any framing | +| [`nvx::host::runtime`](#nvxhostruntime) | `Runtime`: what every launch shares, the runtime's files and their identity, the hypervisor and its probe, and the launch options | | [`nvx::host::artifacts`](#nvxhostartifacts) | Locating the OpenVMM executable and the guest artifacts that a VM runs, and their runtime identity | | [`nvx::host::launch`](#nvxhostlaunch) | `LaunchSpec` and `VmmLaunch`: the OpenVMM command line of a managed microVM, its checks, and the inputs that it shares with an adapter's launch | | [`nvx::host::network`](#nvxhostnetwork) | The one lowering of a network posture into OpenVMM options, for the CLI and external orchestrators | @@ -379,6 +380,26 @@ The adapter chooses one byte window per stream, or frames charged against one pool that stdout and stderr share, and the maximum chunk, in `Limits`; a `Key`'s generation tells stale messages from foreign ones. +### `nvx::host::runtime` + +`Runtime`: what every launch shares, the paths of the OpenVMM executable, guest +kernel, and guest initramfs, their `RuntimeIdentity`, the hypervisor, an +optional CPU profile, whether OpenVMM breaks away from the caller's Windows job +object, and the `LaunchOptions`. Those are what OpenVMM does when the guest +powers off, a `GuestPowerAction`, by default `Exit`, which ends the VM, or +`Halt` or `Reset`, and how long OpenVMM waits for the host to authenticate on +the control console. The boot console is not among them: each VM attaches its +own. + +`RuntimeIdentity` labels the three files with the `ArtifactIdentity` tokens that +their install or staging metadata gives, such as `sha256:` and a digest, which +NVX compares and never computes, and adds the versions of the contracts that the +crate speaks; `differences` names the `IdentityPart`s in which two identities +differ. The sandbox store records it with each VM, and re-exports its types. + +`probe_hypervisor` checks that the host has the hypervisor: its device on Linux, +and the Windows Hypervisor Platform on Windows. + ### `nvx::host::artifacts` `Artifacts` (also `nvx::Artifacts`): the OpenVMM executable, guest kernel, and @@ -2563,12 +2584,13 @@ optional cause. | `ErrorKind` | Meaning | | --- | --- | | `Unavailable` | A missing or unusable artifact, hypervisor, or release, a guest image without a required control feature, or an unsupported host | -| `Unsupported` | A well-formed request that NVX cannot represent or enforce, such as a policy beyond a limit of OpenVMM or the guest agent | +| `Unsupported` | A request that NVX refuses: a value that it does not accept, such as a malformed or out-of-range one, options that it cannot combine, or a request that it cannot represent or enforce, such as a policy beyond a limit of OpenVMM or the guest agent | | `Exited` | The VM's OpenVMM process exited | | `Io` | A host I/O failure, or state that NVX recorded on the host and cannot read | | `NotFound` | State that an operation names and that does not exist on the host, such as the store of a sandbox that was never created or that a removal deleted | | `Protocol` | A control session that failed, or a request that the guest agent refused | | `TimedOut` | A deadline that passed | +| `RuntimeMismatch` | A VM that runs under another runtime identity than the artifacts', which NVX does not drive: only a stop, which terminates it, applies | | `Other` | Any other failure | Messages call an instance `sandbox NAME`, after the name that `Instance::create` @@ -2612,7 +2634,7 @@ usage errors, which no library error has. | `sandbox-running` | `Other` | The operation needs a stopped sandbox, but its VM, or a launch of it, may still run | | `sandbox-exited` | `Exited` | The sandbox's VM exited without a stop: the transition that finds it clears its runtime record, and a start launches it again | | `sandbox-busy` | `TimedOut` | Another lifecycle transition of the sandbox held its lock until the deadline passed | -| `runtime-mismatch` | `Other` | The sandbox's VM runs under another runtime identity than the artifacts', whose differing parts the message names, so only a stop, which terminates it, applies | +| `runtime-mismatch` | `RuntimeMismatch` | The sandbox's VM runs under another runtime identity than the artifacts', whose differing parts the message names, so only a stop, which terminates it, applies | | `vmm-indeterminate` | `Other` | Whether the OpenVMM process of the sandbox runs cannot be determined, so its records remain for a later attempt | | `format-unsupported` | `Io` | A record or report has a format or schema version that this build does not read, such as the state of a sandbox that another build provisioned or started | | `foreign-files` | `Other` | A directory of NVX's state, such as a sandbox's state directory or a template store, holds files that NVX does not own, which the message names and NVX keeps | diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 1f5fa02b3..933df01ac 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -72,6 +72,7 @@ variant nvx::ErrorKind::Io variant nvx::ErrorKind::NotFound variant nvx::ErrorKind::Other variant nvx::ErrorKind::Protocol +variant nvx::ErrorKind::RuntimeMismatch variant nvx::ErrorKind::TimedOut variant nvx::ErrorKind::Unavailable variant nvx::ErrorKind::Unsupported @@ -2015,7 +2016,6 @@ method nvx::host::platform::Transport::write_all type nvx::host::platform::TransportHandle fn nvx::host::platform::create_private_dir fn nvx::host::platform::file_identity -fn nvx::host::platform::probe_hypervisor fn nvx::host::platform::process_start_time mod nvx::host::process struct nvx::host::process::BootId @@ -2044,6 +2044,48 @@ field nvx::host::report::Report::category field nvx::host::report::Report::status_code field nvx::host::report::Report::teardown_complete fn nvx::host::report::read +mod nvx::host::runtime +struct nvx::host::runtime::ArtifactIdentity +method nvx::host::runtime::ArtifactIdentity::as_str +method nvx::host::runtime::ArtifactIdentity::new +method nvx::host::runtime::ArtifactIdentity::sha256 +enum nvx::host::runtime::GuestPowerAction +variant nvx::host::runtime::GuestPowerAction::Exit +variant nvx::host::runtime::GuestPowerAction::Halt +variant nvx::host::runtime::GuestPowerAction::Reset +enum nvx::host::runtime::IdentityPart +const nvx::host::runtime::IdentityPart::ALL +variant nvx::host::runtime::IdentityPart::BootContract +variant nvx::host::runtime::IdentityPart::ControlContract +variant nvx::host::runtime::IdentityPart::ControlSession +variant nvx::host::runtime::IdentityPart::Initramfs +variant nvx::host::runtime::IdentityPart::Kernel +variant nvx::host::runtime::IdentityPart::OpenVmm +method nvx::host::runtime::IdentityPart::name +struct nvx::host::runtime::LaunchOptions +field nvx::host::runtime::LaunchOptions::control_auth_timeout +field nvx::host::runtime::LaunchOptions::guest_shutdown +const nvx::host::runtime::MAX_IDENTITY_LEN +struct nvx::host::runtime::Runtime +field nvx::host::runtime::Runtime::cpu_profile +field nvx::host::runtime::Runtime::hypervisor +field nvx::host::runtime::Runtime::identity +field nvx::host::runtime::Runtime::initramfs +field nvx::host::runtime::Runtime::kernel +field nvx::host::runtime::Runtime::openvmm +field nvx::host::runtime::Runtime::options +field nvx::host::runtime::Runtime::windows_breakaway +struct nvx::host::runtime::RuntimeIdentity +field nvx::host::runtime::RuntimeIdentity::boot_contract +method nvx::host::runtime::RuntimeIdentity::check +field nvx::host::runtime::RuntimeIdentity::control_contract +field nvx::host::runtime::RuntimeIdentity::control_session +method nvx::host::runtime::RuntimeIdentity::differences +field nvx::host::runtime::RuntimeIdentity::initramfs +field nvx::host::runtime::RuntimeIdentity::kernel +method nvx::host::runtime::RuntimeIdentity::new +field nvx::host::runtime::RuntimeIdentity::openvmm +fn nvx::host::runtime::probe_hypervisor mod nvx::host::sandbox enum nvx::host::sandbox::BlockRole variant nvx::host::sandbox::BlockRole::Distro diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index b3952abbd..2de59d619 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -72,6 +72,7 @@ variant nvx::ErrorKind::Io variant nvx::ErrorKind::NotFound variant nvx::ErrorKind::Other variant nvx::ErrorKind::Protocol +variant nvx::ErrorKind::RuntimeMismatch variant nvx::ErrorKind::TimedOut variant nvx::ErrorKind::Unavailable variant nvx::ErrorKind::Unsupported @@ -953,7 +954,6 @@ method nvx::host::platform::Transport::write_all type nvx::host::platform::TransportHandle fn nvx::host::platform::create_private_dir fn nvx::host::platform::file_identity -fn nvx::host::platform::probe_hypervisor fn nvx::host::platform::process_start_time mod nvx::host::process struct nvx::host::process::BootId @@ -982,6 +982,48 @@ field nvx::host::report::Report::category field nvx::host::report::Report::status_code field nvx::host::report::Report::teardown_complete fn nvx::host::report::read +mod nvx::host::runtime +struct nvx::host::runtime::ArtifactIdentity +method nvx::host::runtime::ArtifactIdentity::as_str +method nvx::host::runtime::ArtifactIdentity::new +method nvx::host::runtime::ArtifactIdentity::sha256 +enum nvx::host::runtime::GuestPowerAction +variant nvx::host::runtime::GuestPowerAction::Exit +variant nvx::host::runtime::GuestPowerAction::Halt +variant nvx::host::runtime::GuestPowerAction::Reset +enum nvx::host::runtime::IdentityPart +const nvx::host::runtime::IdentityPart::ALL +variant nvx::host::runtime::IdentityPart::BootContract +variant nvx::host::runtime::IdentityPart::ControlContract +variant nvx::host::runtime::IdentityPart::ControlSession +variant nvx::host::runtime::IdentityPart::Initramfs +variant nvx::host::runtime::IdentityPart::Kernel +variant nvx::host::runtime::IdentityPart::OpenVmm +method nvx::host::runtime::IdentityPart::name +struct nvx::host::runtime::LaunchOptions +field nvx::host::runtime::LaunchOptions::control_auth_timeout +field nvx::host::runtime::LaunchOptions::guest_shutdown +const nvx::host::runtime::MAX_IDENTITY_LEN +struct nvx::host::runtime::Runtime +field nvx::host::runtime::Runtime::cpu_profile +field nvx::host::runtime::Runtime::hypervisor +field nvx::host::runtime::Runtime::identity +field nvx::host::runtime::Runtime::initramfs +field nvx::host::runtime::Runtime::kernel +field nvx::host::runtime::Runtime::openvmm +field nvx::host::runtime::Runtime::options +field nvx::host::runtime::Runtime::windows_breakaway +struct nvx::host::runtime::RuntimeIdentity +field nvx::host::runtime::RuntimeIdentity::boot_contract +method nvx::host::runtime::RuntimeIdentity::check +field nvx::host::runtime::RuntimeIdentity::control_contract +field nvx::host::runtime::RuntimeIdentity::control_session +method nvx::host::runtime::RuntimeIdentity::differences +field nvx::host::runtime::RuntimeIdentity::initramfs +field nvx::host::runtime::RuntimeIdentity::kernel +method nvx::host::runtime::RuntimeIdentity::new +field nvx::host::runtime::RuntimeIdentity::openvmm +fn nvx::host::runtime::probe_hypervisor mod nvx::host::sandbox enum nvx::host::sandbox::BlockRole variant nvx::host::sandbox::BlockRole::Distro diff --git a/nvx-rs/src/contract.rs b/nvx-rs/src/contract.rs index 1f95e9560..666a817ca 100644 --- a/nvx-rs/src/contract.rs +++ b/nvx-rs/src/contract.rs @@ -2,10 +2,10 @@ //! compatibility of NVX releases with them. //! //! The store records the boot contract, control contract, and control session versions of the -//! build that started a VM in the VM's runtime identity, `nvx::host::store::RuntimeIdentity`, and a -//! build drives only the VMs whose runtime identity is its own. So each version changes whenever a -//! guest that a build of the earlier version started could not be driven as one that a build of -//! the later version starts: +//! build that started a VM in the VM's runtime identity, `nvx::host::runtime::RuntimeIdentity`, +//! and a build drives only the VMs whose runtime identity is its own. So each version changes +//! whenever a guest that a build of the earlier version started could not be driven as one that a +//! build of the later version starts: //! //! - [`MICROVM_ABI_VERSION`] is the microVM ABI of the pinned OpenVMM: the machine, its devices, //! and their addresses, through which OpenVMM boots the guest and saves its state. It changes diff --git a/nvx-rs/src/error.rs b/nvx-rs/src/error.rs index 9cfdf3701..989538bab 100644 --- a/nvx-rs/src/error.rs +++ b/nvx-rs/src/error.rs @@ -13,8 +13,9 @@ pub enum ErrorKind { /// guest image without the control contract or features that this crate needs, or a host /// that NVX does not support. Unavailable, - /// The request is well formed, but NVX cannot represent or enforce it, for example because it - /// exceeds a limit of OpenVMM or the guest agent. + /// NVX refuses the request: a value that it does not accept, such as a malformed or + /// out-of-range one, options that it cannot combine, or a request that it cannot represent or + /// enforce, for example because it exceeds a limit of OpenVMM or the guest agent. Unsupported, /// The OpenVMM process of a VM exited. Exited, @@ -29,6 +30,9 @@ pub enum ErrorKind { Protocol, /// A deadline passed. TimedOut, + /// A VM runs under another runtime identity than the artifacts', so NVX does not drive it: + /// only a stop, which terminates it, applies. + RuntimeMismatch, /// Any other failure. Other, } @@ -44,6 +48,7 @@ impl ErrorKind { Self::NotFound => "not-found", Self::Protocol => "protocol", Self::TimedOut => "timed-out", + Self::RuntimeMismatch => "runtime-mismatch", Self::Other => "other", } } @@ -160,7 +165,7 @@ pub enum ErrorCode { } impl ErrorCode { - /// Every code of this release, the codes of the kinds first. + /// Every code of this release. pub const ALL: &'static [ErrorCode] = &[ Self::Unavailable, Self::Unsupported, @@ -216,7 +221,7 @@ impl ErrorCode { Self::SandboxRunning => "sandbox-running", Self::SandboxExited => "sandbox-exited", Self::SandboxBusy => "sandbox-busy", - Self::RuntimeMismatch => "runtime-mismatch", + Self::RuntimeMismatch => ErrorKind::RuntimeMismatch.as_str(), Self::VmmIndeterminate => "vmm-indeterminate", Self::FormatUnsupported => "format-unsupported", Self::ForeignFiles => "foreign-files", @@ -247,11 +252,11 @@ impl ErrorCode { Self::NotFound | Self::SandboxNotProvisioned => ErrorKind::NotFound, Self::Protocol | Self::ExecRefused | Self::EndpointClosed => ErrorKind::Protocol, Self::TimedOut | Self::SandboxBusy | Self::ScratchClaimed => ErrorKind::TimedOut, + Self::RuntimeMismatch => ErrorKind::RuntimeMismatch, Self::Other | Self::SandboxProvisioned | Self::SandboxNotRunning | Self::SandboxRunning - | Self::RuntimeMismatch | Self::VmmIndeterminate | Self::ForeignFiles | Self::OutcomeReportExists @@ -277,6 +282,7 @@ impl From for ErrorCode { ErrorKind::NotFound => Self::NotFound, ErrorKind::Protocol => Self::Protocol, ErrorKind::TimedOut => Self::TimedOut, + ErrorKind::RuntimeMismatch => Self::RuntimeMismatch, ErrorKind::Other => Self::Other, } } diff --git a/nvx-rs/src/error/tests.rs b/nvx-rs/src/error/tests.rs index ea926d394..eb87d7dc0 100644 --- a/nvx-rs/src/error/tests.rs +++ b/nvx-rs/src/error/tests.rs @@ -24,10 +24,21 @@ fn codes_determine_kinds() { assert_eq!(error.code(), ErrorCode::SandboxExited); assert_eq!(error.kind(), ErrorKind::Exited); assert_eq!(error.into_parts().0, ErrorKind::Exited); + let mismatch = Error::new(ErrorCode::RuntimeMismatch, "another runtime"); + assert_eq!(mismatch.code(), ErrorCode::RuntimeMismatch); + assert_eq!(mismatch.kind(), ErrorKind::RuntimeMismatch); + let (kind, message, source) = mismatch.into_parts(); + assert_eq!(kind, ErrorKind::RuntimeMismatch); + assert_eq!(message, "another runtime"); + assert!(source.is_none()); assert_eq!( Error::new(ErrorKind::TimedOut, "late").code(), ErrorCode::TimedOut ); + assert_eq!( + Error::new(ErrorKind::RuntimeMismatch, "elsewhere").code(), + ErrorCode::RuntimeMismatch + ); assert_eq!(Error::unsupported("no").code(), ErrorCode::Unsupported); assert_eq!( ErrorCode::SandboxNotRunning.to_string(), @@ -45,6 +56,7 @@ fn every_kind_has_a_code_of_its_own_spelling() { ErrorKind::NotFound, ErrorKind::Protocol, ErrorKind::TimedOut, + ErrorKind::RuntimeMismatch, ErrorKind::Other, ] { let code = ErrorCode::from(kind); diff --git a/nvx-rs/src/host/artifacts/mod.rs b/nvx-rs/src/host/artifacts/mod.rs index 3deabc5d0..508c2b2d4 100644 --- a/nvx-rs/src/host/artifacts/mod.rs +++ b/nvx-rs/src/host/artifacts/mod.rs @@ -146,7 +146,7 @@ impl Artifacts { /// /// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) otherwise, with the /// code [`ErrorCode::ArtifactMissing`] for a missing file. It does not check the hypervisor; - /// [`probe_hypervisor`](super::platform::probe_hypervisor) does. + /// [`probe_hypervisor`](super::runtime::probe_hypervisor) does. pub fn probe(&self) -> Result<()> { if !platform::SUPPORTED { return Err(Error::unavailable( diff --git a/nvx-rs/src/host/mod.rs b/nvx-rs/src/host/mod.rs index 0e4bc2254..14b7299ee 100644 --- a/nvx-rs/src/host/mod.rs +++ b/nvx-rs/src/host/mod.rs @@ -2,6 +2,10 @@ //! //! - [`artifacts`] locates the OpenVMM executable and the guest kernel and initramfs, and every //! artifact of the guests that `nvx.py` runs, in a repository checkout or a release. +//! - [`runtime`] holds what every launch shares: the paths of those three files and their +//! [`RuntimeIdentity`](runtime::RuntimeIdentity), the hypervisor, which +//! [`probe_hypervisor`](runtime::probe_hypervisor) checks, and the +//! [`LaunchOptions`](runtime::LaunchOptions). //! - [`launch`] builds OpenVMM's command line from a [`LaunchSpec`](launch::LaunchSpec): the //! managed lifecycle, the workload identity, the kernel command line, and the network and //! filesystem options. @@ -73,6 +77,7 @@ pub(crate) mod paths; pub mod platform; pub mod process; pub mod report; +pub mod runtime; pub mod sandbox; pub mod seal; pub mod session; diff --git a/nvx-rs/src/host/platform/mod.rs b/nvx-rs/src/host/platform/mod.rs index 3b4c990c6..b990f92d6 100644 --- a/nvx-rs/src/host/platform/mod.rs +++ b/nvx-rs/src/host/platform/mod.rs @@ -12,8 +12,9 @@ use std::io; use std::path::Path; use std::time::Duration; -use crate::error::{Error, Result}; -use crate::hypervisor::Hypervisor; +#[cfg(windows)] +use crate::error::Error; +use crate::error::Result; pub(crate) mod inherit; #[cfg(target_os = "linux")] @@ -36,14 +37,6 @@ use windows as imp; pub(crate) use imp::*; pub use imp::{SUPPORTED, create_private_dir, file_identity, process_start_time}; -/// Checks that `hypervisor` is present and accessible on this host: its device on Linux, and the -/// Windows Hypervisor Platform on Windows. -/// -/// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) otherwise. -pub fn probe_hypervisor(hypervisor: Hypervisor) -> Result<()> { - imp::probe_hypervisor(hypervisor).map_err(Error::unavailable) -} - /// Refuses a VMM working directory that exceeds Windows' process-creation limit, without /// accessing the filesystem. Other hosts have no such limit. pub(crate) fn check_working_dir(path: &Path) -> Result<()> { diff --git a/nvx-rs/src/host/store/identity.rs b/nvx-rs/src/host/runtime/mod.rs similarity index 75% rename from nvx-rs/src/host/store/identity.rs rename to nvx-rs/src/host/runtime/mod.rs index d0b3f715c..90ba8edd2 100644 --- a/nvx-rs/src/host/store/identity.rs +++ b/nvx-rs/src/host/runtime/mod.rs @@ -1,12 +1,78 @@ -//! The runtime identity of a VM: the identities of its VMM, kernel, and initramfs, and the -//! versions of the contracts between its host, its VMM, and its guest. +//! What every launch shares: a [`Runtime`], the OpenVMM executable, guest kernel, and guest +//! initramfs that it runs, with their [`RuntimeIdentity`], the hypervisor, which +//! [`probe_hypervisor`] checks, and the [`LaunchOptions`]. +//! +//! The runtime identity of a VM is the identities of its VMM, kernel, and initramfs, and the +//! versions of the contracts between its host, its VMM, and its guest. The sandbox +//! [`store`](crate::host::store) records it, and re-exports its types. use std::fmt; +use std::path::PathBuf; +use std::time::Duration; use serde::{Deserialize, Serialize}; +use super::platform; use crate::contract; use crate::error::{Error, Result}; +use crate::hypervisor::Hypervisor; + +/// What every launch shares: the runtime's files and their identity, the hypervisor, and the +/// options of OpenVMM that do not depend on the VM. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Runtime { + /// The OpenVMM executable. + pub openvmm: PathBuf, + /// The guest kernel. + pub kernel: PathBuf, + /// The guest initramfs. + pub initramfs: PathBuf, + /// The identity of the three files and of the contracts that they speak, which the caller + /// takes from their install or staging metadata: NVX compares identities and never reads the + /// files. + pub identity: RuntimeIdentity, + /// The hypervisor that OpenVMM runs the VM on. + pub hypervisor: Hypervisor, + /// The CPU profile that OpenVMM presents to the guest, by OpenVMM's name for it, or `None` + /// for OpenVMM's default. + pub cpu_profile: Option, + /// Windows only: start OpenVMM outside the caller's job object, so that the VM survives the + /// caller. The job must permit breakaway. + pub windows_breakaway: bool, + /// The options of OpenVMM that every launch shares. + pub options: LaunchOptions, +} + +/// The options of OpenVMM that every launch shares. The boot console is not one of them: each VM +/// attaches its own. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct LaunchOptions { + /// What OpenVMM does when the guest powers off. + pub guest_shutdown: GuestPowerAction, + /// How long OpenVMM waits for the host to authenticate on the control console, or `None` for + /// OpenVMM's default. + pub control_auth_timeout: Option, +} + +/// What OpenVMM does when the guest powers off. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub enum GuestPowerAction { + /// OpenVMM exits, which ends the VM: the default, on which stopping a VM relies. + #[default] + Exit, + /// OpenVMM halts the VM and keeps running. + Halt, + /// OpenVMM restarts the guest. + Reset, +} + +/// Checks that `hypervisor` is present and accessible on this host: its device on Linux, and the +/// Windows Hypervisor Platform on Windows. +/// +/// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) otherwise. +pub fn probe_hypervisor(hypervisor: Hypervisor) -> Result<()> { + platform::probe_hypervisor(hypervisor).map_err(Error::unavailable) +} /// Longest [`ArtifactIdentity`] and control contract revision, in bytes. pub const MAX_IDENTITY_LEN: usize = 128; @@ -159,7 +225,7 @@ impl fmt::Display for IdentityPart { /// build of the host's own code keeps the VM running and reattaches to it. A different runtime /// identity would drive the VM through artifacts or contracts that did not start it, so the VM /// must be stopped instead, through its verified process identity; see -/// [`RuntimeRecord::check_identity`](super::RuntimeRecord::check_identity). +/// [`RuntimeRecord::check_identity`](crate::host::store::RuntimeRecord::check_identity). /// /// The caller takes the artifact identities from install or staging metadata, and /// [`RuntimeIdentity::new`] adds the contract versions that this crate speaks. @@ -233,13 +299,5 @@ impl RuntimeIdentity { } } -/// How the runtime identity of a record compares with the current one. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum IdentityCheck { - /// The identities are equal: the host may reattach to the VM and keep it running. - Matches, - /// The identities differ in the parts that this lists, as - /// [`RuntimeIdentity::differences`] returns them: the VM must be stopped, and only through - /// its verified process identity. - Mismatched(Vec), -} +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/runtime/tests.rs b/nvx-rs/src/host/runtime/tests.rs new file mode 100644 index 000000000..bda10ef06 --- /dev/null +++ b/nvx-rs/src/host/runtime/tests.rs @@ -0,0 +1,148 @@ +//! Tests of the runtime identities, of the values that every launch shares, and of the probe of a +//! hypervisor. + +use super::*; +use crate::ErrorKind; +/// Returns the identity `sha256:` and 64 copies of `digit`. +fn artifact(digit: char) -> ArtifactIdentity { + ArtifactIdentity::sha256(&digit.to_string().repeat(64)).unwrap() +} + +/// Returns a runtime identity under this crate's contracts. +fn runtime() -> RuntimeIdentity { + RuntimeIdentity::new(artifact('a'), artifact('b'), artifact('c')) +} + +#[test] +fn artifact_identities_are_printable_tokens() { + assert_eq!( + ArtifactIdentity::new("openvmm-1.2+build.7") + .unwrap() + .as_str(), + "openvmm-1.2+build.7" + ); + for invalid in [ + String::new(), + "with space".to_owned(), + "tab\there".to_owned(), + "caf\u{e9}".to_owned(), + "x".repeat(MAX_IDENTITY_LEN + 1), + ] { + let error = ArtifactIdentity::new(invalid.clone()).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{invalid:?}"); + } + assert!(ArtifactIdentity::new("x".repeat(MAX_IDENTITY_LEN)).is_ok()); + + let digest = "AB".repeat(32); + assert_eq!( + ArtifactIdentity::sha256(&digest).unwrap().to_string(), + format!("sha256:{}", "ab".repeat(32)) + ); + for invalid in ["ab".repeat(31), "g".repeat(64), "ab".repeat(33)] { + assert_eq!( + ArtifactIdentity::sha256(&invalid).unwrap_err().kind(), + ErrorKind::Unsupported + ); + } + // The serde form checks the token too. + assert!(serde_json::from_value::(serde_json::json!("a b")).is_err()); +} + +#[test] +fn runtime_identities_bind_this_crates_contracts() { + let identity = runtime(); + assert_eq!(identity.boot_contract, contract::BOOT_CONTRACT_VERSION); + assert_eq!( + identity.control_contract, + contract::CONTROL_CONTRACT_REVISION + ); + assert_eq!( + identity.control_session, + contract::CONTROL_SESSION_PROTOCOL_VERSION + ); + identity.check().unwrap(); + assert!(identity.differences(&runtime()).is_empty()); + + let mut other = runtime(); + other.kernel = artifact('d'); + other.control_session += 1; + assert_eq!( + identity.differences(&other), + [IdentityPart::Kernel, IdentityPart::ControlSession] + ); + other = RuntimeIdentity::new(artifact('e'), artifact('b'), artifact('f')); + other.boot_contract += 1; + other.control_contract = "another-revision".to_owned(); + assert_eq!( + identity.differences(&other), + [ + IdentityPart::OpenVmm, + IdentityPart::Initramfs, + IdentityPart::BootContract, + IdentityPart::ControlContract + ] + ); + let names: Vec<_> = IdentityPart::ALL.iter().map(ToString::to_string).collect(); + assert_eq!( + names, + [ + "OpenVMM", + "kernel", + "initramfs", + "boot contract", + "control contract", + "control session" + ] + ); + for invalid in ["", "two words"] { + other.control_contract = invalid.to_owned(); + assert_eq!(other.check().unwrap_err().kind(), ErrorKind::Unsupported); + } +} + +/// Returns a runtime that supplies each field that every launch shares, with the default launch +/// options. +fn shared() -> Runtime { + Runtime { + openvmm: PathBuf::from("openvmm"), + kernel: PathBuf::from("vmlinux"), + initramfs: PathBuf::from("initramfs.cpio.gz"), + identity: runtime(), + hypervisor: Hypervisor::Kvm, + cpu_profile: Some("auto".to_owned()), + windows_breakaway: false, + options: LaunchOptions::default(), + } +} + +#[test] +fn by_default_a_guest_that_powers_off_ends_its_vm() { + assert_eq!(GuestPowerAction::default(), GuestPowerAction::Exit); + assert_eq!( + shared().options, + LaunchOptions { + guest_shutdown: GuestPowerAction::Exit, + control_auth_timeout: None, + } + ); +} + +#[test] +fn a_hypervisor_that_the_host_never_has_is_unavailable() { + let foreign: &[Hypervisor] = if cfg!(target_os = "linux") { + &[Hypervisor::Whp] + } else if cfg!(windows) { + &[Hypervisor::Kvm, Hypervisor::Mshv] + } else { + &[Hypervisor::Kvm, Hypervisor::Mshv, Hypervisor::Whp] + }; + for &hypervisor in foreign { + let error = probe_hypervisor(hypervisor).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unavailable, "{hypervisor}"); + assert_eq!( + error.to_string(), + platform::probe_hypervisor(hypervisor).unwrap_err(), + "{hypervisor}" + ); + } +} diff --git a/nvx-rs/src/host/store/mod.rs b/nvx-rs/src/host/store/mod.rs index 68c366a8f..912005938 100644 --- a/nvx-rs/src/host/store/mod.rs +++ b/nvx-rs/src/host/store/mod.rs @@ -260,7 +260,24 @@ mod caller; pub(crate) mod files; -mod identity; +/// The identities of runtimes, which [`crate::host::runtime`] defines, and how the one of a record +/// compares with the current one. +mod identity { + pub use crate::host::runtime::{ + ArtifactIdentity, IdentityPart, MAX_IDENTITY_LEN, RuntimeIdentity, + }; + + /// How the runtime identity of a record compares with the current one. + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum IdentityCheck { + /// The identities are equal: the host may reattach to the VM and keep it running. + Matches, + /// The identities differ in the parts that this lists, as + /// [`RuntimeIdentity::differences`] returns them: the VM must be stopped, and only through + /// its verified process identity. + Mismatched(Vec), + } +} mod launch; pub(crate) mod lease; mod lifecycle; diff --git a/nvx-rs/src/host/store/tests.rs b/nvx-rs/src/host/store/tests.rs index 25670c3cd..bffe25bf6 100644 --- a/nvx-rs/src/host/store/tests.rs +++ b/nvx-rs/src/host/store/tests.rs @@ -1,5 +1,5 @@ -//! Tests of the store's identities, records, lock, and launch bookkeeping. The launches of real -//! processes and the recovery of interrupted ones are tested in `tests/store.rs`. +//! Tests of the store's records, lock, and launch bookkeeping. The launches of real processes and +//! the recovery of interrupted ones are tested in `tests/store.rs`. use std::fs; use std::sync::mpsc; @@ -46,93 +46,6 @@ fn write(store: &SandboxStore, name: &str, value: &serde_json::Value) { fs::write(store.path(name), serde_json::to_vec(value).unwrap()).unwrap(); } -#[test] -fn artifact_identities_are_printable_tokens() { - assert_eq!( - ArtifactIdentity::new("openvmm-1.2+build.7") - .unwrap() - .as_str(), - "openvmm-1.2+build.7" - ); - for invalid in [ - String::new(), - "with space".to_owned(), - "tab\there".to_owned(), - "caf\u{e9}".to_owned(), - "x".repeat(MAX_IDENTITY_LEN + 1), - ] { - let error = ArtifactIdentity::new(invalid.clone()).unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unsupported, "{invalid:?}"); - } - assert!(ArtifactIdentity::new("x".repeat(MAX_IDENTITY_LEN)).is_ok()); - - let digest = "AB".repeat(32); - assert_eq!( - ArtifactIdentity::sha256(&digest).unwrap().to_string(), - format!("sha256:{}", "ab".repeat(32)) - ); - for invalid in ["ab".repeat(31), "g".repeat(64), "ab".repeat(33)] { - assert_eq!( - ArtifactIdentity::sha256(&invalid).unwrap_err().kind(), - ErrorKind::Unsupported - ); - } - // The serde form checks the token too. - assert!(serde_json::from_value::(serde_json::json!("a b")).is_err()); -} - -#[test] -fn runtime_identities_bind_this_crates_contracts() { - let identity = runtime(); - assert_eq!(identity.boot_contract, contract::BOOT_CONTRACT_VERSION); - assert_eq!( - identity.control_contract, - contract::CONTROL_CONTRACT_REVISION - ); - assert_eq!( - identity.control_session, - contract::CONTROL_SESSION_PROTOCOL_VERSION - ); - identity.check().unwrap(); - assert!(identity.differences(&runtime()).is_empty()); - - let mut other = runtime(); - other.kernel = artifact('d'); - other.control_session += 1; - assert_eq!( - identity.differences(&other), - [IdentityPart::Kernel, IdentityPart::ControlSession] - ); - other = RuntimeIdentity::new(artifact('e'), artifact('b'), artifact('f')); - other.boot_contract += 1; - other.control_contract = "another-revision".to_owned(); - assert_eq!( - identity.differences(&other), - [ - IdentityPart::OpenVmm, - IdentityPart::Initramfs, - IdentityPart::BootContract, - IdentityPart::ControlContract - ] - ); - let names: Vec<_> = IdentityPart::ALL.iter().map(ToString::to_string).collect(); - assert_eq!( - names, - [ - "OpenVMM", - "kernel", - "initramfs", - "boot contract", - "control contract", - "control session" - ] - ); - for invalid in ["", "two words"] { - other.control_contract = invalid.to_owned(); - assert_eq!(other.check().unwrap_err().kind(), ErrorKind::Unsupported); - } -} - #[test] fn records_keep_their_json_form() { let journal = LaunchJournal::prepared(