From 71f3c50b0dab5718d7eaab853313b13c6a9d8ff5 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Tue, 10 Nov 2026 10:41:23 -0800 Subject: [PATCH 01/15] host: prepare blockless capture launches Prepare Fresh captures through the existing VM creation path with private sparse disks, checked geometry, platform-only arguments and a caller-persisted capture context. Leave capture completion and snapshot publication to their separate operations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 095bfb08-a5d0-4331-b2b2-dd76d2fbadd8 --- nvx-rs/README.md | 40 ++- nvx-rs/src/host/config/mod.rs | 177 ++++++++++-- nvx-rs/src/host/config/tests.rs | 118 +++++++- nvx-rs/src/host/snapshot.rs | 28 +- nvx-rs/src/host/vm.rs | 157 +++++++++-- nvx-rs/src/host/vm/tests.rs | 425 ++++++++++++++++++++++++++++- scripts/test-layout-allowlist.json | 2 +- 7 files changed, 886 insertions(+), 61 deletions(-) diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 8c0198d71..cdd2bb891 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -124,7 +124,7 @@ changes only when the module's purpose does. | [`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::vm`](#nvxhostvm) | A private VM directory, cold preparation, verified detached launch, confirmed stop, and caller-saved reattachment | +| [`nvx::host::vm`](#nvxhostvm) | A private VM directory, cold and capture preparation, verified detached launch, confirmed stop, and caller-saved reattachment | | [`nvx::host::snapshot`](#nvxhostsnapshot) | Strict private snapshot directories, verified keys and manifests, time eligibility, and the input of a future VM restore | | [`nvx::host::config`](#nvxhostconfig) | `VmConfig` and `CaptureConfig`: pure configuration, cold-boot kernel/OpenVMM arguments, portable length checks, and capture identity | | [`nvx::host::restore`](#nvxhostrestore) | A caller-driven restore to Ready without I/O or a clock, and its peer-verified restore-ready listener | @@ -515,11 +515,23 @@ failure never hides confirmed exit. `Prepared` launch without starting a process or binding a listener. It requires `config.boot`, refuses a snapshot input, rechecks the directory's privacy, and creates a new private log. Existing log, outcome and Linux endpoint entries -are refused rather than replaced. Only the log is created before a spawn; -OpenVMM creates the remaining outputs when it runs. A failed preparation -closes and removes only its new log, preserving all pre-existing entries and +are refused rather than replaced. An ordinary cold boot creates only the log +before a spawn; OpenVMM creates its remaining outputs when it runs. A failed preparation +closes and removes only its new files, preserving all pre-existing entries and caller-owned inputs, and reports a cleanup failure with the original error. +With `boot.capture`, creation requires a blockless root and refuses every +filesystem export. It creates private sparse `capture-image.raw` and +`capture-scratch.raw` files, verifies their actual complete geometries, and +targets `/openvmm`. A configured NIC keeps its resolved address but +uses the complete deny-all policy, whatever its configured policy. +`Prepared::capture_context()` exposes a cloneable, serializable +`snapshot::CaptureContext` with the checked capture configuration and disk +geometries, NIC and console devices, runtime identity, hypervisor and CPU +profile. The caller chooses whether and how to persist it. Creation writes +no context file, `key.bin` or `snapshot.json`, and does not finish or publish +a capture. + On Linux, the generated control and boot-console socket names must be valid Unicode and shorter than 108 bytes both as supplied and with the directory's ancestor links resolved. Supplied names must hold no comma or equals sign, @@ -593,6 +605,11 @@ or lock. `Snapshot` remains an opaque input of a future restore: `vm::create` still refuses `Some(snapshot)` with `ErrorKind::Unsupported`; only cold boots are supported. +`CaptureContext` records a prepared capture's checked configuration and +effective devices independently of runtime paths or network policy. +`vm::Prepared::capture_context` returns it for the caller to clone and +serialize; NVX owns no capture-context store or persistence format. + ### `nvx::host::config` `VmConfig`: the configuration of a VM as one value, its processors, its memory @@ -670,12 +687,15 @@ The renderer emits neither `--microvm-lifecycle` nor `--microvm-workload-identity`. Callers inspect the complete arguments through `Prepared::arguments()`. -Both argument entry points refuse a `Boot` with a capture as `Unsupported` -rather than silently omit its devices and resource capacities. -They also refuse non-Unicode export child roots instead of lossily rendering -a different host path. -`VmConfig::check` and `kernel_command_line` retain their existing behavior for -valid capture configurations. +For a capture, `CaptureConfig` supplies the processor capacity, the boot-online +processor limit, and the boot memory and its capacity; the ordinary VM's cold +resource counts need not match. Boot memory must be aligned to 128 MiB. +The kernel text omits root, view, mapping and workload tokens and admits only +OpenVMM's named platform allowlist. Caller-supplied tier, processor, console, +device and network tokens are refused by name even when allowlisted: the +builder owns them. OpenVMM adds exactly one `nvx_snapshot_tier=platform` from +`--snapshot-tier platform`; callers may not supply or duplicate it. +Non-Unicode export child roots are still refused rather than lossily rendered. `VmConfig::check_command_line()` checks the configuration's known arguments on every host, before any host change, with 2048 UTF-16 code units reserved for the diff --git a/nvx-rs/src/host/config/mod.rs b/nvx-rs/src/host/config/mod.rs index 1e0e25b6a..a9a136c3b 100644 --- a/nvx-rs/src/host/config/mod.rs +++ b/nvx-rs/src/host/config/mod.rs @@ -129,7 +129,7 @@ impl UpperConfig { } /// Where the output of a VM's boot console goes. The console's device exists either way. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub enum ConsoleAttachment { /// OpenVMM listens for a client of the console. Listener, @@ -148,7 +148,7 @@ pub struct NetworkConfig { } /// The NIC that a VM has, with the static address that it settled on. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct NicConfig { /// The NIC's static address. pub guest_network: Ipv4Cidr, @@ -160,6 +160,8 @@ pub struct Boot { /// The caller's own kernel command-line tokens, to which NVX adds those of what it lowers. /// The root's tokens, `nvx_hostname`, `nvx_machine_id`, `nvx_maps` and `virtfs_*` are reserved; /// every other token, including `nvx_entrypoint`, `nvx_arg` and `nvx_share`, is caller-owned. + /// A capture permits only OpenVMM's platform allowlist and refuses caller tokens whose + /// tier, processor, console, device or network settings the builder supplies. pub command_line: String, /// What a capture of the VM adds, if the VM may be captured. pub capture: Option, @@ -169,7 +171,7 @@ pub struct Boot { /// the disks, the processors and the memory that the VM boots with and that a restore may grow /// it to, and the caller's invariant settings. The capture VM's own configuration gives the /// rest, such as its NIC, its console, and its export. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct CaptureConfig { /// How the snapshot treats the disks. pub disks: DiskCapture, @@ -184,7 +186,7 @@ pub struct CaptureConfig { } /// How a capture's snapshot treats the disks. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[non_exhaustive] pub enum DiskCapture { /// A fresh capture of a blockless VM, to which the capture attaches a placeholder of the @@ -198,7 +200,7 @@ pub enum DiskCapture { } /// A resource at boot, and the capacity to which a restore may grow it. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct Capacity { /// The resource at boot. pub boot: u64, @@ -207,7 +209,7 @@ pub struct Capacity { } /// The length and block sizes of a sandbox block device. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct BlockGeometry { /// The length that the device presents, in bytes: nonzero, and a multiple of the logical /// block size. @@ -314,6 +316,24 @@ impl Invariants { } } +impl serde::Serialize for Invariants { + fn serialize(&self, serializer: S) -> Result { + serde::Serialize::serialize(&self.0, serializer) + } +} + +impl<'de> serde::Deserialize<'de> for Invariants { + fn deserialize>(deserializer: D) -> Result { + let settings = as serde::Deserialize>::deserialize(deserializer)?; + settings + .into_iter() + .try_fold(Self::new(), |settings, (name, value)| { + settings.with(&name, &value) + }) + .map_err(serde::de::Error::custom) + } +} + /// Explains why `name` and `value` are not an invariant setting, if they are not. pub(crate) fn check_setting(name: &str, value: &str) -> Result<(), String> { let name_valid = (1..=Invariants::MAX_NAME_LEN).contains(&name.len()) @@ -397,12 +417,17 @@ impl VmConfig { /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) when /// [`check`](Self::check) refuses the configuration or when it has no [`Boot`]. A restore /// receives its view through its activation record, not a kernel command line. + /// A capture renders only accepted platform tokens and its boot-online processor count; + /// OpenVMM adds the single tier token from the capture's `--snapshot-tier platform` option. pub fn kernel_command_line(&self) -> Result { self.check()?; let boot = self .boot .as_ref() .ok_or_else(|| Error::unsupported("a kernel command line needs a cold boot's Boot"))?; + if let Some(capture) = &boot.capture { + return self.capture_command_line(boot, capture); + } let mut tokens = Vec::new(); if let RootConfig::Layered(root) = &self.root { let (upper, journal, max_memory_percent) = match &root.upper { @@ -436,7 +461,58 @@ impl VmConfig { Ok(tokens.join(" ")) } - /// Renders a non-capture cold boot's complete OpenVMM arguments from this configuration and + /// Renders a blockless capture without root, view, mapping or workload tokens. + fn capture_command_line(&self, boot: &Boot, capture: &CaptureConfig) -> Result { + if self.root != RootConfig::Blockless { + return Err(Error::unsupported("a fresh capture needs a blockless root")); + } + if self.filesystem.is_some() { + return Err(Error::unsupported( + "a fresh capture refuses any filesystem export", + )); + } + if !capture.memory.boot.is_multiple_of(MEMORY_BLOCK_MIB) { + return Err(Error::unsupported(format!( + "a fresh capture's boot memory of {} MiB is not a multiple of {MEMORY_BLOCK_MIB} MiB", + capture.memory.boot + ))); + } + for token in boot.command_line.split_ascii_whitespace() { + let name = token.split_once('=').map_or(token, |(name, _)| name); + if !platform_command_line_token_is_invariant(token) + || [ + "earlycon", + "console", + "virtio_mmio.device", + "virtnet_ip", + "virtnet_mask", + "virtnet_gw", + "virtnet_dns", + "virtnet_ip6", + "virtnet_gw6", + "nvx_snapshot_tier", + "nr_cpus", + "maxcpus", + "nvx_control_tty", + ] + .contains(&name) + { + return Err(Error::unsupported(format!( + "Boot.command_line uses the forbidden platform capture token {name:?}" + ))); + } + } + let mut command_line = boot.command_line.clone(); + if capture.processors.boot < capture.processors.capacity { + if !command_line.is_empty() { + command_line.push(' '); + } + command_line.push_str(&format!("maxcpus={}", capture.processors.boot)); + } + Ok(command_line) + } + + /// Renders a cold boot's OpenVMM arguments from this configuration and /// `runtime`. /// /// `endpoint`, `report` and `console_endpoint` are already chosen output names, not launch @@ -451,9 +527,11 @@ impl VmConfig { /// [`crate::host::vm::create`] calls this internal whole-config renderer. Neither managed /// lifecycle nor workload-identity flags are rendered; the caller owns its guest tokens. /// `runtime.windows_breakaway` is process-launch behavior, not an OpenVMM argument. + /// A capture's resources and tier are rendered here; creation adds its checked block paths + /// and snapshot destination, then rechecks the complete command line. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a refused - /// configuration, no [`Boot`], a capture-bearing boot, empty or NUL-containing required names + /// configuration, no [`Boot`], empty or NUL-containing required names /// or runtime paths, serial-option delimiters in listener names, duplicate listener names, /// non-Unicode export child roots, block paths that are not Unicode, hold /// disk-option delimiters or @@ -508,13 +586,17 @@ impl VmConfig { } }; let mut arguments = self.device_arguments()?; + let capture = self.boot.as_ref().and_then(|boot| boot.capture.as_ref()); arguments.extend(boot_arguments( runtime.hypervisor, - self.memory_mib, + capture.map_or(self.memory_mib, |capture| capture.memory.boot), &runtime.kernel, &runtime.initramfs, &command_line, )); + if let Some(capture) = capture { + arguments.extend(capture.openvmm_arguments()); + } arguments.extend(console_arguments(&console, endpoint, report)); if let Some(profile) = &runtime.cpu_profile { if profile.is_empty() { @@ -547,7 +629,7 @@ impl VmConfig { Ok(arguments) } - /// Checks the configuration's part of a non-capture cold boot's OpenVMM command line on every + /// Checks the configuration's part of a cold boot's OpenVMM command line on every /// host, without reading or changing the host, with 2048 UTF-16 code units reserved for the /// runtime's files, launch options, and the VM's output paths. /// @@ -558,7 +640,7 @@ impl VmConfig { /// A listener's backend prefix is counted here; its endpoint name stays in the reserve. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a refused - /// configuration or no [`Boot`], a capture-bearing boot, an unlowerable network policy, a + /// configuration or no [`Boot`], an unlowerable non-capture network policy, a /// NUL-containing argument, a non-Unicode export child root or block path, one that OpenVMM's /// disk syntax cannot hold literally, or one whose /// extension selects a non-raw backend, kernel text with double quotes or the standalone `--` @@ -566,9 +648,14 @@ impl VmConfig { pub fn check_command_line(&self) -> Result<()> { let command_line = self.control_kernel_command_line()?; let mut arguments = self.device_arguments()?; + let capture = self.boot.as_ref().and_then(|boot| boot.capture.as_ref()); arguments.extend([ "--memory".into(), - format!("{}M", self.memory_mib).into(), + format!( + "{}M", + capture.map_or(self.memory_mib, |capture| capture.memory.boot) + ) + .into(), "--cmdline".into(), command_line.into(), "--virtio-console".into(), @@ -578,22 +665,16 @@ impl VmConfig { } .into(), ]); + if let Some(capture) = capture { + arguments.extend(capture.openvmm_arguments()); + } check_argument_nuls(&arguments)?; fit_arguments(&arguments, OPENVMM_COMMAND_LINE_RESERVE) } - /// Renders a non-capture boot's kernel text that OpenVMM's control console accepts. + /// Renders a boot's kernel text that OpenVMM's control console accepts. fn control_kernel_command_line(&self) -> Result { let command_line = self.kernel_command_line()?; - if self - .boot - .as_ref() - .is_some_and(|boot| boot.capture.is_some()) - { - return Err(Error::unsupported( - "cold-boot argument rendering does not support capture configurations", - )); - } if command_line.contains('"') || command_line .split_ascii_whitespace() @@ -609,11 +690,17 @@ impl VmConfig { /// Renders only the configuration's devices; no runtime or output name is needed. fn device_arguments(&self) -> Result> { + let capture = self.boot.as_ref().and_then(|boot| boot.capture.as_ref()); let mut arguments = vec![ "--machine".into(), "microvm".into(), "--processors".into(), - self.processors.to_string().into(), + capture + .map_or(u64::from(self.processors), |capture| { + capture.processors.capacity + }) + .to_string() + .into(), ]; if let RootConfig::Layered(root) = &self.root { let scratch = match &root.upper { @@ -648,7 +735,11 @@ impl VmConfig { || DEFAULT_GUEST_NETWORK.to_owned(), |address| address.to_string(), ); - let mut nic = network::openvmm_arguments(Some(&network.policy), &guest_network)?; + let mut nic = if capture.is_some() { + network::template_arguments(&guest_network) + } else { + network::openvmm_arguments(Some(&network.policy), &guest_network)? + }; if nic.is_empty() { // A configured NIC remains present even under the deny-all posture. nic = network::template_arguments(&guest_network); @@ -747,6 +838,34 @@ impl VmConfig { } } +/// OpenVMM's named platform snapshot command-line allowlist. +fn platform_command_line_token_is_invariant(token: &str) -> bool { + matches!( + token, + "earlycon=xe9" + | "console=hvc0" + | "console=hvc1" + | "reboot=t" + | "panic=-1" + | "nvx_sandbox=1" + | "nvx_config=0xd0010000,65536" + | "nvx_snapshot_tier=platform" + | "nvx_control_tty=hvc2" + ) || token.starts_with("nr_cpus=") + || token.starts_with("maxcpus=") + || [ + "virtio_mmio.device=", + "virtnet_ip=", + "virtnet_mask=", + "virtnet_gw=", + "virtnet_dns=", + "virtnet_ip6=", + "virtnet_gw6=", + ] + .iter() + .any(|prefix| token.starts_with(prefix)) +} + impl LayeredRoot { /// Checks the root's fields, as [`VmConfig::check`] describes. fn check(&self) -> Result<()> { @@ -785,6 +904,16 @@ impl LayeredRoot { } impl CaptureConfig { + /// The capacity and platform tier shared by capture argument checks and rendering. + fn openvmm_arguments(&self) -> [OsString; 4] { + [ + "--memory-capacity".into(), + format!("{}M", self.memory.capacity).into(), + "--snapshot-tier".into(), + "platform".into(), + ] + } + /// Checks the capture's settings without reading the host. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for processors online diff --git a/nvx-rs/src/host/config/tests.rs b/nvx-rs/src/host/config/tests.rs index ac8f140c8..3c3668fd2 100644 --- a/nvx-rs/src/host/config/tests.rs +++ b/nvx-rs/src/host/config/tests.rs @@ -346,6 +346,26 @@ fn block_geometries_hold_whole_blocks_of_power_of_two_sizes() { } } +#[test] +fn capture_invariants_serde_cannot_bypass_their_checked_form() { + let invariants = Invariants::new().with("isolation.mode", "strict").unwrap(); + assert_eq!( + serde_json::from_str::(&serde_json::to_string(&invariants).unwrap()).unwrap(), + invariants + ); + for settings in [ + BTreeMap::from([("Invalid".to_owned(), "value".to_owned())]), + BTreeMap::from([("valid".to_owned(), "\n".to_owned())]), + (0..=Invariants::MAX_SETTINGS) + .map(|index| (format!("setting{index}"), String::new())) + .collect(), + ] { + assert!( + serde_json::from_value::(serde_json::to_value(settings).unwrap()).is_err() + ); + } +} + /// Returns a fresh capture of 1 processor online of 2, and of 256 MiB of memory of 512. fn capture() -> CaptureConfig { let image = BlockGeometry { @@ -1120,11 +1140,11 @@ fn values<'a>(arguments: &'a [OsString], option: &str) -> Vec<&'a OsStr> { } #[test] -fn cold_boot_arguments_refuse_capture_configurations() { - let config = captured(); +fn capture_arguments_accept_platform_tokens_and_refuse_nonplatform_tokens() { + let mut config = captured(); config.check().unwrap(); - config.kernel_command_line().unwrap(); for result in [ + config.kernel_command_line().map(|_| ()), config.check_command_line(), config .openvmm_arguments( @@ -1137,7 +1157,97 @@ fn cold_boot_arguments_refuse_capture_configurations() { ] { let error = result.unwrap_err(); assert_eq!(error.kind(), ErrorKind::Unsupported); - assert!(error.to_string().contains("capture")); + assert!(error.to_string().contains("quiet"), "{error}"); + } + config.boot.as_mut().unwrap().command_line = "nvx_sandbox=1".to_owned(); + config.check_command_line().unwrap(); + let arguments = config + .openvmm_arguments( + &runtime(), + "/missing/control", + Path::new("/missing/report"), + Some("/missing/console"), + ) + .unwrap(); + assert_eq!( + values(&arguments, "--cmdline"), + [OsStr::new("nvx_sandbox=1 maxcpus=1")] + ); + assert_eq!(values(&arguments, "--processors"), [OsStr::new("2")]); + assert_eq!(values(&arguments, "--memory"), [OsStr::new("256M")]); + assert_eq!( + values(&arguments, "--memory-capacity"), + [OsStr::new("512M")] + ); + assert_eq!( + values(&arguments, "--snapshot-tier"), + [OsStr::new("platform")] + ); +} + +#[test] +fn capture_command_lines_keep_only_unowned_platform_tokens() { + for token in [ + "", + "reboot=t", + "panic=-1", + "nvx_sandbox=1", + "nvx_config=0xd0010000,65536", + ] { + let mut config = captured(); + config.boot.as_mut().unwrap().command_line = token.to_owned(); + let expected = if token.is_empty() { + "maxcpus=1".to_owned() + } else { + format!("{token} maxcpus=1") + }; + assert_eq!(config.kernel_command_line().unwrap(), expected); + config.check_command_line().unwrap(); + } +} + +#[test] +#[allow(unreachable_pub, clippy::missing_docs_in_private_items)] +fn capture_platform_allowlist_matches_openvmms_named_contract() { + mod openvmm_defs { + pub(super) mod microvm { + include!("../../../tests/vectors/openvmm/microvm_control_tty_command_line.rs"); + } + } + include!("../../../tests/vectors/openvmm/platform_command_line_token_is_invariant.rs"); + + for token in [ + "earlycon=xe9", + "console=hvc0", + "console=hvc1", + "reboot=t", + "panic=-1", + "nvx_sandbox=1", + "nvx_config=0xd0010000,65536", + "nvx_snapshot_tier=platform", + "nr_cpus=4", + "maxcpus=1", + "nvx_control_tty=hvc2", + "virtio_mmio.device=0x1000@0xd0000000:1", + "virtnet_ip=10.42.0.2", + "virtnet_mask=255.255.255.0", + "virtnet_gw=10.42.0.1", + "virtnet_dns=10.42.0.1", + "virtnet_ip6=fd00::2", + "virtnet_gw6=fd00::1", + "nvx_snapshot_tier=workload", + "nvx_entrypoint=/bin/sh", + "nvx_hostname=tenant", + "virtfs_tag=tenant", + "nvx_maps=1", + "quiet", + "custom=tenant", + ] { + assert_eq!( + super::platform_command_line_token_is_invariant(token), + platform_command_line_token_is_invariant(token), + "{token}" + ); } } diff --git a/nvx-rs/src/host/snapshot.rs b/nvx-rs/src/host/snapshot.rs index 7fcb88f6e..8aceeb575 100644 --- a/nvx-rs/src/host/snapshot.rs +++ b/nvx-rs/src/host/snapshot.rs @@ -40,10 +40,14 @@ use std::fs; use std::io; use std::path::{Path, PathBuf}; +use serde::{Deserialize, Serialize}; + +use crate::host::config::{CaptureConfig, ConsoleAttachment, NicConfig}; use crate::host::paths::{absolute, normalized}; +use crate::host::runtime::RuntimeIdentity; use crate::host::store::regular; use crate::host::{filesystem, platform}; -use crate::{Error, Result}; +use crate::{Error, Hypervisor, Result}; pub use self::eligibility::{Downtime, DowntimeSource, Eligibility, HostTime, MAX_DOWNTIME_NS}; pub use self::key::{CaptureClass, ClassIdentity, Invariants, KeyIdentity, SnapshotKey}; @@ -58,6 +62,28 @@ pub const DESCRIPTION_FILE: &str = "snapshot.json"; /// The directory of OpenVMM's manifest, RAM and saved state. pub const OPENVMM_DIR: &str = "openvmm"; +/// The checked configuration and effective devices of a blockless platform capture. +/// +/// [`crate::host::vm::Prepared::capture_context`] returns it after verifying the actual sparse +/// disk geometries. Callers choose whether and how to persist it; creation writes no context, +/// key or snapshot description into the VM directory. Runtime paths and network policy are +/// not part of the context. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct CaptureContext { + /// Capture resources, invariants and verified image and scratch geometries. + pub(crate) capture: CaptureConfig, + /// The attached NIC's resolved address, without the capture's deny-all policy. + pub(crate) nic: Option, + /// The boot console's effective attachment. + pub(crate) console: ConsoleAttachment, + /// The identities of the runtime artifacts and their contracts. + pub(crate) runtime: RuntimeIdentity, + /// The backend selected for the capture. + pub(crate) hypervisor: Hypervisor, + /// The requested CPU profile, or OpenVMM's default. + pub(crate) cpu_profile: Option, +} + /// A checked snapshot directory and its verified metadata, for a future VM restore. /// /// The caller must keep the directory and its entries private and immutable while using this diff --git a/nvx-rs/src/host/vm.rs b/nvx-rs/src/host/vm.rs index 142ca7c0c..7b02d7721 100644 --- a/nvx-rs/src/host/vm.rs +++ b/nvx-rs/src/host/vm.rs @@ -18,7 +18,10 @@ use std::io; use std::path::{Path, PathBuf}; use std::time::Instant; -use crate::host::config::{ConsoleAttachment, RootConfig, UpperConfig, VmConfig}; +use crate::host::config::{ + BlockGeometry, CaptureConfig, ConsoleAttachment, DEFAULT_GUEST_NETWORK, DiskCapture, NicConfig, + RootConfig, UpperConfig, VmConfig, fit_process_command_line, reject_disk_path, +}; use crate::host::filesystem; use crate::host::paths::{absolute, normalized}; use crate::host::platform; @@ -28,13 +31,14 @@ use crate::host::platform::{check_store_dir as check_dir, create_store_dir as cr use crate::host::platform::{check_vm_dir as check_dir, create_vm_dir as create_dir}; use crate::host::report::{self, Report}; use crate::host::runtime::{Runtime, RuntimeIdentity}; -use crate::host::snapshot::Snapshot; +use crate::host::snapshot::{CaptureContext, Snapshot}; use crate::host::spawn::{self, VmmCommand}; use crate::host::transport::{self, HostConnection}; use crate::host::{ platform::inherit::Inherited, process::{Opened, Process, ProcessRef}, }; +use crate::net::Ipv4Cidr; use crate::protocol::CAPABILITY_LEN; use crate::{Error, Result}; @@ -49,10 +53,6 @@ const CONSOLE: &str = "console.sock"; /// The restore-ready socket on Linux. const RESTORE_READY: &str = "restore-ready.sock"; /// OpenVMM's snapshot files: a capture's destination and a restore's snapshot view. -#[cfg_attr( - not(test), - expect(dead_code, reason = "SL2.12b and SL2.13 use it (#1102)") -)] const OPENVMM: &str = "openvmm"; /// A finished capture's key. #[cfg_attr( @@ -67,16 +67,8 @@ const KEY: &str = "key.bin"; )] const DESCRIPTION: &str = "snapshot.json"; /// The sparse placeholder image that a capture VM attaches. -#[cfg_attr( - not(test), - expect(dead_code, reason = "SL2.13's capture creates it (#1102)") -)] const CAPTURE_IMAGE: &str = "capture-image.raw"; /// The dormant scratch that a capture VM attaches. -#[cfg_attr( - not(test), - expect(dead_code, reason = "SL2.13's capture creates it (#1102)") -)] const CAPTURE_SCRATCH: &str = "capture-scratch.raw"; /// The prefix OpenVMM requires of its Windows console pipes. const OPENVMM_PIPE: &str = "openvmm-microvm"; @@ -317,6 +309,8 @@ pub struct Prepared { arguments: Vec, /// Already opened resources added by the caller, retained until the preparation is dropped. inherit: Vec, + /// The checked context of a capture, absent for an ordinary cold launch. + capture_context: Option, } impl Prepared { @@ -346,6 +340,13 @@ impl Prepared { pub fn arguments(&self) -> &[OsString] { &self.arguments } + + /// Returns the checked capture context, if this launch prepares a blockless capture. + /// + /// The caller chooses whether and how to persist it; creation writes no context file. + pub fn capture_context(&self) -> Option<&CaptureContext> { + self.capture_context.as_ref() + } } /// Creates an inspectable cold launch in `dir`, without starting a process or binding endpoints. @@ -357,8 +358,12 @@ impl Prepared { /// /// The log is created anew, privately, and kept open in [`Prepared`]. Other outputs are reserved /// names that OpenVMM creates when it runs: existing entries at those names are refused, not -/// replaced. A failure closes and removes only the log that this invocation created, preserving +/// replaced. A failure closes and removes only files that this invocation created, preserving /// every pre-existing entry and the caller's inputs. The capability is never written to disk. +/// With `boot.capture`, the root must be blockless and no export is accepted. Creation also +/// prepares sparse image and scratch files, verifies their actual geometry, and retains the +/// checked capture context. OpenVMM captures to `openvmm/` under this VM directory, with a +/// configured NIC under deny-all. No key, description or context file is written. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a restore, a missing /// boot, a refused configuration, unrepresentable Linux socket names, or a complete Windows @@ -380,6 +385,11 @@ pub fn create( if config.boot.is_none() { return Err(Error::unsupported("creating a cold VM needs Boot")); } + let capture = config.boot.as_ref().and_then(|boot| boot.capture.as_ref()); + if capture.is_some() { + config.kernel_command_line()?; + runtime.identity.check()?; + } VmDir::open(dir.path())?; platform::check_working_dir(dir.path())?; let outputs = dir.outputs(config.console == ConsoleAttachment::Listener)?; @@ -436,6 +446,7 @@ pub fn create( )) .with_source(error) })?; + let mut created = Vec::new(); let checked = (|| { #[cfg(unix)] { @@ -458,16 +469,68 @@ pub fn create( } } let capability = Capability(crate::host::instance::generate_capability()?); - let arguments = config.openvmm_arguments( + let mut arguments = config.openvmm_arguments( &runtime, &outputs.endpoint, &outputs.outcome, outputs.console.as_deref(), )?; - Ok((capability, arguments)) + let capture_context = if let Some(capture) = capture { + let image_path = dir.path.join(CAPTURE_IMAGE); + let scratch_path = dir.path.join(CAPTURE_SCRATCH); + let destination = dir.path.join(OPENVMM); + for path in [&image_path, &scratch_path, &destination] { + unused_output(path)?; + } + for path in [&image_path, &scratch_path] { + if path.to_str().is_none() { + return Err(Error::unsupported( + "capture block paths must be Unicode for OpenVMM", + )); + } + reject_disk_path(path, &path.display().to_string())?; + } + arguments.extend([ + "--microvm-sandbox-block".into(), + format!("distro:file:{},ro", image_path.display()).into(), + "--microvm-sandbox-block".into(), + format!("scratch:file:{}", scratch_path.display()).into(), + "--snapshot-destination".into(), + destination.into_os_string(), + ]); + fit_process_command_line(runtime.openvmm.as_os_str(), &arguments, 0)?; + let DiskCapture::Fresh { image, scratch } = capture.disks; + let image = capture_disk(&image_path, image, &mut created)?; + let scratch = capture_disk(&scratch_path, scratch, &mut created)?; + let nic = config + .network + .as_ref() + .map(|network| { + network.guest_network.map_or_else( + || Ipv4Cidr::parse(DEFAULT_GUEST_NETWORK).map_err(Error::unsupported), + Ok, + ) + }) + .transpose()? + .map(|guest_network| NicConfig { guest_network }); + Some(CaptureContext { + capture: CaptureConfig { + disks: DiskCapture::Fresh { image, scratch }, + ..capture.clone() + }, + nic, + console: config.console, + runtime: runtime.identity.clone(), + hypervisor: runtime.hypervisor, + cpu_profile: runtime.cpu_profile.clone(), + }) + } else { + None + }; + Ok((capability, arguments, capture_context)) })(); match checked { - Ok((capability, arguments)) => Ok(Prepared { + Ok((capability, arguments, capture_context)) => Ok(Prepared { program: runtime.openvmm.clone(), working_dir: dir.path().to_owned(), windows_breakaway: runtime.windows_breakaway, @@ -476,9 +539,24 @@ pub fn create( log, arguments, inherit: Vec::new(), + capture_context, }), - Err(error) => { + Err(mut error) => { drop(log); + for path in created.iter().rev() { + if let Err(cleanup) = fs::remove_file(path) + && cleanup.kind() != io::ErrorKind::NotFound + { + error = Error::new( + error.kind(), + format!( + "{error}; cannot remove created capture file {}: {cleanup}", + path.display() + ), + ) + .with_source(error); + } + } match fs::remove_file(&outputs.log) { Ok(()) => Err(error), Err(cleanup) if cleanup.kind() == io::ErrorKind::NotFound => Err(error), @@ -617,6 +695,7 @@ fn spawn_verified( log, arguments, inherit, + capture_context: _, } = prepared; let command = VmmCommand { program, @@ -648,6 +727,46 @@ fn spawn_verified( }) } +/// Creates one private sparse capture disk and verifies the geometry OpenVMM presents. +fn capture_disk( + path: &Path, + expected: BlockGeometry, + created: &mut Vec, +) -> Result { + let failed = |error| { + Error::io(format!("cannot create capture disk {}", path.display())).with_source(error) + }; + let mut options = OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600); + let file = options.open(path).map_err(failed)?; + created.push(path.to_owned()); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + file.set_permissions(fs::Permissions::from_mode(0o600)) + .map_err(failed)?; + } + if let Some(problem) = platform::private_file_problem(&file).map_err(failed)? { + return Err(Error::io(format!( + "the capture disk {} {problem}", + path.display() + ))); + } + platform::make_sparse(&file).map_err(failed)?; + file.set_len(expected.length).map_err(failed)?; + let actual = crate::host::sandbox::block_geometry(&file, path, "capture disk")?; + if actual != expected { + return Err(Error::unsupported(format!( + "capture disk {} has actual geometry {actual:?}, not requested {expected:?}", + path.display() + ))); + } + Ok(actual) +} + /// Refuses an existing output entry without opening, replacing, or removing it. fn unused_output(path: &Path) -> Result<()> { let error = match fs::symlink_metadata(path) { diff --git a/nvx-rs/src/host/vm/tests.rs b/nvx-rs/src/host/vm/tests.rs index 97b4b5ca6..2c7728f4d 100644 --- a/nvx-rs/src/host/vm/tests.rs +++ b/nvx-rs/src/host/vm/tests.rs @@ -1,3 +1,4 @@ +use std::ffi::OsStr; use std::fs; #[cfg(any(target_os = "linux", windows))] use std::process::{Command, Stdio}; @@ -8,9 +9,10 @@ use super::*; use crate::ErrorKind; use crate::activation::{Journal, MachineIdMode, OverlayMode, View}; use crate::host::config::{ - Boot, LayeredRoot, PROCESS_COMMAND_LINE_LIMIT, RootConfig, UpperConfig, - process_command_line_units, + Boot, Capacity, Invariants, LayeredRoot, NetworkConfig, PROCESS_COMMAND_LINE_LIMIT, RootConfig, + UpperConfig, process_command_line_units, }; +use crate::host::network::{Access, HostLoopback, NetworkPolicy}; #[cfg(any(target_os = "linux", windows))] use crate::host::process::Opened; use crate::host::runtime::{ArtifactIdentity, LaunchOptions, RuntimeIdentity}; @@ -454,6 +456,424 @@ fn cold_runtime() -> Runtime { } } +fn capture_config(vm: &VmDir) -> VmConfig { + #[cfg(target_os = "linux")] + let physical_block_size = { + use std::os::unix::fs::MetadataExt; + + u32::try_from(fs::metadata(vm.path()).unwrap().blksize()).unwrap() + }; + #[cfg(windows)] + let physical_block_size = 4096; + #[cfg(not(any(target_os = "linux", windows)))] + let physical_block_size = 512; + let image = BlockGeometry { + length: 16 << 20, + logical_block_size: 512, + physical_block_size, + }; + VmConfig { + processors: 2, + boot: Some(Boot { + command_line: "nvx_sandbox=1".to_owned(), + capture: Some(CaptureConfig { + disks: DiskCapture::Fresh { + image, + scratch: BlockGeometry { + length: 8 << 20, + ..image + }, + }, + processors: Capacity { + boot: 2, + capacity: 2, + }, + memory: Capacity { + boot: 256, + capacity: 512, + }, + invariants: Invariants::new().with("isolation.mode", "strict").unwrap(), + }), + }), + ..cold_config() + } +} + +fn argument_values<'a>(arguments: &'a [OsString], option: &str) -> Vec<&'a OsStr> { + arguments + .windows(2) + .filter(|pair| pair[0] == option) + .map(|pair| pair[1].as_os_str()) + .collect() +} + +#[test] +fn capture_creation_records_sparse_disks_devices_and_a_complete_serde_context() { + for console in [ConsoleAttachment::Listener, ConsoleAttachment::Disconnected] { + for nic in [ + None, + Some(None), + Some(Some(Ipv4Cidr::parse("10.42.0.2/24").unwrap())), + ] { + let (_directory, vm) = vm_dir(); + let mut runtime = cold_runtime(); + runtime.identity = RuntimeIdentity::new( + ArtifactIdentity::new("capture-openvmm").unwrap(), + ArtifactIdentity::new("capture-kernel").unwrap(), + ArtifactIdentity::new("capture-initramfs").unwrap(), + ); + runtime.cpu_profile = nic.map(|_| "amd.genoa.v1".to_owned()); + let mut policy = NetworkPolicy::new(Access::Allow); + policy.ingress = Access::Allow; + policy.host_loopback = HostLoopback::Allow; + let config = VmConfig { + console, + network: nic.map(|guest_network| NetworkConfig { + guest_network, + policy, + }), + ..capture_config(&vm) + }; + let prepared = create(&runtime, &config, None, &vm).unwrap(); + let context = prepared.capture_context().unwrap(); + assert_eq!( + context.capture, + *config.boot.as_ref().unwrap().capture.as_ref().unwrap() + ); + assert_eq!(context.console, console); + assert_eq!(context.runtime, runtime.identity); + assert_eq!(context.hypervisor, runtime.hypervisor); + assert_eq!(context.cpu_profile, runtime.cpu_profile); + assert_eq!( + context.nic, + nic.map(|address| NicConfig { + guest_network: address + .unwrap_or_else(|| Ipv4Cidr::parse(DEFAULT_GUEST_NETWORK).unwrap()), + }) + ); + let serialized = serde_json::to_string(context).unwrap(); + assert_eq!( + serde_json::from_str::(&serialized).unwrap(), + context.clone() + ); + assert!(!serialized.contains("policy")); + let DiskCapture::Fresh { image, scratch } = context.capture.disks; + for (name, geometry) in [(CAPTURE_IMAGE, image), (CAPTURE_SCRATCH, scratch)] { + let path = vm.path().join(name); + let file = File::open(&path).unwrap(); + let metadata = file.metadata().unwrap(); + assert_eq!(metadata.len(), geometry.length); + assert_eq!(platform::private_file_problem(&file).unwrap(), None); + #[cfg(target_os = "linux")] + { + use std::os::unix::fs::MetadataExt; + + assert!(metadata.blocks() * 512 < metadata.len()); + assert_eq!(metadata.mode() & 0o777, 0o600); + assert_eq!(u64::from(geometry.physical_block_size), metadata.blksize()); + } + #[cfg(windows)] + { + use std::os::windows::fs::MetadataExt; + + assert_ne!(metadata.file_attributes() & 0x200, 0, "not a sparse file"); + } + } + let arguments = prepared.arguments(); + assert_eq!( + argument_values(arguments, "--snapshot-tier"), + [OsStr::new("platform")] + ); + assert_eq!( + argument_values(arguments, "--snapshot-destination"), + [vm.path().join(OPENVMM).as_os_str()] + ); + assert_eq!( + argument_values(arguments, "--memory-capacity"), + [OsStr::new("512M")] + ); + assert_eq!( + argument_values(arguments, "--microvm-sandbox-block"), + [ + OsStr::new(&format!( + "distro:file:{},ro", + vm.path().join(CAPTURE_IMAGE).display() + )), + OsStr::new(&format!( + "scratch:file:{}", + vm.path().join(CAPTURE_SCRATCH).display() + )), + ] + ); + assert_eq!( + argument_values(arguments, "--cmdline"), + [OsStr::new("nvx_sandbox=1")] + ); + if let Some(nic) = context.nic { + assert_eq!( + argument_values(arguments, "--net"), + [OsStr::new(&nic.guest_network.to_string())] + ); + for option in ["--network-egress", "--network-ingress", "--host-loopback"] { + assert_eq!(argument_values(arguments, option), [OsStr::new("deny")]); + } + } else { + assert!(argument_values(arguments, "--net").is_empty()); + } + for option in [ + "--network-egress-allow", + "--network-egress-deny", + "--host-loopback-forward", + "--proxy", + ] { + assert!(argument_values(arguments, option).is_empty(), "{option}"); + } + assert_eq!(fs::read_dir(vm.path()).unwrap().count(), 3); + for name in [OPENVMM, KEY, DESCRIPTION] { + assert!(!vm.path().join(name).exists(), "{name}"); + } + } + } +} + +#[test] +fn capture_creation_refuses_nonplatform_tokens_by_name_without_leaving_outputs() { + for token in [ + "quiet", + "loglevel=0", + "nvx_entrypoint=/bin/sh", + "nvx_arg=tenant", + "nvx_share=/tenant", + "nvx_workload_account=create", + "nvx_layer=erofs", + "nvx_hostname=tenant", + "nvx_maps=1", + "virtfs_tag=tenant", + "custom=tenant", + "earlycon=xe9", + "console=hvc1", + "virtio_mmio.device=0x1000@0xd0000000:1", + "virtnet_ip=10.42.0.2", + "virtnet_mask=255.255.255.0", + "virtnet_gw=10.42.0.1", + "virtnet_dns=10.42.0.1", + "virtnet_ip6=fd00::2", + "virtnet_gw6=fd00::1", + "nvx_snapshot_tier=platform", + "nvx_snapshot_tier=platform nvx_snapshot_tier=platform", + "nr_cpus=2", + "maxcpus=2", + "nvx_control_tty=hvc2", + ] { + let (_directory, vm) = vm_dir(); + let mut config = capture_config(&vm); + config.boot.as_mut().unwrap().command_line = token.to_owned(); + let error = create(&cold_runtime(), &config, None, &vm).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + assert!( + error.to_string().contains(token.split('=').next().unwrap()), + "{error}" + ); + assert_eq!(fs::read_dir(vm.path()).unwrap().count(), 0); + } +} + +#[test] +fn capture_resources_not_the_vms_cold_resources_determine_the_launch() { + for (boot, capacity) in [(1, 4), (2, 2)] { + let (_directory, vm) = vm_dir(); + let mut config = capture_config(&vm); + config.processors = 3; + config.memory_mib = 17; + let capture = config.boot.as_mut().unwrap().capture.as_mut().unwrap(); + capture.processors = Capacity { boot, capacity }; + capture.memory.boot = 128; + let expected = capture.clone(); + let prepared = create(&cold_runtime(), &config, None, &vm).unwrap(); + assert_eq!(prepared.capture_context().unwrap().capture, expected); + assert_eq!( + argument_values(prepared.arguments(), "--processors"), + [OsStr::new(&capacity.to_string())] + ); + assert_eq!( + argument_values(prepared.arguments(), "--memory"), + [OsStr::new("128M")] + ); + assert_eq!( + argument_values(prepared.arguments(), "--memory-capacity"), + [OsStr::new("512M")] + ); + assert_eq!( + argument_values(prepared.arguments(), "--cmdline"), + [OsStr::new(if boot < capacity { + "nvx_sandbox=1 maxcpus=1" + } else { + "nvx_sandbox=1" + })] + ); + } +} + +#[test] +fn capture_creation_refuses_unaligned_boot_memory_before_creating_outputs() { + let (_directory, vm) = vm_dir(); + let mut config = capture_config(&vm); + config + .boot + .as_mut() + .unwrap() + .capture + .as_mut() + .unwrap() + .memory + .boot = 129; + config.check().unwrap(); + let error = create(&cold_runtime(), &config, None, &vm).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + assert!( + error.to_string().contains("boot memory of 129 MiB"), + "{error}" + ); + assert_eq!(fs::read_dir(vm.path()).unwrap().count(), 0); +} + +#[test] +fn capture_creation_refuses_layered_roots_and_exports_without_creating_outputs() { + use crate::host::filesystem::{Bind, ExportChild, ExportPlan, Owner}; + + let (_directory, vm) = vm_dir(); + let base = capture_config(&vm); + let layered = VmConfig { + root: RootConfig::Layered(LayeredRoot { + image: vm.path().join("caller-image.erofs"), + format: RootImage::erofs(None), + gpt_first_partition_bound: None, + upper: UpperConfig::Ram { + max_memory_percent: 50, + }, + overlay: OverlayMode::TrustedLowers, + }), + ..base.clone() + }; + let exported = VmConfig { + filesystem: Some(ExportPlan { + children: vec![ExportChild { + root: vm.path().to_owned(), + file: false, + writable: false, + denied: Vec::new(), + denied_identities: Vec::new(), + allowed: Vec::new(), + write: Vec::new(), + }], + binds: vec![Bind { + child: 0, + source: String::new(), + target: "/mnt/shared".to_owned(), + read_only: true, + identity: None, + }], + owner: Owner::Vmm, + }), + ..base + }; + for (config, refusal) in [(layered, "blockless"), (exported, "export")] { + config.check().unwrap(); + let error = create(&cold_runtime(), &config, None, &vm).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + assert!(error.to_string().contains(refusal), "{error}"); + assert_eq!(fs::read_dir(vm.path()).unwrap().count(), 0); + } +} + +#[test] +fn capture_geometry_mismatches_roll_back_only_the_new_files() { + for scratch_disk in [false, true] { + for logical in [false, true] { + let (_directory, vm) = vm_dir(); + let input = vm.path().join("caller-input"); + fs::write(&input, b"preserved").unwrap(); + let mut config = capture_config(&vm); + let capture = config.boot.as_mut().unwrap().capture.as_mut().unwrap(); + let DiskCapture::Fresh { image, scratch } = &mut capture.disks; + let geometry = if scratch_disk { scratch } else { image }; + if logical { + geometry.logical_block_size = 1024; + geometry.physical_block_size = geometry.physical_block_size.max(1024); + } else { + geometry.physical_block_size *= 2; + } + let error = create(&cold_runtime(), &config, None, &vm).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + assert!(error.to_string().contains("actual geometry"), "{error}"); + assert!( + error.to_string().contains(if scratch_disk { + CAPTURE_SCRATCH + } else { + CAPTURE_IMAGE + }), + "{error}" + ); + assert_eq!(fs::read_dir(vm.path()).unwrap().count(), 1); + assert_eq!(fs::read(input).unwrap(), b"preserved"); + } + } +} + +#[test] +fn capture_creation_never_replaces_existing_disks_or_snapshot_destinations() { + for name in [CAPTURE_IMAGE, CAPTURE_SCRATCH, OPENVMM] { + let (_directory, vm) = vm_dir(); + let existing = vm.path().join(name); + fs::write(&existing, b"preserved").unwrap(); + let error = create(&cold_runtime(), &capture_config(&vm), None, &vm).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Io, "{error}"); + assert!(error.to_string().contains(name), "{error}"); + assert_eq!(fs::read(&existing).unwrap(), b"preserved"); + assert_eq!(fs::read_dir(vm.path()).unwrap().count(), 1); + } +} + +#[test] +fn capture_creation_checks_the_complete_command_line_including_disk_and_destination_paths() { + let (_directory, vm) = vm_dir(); + let config = capture_config(&vm); + let runtime = cold_runtime(); + let prepared = create(&runtime, &config, None, &vm).unwrap(); + let length = process_command_line_units(runtime.openvmm.as_os_str(), prepared.arguments()); + drop(prepared); + for name in [LOG, CAPTURE_IMAGE, CAPTURE_SCRATCH] { + fs::remove_file(vm.path().join(name)).unwrap(); + } + for extra in [0, 1] { + let mut runtime = runtime.clone(); + runtime + .openvmm + .as_mut_os_string() + .push("x".repeat(PROCESS_COMMAND_LINE_LIMIT - length + extra)); + let result = create(&runtime, &config, None, &vm); + if extra == 0 { + let prepared = result.unwrap(); + assert_eq!( + process_command_line_units(runtime.openvmm.as_os_str(), prepared.arguments()), + PROCESS_COMMAND_LINE_LIMIT + ); + drop(prepared); + for name in [LOG, CAPTURE_IMAGE, CAPTURE_SCRATCH] { + fs::remove_file(vm.path().join(name)).unwrap(); + } + } else { + let error = result.unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); + assert!( + error.to_string().contains("Windows process command line"), + "{error}" + ); + } + assert_eq!(fs::read_dir(vm.path()).unwrap().count(), 0); + } +} + #[test] fn cold_creation_is_inspectable_without_a_process_listener_or_capability_file() { for console in [ConsoleAttachment::Listener, ConsoleAttachment::Disconnected] { @@ -464,6 +884,7 @@ fn cold_creation_is_inspectable_without_a_process_listener_or_capability_file() ..cold_config() }; let prepared = create(&runtime, &config, None, &vm).unwrap(); + assert!(prepared.capture_context().is_none()); let outputs = prepared.outputs(); assert_eq!(outputs.log, vm.path().join("openvmm.log")); assert_eq!( diff --git a/scripts/test-layout-allowlist.json b/scripts/test-layout-allowlist.json index 399c715f3..ab7b22a0e 100644 --- a/scripts/test-layout-allowlist.json +++ b/scripts/test-layout-allowlist.json @@ -18,7 +18,7 @@ "nvx-rs/src/host/sandbox/plan.rs": 5, "nvx-rs/src/host/session.rs": 1, "nvx-rs/src/host/snapshot.rs": 1, - "nvx-rs/src/host/vm.rs": 5, + "nvx-rs/src/host/vm.rs": 2, "nvx-rs/src/protocol/app.rs": 6 } } From 531673149e99ae8187fca0356a2267e76c4bd1c0 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Tue, 10 Nov 2026 10:46:59 -0800 Subject: [PATCH 02/15] host: compile the capture allowlist reference as an item Keep the OpenVMM source excerpt in a test-only module so its function definition is included in item context. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 095bfb08-a5d0-4331-b2b2-dd76d2fbadd8 --- nvx-rs/src/host/config/tests.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/nvx-rs/src/host/config/tests.rs b/nvx-rs/src/host/config/tests.rs index 3c3668fd2..f668618ad 100644 --- a/nvx-rs/src/host/config/tests.rs +++ b/nvx-rs/src/host/config/tests.rs @@ -1209,12 +1209,18 @@ fn capture_command_lines_keep_only_unowned_platform_tokens() { #[test] #[allow(unreachable_pub, clippy::missing_docs_in_private_items)] fn capture_platform_allowlist_matches_openvmms_named_contract() { - mod openvmm_defs { - pub(super) mod microvm { - include!("../../../tests/vectors/openvmm/microvm_control_tty_command_line.rs"); + mod openvmm { + mod openvmm_defs { + pub(super) mod microvm { + include!("../../../tests/vectors/openvmm/microvm_control_tty_command_line.rs"); + } + } + include!("../../../tests/vectors/openvmm/platform_command_line_token_is_invariant.rs"); + + pub(super) fn admits(token: &str) -> bool { + platform_command_line_token_is_invariant(token) } } - include!("../../../tests/vectors/openvmm/platform_command_line_token_is_invariant.rs"); for token in [ "earlycon=xe9", @@ -1245,7 +1251,7 @@ fn capture_platform_allowlist_matches_openvmms_named_contract() { ] { assert_eq!( super::platform_command_line_token_is_invariant(token), - platform_command_line_token_is_invariant(token), + openvmm::admits(token), "{token}" ); } From 8df64c298811d6b642910460cc843450c7a1e271 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Tue, 10 Nov 2026 10:57:03 -0800 Subject: [PATCH 03/15] host: record the capture preparation API Regenerate both platform snapshots for the opaque capture context and the prepared-launch accessor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 095bfb08-a5d0-4331-b2b2-dd76d2fbadd8 --- nvx-rs/public-api/linux.txt | 2 ++ nvx-rs/public-api/windows.txt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index e52582295..356ee33b1 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -3037,6 +3037,7 @@ field nvx::host::snapshot::CaptureClass::processors field nvx::host::snapshot::CaptureClass::runtime field nvx::host::snapshot::CaptureClass::scratch method nvx::host::snapshot::CaptureClass::to_bytes +struct nvx::host::snapshot::CaptureContext struct nvx::host::snapshot::ClassIdentity method nvx::host::snapshot::ClassIdentity::as_bytes method nvx::host::snapshot::ClassIdentity::from_bytes @@ -3770,6 +3771,7 @@ field nvx::host::vm::Launched::process struct nvx::host::vm::Prepared method nvx::host::vm::Prepared::arguments method nvx::host::vm::Prepared::capability +method nvx::host::vm::Prepared::capture_context method nvx::host::vm::Prepared::inherit method nvx::host::vm::Prepared::log method nvx::host::vm::Prepared::outputs diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index fcb344468..812a5a3d4 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -1623,6 +1623,7 @@ field nvx::host::snapshot::CaptureClass::processors field nvx::host::snapshot::CaptureClass::runtime field nvx::host::snapshot::CaptureClass::scratch method nvx::host::snapshot::CaptureClass::to_bytes +struct nvx::host::snapshot::CaptureContext struct nvx::host::snapshot::ClassIdentity method nvx::host::snapshot::ClassIdentity::as_bytes method nvx::host::snapshot::ClassIdentity::from_bytes @@ -2355,6 +2356,7 @@ field nvx::host::vm::Launched::process struct nvx::host::vm::Prepared method nvx::host::vm::Prepared::arguments method nvx::host::vm::Prepared::capability +method nvx::host::vm::Prepared::capture_context method nvx::host::vm::Prepared::inherit method nvx::host::vm::Prepared::log method nvx::host::vm::Prepared::outputs From 4ca3e62da07723368edec89ced8d02f2cd3fbae4 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Tue, 10 Nov 2026 11:06:28 -0800 Subject: [PATCH 04/15] host: keep the capture fixture warning-free on Windows The fixture directory is needed only for Linux physical-block metadata. Mark the parameter accordingly on the other supported hosts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 095bfb08-a5d0-4331-b2b2-dd76d2fbadd8 --- nvx-rs/src/host/vm/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nvx-rs/src/host/vm/tests.rs b/nvx-rs/src/host/vm/tests.rs index 2c7728f4d..2ff425c72 100644 --- a/nvx-rs/src/host/vm/tests.rs +++ b/nvx-rs/src/host/vm/tests.rs @@ -456,12 +456,12 @@ fn cold_runtime() -> Runtime { } } -fn capture_config(vm: &VmDir) -> VmConfig { +fn capture_config(_vm: &VmDir) -> VmConfig { #[cfg(target_os = "linux")] let physical_block_size = { use std::os::unix::fs::MetadataExt; - u32::try_from(fs::metadata(vm.path()).unwrap().blksize()).unwrap() + u32::try_from(fs::metadata(_vm.path()).unwrap().blksize()).unwrap() }; #[cfg(windows)] let physical_block_size = 4096; From d9537c1ad27c33a677d628056c0a3e3aebe2670a Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Tue, 10 Nov 2026 12:12:45 -0800 Subject: [PATCH 05/15] host: require one init-parsed capture sandbox selector Reject absent, repeated or malformed sandbox selectors without hiding named forbidden-token diagnostics. Cover init IFS boundaries through each pure capture command-line entry point. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1d7e92b7-acbd-4068-8517-e07a795b608d --- nvx-rs/src/host/config/mod.rs | 10 +++++- nvx-rs/src/host/config/tests.rs | 59 ++++++++++++++++++++++++++------- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/nvx-rs/src/host/config/mod.rs b/nvx-rs/src/host/config/mod.rs index a9a136c3b..2b1a6a4e3 100644 --- a/nvx-rs/src/host/config/mod.rs +++ b/nvx-rs/src/host/config/mod.rs @@ -161,7 +161,8 @@ pub struct Boot { /// The root's tokens, `nvx_hostname`, `nvx_machine_id`, `nvx_maps` and `virtfs_*` are reserved; /// every other token, including `nvx_entrypoint`, `nvx_arg` and `nvx_share`, is caller-owned. /// A capture permits only OpenVMM's platform allowlist and refuses caller tokens whose - /// tier, processor, console, device or network settings the builder supplies. + /// tier, processor, console, device or network settings the builder supplies. It requires + /// exactly one `nvx_sandbox=1` token, as the guest's init splits the command line. pub command_line: String, /// What a capture of the VM adds, if the VM may be captured. pub capture: Option, @@ -502,6 +503,13 @@ impl VmConfig { ))); } } + let mut sandbox = cmdline::init_tokens(boot.command_line.as_bytes()) + .filter(|token| token.name == b"nvx_sandbox"); + if sandbox.next().and_then(|token| token.value) != Some(b"1") || sandbox.next().is_some() { + return Err(Error::unsupported( + "a fresh capture needs exactly one nvx_sandbox=1 token", + )); + } let mut command_line = boot.command_line.clone(); if capture.processors.boot < capture.processors.capacity { if !command_line.is_empty() { diff --git a/nvx-rs/src/host/config/tests.rs b/nvx-rs/src/host/config/tests.rs index f668618ad..49decd2dc 100644 --- a/nvx-rs/src/host/config/tests.rs +++ b/nvx-rs/src/host/config/tests.rs @@ -1187,25 +1187,60 @@ fn capture_arguments_accept_platform_tokens_and_refuse_nonplatform_tokens() { #[test] fn capture_command_lines_keep_only_unowned_platform_tokens() { - for token in [ - "", - "reboot=t", - "panic=-1", + for command_line in [ "nvx_sandbox=1", - "nvx_config=0xd0010000,65536", + "nvx_sandbox=1 reboot=t", + "nvx_sandbox=1 panic=-1", + "nvx_sandbox=1 nvx_config=0xd0010000,65536", + "\t nvx_sandbox=1\nreboot=t\tpanic=-1\n", ] { let mut config = captured(); - config.boot.as_mut().unwrap().command_line = token.to_owned(); - let expected = if token.is_empty() { - "maxcpus=1".to_owned() - } else { - format!("{token} maxcpus=1") - }; - assert_eq!(config.kernel_command_line().unwrap(), expected); + config.boot.as_mut().unwrap().command_line = command_line.to_owned(); + assert_eq!( + config.kernel_command_line().unwrap(), + format!("{command_line} maxcpus=1") + ); config.check_command_line().unwrap(); } } +#[test] +fn capture_command_lines_require_one_init_sandbox_selector() { + for command_line in [ + "", + " \t\n", + "reboot=t", + "nvx_sandbox=1 nvx_sandbox=1", + "nvx_sandbox=1\tnvx_sandbox=1", + "nvx_sandbox=1\nnvx_sandbox=1", + "nvx_sandbox=1\rreboot=t", + "nvx_sandbox=1\x0creboot=t", + "\rnvx_sandbox=1", + "nvx_sandbox", + "nvx_sandbox=", + "nvx_sandbox=0", + ] { + let mut config = captured(); + config.boot.as_mut().unwrap().command_line = command_line.to_owned(); + for result in [ + config.kernel_command_line().map(|_| ()), + config.check_command_line(), + config + .openvmm_arguments( + &runtime(), + "/missing/control", + Path::new("/missing/report"), + Some("/missing/console"), + ) + .map(|_| ()), + ] { + let error = result.unwrap_err(); + assert_eq!(error.kind(), ErrorKind::Unsupported, "{command_line:?}"); + assert!(error.to_string().contains("nvx_sandbox"), "{error}"); + } + } +} + #[test] #[allow(unreachable_pub, clippy::missing_docs_in_private_items)] fn capture_platform_allowlist_matches_openvmms_named_contract() { From 8d6cbce4e649ae2dc6a9275f61541135171bd854 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Tue, 10 Nov 2026 12:22:59 -0800 Subject: [PATCH 06/15] host: format reconciled capture imports Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1d7e92b7-acbd-4068-8517-e07a795b608d --- nvx-rs/src/host/snapshot.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nvx-rs/src/host/snapshot.rs b/nvx-rs/src/host/snapshot.rs index 12fdce4db..f464c1958 100644 --- a/nvx-rs/src/host/snapshot.rs +++ b/nvx-rs/src/host/snapshot.rs @@ -44,7 +44,8 @@ use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use crate::host::config::{ - CaptureConfig, ConsoleAttachment, NicConfig, PROCESSOR_COUNTS, RootConfig, UpperConfig, VmConfig, + CaptureConfig, ConsoleAttachment, NicConfig, PROCESSOR_COUNTS, RootConfig, UpperConfig, + VmConfig, }; use crate::host::paths::{absolute, normalized}; use crate::host::runtime::{Runtime, RuntimeIdentity}; From b2bf182feaf3784c0ff82eca972f087941534731 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Tue, 10 Nov 2026 11:36:26 -0800 Subject: [PATCH 07/15] host: finish exited captures into snapshot entries Validate the proved-exited capture outcome, strict OpenVMM artifacts and checked capture context. Write retryable private key and description files without moving or cleaning VM-directory entries. The Draft keeps SnapshotKey::time and its encoding/identity semantics pending the explicit user decision. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b718df7e-13ad-474d-b3c5-b0f93a7e18e6 Copilot-Session: bc910877-842b-4ceb-8b01-c50d4c0d7af9 --- nvx-rs/README.md | 11 +- nvx-rs/public-api/linux.txt | 3 + nvx-rs/public-api/windows.txt | 3 + nvx-rs/src/host/snapshot.rs | 33 +- nvx-rs/src/host/snapshot/capture.rs | 230 +++++++++ nvx-rs/src/host/snapshot/capture/tests.rs | 468 ++++++++++++++++++ nvx-rs/src/host/snapshot/key.rs | 112 ++++- nvx-rs/src/host/template/store/capture.rs | 139 +----- .../src/host/template/store/capture/tests.rs | 6 + nvx-rs/src/host/template/store/tests.rs | 2 +- nvx-rs/src/host/vm.rs | 2 +- 11 files changed, 835 insertions(+), 174 deletions(-) create mode 100644 nvx-rs/src/host/snapshot/capture.rs create mode 100644 nvx-rs/src/host/snapshot/capture/tests.rs diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 6a16210ac..0352ee621 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -125,7 +125,7 @@ changes only when the module's purpose does. | [`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::vm`](#nvxhostvm) | A private VM directory, cold and capture preparation, verified detached launch, confirmed stop, and caller-saved reattachment | -| [`nvx::host::snapshot`](#nvxhostsnapshot) | Strict private snapshot directories, verified metadata, VM compatibility, captured NIC addresses, and time eligibility | +| [`nvx::host::snapshot`](#nvxhostsnapshot) | Strict private snapshots, post-exit capture finishing, verified metadata, VM compatibility, captured NIC addresses, and time eligibility | | [`nvx::host::config`](#nvxhostconfig) | `VmConfig` and `CaptureConfig`: pure configuration, cold-boot kernel/OpenVMM arguments, portable length checks, and capture identity | | [`nvx::host::restore`](#nvxhostrestore) | A caller-driven restore to Ready without I/O or a clock, and its peer-verified restore-ready listener | | [`nvx::host::artifacts`](#nvxhostartifacts) | Locating the OpenVMM executable and the guest artifacts that a VM runs, and their runtime identity | @@ -638,6 +638,15 @@ effective devices independently of runtime paths or network policy. `vm::Prepared::capture_context` returns it for the caller to clone and serialize; NVX owns no capture-context store or persistence format. +`finish_capture(&VmDir, &CaptureContext, &Exited)` requires the capture VM's +proved exit and successful outcome, checks the OpenVMM snapshot against the +context, and atomically writes `key.bin` and informational `snapshot.json` +beside `openvmm/`. It returns `CapturedSnapshot { key }`. The caller moves +only those three entries into its own private, immutable snapshot directory; +logs, outcomes and other VM files remain where they are. A failure can be +corrected and retried while the VM directory and capture context stay intact, +including when one metadata file was already written. + ### `nvx::host::config` `VmConfig`: the configuration of a VM as one value, its processors, its memory diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 0f5672b0a..2493b033a 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -3038,6 +3038,8 @@ field nvx::host::snapshot::CaptureClass::runtime field nvx::host::snapshot::CaptureClass::scratch method nvx::host::snapshot::CaptureClass::to_bytes struct nvx::host::snapshot::CaptureContext +struct nvx::host::snapshot::CapturedSnapshot +field nvx::host::snapshot::CapturedSnapshot::key struct nvx::host::snapshot::ClassIdentity method nvx::host::snapshot::ClassIdentity::as_bytes method nvx::host::snapshot::ClassIdentity::from_bytes @@ -3138,6 +3140,7 @@ field nvx::host::snapshot::TimeContract::host_id field nvx::host::snapshot::TimeContract::lineage field nvx::host::snapshot::TimeContract::tsc_frequency_hz field nvx::host::snapshot::TimeContract::tsc_tolerance_ppm +fn nvx::host::snapshot::finish_capture mod nvx::host::store struct nvx::host::store::ArtifactIdentity method nvx::host::store::ArtifactIdentity::as_str diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index cc3fd9ccb..056639c16 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -1624,6 +1624,8 @@ field nvx::host::snapshot::CaptureClass::runtime field nvx::host::snapshot::CaptureClass::scratch method nvx::host::snapshot::CaptureClass::to_bytes struct nvx::host::snapshot::CaptureContext +struct nvx::host::snapshot::CapturedSnapshot +field nvx::host::snapshot::CapturedSnapshot::key struct nvx::host::snapshot::ClassIdentity method nvx::host::snapshot::ClassIdentity::as_bytes method nvx::host::snapshot::ClassIdentity::from_bytes @@ -1724,6 +1726,7 @@ field nvx::host::snapshot::TimeContract::host_id field nvx::host::snapshot::TimeContract::lineage field nvx::host::snapshot::TimeContract::tsc_frequency_hz field nvx::host::snapshot::TimeContract::tsc_tolerance_ppm +fn nvx::host::snapshot::finish_capture mod nvx::host::store struct nvx::host::store::ArtifactIdentity method nvx::host::store::ArtifactIdentity::as_str diff --git a/nvx-rs/src/host/snapshot.rs b/nvx-rs/src/host/snapshot.rs index f464c1958..e1b3a6ca9 100644 --- a/nvx-rs/src/host/snapshot.rs +++ b/nvx-rs/src/host/snapshot.rs @@ -4,6 +4,9 @@ //! description. The caller keeps the directory and its entries private and immutable while //! using the opened snapshot. //! +//! [`finish_capture`] validates an exited capture and writes its key and informational +//! description without publishing or removing any VM-directory entry. +//! //! [`SnapshotKey`] derives a compatibility key from a capture's class and manifest. //! [`SnapshotManifest::read`] reads the manifest that OpenVMM wrote, and //! [`TimeContract::eligibility`] checks its restore window at a [`HostTime`]. @@ -29,6 +32,7 @@ //! in `tests/vectors/snapshot-key-v1.txt` fix version 1 of a key's encoding and of a class's. mod canonical; +mod capture; pub(super) mod eligibility; pub(super) mod key; pub(super) mod manifest; @@ -54,6 +58,8 @@ use crate::host::{filesystem, platform, sandbox, seal}; use crate::net::Ipv4Cidr; use crate::{Error, Hypervisor, Result}; +pub use self::capture::{CapturedSnapshot, finish_capture}; +pub(super) use self::capture::{check_exit, check_snapshot}; pub use self::eligibility::{Downtime, DowntimeSource, Eligibility, HostTime, MAX_DOWNTIME_NS}; pub use self::key::{CaptureClass, ClassIdentity, Invariants, KeyIdentity, SnapshotKey}; pub use self::manifest::{ @@ -67,6 +73,9 @@ pub const DESCRIPTION_FILE: &str = "snapshot.json"; /// The directory of OpenVMM's manifest, RAM and saved state. pub const OPENVMM_DIR: &str = "openvmm"; +/// OpenVMM's exact platform-snapshot inventory, in name order. +pub(super) const SNAPSHOT_ARTIFACTS: [&str; 3] = [MANIFEST_NAME, "memory.bin", "state.bin"]; + /// The checked configuration and effective devices of a blockless platform capture. /// /// [`crate::host::vm::Prepared::capture_context`] returns it after verifying the actual sparse @@ -129,30 +138,10 @@ impl Snapshot { pub fn open(dir: &Path) -> Result { let dir = absolute(&normalized(dir)).map_err(|error| entry_error(dir, error))?; check_directory(&dir, &[KEY_FILE, DESCRIPTION_FILE, OPENVMM_DIR])?; - let openvmm = dir.join(OPENVMM_DIR); - check_directory(&openvmm, &[MANIFEST_NAME, "memory.bin", "state.bin"])?; - for path in [ - dir.join(KEY_FILE), - dir.join(DESCRIPTION_FILE), - openvmm.join(MANIFEST_NAME), - ] { + for path in [dir.join(KEY_FILE), dir.join(DESCRIPTION_FILE)] { check_file(&path)?; } - let manifest = SnapshotManifest::read(&openvmm)?; - for (name, expected) in [ - ("memory.bin", manifest.memory_len()), - ("state.bin", manifest.state_len()), - ] { - let path = openvmm.join(name); - let found = check_file(&path)?.len(); - if found != expected { - return Err(Error::io(format!( - "the snapshot artifact {} is {found} bytes long, not the {expected} bytes \ - that its manifest records", - path.display() - ))); - } - } + let manifest = check_snapshot(&dir.join(OPENVMM_DIR))?; let path = dir.join(KEY_FILE); let bytes = regular::open_regular(&path) .and_then(|file| { diff --git a/nvx-rs/src/host/snapshot/capture.rs b/nvx-rs/src/host/snapshot/capture.rs new file mode 100644 index 000000000..9415981c7 --- /dev/null +++ b/nvx-rs/src/host/snapshot/capture.rs @@ -0,0 +1,230 @@ +//! Post-exit capture validation and the two metadata files beside OpenVMM's snapshot. + +use std::fs; +use std::io; +use std::path::Path; + +use serde::Deserialize; +use serde_json::json; + +use super::{ + CaptureContext, DESCRIPTION_FILE, KEY_FILE, MANIFEST_NAME, OPENVMM_DIR, SNAPSHOT_ARTIFACTS, + SnapshotKey, SnapshotManifest, +}; +use crate::host::platform; +use crate::host::process::Exited; +use crate::host::store::files::{sync_dir, write_private}; +use crate::host::store::regular::{open_regular, read_at_most}; +use crate::host::vm::{OUTCOME, VmDir}; +use crate::{Error, ErrorKind, Result}; + +/// Longest outcome report of a capture's VM that the capture reads, in bytes. +const MAX_REPORT_LEN: u64 = 1024 * 1024; + +/// The metadata of a finished capture, whose files the caller may now publish. +#[derive(Debug)] +pub struct CapturedSnapshot { + /// The checked key written to [`KEY_FILE`]. + pub key: SnapshotKey, +} + +/// Finishes a capture after its VM has provably exited. +/// +/// `exited` must prove the exit of the VM that produced `dir` and `context`; the caller keeps +/// that association. Exit proof alone does not establish success: the outcome report must +/// record a successful capture, and the private OpenVMM snapshot must pass the existing strict +/// manifest, artifact and capture-context checks. +/// +/// Writes [`KEY_FILE`] and the informational [`DESCRIPTION_FILE`] beside [`OPENVMM_DIR`]. +/// The caller moves only those three entries into its own private snapshot directory and keeps +/// them immutable while using it. No other VM-directory entry is moved, removed or published. +/// A failure may be corrected and retried while the directory and capture context remain intact; +/// either metadata file may already have been replaced. +/// +/// Fails with [`ErrorKind::Exited`] for an absent or unsuccessful capture outcome, with +/// [`ErrorKind::Io`] for invalid files or a context/manifest mismatch, and otherwise as +/// [`SnapshotManifest::read`] and the checked context's configuration do. +/// +/// A caller cannot finish without exit proof: +/// +/// ```compile_fail +/// use nvx::host::{snapshot::{CaptureContext, finish_capture}, vm::VmDir}; +/// +/// fn without_exit(dir: &VmDir, context: &CaptureContext) { +/// finish_capture(dir, context); +/// } +/// ``` +pub fn finish_capture( + dir: &VmDir, + context: &CaptureContext, + _exited: &Exited, +) -> Result { + VmDir::open(dir.path())?; + check_exit(&dir.path().join(OUTCOME))?; + let manifest = check_snapshot(&dir.path().join(OPENVMM_DIR))?; + let key = SnapshotKey::from_capture(context, &manifest)?; + let time = manifest.time(); + let description = json!({ + "key": key.identity().to_string(), + "runtime": key.runtime(), + "hypervisor": key.hypervisor(), + "cpuProfile": key.cpu_profile(), + "capture": context.capture, + "nic": context.nic, + "console": context.console, + "createdAt": manifest.created_at(), + "openvmmVersion": manifest.openvmm_version(), + "memoryLen": manifest.memory_len(), + "stateLen": manifest.state_len(), + "time": { + "tscFrequencyHz": time.tsc_frequency_hz, + "tscTolerancePpm": time.tsc_tolerance_ppm, + "apicFrequencyHz": time.apic_frequency_hz, + "captureTsc": time.capture_tsc, + "captureUtcNs": time.capture_utc_ns, + "captureMonotonicNs": time.capture_monotonic_ns, + "hostClock": time.host_clock.as_str(), + "hostId": time.host_id.iter().map(|byte| format!("{byte:02x}")).collect::(), + "bootId": time.boot_id, + "lineage": time.lineage, + "cutToAnchorNs": time.cut_to_anchor_ns, + }, + }); + let description = serde_json::to_vec_pretty(&description).map_err(|error| { + Error::other("cannot encode the snapshot description").with_source(error) + })?; + write_private(&dir.path().join(KEY_FILE), &key.to_bytes())?; + write_private(&dir.path().join(DESCRIPTION_FILE), &description)?; + sync_dir(dir.path())?; + Ok(CapturedSnapshot { key }) +} + +/// The outcome report of a VM, as OpenVMM writes it, of which a capture reads the outcome. +#[derive(Deserialize)] +struct OutcomeReport { + /// The outcome of the VM. + outcome: Outcome, +} + +/// The outcome of a VM, as its outcome report records it. +#[derive(Deserialize)] +struct Outcome { + /// The operation of the VM: `capture` for a VM that OpenVMM captures. + operation: String, + /// The category of the outcome: `success` for a capture that OpenVMM published. + category: String, + /// The status code of the outcome: 0 for a capture that OpenVMM published. + status_code: i64, +} + +/// Checks that the outcome report records a capture that OpenVMM published successfully. +pub(in crate::host) fn check_exit(path: &Path) -> Result<()> { + let description = format!("the outcome report {}", path.display()); + let file = match open_regular(path) { + Ok(file) => file, + Err(error) if error.kind() == io::ErrorKind::NotFound => { + return Err(Error::exited(format!( + "the VM of the capture ended without {description}" + ))); + } + Err(error) => { + return Err(Error::io(format!("cannot read {description}")).with_source(error)); + } + }; + let bytes = read_at_most(file, MAX_REPORT_LEN, "an outcome report") + .map_err(|error| Error::io(format!("cannot read {description}")).with_source(error))?; + let report: OutcomeReport = serde_json::from_slice(&bytes) + .map_err(|error| Error::io(format!("{description} is malformed")).with_source(error))?; + let Outcome { + operation, + category, + status_code, + } = report.outcome; + if operation != "capture" || category != "success" || status_code != 0 { + return Err(Error::exited(format!( + "the VM of the capture ended without a capture: {description} records the outcome \ + {category} with status {status_code} of the operation {operation}" + ))); + } + Ok(()) +} + +/// Checks the private OpenVMM directory, strict manifest and exact regular private artifacts. +pub(in crate::host) fn check_snapshot(snapshot: &Path) -> Result { + let what = "the snapshot of the capture"; + let problem = platform::check_snapshot_dir(snapshot).map_err(|error| { + Error::io(format!("cannot open {what} {}", snapshot.display())).with_source(error) + })?; + if let Some(problem) = problem { + return Err(Error::io(format!( + "{what} {} {problem}", + snapshot.display() + ))); + } + let manifest = SnapshotManifest::read(snapshot).map_err(|error| { + if error.kind() != ErrorKind::NotFound { + return error; + } + let (_, message, source) = error.into_parts(); + let error = Error::io(message); + match source { + Some(source) => error.with_source(source), + None => error, + } + })?; + let mut names = fs::read_dir(snapshot) + .and_then(|entries| { + entries + .map(|entry| { + entry?.file_name().into_string().map_err(|name| { + io::Error::other(format!("the snapshot entry {name:?} is not Unicode")) + }) + }) + .collect::>>() + }) + .map_err(|error| super::entry_error(snapshot, error))?; + names.sort(); + if names != SNAPSHOT_ARTIFACTS { + return Err(Error::io(format!( + "{what} {} holds {}, not exactly the manifest, RAM, and saved state of a platform-tier \ + snapshot", + snapshot.display(), + names.join(", ") + ))); + } + for (name, length) in [ + (MANIFEST_NAME, None), + ("memory.bin", Some(manifest.memory_len())), + ("state.bin", Some(manifest.state_len())), + ] { + let path = snapshot.join(name); + let refused = |problem: String| { + Error::io(format!( + "the artifact {} of {what} {problem}", + path.display() + )) + }; + let file = + open_regular(&path).map_err(|error| refused(format!("cannot be used: {error}")))?; + if let Some(problem) = platform::private_file_problem(&file) + .map_err(|error| refused(format!("cannot be checked: {error}")))? + { + return Err(refused(problem)); + } + let found = file + .metadata() + .map_err(|error| refused(format!("cannot be examined: {error}")))? + .len(); + if let Some(length) = length + && found != length + { + return Err(refused(format!( + "is {found} bytes long, not the {length} bytes that its manifest records" + ))); + } + } + Ok(manifest) +} + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/snapshot/capture/tests.rs b/nvx-rs/src/host/snapshot/capture/tests.rs new file mode 100644 index 000000000..732b147ba --- /dev/null +++ b/nvx-rs/src/host/snapshot/capture/tests.rs @@ -0,0 +1,468 @@ +//! Finishing emulated capture output with genuine OS exit proof, without launching a VM. + +#![cfg(any(target_os = "linux", windows))] + +use std::io::Read; +use std::process::{Child, Command, Stdio}; +use std::time::{Duration, Instant}; + +use serde_json::Value; + +use super::*; +use crate::Hypervisor; +use crate::host::config::{Capacity, CaptureConfig, ConsoleAttachment, DiskCapture, NicConfig}; +use crate::host::process::{BootId, Opened, Process, ProcessRef}; +use crate::host::snapshot::Snapshot; +use crate::host::snapshot::tests::{MIB, ManifestSpec, SnapshotFixture, class}; +use crate::host::vm::create_private_dir; + +/// A producer kept alive through its stdin until its parent records its real identity. +#[test] +fn exit_helper() { + if std::env::var_os("NVX_FINISH_CAPTURE_EXIT_HELPER").is_some() { + std::io::stdin().read_to_end(&mut Vec::new()).unwrap(); + } +} + +/// Reaps only the child that this test owns, including when a proof assertion fails. +struct OwnedChild(Child); + +impl Drop for OwnedChild { + fn drop(&mut self) { + if self.0.try_wait().unwrap().is_none() { + self.0.kill().unwrap(); + } + self.0.wait().unwrap(); + } +} + +/// Waits for an owned emulated producer's actual exit, never forging a process reference. +fn exited() -> Exited { + let mut command = Command::new(std::env::current_exe().unwrap()); + command + .args(["--exact", "host::snapshot::capture::tests::exit_helper"]) + .env("NVX_FINISH_CAPTURE_EXIT_HELPER", "1") + .stdin(Stdio::piped()) + .stdout(Stdio::null()); + let mut child = OwnedChild(crate::host::tests::spawn_locked(&mut command).unwrap()); + let reference = ProcessRef { + pid: child.0.id(), + start_time: platform::process_start_time(child.0.id()).unwrap().unwrap(), + boot: BootId::current().unwrap(), + }; + let Opened::Running(process) = Process::open(&reference).unwrap() else { + panic!("the owned producer is waiting for its parent"); + }; + assert!(process.wait(Instant::now()).unwrap().is_none()); + drop(child.0.stdin.take()); + let proof = process + .wait(Instant::now() + Duration::from_secs(10)) + .unwrap() + .expect("the owned producer exited"); + assert!(child.0.wait().unwrap().success()); + proof +} + +/// A private VM directory containing OpenVMM's output but neither NVX metadata file. +struct Fixture { + /// Keeps the sparse snapshot's private temporary directory alive. + _snapshot: SnapshotFixture, + /// The caller-owned VM directory. + dir: VmDir, + /// The checked inputs that the emulated capture used. + context: CaptureContext, +} + +impl Fixture { + /// Uses the existing strict snapshot fixture and removes only its already-finished metadata. + fn new() -> Self { + let snapshot = SnapshotFixture::new(); + for name in [KEY_FILE, DESCRIPTION_FILE] { + fs::remove_file(snapshot.dir.join(name)).unwrap(); + } + let dir = VmDir::open(&snapshot.dir).unwrap(); + let class = class(); + let context = CaptureContext { + capture: CaptureConfig { + disks: DiskCapture::Fresh { + image: class.distro, + scratch: class.scratch, + }, + processors: Capacity { + boot: u64::from(class.boot_processors), + capacity: u64::from(class.processors), + }, + memory: Capacity { + boot: class.memory / MIB, + capacity: class.memory_capacity / MIB, + }, + invariants: class.invariants, + }, + nic: None, + console: ConsoleAttachment::Disconnected, + runtime: class.runtime, + hypervisor: class.hypervisor, + cpu_profile: Some(class.cpu_profile), + }; + let fixture = Self { + _snapshot: snapshot, + dir, + context, + }; + fixture.outcome("capture", "success", 0); + fixture + } + + /// Writes OpenVMM's capture outcome without any launch state. + fn outcome(&self, operation: &str, category: &str, status: i64) { + write_private( + &self.dir.path().join(OUTCOME), + &serde_json::to_vec(&json!({ + "outcome": { + "operation": operation, + "category": category, + "status_code": status, + }, + })) + .unwrap(), + ) + .unwrap(); + } + + /// Returns one of OpenVMM's artifact paths. + fn artifact(&self, name: &str) -> std::path::PathBuf { + self.dir.path().join(OPENVMM_DIR).join(name) + } + + /// Calls the public finalizer with the fixture's checked inputs. + fn finish(&self, exited: &Exited) -> Result { + finish_capture(&self.dir, &self.context, exited) + } + + /// Checks that validation has not written either metadata file. + fn unfinished(&self) { + assert!(!self.dir.path().join(KEY_FILE).exists()); + assert!(!self.dir.path().join(DESCRIPTION_FILE).exists()); + } +} + +#[test] +fn finish_writes_matching_metadata_and_only_its_three_entries_are_published() { + let mut fixture = Fixture::new(); + fixture.context.capture.invariants = super::super::Invariants::new() + .with("isolation.test", "kept") + .unwrap(); + let proof = exited(); + let outcome = fs::read(fixture.dir.path().join(OUTCOME)).unwrap(); + for (name, bytes) in [ + ("openvmm.log", b"VM log".as_slice()), + ("caller.txt", b"caller data"), + ] { + write_private(&fixture.dir.path().join(name), bytes).unwrap(); + } + let captured = fixture.finish(&proof).unwrap(); + let manifest = SnapshotManifest::read(&fixture.dir.path().join(OPENVMM_DIR)).unwrap(); + let key = + SnapshotKey::from_bytes(&fs::read(fixture.dir.path().join(KEY_FILE)).unwrap()).unwrap(); + assert_eq!(key, captured.key); + assert_eq!(key.runtime(), &fixture.context.runtime); + assert_eq!(key.invariants(), &fixture.context.capture.invariants); + key.check_manifest(&manifest).unwrap(); + let description: Value = + serde_json::from_slice(&fs::read(fixture.dir.path().join(DESCRIPTION_FILE)).unwrap()) + .unwrap(); + assert_eq!(description["key"], key.identity().to_string()); + assert_eq!( + description["runtime"], + serde_json::to_value(key.runtime()).unwrap() + ); + assert_eq!( + description["capture"], + serde_json::to_value(&fixture.context.capture).unwrap() + ); + assert_eq!(description["memoryLen"], manifest.memory_len()); + assert_eq!(description["stateLen"], manifest.state_len()); + assert_eq!( + description["time"]["captureTsc"], + manifest.time().capture_tsc + ); + assert_eq!( + description["time"]["captureUtcNs"], + manifest.time().capture_utc_ns + ); + assert_eq!( + description["time"]["bootId"], + manifest.time().boot_id.to_string() + ); + let published = fixture.dir.path().parent().unwrap().join("published"); + create_private_dir(&published).unwrap(); + for name in [KEY_FILE, DESCRIPTION_FILE, OPENVMM_DIR] { + fs::rename(fixture.dir.path().join(name), published.join(name)).unwrap(); + } + assert_eq!(Snapshot::open(&published).unwrap().key(), &captured.key); + assert_eq!(fs::read_dir(&published).unwrap().count(), 3); + assert_eq!(fs::read(fixture.dir.path().join(OUTCOME)).unwrap(), outcome); + assert_eq!( + fs::read(fixture.dir.path().join("openvmm.log")).unwrap(), + b"VM log" + ); + assert_eq!( + fs::read(fixture.dir.path().join("caller.txt")).unwrap(), + b"caller data" + ); + assert!(Snapshot::open(fixture.dir.path()).is_err()); +} + +#[test] +fn finish_refuses_absent_and_unsuccessful_capture_outcomes() { + let proof = exited(); + let fixture = Fixture::new(); + fs::remove_file(fixture.dir.path().join(OUTCOME)).unwrap(); + assert_eq!( + fixture.finish(&proof).unwrap_err().kind(), + ErrorKind::Exited + ); + fixture.unfinished(); + for (operation, category, status) in [ + ("run", "success", 0), + ("capture", "guest-exit", 3), + ("capture", "vmm-failure", 1), + ("capture", "success", 1), + ("capture", "failure", 0), + ] { + fixture.outcome(operation, category, status); + assert_eq!( + fixture.finish(&proof).unwrap_err().kind(), + ErrorKind::Exited + ); + fixture.unfinished(); + } +} + +#[test] +fn finish_uses_the_bounded_regular_outcome_reader() { + let proof = exited(); + let fixture = Fixture::new(); + let path = fixture.dir.path().join(OUTCOME); + for bytes in [ + b"not JSON".to_vec(), + br#"{"outcome":{"category":"success","status_code":0}}"#.to_vec(), + vec![b' '; MAX_REPORT_LEN as usize + 1], + ] { + write_private(&path, &bytes).unwrap(); + assert_eq!(fixture.finish(&proof).unwrap_err().kind(), ErrorKind::Io); + fixture.unfinished(); + } + fs::remove_file(&path).unwrap(); + fs::create_dir(&path).unwrap(); + assert_eq!(fixture.finish(&proof).unwrap_err().kind(), ErrorKind::Io); + fixture.unfinished(); +} + +#[test] +fn finish_refuses_every_missing_or_extra_openvmm_artifact() { + let proof = exited(); + for missing in [ + None, + Some(MANIFEST_NAME), + Some("memory.bin"), + Some("state.bin"), + ] { + let fixture = Fixture::new(); + match missing { + Some(name) => fs::remove_file(fixture.artifact(name)).unwrap(), + None => fs::rename( + fixture.dir.path().join(OPENVMM_DIR), + fixture.dir.path().join("saved-openvmm"), + ) + .unwrap(), + } + assert_eq!(fixture.finish(&proof).unwrap_err().kind(), ErrorKind::Io); + fixture.unfinished(); + } + let fixture = Fixture::new(); + write_private(&fixture.artifact("extra"), b"unexpected").unwrap(); + assert_eq!(fixture.finish(&proof).unwrap_err().kind(), ErrorKind::Io); + fixture.unfinished(); +} + +#[test] +fn finish_refuses_irregular_artifacts_and_both_recorded_length_mismatches() { + let proof = exited(); + for name in SNAPSHOT_ARTIFACTS { + let fixture = Fixture::new(); + fs::remove_file(fixture.artifact(name)).unwrap(); + fs::create_dir(fixture.artifact(name)).unwrap(); + assert_eq!(fixture.finish(&proof).unwrap_err().kind(), ErrorKind::Io); + fixture.unfinished(); + } + for name in ["memory.bin", "state.bin"] { + for difference in [-1, 1] { + let fixture = Fixture::new(); + let file = fs::OpenOptions::new() + .write(true) + .open(fixture.artifact(name)) + .unwrap(); + let length = file.metadata().unwrap().len(); + file.set_len(length.checked_add_signed(difference).unwrap()) + .unwrap(); + drop(file); + assert_eq!(fixture.finish(&proof).unwrap_err().kind(), ErrorKind::Io); + fixture.unfinished(); + } + } +} + +#[test] +fn finish_keeps_the_strict_manifest_checks() { + let proof = exited(); + let fixture = Fixture::new(); + for bytes in [ + vec![0x80], + vec![0; super::super::MAX_MANIFEST_LEN as usize + 1], + ] { + write_private(&fixture.artifact(MANIFEST_NAME), &bytes).unwrap(); + assert_eq!(fixture.finish(&proof).unwrap_err().kind(), ErrorKind::Io); + fixture.unfinished(); + } + let mut spec = ManifestSpec::template(); + spec.tier = "workload".to_owned(); + write_private(&fixture.artifact(MANIFEST_NAME), &spec.encode()).unwrap(); + assert_eq!(fixture.finish(&proof).unwrap_err().kind(), ErrorKind::Io); + fixture.unfinished(); +} + +#[test] +fn finish_checks_the_context_against_the_effective_capture() { + let proof = exited(); + let changes: &[fn(&mut CaptureContext)] = &[ + |context| context.hypervisor = Hypervisor::Mshv, + |context| context.cpu_profile = Some("amd.zen4.v1".to_owned()), + |context| context.capture.processors.capacity = 4, + |context| context.capture.processors.boot = 1, + |context| context.capture.memory.boot = 256, + |context| context.capture.memory.capacity = 1024, + |context| { + let DiskCapture::Fresh { image, .. } = &mut context.capture.disks; + image.length *= 2; + }, + |context| { + let DiskCapture::Fresh { scratch, .. } = &mut context.capture.disks; + scratch.physical_block_size *= 2; + }, + |context| { + context.nic = Some(NicConfig { + guest_network: crate::net::Ipv4Cidr::parse("10.0.0.2/24").unwrap(), + }); + }, + |context| context.runtime.control_contract.clear(), + |context| context.capture.memory.capacity = u64::MAX - 127, + ]; + for change in changes { + let mut fixture = Fixture::new(); + change(&mut fixture.context); + assert!(fixture.finish(&proof).is_err()); + fixture.unfinished(); + } +} + +#[test] +fn a_correctable_failure_and_a_partial_metadata_write_can_both_be_retried() { + let proof = exited(); + let fixture = Fixture::new(); + let state = fixture.artifact("state.bin"); + let length = fs::metadata(&state).unwrap().len(); + fs::OpenOptions::new() + .write(true) + .open(&state) + .unwrap() + .set_len(length - 1) + .unwrap(); + assert!(fixture.finish(&proof).is_err()); + fixture.unfinished(); + fs::OpenOptions::new() + .write(true) + .open(&state) + .unwrap() + .set_len(length) + .unwrap(); + let description = fixture.dir.path().join(DESCRIPTION_FILE); + fs::create_dir(&description).unwrap(); + assert!(fixture.finish(&proof).is_err()); + assert!(fixture.dir.path().join(KEY_FILE).is_file()); + assert!(description.is_dir()); + fs::remove_dir(&description).unwrap(); + let captured = fixture.finish(&proof).unwrap(); + assert_eq!(fixture.finish(&proof).unwrap().key, captured.key); + assert!(Snapshot::open(fixture.dir.path()).is_err()); + let published = fixture.dir.path().parent().unwrap().join("published"); + create_private_dir(&published).unwrap(); + for name in [KEY_FILE, DESCRIPTION_FILE, OPENVMM_DIR] { + fs::rename(fixture.dir.path().join(name), published.join(name)).unwrap(); + } + assert_eq!(Snapshot::open(&published).unwrap().key(), &captured.key); + let names = fs::read_dir(fixture.dir.path()) + .unwrap() + .map(|entry| entry.unwrap().file_name()) + .collect::>(); + assert_eq!(names, [std::ffi::OsString::from(OUTCOME)]); +} + +#[cfg(target_os = "linux")] +#[test] +fn finish_refuses_shared_snapshot_entries_links_fifos_and_sockets() { + use std::os::unix::fs::{PermissionsExt, symlink}; + use std::os::unix::net::UnixListener; + + let proof = exited(); + for name in SNAPSHOT_ARTIFACTS { + let fixture = Fixture::new(); + let path = fixture.artifact(name); + fs::set_permissions(&path, fs::Permissions::from_mode(0o640)).unwrap(); + assert!(fixture.finish(&proof).is_err()); + fs::set_permissions(&path, fs::Permissions::from_mode(0o600)).unwrap(); + let saved = fixture.dir.path().join("saved"); + fs::rename(&path, &saved).unwrap(); + symlink(&saved, &path).unwrap(); + assert!(fixture.finish(&proof).is_err()); + fs::remove_file(&path).unwrap(); + nix::unistd::mkfifo(&path, nix::sys::stat::Mode::S_IRUSR).unwrap(); + assert!(fixture.finish(&proof).is_err()); + fs::remove_file(&path).unwrap(); + let _socket = UnixListener::bind(&path).unwrap(); + assert!(fixture.finish(&proof).is_err()); + fixture.unfinished(); + } + let fixture = Fixture::new(); + fs::set_permissions( + fixture.dir.path().join(OPENVMM_DIR), + fs::Permissions::from_mode(0o750), + ) + .unwrap(); + assert!(fixture.finish(&proof).is_err()); + fixture.unfinished(); +} + +#[cfg(windows)] +#[test] +fn finish_refuses_shared_snapshot_acls_and_reparse_artifacts() { + use crate::host::platform::tests::{set_access, user_sid}; + + let proof = exited(); + let shared = format!("D:P(A;;FA;;;{})(A;;FR;;;WD)", user_sid()); + for name in SNAPSHOT_ARTIFACTS { + let fixture = Fixture::new(); + let path = fixture.artifact(name); + set_access(&path, &shared).unwrap(); + assert!(fixture.finish(&proof).is_err()); + fs::remove_file(&path).unwrap(); + crate::host::tests::junction(&path, fixture.dir.path()); + assert!(fixture.finish(&proof).is_err()); + fixture.unfinished(); + fs::remove_dir(&path).unwrap(); + } + let fixture = Fixture::new(); + set_access(&fixture.dir.path().join(OPENVMM_DIR), &shared).unwrap(); + assert!(fixture.finish(&proof).is_err()); + fixture.unfinished(); +} diff --git a/nvx-rs/src/host/snapshot/key.rs b/nvx-rs/src/host/snapshot/key.rs index e0c29175c..9a62aa901 100644 --- a/nvx-rs/src/host/snapshot/key.rs +++ b/nvx-rs/src/host/snapshot/key.rs @@ -6,6 +6,7 @@ use std::net::{Ipv4Addr, Ipv6Addr}; use sha2::{Digest, Sha256}; +use super::CaptureContext; use super::canonical::{CanonicalError, Reader, Writer}; use super::manifest::{ AddressSpace, BlockGeometry, CPUID_ENTRY_LEN, CpuProfile, Device, DeviceRange, @@ -17,7 +18,7 @@ use super::manifest::{ use crate::activation; use crate::contract; use crate::error::{Error, Result}; -use crate::host::config::check_setting; +use crate::host::config::{DiskCapture, check_setting}; use crate::host::platform::parse_hex32; use crate::host::sandbox::SandboxNetwork; use crate::host::sandbox::mount::MOUNT_TAG; @@ -502,6 +503,32 @@ pub struct SnapshotKey { pub(in crate::host) use SnapshotKey as TemplateKey; +/// The checked inputs shared by a legacy capture class and a caller-owned capture context. +struct CaptureInputs<'a> { + /// The runtime's artifact and contract identities. + runtime: &'a RuntimeIdentity, + /// The requested backend. + hypervisor: Hypervisor, + /// The requested CPU profile. + cpu_profile: &'a str, + /// The processor capacity. + processors: u64, + /// The processors online at boot. + boot_processors: u64, + /// The RAM at boot, in bytes. + memory: u64, + /// The RAM capacity, in bytes. + memory_capacity: u64, + /// The placeholder image's geometry. + distro: &'a BlockGeometry, + /// The dormant scratch's geometry. + scratch: &'a BlockGeometry, + /// The resolved NIC address, without a network policy. + guest_network: Option, + /// The caller's invariant settings. + invariants: &'a Invariants, +} + impl SnapshotKey { /// Version of the canonical encoding of a key, its first two bytes. /// @@ -541,6 +568,65 @@ impl SnapshotKey { /// requested. pub fn new(class: &CaptureClass, manifest: &SnapshotManifest) -> Result { class.check()?; + let attached = !class.network_arguments()?.is_empty(); + Self::from_inputs( + CaptureInputs { + runtime: &class.runtime, + hypervisor: class.hypervisor, + cpu_profile: &class.cpu_profile, + processors: u64::from(class.processors), + boot_processors: u64::from(class.boot_processors), + memory: class.memory, + memory_capacity: class.memory_capacity, + distro: &class.distro, + scratch: &class.scratch, + guest_network: class + .network + .as_ref() + .filter(|_| attached) + .map(|network| network.guest_network().to_owned()), + invariants: &class.invariants, + }, + manifest, + ) + } + + /// Derives the key from the checked creation context and OpenVMM's validated manifest. + pub(super) fn from_capture( + context: &CaptureContext, + manifest: &SnapshotManifest, + ) -> Result { + context.capture.check()?; + context.runtime.check()?; + let cpu_profile = context.cpu_profile.as_deref().unwrap_or("auto"); + if let Some(problem) = cpu_profile_problem(cpu_profile) { + return Err(Error::unsupported(problem)); + } + let bytes = |mib: u64| { + mib.checked_mul(1024 * 1024) + .ok_or_else(|| Error::unsupported("capture memory in MiB overflows bytes")) + }; + let DiskCapture::Fresh { image, scratch } = &context.capture.disks; + Self::from_inputs( + CaptureInputs { + runtime: &context.runtime, + hypervisor: context.hypervisor, + cpu_profile, + processors: context.capture.processors.capacity, + boot_processors: context.capture.processors.boot, + memory: bytes(context.capture.memory.boot)?, + memory_capacity: bytes(context.capture.memory.capacity)?, + distro: image, + scratch, + guest_network: context.nic.map(|nic| nic.guest_network.to_string()), + invariants: &context.capture.invariants, + }, + manifest, + ) + } + + /// Checks the manifest's machine against the capture's inputs and takes its effective shape. + fn from_inputs(class: CaptureInputs<'_>, manifest: &SnapshotManifest) -> Result { let machine = &manifest.machine; let mismatch = |found: String, requested: String| { Err(Error::io(format!( @@ -557,22 +643,27 @@ impl SnapshotKey { let profile = machine.cpu.id(); // `auto` selects the built-in profile of the host's CPU, or falls back to a profile that // the host derives, whose generation is `host`, when no built-in profile serves it. - let selected = cpu_profile_matches(&machine.cpu, &class.cpu_profile); + let selected = cpu_profile_matches(&machine.cpu, class.cpu_profile); if !selected { return mismatch( format!("its CPU profile is {profile}"), - class.cpu_profile.clone(), + class.cpu_profile.to_owned(), ); } let boot_processors = match machine.boot_processors { 0 => machine.processors, count => count, }; - if (machine.processors, boot_processors) != (class.processors, class.boot_processors) { + if (u64::from(machine.processors), u64::from(boot_processors)) + != (class.processors, class.boot_processors) + { let describe = |processors, online| format!("{processors} processors, {online} online at boot"); return mismatch( - format!("it has {}", describe(machine.processors, boot_processors)), + format!( + "it has {}", + describe(u64::from(machine.processors), u64::from(boot_processors)) + ), describe(class.processors, class.boot_processors), ); } @@ -588,8 +679,8 @@ impl SnapshotKey { ); } for (role, found, requested) in [ - ("distro", &machine.distro, &class.distro), - ("scratch", &machine.scratch, &class.scratch), + ("distro", &machine.distro, class.distro), + ("scratch", &machine.scratch, class.scratch), ] { if found != requested { return mismatch( @@ -598,12 +689,7 @@ impl SnapshotKey { ); } } - let attached = !class.network_arguments()?.is_empty(); - let requested_network = class - .network - .as_ref() - .filter(|_| attached) - .map(|network| network.guest_network().to_owned()); + let requested_network = class.guest_network; // A network that `SandboxNetwork` accepts has the canonical form that this writes. let found_network = machine.nic.as_ref().map(guest_network_of); if found_network != requested_network { diff --git a/nvx-rs/src/host/template/store/capture.rs b/nvx-rs/src/host/template/store/capture.rs index 74a519049..660312d3e 100644 --- a/nvx-rs/src/host/template/store/capture.rs +++ b/nvx-rs/src/host/template/store/capture.rs @@ -26,8 +26,9 @@ use crate::host::instance::generate_capability; use crate::host::platform; use crate::host::sandbox::distro::{CheckedDistro, DistroBinding, DistroFormat}; use crate::host::sandbox::{LayerRole, SandboxLayer, SandboxPlan}; +use crate::host::snapshot::{check_exit, check_snapshot}; use crate::host::store::files::{create_new, remove_if_present, sync_dir, write_private}; -use crate::host::store::regular::{identity_of, open_regular, read_at_most}; +use crate::host::store::regular::{identity_of, open_regular}; use crate::host::store::{ JOURNAL_NAME, LOCK_NAME, LOG_NAME, LaunchId, LockedStore, OUTCOME_NAME, Recovery, SOCKET_NAME, SandboxStore, @@ -35,7 +36,7 @@ use crate::host::store::{ use crate::host::supervisor::{self, VmmCommand}; use crate::host::template::eligibility::HostTime; use crate::host::template::key::{CaptureClass, ClassIdentity, KeyIdentity, TemplateKey}; -use crate::host::template::manifest::{MANIFEST_NAME, TemplateManifest, TimeContract}; +use crate::host::template::manifest::{TemplateManifest, TimeContract}; use crate::host::template::plan::{SCRATCH_NAME, TemplatePlan, TemplateResources}; /// Name of the directory of a store's captures, in which each capture class has a slot. @@ -46,15 +47,6 @@ pub const TRANSACTION_NAME: &str = "transaction"; /// Name of the record of a capture, in its transaction and then in its generation's payload. pub const CAPTURE_RECORD_NAME: &str = "capture.json"; -/// Name of the saved state of a snapshot. -const STATE_NAME: &str = "state.bin"; -/// Name of the RAM of a snapshot. -const MEMORY_NAME: &str = "memory.bin"; -/// The artifacts of a platform-tier snapshot, in the order of their names: its manifest, its RAM, -/// and its saved state. -pub(in crate::host::template) const SNAPSHOT_ARTIFACTS: [&str; 3] = - [MANIFEST_NAME, MEMORY_NAME, STATE_NAME]; - /// Prefix of the name of the file that backs the RAM of a capture's VM, which OpenVMM creates /// beside the snapshot's destination, and removes as the VM ends. const MEMORY_FILE_PREFIX: &str = ".openvmm-microvm-memory-"; @@ -73,9 +65,6 @@ pub(in crate::host::template) const SOCKET_PATH_LIMIT: usize = 108; /// Longest record of a capture, in bytes. const MAX_CAPTURE_RECORD_LEN: u64 = 4 * 1024; -/// Longest outcome report of a capture's VM that the capture reads, in bytes. -const MAX_REPORT_LEN: u64 = 1024 * 1024; - /// Interval at which a rename that Windows refuses, while another process holds a file below the /// renamed directory open, is tried again, until the capture's deadline. const RENAME_RETRY_INTERVAL: Duration = Duration::from_millis(25); @@ -1114,128 +1103,6 @@ fn check_scratch(template: &TemplatePlan, created: (u64, u128)) -> Result<()> { Ok(()) } -/// The outcome report of a VM, as OpenVMM writes it, of which a capture reads the outcome. -#[derive(Deserialize)] -struct OutcomeReport { - /// The outcome of the VM. - outcome: Outcome, -} - -/// The outcome of a VM, as its outcome report records it. -#[derive(Deserialize)] -struct Outcome { - /// The operation of the VM: `capture` for a VM that OpenVMM captures. - operation: String, - /// The category of the outcome: `success` for a capture that OpenVMM published. - category: String, - /// The status code of the outcome: 0 for a capture that OpenVMM published. - status_code: i64, -} - -/// Checks the source's exit through the outcome report of the capture's VM at `path`: that -/// OpenVMM wrote one, for a capture that succeeded, as it does once it has published the snapshot -/// and torn the VM down, and before it exits. The launch removed the report of the slot's previous -/// VM, as [`LockedStore::begin_launch`] does, and OpenVMM publishes its report without replacing -/// a file, so the report is the VM's own. -fn check_exit(path: &Path) -> Result<()> { - let description = format!("the outcome report {}", path.display()); - let file = match open_regular(path) { - Ok(file) => file, - Err(error) if error.kind() == io::ErrorKind::NotFound => { - return Err(Error::exited(format!( - "the VM of the capture ended without {description}" - ))); - } - Err(error) => { - return Err(Error::io(format!("cannot read {description}")).with_source(error)); - } - }; - let bytes = read_at_most(file, MAX_REPORT_LEN, "an outcome report") - .map_err(|error| Error::io(format!("cannot read {description}")).with_source(error))?; - let report: OutcomeReport = serde_json::from_slice(&bytes) - .map_err(|error| Error::io(format!("{description} is malformed")).with_source(error))?; - let Outcome { - operation, - category, - status_code, - } = report.outcome; - if operation != "capture" || category != "success" || status_code != 0 { - return Err(Error::exited(format!( - "the VM of the capture ended without a capture: {description} records the outcome \ - {category} with status {status_code} of the operation {operation}" - ))); - } - Ok(()) -} - -/// Checks the snapshot at `snapshot` that OpenVMM published: a directory private to the current -/// user, as [`platform::check_snapshot_dir`] checks it, whose manifest -/// [`TemplateManifest::read`] reads, and whose artifacts are exactly its manifest, RAM, and saved -/// state, each a regular file private to the current user, of the length that the manifest -/// records. Reads no artifact but the manifest. Returns the manifest. -fn check_snapshot(snapshot: &Path) -> Result { - let what = "the snapshot of the capture"; - // A snapshot that is missing once its capture reported it is lost, not merely absent. - let problem = platform::check_snapshot_dir(snapshot).map_err(|error| { - Error::io(format!("cannot open {what} {}", snapshot.display())).with_source(error) - })?; - refuse_problem(snapshot, what, problem)?; - // So is a manifest that is missing from the snapshot. - let manifest = TemplateManifest::read(snapshot).map_err(|error| { - if error.kind() != ErrorKind::NotFound { - return error; - } - let (_, message, source) = error.into_parts(); - let error = Error::io(message); - match source { - Some(source) => error.with_source(source), - None => error, - } - })?; - let mut names = entries(snapshot)?; - names.sort(); - if names != SNAPSHOT_ARTIFACTS { - return Err(Error::io(format!( - "{what} {} holds {}, not exactly the manifest, RAM, and saved state of a platform-tier \ - snapshot", - snapshot.display(), - names.join(", ") - ))); - } - for (name, length) in [ - (MANIFEST_NAME, None), - (MEMORY_NAME, Some(manifest.memory_len())), - (STATE_NAME, Some(manifest.state_len())), - ] { - let path = snapshot.join(name); - let refused = |problem: String| { - Error::io(format!( - "the artifact {} of {what} {problem}", - path.display() - )) - }; - let file = - open_regular(&path).map_err(|error| refused(format!("cannot be used: {error}")))?; - if let Some(problem) = platform::private_file_problem(&file) - .map_err(|error| refused(format!("cannot be checked: {error}")))? - { - return Err(refused(problem)); - } - let found = file - .metadata() - .map_err(|error| refused(format!("cannot be examined: {error}")))? - .len(); - if let Some(length) = length - && found != length - { - return Err(refused(format!( - "is {found} bytes long, not the {length} bytes that its manifest records" - ))); - } - } - Ok(manifest) -} - /// Removes from the capture's transaction `transaction` what the template does not keep: the lock /// file of its sandbox store, which the capture closed once its VM had ended, the dormant scratch /// device's file, the file that backed the VM's RAM, which OpenVMM removes as the VM ends unless diff --git a/nvx-rs/src/host/template/store/capture/tests.rs b/nvx-rs/src/host/template/store/capture/tests.rs index b05b912d1..42b20b1d9 100644 --- a/nvx-rs/src/host/template/store/capture/tests.rs +++ b/nvx-rs/src/host/template/store/capture/tests.rs @@ -15,6 +15,7 @@ use super::*; use crate::host::sandbox::distro::{DISTRO_POLICY_REVISION, PolicyVerdict}; use crate::host::sandbox::{SandboxDistro, SandboxUpper}; use crate::host::snapshot::tests::{MIB, ManifestSpec, class, network_class}; +use crate::host::snapshot::{MANIFEST_NAME, SNAPSHOT_ARTIFACTS}; use crate::host::store::{CallerFiles, JOURNAL_NAME, LaunchId}; use crate::host::template::eligibility::MAX_DOWNTIME_NS; use crate::host::template::store::Lease; @@ -30,6 +31,11 @@ const GIB: u64 = 1024 * MIB; /// Nanoseconds in a second. const SECOND: u64 = 1_000_000_000; +/// Name of the saved state in the emulated snapshot. +const STATE_NAME: &str = "state.bin"; +/// Name of the RAM in the emulated snapshot. +const MEMORY_NAME: &str = "memory.bin"; + /// The identity of the placeholder. const IDENTITY: &str = "11111111-1111-1111-1111-111111111111"; diff --git a/nvx-rs/src/host/template/store/tests.rs b/nvx-rs/src/host/template/store/tests.rs index 59d2cb740..f38b612af 100644 --- a/nvx-rs/src/host/template/store/tests.rs +++ b/nvx-rs/src/host/template/store/tests.rs @@ -13,7 +13,7 @@ use crate::host::template::key::CaptureClass; use crate::host::template::manifest::TemplateManifest; #[cfg(any(target_os = "linux", windows))] -pub(in crate::host::template) use super::capture::SNAPSHOT_ARTIFACTS; +pub(in crate::host::template) use crate::host::snapshot::SNAPSHOT_ARTIFACTS; impl Store { /// Publishes a generation of a template, whose fixed time contract names a host and boot of diff --git a/nvx-rs/src/host/vm.rs b/nvx-rs/src/host/vm.rs index 7b02d7721..89a38ddf0 100644 --- a/nvx-rs/src/host/vm.rs +++ b/nvx-rs/src/host/vm.rs @@ -45,7 +45,7 @@ use crate::{Error, Result}; /// OpenVMM's log of a launch. const LOG: &str = "openvmm.log"; /// OpenVMM's outcome report of a launch. -const OUTCOME: &str = "outcome.json"; +pub(super) const OUTCOME: &str = "outcome.json"; /// The control socket on Linux. const CONTROL: &str = "control.sock"; /// The guest's boot-console socket on Linux. From 1148a7487995e02f3a39c6cf384f5d6d825f40ce Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Tue, 10 Nov 2026 12:06:12 -0800 Subject: [PATCH 08/15] host: retain capture time without changing compatibility identity Persist and strictly decode the complete per-snapshot time contract; hash only the unchanged compatibility prefix and reject key/manifest time mismatches. Keep the single Fresh encoding in place without a version bump or backward reader. Apply owner-only artifact validation at the snapshot boundary while preserving the legacy template validator and its existing callers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b718df7e-13ad-474d-b3c5-b0f93a7e18e6 Copilot-Session: bc910877-842b-4ceb-8b01-c50d4c0d7af9 --- nvx-rs/README.md | 10 +- nvx-rs/public-api/linux.txt | 2 + nvx-rs/public-api/windows.txt | 2 + nvx-rs/src/host/snapshot.rs | 18 ++- nvx-rs/src/host/snapshot/canonical.rs | 4 +- nvx-rs/src/host/snapshot/capture.rs | 6 +- nvx-rs/src/host/snapshot/capture/tests.rs | 1 + nvx-rs/src/host/snapshot/key.rs | 132 +++++++++++++++++----- nvx-rs/src/host/snapshot/key/tests.rs | 68 ++++++++++- nvx-rs/src/host/snapshot/manifest.rs | 6 +- nvx-rs/src/host/snapshot/tests.rs | 27 +++++ nvx-rs/tests/vectors/README.md | 6 +- nvx-rs/tests/vectors/snapshot-key-v1.txt | 68 +++++------ 13 files changed, 269 insertions(+), 81 deletions(-) diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 0352ee621..80adea1fc 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -581,8 +581,8 @@ responsive shell input, and a verified clean exit; it downloads no artifact. ### `nvx::host::snapshot` `SnapshotKey` and `SnapshotManifest` own the compatibility key and strict -OpenVMM manifest reader. `SnapshotManifest::time` exposes the capture's -`TimeContract`, whose `eligibility` checks its restore window at a `HostTime`; +OpenVMM manifest reader. `SnapshotManifest::time` and `SnapshotKey::time` expose +the capture's `TimeContract`, whose `eligibility` checks its restore window at a `HostTime`; `HostTime::now` samples the host's clock and identities. The existing `host::template` names remain aliases of these same types. @@ -591,6 +591,12 @@ The key's canonical encoding and identity use the snapshot names and omit and no reader or migration of the prior encoding is supported. `CaptureClass` still records the legacy capture inputs, including its guest build. +The persisted key includes the complete per-snapshot time contract and must +agree with the manifest's time. Its compatibility identity excludes capture +anchors, host and boot identities, lineage and cut-to-anchor time: captures +at different times can have different key bytes and the same `KeyIdentity`. +The declared rates and time ABI remain compatibility inputs. + `Snapshot::open` checks exactly `key.bin`, `snapshot.json` and `openvmm/{manifest.bin,memory.bin,state.bin}`. Directories are private, files are regular and private to their owner, artifact lengths equal the strict diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 2493b033a..cb2eee260 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -3117,6 +3117,7 @@ method nvx::host::snapshot::SnapshotKey::processors method nvx::host::snapshot::SnapshotKey::runtime method nvx::host::snapshot::SnapshotKey::scratch method nvx::host::snapshot::SnapshotKey::speaks_current_contracts +method nvx::host::snapshot::SnapshotKey::time method nvx::host::snapshot::SnapshotKey::to_bytes struct nvx::host::snapshot::SnapshotManifest method nvx::host::snapshot::SnapshotManifest::created_at @@ -3695,6 +3696,7 @@ method nvx::host::template::TemplateKey::processors method nvx::host::template::TemplateKey::runtime method nvx::host::template::TemplateKey::scratch method nvx::host::template::TemplateKey::speaks_current_contracts +method nvx::host::template::TemplateKey::time method nvx::host::template::TemplateKey::to_bytes struct nvx::host::template::TemplateManifest method nvx::host::template::TemplateManifest::created_at diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index 056639c16..fece85530 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -1703,6 +1703,7 @@ method nvx::host::snapshot::SnapshotKey::processors method nvx::host::snapshot::SnapshotKey::runtime method nvx::host::snapshot::SnapshotKey::scratch method nvx::host::snapshot::SnapshotKey::speaks_current_contracts +method nvx::host::snapshot::SnapshotKey::time method nvx::host::snapshot::SnapshotKey::to_bytes struct nvx::host::snapshot::SnapshotManifest method nvx::host::snapshot::SnapshotManifest::created_at @@ -2280,6 +2281,7 @@ method nvx::host::template::TemplateKey::processors method nvx::host::template::TemplateKey::runtime method nvx::host::template::TemplateKey::scratch method nvx::host::template::TemplateKey::speaks_current_contracts +method nvx::host::template::TemplateKey::time method nvx::host::template::TemplateKey::to_bytes struct nvx::host::template::TemplateManifest method nvx::host::template::TemplateManifest::created_at diff --git a/nvx-rs/src/host/snapshot.rs b/nvx-rs/src/host/snapshot.rs index e1b3a6ca9..6f65cefbf 100644 --- a/nvx-rs/src/host/snapshot.rs +++ b/nvx-rs/src/host/snapshot.rs @@ -15,9 +15,10 @@ //! # Canonical encoding //! //! A key and a class each have one canonical encoding, which [`SnapshotKey::to_bytes`] and -//! [`CaptureClass::to_bytes`] write, and whose SHA-256 digest, after a domain of its own, is its -//! identity, a [`KeyIdentity`] or a [`ClassIdentity`]. The encoding starts with its version, and -//! each value in it has exactly one encoding: +//! [`CaptureClass::to_bytes`] write. A key's [`KeyIdentity`] digests its compatibility prefix, +//! excluding the appended capture-time anchors and identities; a [`ClassIdentity`] digests the +//! complete class encoding. Both use SHA-256 after a domain of their own. Each encoding starts +//! with its version, and each value in it has exactly one encoding: //! //! - An integer is its fixed number of little-endian bytes: one for a `u8`, two for a `u16`, four //! for a `u32`, and eight for a `u64`. @@ -141,7 +142,7 @@ impl Snapshot { for path in [dir.join(KEY_FILE), dir.join(DESCRIPTION_FILE)] { check_file(&path)?; } - let manifest = check_snapshot(&dir.join(OPENVMM_DIR))?; + let manifest = check_openvmm(&dir.join(OPENVMM_DIR))?; let path = dir.join(KEY_FILE); let bytes = regular::open_regular(&path) .and_then(|file| { @@ -361,6 +362,15 @@ fn restore_geometry(path: &Path, description: &str) -> Result { sandbox::block_geometry(&file, path, description) } +/// Adds the snapshot format's per-file owner-only checks to the legacy capture checks. +fn check_openvmm(path: &Path) -> Result { + let manifest = check_snapshot(path)?; + for name in SNAPSHOT_ARTIFACTS { + check_file(&path.join(name))?; + } + Ok(manifest) +} + /// Names the failing snapshot entry while retaining its I/O error. fn entry_error(path: &Path, error: io::Error) -> Error { Error::io(format!("cannot examine snapshot entry {}", path.display())).with_source(error) diff --git a/nvx-rs/src/host/snapshot/canonical.rs b/nvx-rs/src/host/snapshot/canonical.rs index e2f5aa52a..c300c8cd4 100644 --- a/nvx-rs/src/host/snapshot/canonical.rs +++ b/nvx-rs/src/host/snapshot/canonical.rs @@ -1,5 +1,5 @@ -//! The canonical encoding of a template's compatibility key and of a capture class: the bytes -//! whose SHA-256 digest is their identity. +//! The canonical encoding of a snapshot key and of a capture class. The key's compatibility +//! prefix, and the complete class encoding, are the bytes whose SHA-256 digest is their identity. //! //! Every value has exactly one encoding, and the reader refuses any other: //! diff --git a/nvx-rs/src/host/snapshot/capture.rs b/nvx-rs/src/host/snapshot/capture.rs index 9415981c7..6d2132cc7 100644 --- a/nvx-rs/src/host/snapshot/capture.rs +++ b/nvx-rs/src/host/snapshot/capture.rs @@ -9,7 +9,7 @@ use serde_json::json; use super::{ CaptureContext, DESCRIPTION_FILE, KEY_FILE, MANIFEST_NAME, OPENVMM_DIR, SNAPSHOT_ARTIFACTS, - SnapshotKey, SnapshotManifest, + SnapshotKey, SnapshotManifest, check_openvmm, }; use crate::host::platform; use crate::host::process::Exited; @@ -61,7 +61,7 @@ pub fn finish_capture( ) -> Result { VmDir::open(dir.path())?; check_exit(&dir.path().join(OUTCOME))?; - let manifest = check_snapshot(&dir.path().join(OPENVMM_DIR))?; + let manifest = check_openvmm(&dir.path().join(OPENVMM_DIR))?; let key = SnapshotKey::from_capture(context, &manifest)?; let time = manifest.time(); let description = json!({ @@ -149,7 +149,7 @@ pub(in crate::host) fn check_exit(path: &Path) -> Result<()> { Ok(()) } -/// Checks the private OpenVMM directory, strict manifest and exact regular private artifacts. +/// Preserves the legacy capture's directory, strict manifest, regular artifact and length checks. pub(in crate::host) fn check_snapshot(snapshot: &Path) -> Result { let what = "the snapshot of the capture"; let problem = platform::check_snapshot_dir(snapshot).map_err(|error| { diff --git a/nvx-rs/src/host/snapshot/capture/tests.rs b/nvx-rs/src/host/snapshot/capture/tests.rs index 732b147ba..93b534f66 100644 --- a/nvx-rs/src/host/snapshot/capture/tests.rs +++ b/nvx-rs/src/host/snapshot/capture/tests.rs @@ -165,6 +165,7 @@ fn finish_writes_matching_metadata_and_only_its_three_entries_are_published() { let key = SnapshotKey::from_bytes(&fs::read(fixture.dir.path().join(KEY_FILE)).unwrap()).unwrap(); assert_eq!(key, captured.key); + assert_eq!(key.time(), manifest.time()); assert_eq!(key.runtime(), &fixture.context.runtime); assert_eq!(key.invariants(), &fixture.context.capture.invariants); key.check_manifest(&manifest).unwrap(); diff --git a/nvx-rs/src/host/snapshot/key.rs b/nvx-rs/src/host/snapshot/key.rs index 9a62aa901..c4b5b9b4e 100644 --- a/nvx-rs/src/host/snapshot/key.rs +++ b/nvx-rs/src/host/snapshot/key.rs @@ -10,16 +10,17 @@ use super::CaptureContext; use super::canonical::{CanonicalError, Reader, Writer}; use super::manifest::{ AddressSpace, BlockGeometry, CPUID_ENTRY_LEN, CpuProfile, Device, DeviceRange, - FILESYSTEM_SLOT_VERSION, Interrupts, Ipv6Identity, MAX_COMMAND_LINE_LEN, MAX_CPUID_ENTRIES, - MAX_DEVICE_RANGES, MAX_DEVICE_WORDS, MAX_DEVICES, MAX_MEMORY_RANGES, MAX_PROCESSORS, - MAX_TEXT_LEN, Machine, Memory, MemoryRange, Nic, PROCESSOR_COUNTS, PolicyIdentity, Rates, - SnapshotManifest, Topology, parse_profile_id, + FILESYSTEM_SLOT_VERSION, HostClock, Interrupts, Ipv6Identity, MAX_COMMAND_LINE_LEN, + MAX_CPUID_ENTRIES, MAX_DEVICE_RANGES, MAX_DEVICE_WORDS, MAX_DEVICES, MAX_MEMORY_RANGES, + MAX_PROCESSORS, MAX_TEXT_LEN, Machine, Memory, MemoryRange, Nic, PROCESSOR_COUNTS, + PolicyIdentity, Rates, SnapshotManifest, TimeContract, Topology, parse_profile_id, }; use crate::activation; use crate::contract; use crate::error::{Error, Result}; use crate::host::config::{DiskCapture, check_setting}; use crate::host::platform::parse_hex32; +use crate::host::process::BootId; use crate::host::sandbox::SandboxNetwork; use crate::host::sandbox::mount::MOUNT_TAG; use crate::host::store::{ArtifactIdentity, MAX_IDENTITY_LEN, RuntimeIdentity}; @@ -204,8 +205,9 @@ macro_rules! identity { } identity!( - /// The identity of a [`SnapshotKey`]: the SHA-256 digest of its canonical encoding, after the - /// domain `nvx snapshot key` and a NUL, so that it names a key of one encoding version alone. + /// The compatibility identity of a [`SnapshotKey`]: the SHA-256 digest of its canonical + /// compatibility prefix, after the domain `nvx snapshot key` and a NUL. The persisted + /// capture-time anchors do not contribute to it. /// /// Its text form, which `Display` writes and `FromStr` parses, is 64 lowercase hexadecimal /// digits; parsing also accepts uppercase ones. It converts to and from the @@ -302,9 +304,9 @@ fn guest_network_of(nic: &Nic) -> String { /// /// A capture whose manifest matches its class makes a template whose [`SnapshotKey`] combines /// them: [`SnapshotKey::new`] checks that the machine of the manifest is the class's, and takes -/// the rest of the key from it. Captures of one class make templates of one key, unless something -/// that the class does not fix changed between them, such as the host's CPU, which its effective -/// CPUID and its rates follow. +/// the rest of the key from it. Captures of one class have one compatibility identity unless +/// something that the class does not fix changes, such as the host's CPU, which its effective +/// CPUID and its rates follow. Each key also retains its own capture's time contract. /// /// Its [`identity`](Self::identity) names the class: the SHA-256 digest of its canonical encoding, /// which holds every field, and the versions of the contracts that the key holds. @@ -473,12 +475,12 @@ impl CaptureClass { /// the template replaces with its clone's own; and the template's [`Invariants`], its other /// invariant isolation and resource settings. /// -/// A capture's own metadata is not part of it, so the generations of one key differ in it: when -/// the capture was taken, its time contract's anchor, host, boot, and lineage counter, and the -/// lengths of its artifacts. [`SnapshotManifest`] keeps them. Nothing of a clone is part of it -/// either: its image, upper, export, network policy and settings, identity, and activation -/// targets. So a template with a NIC serves the clones of every network policy at its guest's -/// address. +/// The complete capture [`TimeContract`] is retained and encoded with the key, including its +/// anchor, host, boot and lineage counter, but those per-capture fields are not part of the +/// compatibility identity. The capture's creation time and artifact lengths remain in +/// [`SnapshotManifest`]. Nothing of a clone is part of the key: its image, upper, export, network +/// policy and settings, identity, or activation targets. So a template with a NIC serves the +/// clones of every network policy at its guest's address. /// /// The key comes from metadata alone: the artifacts' identities that their install metadata /// records, the class's inputs, and the capture's manifest. It never reads or digests a binary, @@ -486,7 +488,8 @@ impl CaptureClass { /// makes templates of a new key: the templates of the old one are not recertified. /// /// Its [`identity`](Self::identity) is the SHA-256 digest of its -/// [canonical encoding](Self::to_bytes), after a domain that no other digest of this crate shares. +/// canonical compatibility prefix, after a domain that no other digest of this crate shares. +/// [`to_bytes`](Self::to_bytes) appends the per-capture time fields to that prefix. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SnapshotKey { /// The runtime: OpenVMM, the guest kernel and initramfs, and the boot and control contracts. @@ -499,6 +502,8 @@ pub struct SnapshotKey { filesystem_tag: String, /// The invariant settings beyond the machine. invariants: Invariants, + /// The complete time contract of this particular capture. + time: TimeContract, } pub(in crate::host) use SnapshotKey as TemplateKey; @@ -532,8 +537,8 @@ struct CaptureInputs<'a> { impl SnapshotKey { /// Version of the canonical encoding of a key, its first two bytes. /// - /// Version 1 omits the guest build and holds the snapshot's NIC, if it has one, with the - /// identity of the complete + /// Version 1 omits the guest build, appends the complete capture-time contract, and holds + /// the snapshot's NIC, if it has one, with the identity of the complete /// deny-all policy under which the capture attached it, but no network policy of its clones. /// It has not been released, so a template store that an earlier build created is not /// supported: delete it and create a new one. There is no migration. @@ -705,12 +710,18 @@ impl SnapshotKey { machine: machine.clone(), filesystem_tag: MOUNT_TAG.to_owned(), invariants: class.invariants.clone(), + time: manifest.time().clone(), }) } - /// Returns the identity of the key. + /// Returns the compatibility identity, excluding this capture's time anchors and identities. + /// + /// The rates and time ABI remain compatibility inputs. Different capture times may have + /// different [`to_bytes`](Self::to_bytes) with the same identity. pub fn identity(&self) -> KeyIdentity { - KeyIdentity(digest(KEY_DOMAIN, &self.to_bytes())) + let mut writer = Writer::default(); + self.write_compatibility(&mut writer); + KeyIdentity(digest(KEY_DOMAIN, &writer.finish())) } /// Returns the runtime: the identities of OpenVMM, the guest kernel, and the guest initramfs, @@ -728,6 +739,7 @@ impl SnapshotKey { /// Checks the manifest's complete machine, the current key contracts and the filesystem tag. pub(super) fn check_manifest(&self, manifest: &SnapshotManifest) -> Result<()> { if self.machine != manifest.machine + || &self.time != manifest.time() || !self.speaks_current_contracts() || self.filesystem_tag != MOUNT_TAG { @@ -743,6 +755,14 @@ impl SnapshotKey { self.machine.hypervisor } + /// Returns this snapshot's complete capture-time contract, as recorded by its manifest. + /// + /// Its capture anchors, host, boot and lineage are persisted, but do not change the + /// compatibility [`identity`](Self::identity). + pub fn time(&self) -> &TimeContract { + &self.time + } + /// Returns the ID of the CPU profile, `VENDOR.GENERATION.vREVISION`. pub fn cpu_profile(&self) -> String { self.machine.cpu.id() @@ -823,8 +843,10 @@ impl SnapshotKey { &self.invariants } - /// Returns the canonical encoding of the key, which its [`identity`](Self::identity) digests, - /// and [`from_bytes`](Self::from_bytes) reads back. + /// Returns the canonical persisted encoding, which [`from_bytes`](Self::from_bytes) reads. + /// + /// [`identity`](Self::identity) digests the compatibility prefix, items 1 through 8 below, + /// excluding item 9's per-capture time fields. /// /// Version 1 holds, in order, each value encoded as the [module](super) describes: /// @@ -856,18 +878,28 @@ impl SnapshotKey { /// the gateway's IPv4 address, 4 bytes, the optional IPv6 identity, with the guest's /// address, 16 bytes, its prefix length, a `u8`, and the gateway's address, 16 bytes, and /// the network policy's mode, a text, digest, 32 bytes, whether it is required, a boolean, - /// and encoding version, a `u32`; and the invariant settings, a list of names and values, - /// each a text, in the order of their names. + /// and encoding version, a `u32`. + /// 8. The invariant settings, a list of names and values, each a text, in name order. + /// 9. The capture-specific time contract: the capture TSC, UTC and monotonic anchors, + /// `u64`s; the host clock, a text; the host and boot identities, 16 bytes each; the lineage, + /// a `u32`; and the optional cut-to-anchor interval, a `u64`. The complete contract's rates + /// are already encoded in item 3. pub fn to_bytes(&self) -> Vec { let mut writer = Writer::default(); - writer.u16(Self::VERSION); - write_runtime(&mut writer, &self.runtime); - self.contracts.write(&mut writer); - write_machine(&mut writer, &self.machine, &self.filesystem_tag); - self.invariants.write(&mut writer); + self.write_compatibility(&mut writer); + write_time(&mut writer, &self.time); writer.finish() } + /// Writes exactly the compatibility prefix that the key's identity digests. + fn write_compatibility(&self, writer: &mut Writer) { + writer.u16(Self::VERSION); + write_runtime(writer, &self.runtime); + self.contracts.write(writer); + write_machine(writer, &self.machine, &self.filesystem_tag); + self.invariants.write(writer); + } + /// Reads a key from its canonical encoding, as [`to_bytes`](Self::to_bytes) writes it. /// /// The bytes must be the key's one encoding, of version [`VERSION`](Self::VERSION), with no @@ -903,12 +935,14 @@ impl SnapshotKey { let contracts = Contracts::read(reader)?; let (machine, filesystem_tag) = read_machine(reader)?; let invariants = Invariants::read(reader)?; + let time = read_time(reader, machine.rates)?; Ok(Self { runtime, contracts, machine, filesystem_tag, invariants, + time, }) }; let key = read(&mut reader).map_err(malformed)?; @@ -917,6 +951,46 @@ impl SnapshotKey { } } +/// Appends the capture-specific fields; the complete contract's rates are in the machine prefix. +fn write_time(writer: &mut Writer, time: &TimeContract) { + writer.u64(time.capture_tsc); + writer.u64(time.capture_utc_ns); + writer.u64(time.capture_monotonic_ns); + writer.text(time.host_clock.as_str()); + writer.fixed(&time.host_id); + writer.fixed(time.boot_id.as_bytes()); + writer.u32(time.lineage); + writer.option(time.cut_to_anchor_ns.as_ref(), |writer, nanos| { + writer.u64(*nanos) + }); +} + +/// Reads the full capture contract using the compatibility prefix's already-checked rates. +fn read_time(reader: &mut Reader<'_>, rates: Rates) -> Result { + let capture_tsc = reader.u64("the capture TSC")?; + let capture_utc_ns = reader.u64("the capture UTC anchor")?; + let capture_monotonic_ns = reader.u64("the capture monotonic anchor")?; + let clock = reader.text(MAX_TEXT_LEN, "the capture host clock")?; + let host_clock = HostClock::parse(clock).ok_or_else(|| { + CanonicalError::new(format!("the capture host clock {clock:?} is unknown")) + })?; + Ok(TimeContract { + tsc_frequency_hz: rates.tsc_hz, + tsc_tolerance_ppm: rates.tolerance_ppm, + apic_frequency_hz: rates.apic_hz, + capture_tsc, + capture_utc_ns, + capture_monotonic_ns, + host_clock, + host_id: reader.fixed("the capturing host identity")?, + boot_id: BootId::from_bytes(reader.fixed("the capture boot identity")?), + lineage: reader.u32("the capture lineage")?, + cut_to_anchor_ns: reader.option("the cut-to-anchor interval", |reader| { + reader.u64("the cut-to-anchor interval") + })?, + }) +} + /// Writes the machine `machine`, whose filesystem slot has the tag `tag`, as /// [`SnapshotKey::to_bytes`] describes it. fn write_machine(writer: &mut Writer, machine: &Machine, tag: &str) { diff --git a/nvx-rs/src/host/snapshot/key/tests.rs b/nvx-rs/src/host/snapshot/key/tests.rs index 0ee47de27..99c639dcf 100644 --- a/nvx-rs/src/host/snapshot/key/tests.rs +++ b/nvx-rs/src/host/snapshot/key/tests.rs @@ -13,6 +13,21 @@ fn manifest(spec: &ManifestSpec) -> SnapshotManifest { SnapshotManifest::parse(&spec.encode()).unwrap() } +/// The specified suffix length, independently of the key writer, for locating the identity prefix. +fn capture_time_len(time: &TimeContract) -> usize { + 3 * 8 + + 4 + + time.host_clock.as_str().len() + + 2 * 16 + + 4 + + 1 + + if time.cut_to_anchor_ns.is_some() { + 8 + } else { + 0 + } +} + /// Returns the key of the template that a capture of `class` made, whose manifest `spec` /// encodes. fn key(class: &CaptureClass, spec: &ManifestSpec) -> SnapshotKey { @@ -349,6 +364,50 @@ fn the_guest_build_is_not_part_of_a_snapshot_key() { ); } +#[test] +fn complete_capture_time_round_trips_without_changing_compatibility_identity() { + let base = network_key(); + for cut in [None, Some(0), Some(u64::MAX)] { + let mut spec = ManifestSpec::with_network(); + let time = spec.time(); + time.capture_tsc = u64::MAX; + time.capture_utc_ns = u64::MAX - 1; + time.capture_monotonic_ns = u64::MAX - 2; + time.host_clock = "windows-interrupt-time".to_owned(); + time.host_id = vec![0x31; 16]; + time.boot_id = vec![0x72; 16]; + time.lineage = u32::MAX; + time.cut_to_anchor_ns = cut; + let manifest = manifest(&spec); + let key = SnapshotKey::new(&network_class(), &manifest).unwrap(); + assert_eq!(key.time(), manifest.time()); + assert_ne!(key.to_bytes(), base.to_bytes()); + assert_eq!(key.identity(), base.identity()); + let decoded = SnapshotKey::from_bytes(&key.to_bytes()).unwrap(); + assert_eq!(decoded, key); + assert_eq!(decoded.time(), manifest.time()); + } +} + +#[test] +fn canonical_keys_require_capture_time_and_a_known_host_clock() { + let key = network_key(); + let mut prefix = Writer::default(); + key.write_compatibility(&mut prefix); + let prefix = prefix.finish(); + assert_eq!( + SnapshotKey::from_bytes(&prefix).unwrap_err().kind(), + crate::ErrorKind::Io + ); + let mut bytes = key.to_bytes(); + // Three anchors and the clock text's length precede its bytes. + bytes[prefix.len() + 3 * 8 + 4] = b'?'; + let error = SnapshotKey::from_bytes(&bytes).unwrap_err(); + assert_eq!(error.kind(), crate::ErrorKind::Io); + assert!(error.message().contains("host clock"), "{error}"); + assert!(error.message().contains("unknown"), "{error}"); +} + #[test] fn a_key_holds_what_its_capture_made() { let made = network_key(); @@ -670,7 +729,7 @@ fn keys_read_back_from_their_canonical_encoding() { assert_eq!(read.to_bytes(), bytes); let mut digest = Sha256::new(); digest.update(b"nvx snapshot key\0"); - digest.update(&bytes); + digest.update(&bytes[..bytes.len() - capture_time_len(made.time())]); assert_eq!( made.identity().as_bytes(), &<[u8; 32]>::from(digest.finalize()) @@ -849,12 +908,13 @@ fn keys_of_machines_that_no_manifest_describes_are_refused() { .unwrap() .with("b", "2") .unwrap(); - let mut bytes = key(&class, &ManifestSpec::with_network()).to_bytes(); - let len = bytes.len(); + let made = key(&class, &ManifestSpec::with_network()); + let mut bytes = made.to_bytes(); + let end = bytes.len() - capture_time_len(made.time()); let tail = [ 1, 0, 0, 0, b'b', 1, 0, 0, 0, b'2', 1, 0, 0, 0, b'a', 1, 0, 0, 0, b'1', ]; - bytes[len - tail.len()..].copy_from_slice(&tail); + bytes[end - tail.len()..end].copy_from_slice(&tail); assert_eq!( key_refusal(&bytes, ErrorCode::Io), "the snapshot key is malformed: the invariant settings are not in the strict order of \ diff --git a/nvx-rs/src/host/snapshot/manifest.rs b/nvx-rs/src/host/snapshot/manifest.rs index d6f45bafc..0249212ff 100644 --- a/nvx-rs/src/host/snapshot/manifest.rs +++ b/nvx-rs/src/host/snapshot/manifest.rs @@ -146,9 +146,9 @@ impl HostClock { } } -/// The time contract of a capture: the rates that it declares, which its template's key holds, -/// and the anchor at which it was captured, with the identities of the host and the boot that -/// captured it, which the generations of one key differ in. +/// The complete time contract of a capture, retained by its manifest and snapshot key. Its +/// rates contribute to the key's compatibility identity; its capture anchor, host, boot and +/// lineage do not, so captures with different anchors can share that identity. /// /// A restore uses the anchor to measure how long the capture has been down: on the same host /// boot, by the host's monotonic clock, and otherwise by UTC. diff --git a/nvx-rs/src/host/snapshot/tests.rs b/nvx-rs/src/host/snapshot/tests.rs index 7fb45bb31..23b8352f3 100644 --- a/nvx-rs/src/host/snapshot/tests.rs +++ b/nvx-rs/src/host/snapshot/tests.rs @@ -1215,6 +1215,33 @@ fn snapshot_description_contents_do_not_affect_opening() { } } +#[test] +fn snapshot_open_refuses_a_compatible_key_with_another_capture_time() { + use crate::host::store::files::write_private; + + let mutations: &[fn(&mut ManifestSpec)] = &[ + |spec| spec.time().capture_tsc += 1, + |spec| spec.time().capture_utc_ns += 1, + |spec| spec.time().capture_monotonic_ns += 1, + |spec| spec.time().host_clock = "windows-interrupt-time".to_owned(), + |spec| spec.time().host_id[0] ^= 1, + |spec| spec.time().boot_id[0] ^= 1, + |spec| spec.time().lineage += 1, + |spec| spec.time().cut_to_anchor_ns = None, + |spec| spec.time().cut_to_anchor_ns = Some(0), + ]; + for mutate in mutations { + let fixture = SnapshotFixture::new(); + let mut spec = fixture.spec.clone(); + mutate(&mut spec); + let manifest = SnapshotManifest::parse(&spec.encode()).unwrap(); + let key = SnapshotKey::new(&class(), &manifest).unwrap(); + assert_eq!(key.identity(), fixture.key.identity()); + write_private(&fixture.dir.join(KEY_FILE), &key.to_bytes()).unwrap(); + refused(&fixture.dir); + } +} + #[cfg(target_os = "linux")] #[test] fn snapshot_open_refuses_every_shared_file_and_both_shared_directories() { diff --git a/nvx-rs/tests/vectors/README.md b/nvx-rs/tests/vectors/README.md index 23aec531a..9aaefe167 100644 --- a/nvx-rs/tests/vectors/README.md +++ b/nvx-rs/tests/vectors/README.md @@ -73,8 +73,10 @@ the key that the vector's bytes decode to. of a class have not been released, so a template store that an earlier build created is not supported, and is deleted and created again, since nothing migrates it. The snapshot key also drops the obsolete guest-build field and - uses the `nvx snapshot key` hash domain in place: no snapshot outlives a - deployment, and the prior key encoding is not supported. + uses the `nvx snapshot key` hash domain in place, and now appends the full + capture-time contract while its compatibility identity still digests only + the preceding compatibility fields: no snapshot outlives a deployment, + and the prior key encoding is not supported. - The bytes come from the protocol's definition, never from the code that they check, and so do the values of each vector. diff --git a/nvx-rs/tests/vectors/snapshot-key-v1.txt b/nvx-rs/tests/vectors/snapshot-key-v1.txt index 99ad0a9af..46fef1809 100644 --- a/nvx-rs/tests/vectors/snapshot-key-v1.txt +++ b/nvx-rs/tests/vectors/snapshot-key-v1.txt @@ -4,9 +4,9 @@ # rules that the vectors follow. # # KEY|NAME|BYTES|IDENTITY -# The canonical encoding of a key, and its identity: the SHA-256 digest of "nvx snapshot key", a -# NUL byte, and the encoding. A decoder must decode the bytes, and an encoder must encode the key -# that they hold to the same bytes. +# The canonical persisted key, and its compatibility identity: the SHA-256 digest of +# "nvx snapshot key", a NUL byte, and the compatibility prefix before the capture-time fields. +# A decoder must decode the full key, and an encoder must reproduce its bytes. # CLASS|NAME|BYTES|IDENTITY # The canonical encoding of a class, and its identity: the SHA-256 digest of "nvx capture class", # a NUL byte, and the encoding. @@ -25,20 +25,24 @@ # nvx-microvm-v2-control-v2, control session 1, snapshot format 6, device-state schema 1, time # ABI 1, restore packet 4, and activation contract 1. # Capture classes alone also hold the guest build guest-build-1. +# TEMPLATE, NETWORK and HOST_PROFILE append capture TSC 12345678, UTC 1700000000000000000 ns, +# monotonic 123000000000 ns, clock linux-boottime, host and boot IDs of 16 bytes of 01 and 02, +# lineage 3 and cut-to-anchor Some(200). MINIMAL uses zero anchors, zero host/boot IDs, lineage 0 +# and no cut-to-anchor interval on linux-boottime. Their rates remain in the compatibility prefix. # The key of the template of a KVM microVM of 2 processors, all online at boot, and 512 MiB # of RAM, of a capacity of 2 GiB, with a distro of 1 GiB, a scratch of 64 MiB, the dormant # filesystem slot, a console, a control console, and no NIC, on the CPU profile # intel.icelake-sp.v1, whose digest is 32 bytes of 5a and whose effective CPUID is the bytes # 00 to 57: the capture of tests/fixtures/template/platform-kvm. -KEY|TEMPLATE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d05000000696e74656c0a0000006963656c616b652d7370010000005a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a58000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253545556570100000001000000020000000100000002000000000000000100000000752b7d0000000000c2eb0b00000000fa000000020000000000000000000020000000000000008000000000000000080000000001000000000000000000000000000020000000000100000000000020000000000000006000000000530100006561726c79636f6e3d78653920636f6e736f6c653d68766331207265626f6f743d742070616e69633d2d31206e76785f73616e64626f783d31206e76785f636f6e6669673d307864303031303030302c3635353336206e725f637075733d32206e76785f736e617073686f745f746965723d706c6174666f726d2076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030313030303a362076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030323030303a372076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030333030303a342076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030363030303a31312076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030373030303a33206e76785f636f6e74726f6c5f7474793d6876633217f80f8858431ca75fcc7ca694ea68637e017ede02db15e18d0ed4581067858d000000400000000000020000001000000000000400000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000001000000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000003000000767030040000007663707509000000706172746974696f6e00000000000000000000000000000000000000000006000000766d74696d6505000000636c6f636b06000000766d74696d65000000000000000000000000000000000000000000030000007069630300000070696303000000706963000000000200000004000000706d696f2000000000000000020000000000000004000000706d696fa00000000000000002000000000000000000000000000000000000000006000000696f6170696306000000696f6170696306000000696f617069630000000001000000040000006d6d696f0000c0fe00000000001000000000000000000000000000000000000000050000006c61706963050000006c6170696309000000706172746974696f6e0000000001000000040000006d6d696f0000e0fe00000000001000000000000000000000000000000000000000030000007069740300000070697403000000706974000000000100000004000000706d696f400000000000000004000000000000000100000000000000000000000000000000030000007274630300000072746303000000727463000000000100000004000000706d696f7000000000000000020000000000000001080000000000000000000000000000000d0000006d6963726f766d2d706f72746205000000706f7274620d0000006d6963726f766d2d706f727462000000000100000004000000706d696fe900000000000000020000000000000000000000000000000000000000100000006d6963726f766d2d73687574646f776e0800000073687574646f776e100000006d6963726f766d2d73687574646f776e000000000100000004000000706d696f0406000000000000010000000000000000000000000000000000000000180000006d6963726f766d2d736e617073686f742d7265717565737410000000736e617073686f742d72657175657374180000006d6963726f766d2d736e617073686f742d72657175657374000000000100000004000000706d696f05060000000000000100000000000000000000000000000000000000000b00000066733a6d6963726f766d300900000076697274696f2d66731300000076697274696f66732d333438393636353032340b00000076697274696f2d6d6d696f01000000040000006d6d696f001000d000000000001000000000000001060000000200000000000030030000000200000002000000000100000001000017000000636f6e736f6c653a6d6963726f766d2d76697274696f300e00000076697274696f2d636f6e736f6c651900000076697274696f2d636f6e736f6c652d333438393636393132300b00000076697274696f2d6d6d696f01000000040000006d6d696f002000d000000000001000000000000001070000000200000001000030030000000200000002000000000100000001000012000000626c6b3a73616e64626f783a64697374726f0a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393637333231360b00000076697274696f2d6d6d696f01000000040000006d6d696f003000d0000000000010000000000000010400000002000000640600300300000001000000010000000001000013000000626c6b3a73616e64626f783a736372617463680a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393638353530340b00000076697274696f2d6d6d696f01000000040000006d6d696f006000d0000000000010000000000000010b00000002000000440600300300000001000000010000000001000018000000636f6e736f6c653a6d6963726f766d2d636f6e74726f6c301600000076697274696f2d636f6e74726f6c2d636f6e736f6c652100000076697274696f2d636f6e74726f6c2d636f6e736f6c652d333438393638393630300b00000076697274696f2d6d6d696f01000000040000006d6d696f007000d000000000001000000000000001030000000200000001000030030000000200000002000000000100000001000001000000070000006d6963726f766d0000000000|dc593980266a3bc96c177fbfb8fea08e958dc54609cdb07c988a33992d09aa32 +KEY|TEMPLATE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d05000000696e74656c0a0000006963656c616b652d7370010000005a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a58000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253545556570100000001000000020000000100000002000000000000000100000000752b7d0000000000c2eb0b00000000fa000000020000000000000000000020000000000000008000000000000000080000000001000000000000000000000000000020000000000100000000000020000000000000006000000000530100006561726c79636f6e3d78653920636f6e736f6c653d68766331207265626f6f743d742070616e69633d2d31206e76785f73616e64626f783d31206e76785f636f6e6669673d307864303031303030302c3635353336206e725f637075733d32206e76785f736e617073686f745f746965723d706c6174666f726d2076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030313030303a362076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030323030303a372076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030333030303a342076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030363030303a31312076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030373030303a33206e76785f636f6e74726f6c5f7474793d6876633217f80f8858431ca75fcc7ca694ea68637e017ede02db15e18d0ed4581067858d000000400000000000020000001000000000000400000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000001000000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000003000000767030040000007663707509000000706172746974696f6e00000000000000000000000000000000000000000006000000766d74696d6505000000636c6f636b06000000766d74696d65000000000000000000000000000000000000000000030000007069630300000070696303000000706963000000000200000004000000706d696f2000000000000000020000000000000004000000706d696fa00000000000000002000000000000000000000000000000000000000006000000696f6170696306000000696f6170696306000000696f617069630000000001000000040000006d6d696f0000c0fe00000000001000000000000000000000000000000000000000050000006c61706963050000006c6170696309000000706172746974696f6e0000000001000000040000006d6d696f0000e0fe00000000001000000000000000000000000000000000000000030000007069740300000070697403000000706974000000000100000004000000706d696f400000000000000004000000000000000100000000000000000000000000000000030000007274630300000072746303000000727463000000000100000004000000706d696f7000000000000000020000000000000001080000000000000000000000000000000d0000006d6963726f766d2d706f72746205000000706f7274620d0000006d6963726f766d2d706f727462000000000100000004000000706d696fe900000000000000020000000000000000000000000000000000000000100000006d6963726f766d2d73687574646f776e0800000073687574646f776e100000006d6963726f766d2d73687574646f776e000000000100000004000000706d696f0406000000000000010000000000000000000000000000000000000000180000006d6963726f766d2d736e617073686f742d7265717565737410000000736e617073686f742d72657175657374180000006d6963726f766d2d736e617073686f742d72657175657374000000000100000004000000706d696f05060000000000000100000000000000000000000000000000000000000b00000066733a6d6963726f766d300900000076697274696f2d66731300000076697274696f66732d333438393636353032340b00000076697274696f2d6d6d696f01000000040000006d6d696f001000d000000000001000000000000001060000000200000000000030030000000200000002000000000100000001000017000000636f6e736f6c653a6d6963726f766d2d76697274696f300e00000076697274696f2d636f6e736f6c651900000076697274696f2d636f6e736f6c652d333438393636393132300b00000076697274696f2d6d6d696f01000000040000006d6d696f002000d000000000001000000000000001070000000200000001000030030000000200000002000000000100000001000012000000626c6b3a73616e64626f783a64697374726f0a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393637333231360b00000076697274696f2d6d6d696f01000000040000006d6d696f003000d0000000000010000000000000010400000002000000640600300300000001000000010000000001000013000000626c6b3a73616e64626f783a736372617463680a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393638353530340b00000076697274696f2d6d6d696f01000000040000006d6d696f006000d0000000000010000000000000010b00000002000000440600300300000001000000010000000001000018000000636f6e736f6c653a6d6963726f766d2d636f6e74726f6c301600000076697274696f2d636f6e74726f6c2d636f6e736f6c652100000076697274696f2d636f6e74726f6c2d636f6e736f6c652d333438393638393630300b00000076697274696f2d6d6d696f01000000040000006d6d696f007000d000000000001000000000000001030000000200000001000030030000000200000002000000000100000001000001000000070000006d6963726f766d00000000004e61bc000000000000002a36fe9c9717000e5fa31c0000000e0000006c696e75782d626f6f7474696d6501010101010101010101010101010101020202020202020202020202020202020300000001c800000000000000|dc593980266a3bc96c177fbfb8fea08e958dc54609cdb07c988a33992d09aa32 # The same with a fixed NIC at 10.0.0.2/24, with its IPv6 identity, under an allow-list # policy whose digest is 32 bytes of 07, and the invariant settings isolation.seccomp=strict # and limits.pids=512. -KEY|NETWORK|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d05000000696e74656c0a0000006963656c616b652d7370010000005a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a58000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253545556570100000001000000020000000100000002000000000000000100000000752b7d0000000000c2eb0b00000000fa000000020000000000000000000020000000000000008000000000000000080000000001000000000000000000000000000020000000000100000000000020000000000000006000000000f20100006561726c79636f6e3d78653920636f6e736f6c653d68766331207265626f6f743d742070616e69633d2d31206e76785f73616e64626f783d31206e76785f636f6e6669673d307864303031303030302c3635353336206e725f637075733d32206e76785f736e617073686f745f746965723d706c6174666f726d2076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030303030303a31302076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030313030303a362076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030323030303a372076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030333030303a342076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030363030303a31312076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030373030303a33206e76785f636f6e74726f6c5f7474793d6876633220766972746e65745f69703d31302e302e302e3220766972746e65745f6d61736b3d3235352e3235352e3235352e3020766972746e65745f67773d31302e302e302e3120766972746e65745f6970363d666430303a3a6130303a322f31323020766972746e65745f6777363d666430303a3a6130303a315e7d49c200c9d60e74547b549917b665501635035894c15c2d7780210f95caf0000000400000000000020000001000000000000400000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000001100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000003000000767030040000007663707509000000706172746974696f6e00000000000000000000000000000000000000000006000000766d74696d6505000000636c6f636b06000000766d74696d65000000000000000000000000000000000000000000030000007069630300000070696303000000706963000000000200000004000000706d696f2000000000000000020000000000000004000000706d696fa00000000000000002000000000000000000000000000000000000000006000000696f6170696306000000696f6170696306000000696f617069630000000001000000040000006d6d696f0000c0fe00000000001000000000000000000000000000000000000000050000006c61706963050000006c6170696309000000706172746974696f6e0000000001000000040000006d6d696f0000e0fe00000000001000000000000000000000000000000000000000030000007069740300000070697403000000706974000000000100000004000000706d696f400000000000000004000000000000000100000000000000000000000000000000030000007274630300000072746303000000727463000000000100000004000000706d696f7000000000000000020000000000000001080000000000000000000000000000000d0000006d6963726f766d2d706f72746205000000706f7274620d0000006d6963726f766d2d706f727462000000000100000004000000706d696fe900000000000000020000000000000000000000000000000000000000100000006d6963726f766d2d73687574646f776e0800000073687574646f776e100000006d6963726f766d2d73687574646f776e000000000100000004000000706d696f0406000000000000010000000000000000000000000000000000000000180000006d6963726f766d2d736e617073686f742d7265717565737410000000736e617073686f742d72657175657374180000006d6963726f766d2d736e617073686f742d72657175657374000000000100000004000000706d696f05060000000000000100000000000000000000000000000000000000000c0000006e65743a6d6963726f766d300a00000076697274696f2d6e65741500000076697274696f2d6e65742d333438393636303932380b00000076697274696f2d6d6d696f01000000040000006d6d696f000000d0000000000010000000000000010a000000020000002000000001000000020000000200000000010000000100000b00000066733a6d6963726f766d300900000076697274696f2d66731300000076697274696f66732d333438393636353032340b00000076697274696f2d6d6d696f01000000040000006d6d696f001000d000000000001000000000000001060000000200000000000030030000000200000002000000000100000001000017000000636f6e736f6c653a6d6963726f766d2d76697274696f300e00000076697274696f2d636f6e736f6c651900000076697274696f2d636f6e736f6c652d333438393636393132300b00000076697274696f2d6d6d696f01000000040000006d6d696f002000d000000000001000000000000001070000000200000001000030030000000200000002000000000100000001000012000000626c6b3a73616e64626f783a64697374726f0a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393637333231360b00000076697274696f2d6d6d696f01000000040000006d6d696f003000d0000000000010000000000000010400000002000000640600300300000001000000010000000001000013000000626c6b3a73616e64626f783a736372617463680a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393638353530340b00000076697274696f2d6d6d696f01000000040000006d6d696f006000d0000000000010000000000000010b00000002000000440600300300000001000000010000000001000018000000636f6e736f6c653a6d6963726f766d2d636f6e74726f6c301600000076697274696f2d636f6e74726f6c2d636f6e736f6c652100000076697274696f2d636f6e74726f6c2d636f6e736f6c652d333438393638393630300b00000076697274696f2d6d6d696f01000000040000006d6d696f007000d000000000001000000000000001030000000200000001000030030000000200000002000000000100000001000001000000070000006d6963726f766d0108000000706f727461626c655254000000025254000000010a000002180a00000101fd00000000000000000000000a00000278fd00000000000000000000000a0000010a000000616c6c6f772d6c69737407070707070707070707070707070707070707070707070707070707070707070104000000020000001100000069736f6c6174696f6e2e736563636f6d70060000007374726963740b0000006c696d6974732e7069647303000000353132|af73b6bf51095c0c2ab84fb114bfa63d3e0dc1fd2b6f56f9a729391e19c762f9 +KEY|NETWORK|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d05000000696e74656c0a0000006963656c616b652d7370010000005a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a58000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253545556570100000001000000020000000100000002000000000000000100000000752b7d0000000000c2eb0b00000000fa000000020000000000000000000020000000000000008000000000000000080000000001000000000000000000000000000020000000000100000000000020000000000000006000000000f20100006561726c79636f6e3d78653920636f6e736f6c653d68766331207265626f6f743d742070616e69633d2d31206e76785f73616e64626f783d31206e76785f636f6e6669673d307864303031303030302c3635353336206e725f637075733d32206e76785f736e617073686f745f746965723d706c6174666f726d2076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030303030303a31302076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030313030303a362076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030323030303a372076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030333030303a342076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030363030303a31312076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030373030303a33206e76785f636f6e74726f6c5f7474793d6876633220766972746e65745f69703d31302e302e302e3220766972746e65745f6d61736b3d3235352e3235352e3235352e3020766972746e65745f67773d31302e302e302e3120766972746e65745f6970363d666430303a3a6130303a322f31323020766972746e65745f6777363d666430303a3a6130303a315e7d49c200c9d60e74547b549917b665501635035894c15c2d7780210f95caf0000000400000000000020000001000000000000400000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000001100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000003000000767030040000007663707509000000706172746974696f6e00000000000000000000000000000000000000000006000000766d74696d6505000000636c6f636b06000000766d74696d65000000000000000000000000000000000000000000030000007069630300000070696303000000706963000000000200000004000000706d696f2000000000000000020000000000000004000000706d696fa00000000000000002000000000000000000000000000000000000000006000000696f6170696306000000696f6170696306000000696f617069630000000001000000040000006d6d696f0000c0fe00000000001000000000000000000000000000000000000000050000006c61706963050000006c6170696309000000706172746974696f6e0000000001000000040000006d6d696f0000e0fe00000000001000000000000000000000000000000000000000030000007069740300000070697403000000706974000000000100000004000000706d696f400000000000000004000000000000000100000000000000000000000000000000030000007274630300000072746303000000727463000000000100000004000000706d696f7000000000000000020000000000000001080000000000000000000000000000000d0000006d6963726f766d2d706f72746205000000706f7274620d0000006d6963726f766d2d706f727462000000000100000004000000706d696fe900000000000000020000000000000000000000000000000000000000100000006d6963726f766d2d73687574646f776e0800000073687574646f776e100000006d6963726f766d2d73687574646f776e000000000100000004000000706d696f0406000000000000010000000000000000000000000000000000000000180000006d6963726f766d2d736e617073686f742d7265717565737410000000736e617073686f742d72657175657374180000006d6963726f766d2d736e617073686f742d72657175657374000000000100000004000000706d696f05060000000000000100000000000000000000000000000000000000000c0000006e65743a6d6963726f766d300a00000076697274696f2d6e65741500000076697274696f2d6e65742d333438393636303932380b00000076697274696f2d6d6d696f01000000040000006d6d696f000000d0000000000010000000000000010a000000020000002000000001000000020000000200000000010000000100000b00000066733a6d6963726f766d300900000076697274696f2d66731300000076697274696f66732d333438393636353032340b00000076697274696f2d6d6d696f01000000040000006d6d696f001000d000000000001000000000000001060000000200000000000030030000000200000002000000000100000001000017000000636f6e736f6c653a6d6963726f766d2d76697274696f300e00000076697274696f2d636f6e736f6c651900000076697274696f2d636f6e736f6c652d333438393636393132300b00000076697274696f2d6d6d696f01000000040000006d6d696f002000d000000000001000000000000001070000000200000001000030030000000200000002000000000100000001000012000000626c6b3a73616e64626f783a64697374726f0a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393637333231360b00000076697274696f2d6d6d696f01000000040000006d6d696f003000d0000000000010000000000000010400000002000000640600300300000001000000010000000001000013000000626c6b3a73616e64626f783a736372617463680a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393638353530340b00000076697274696f2d6d6d696f01000000040000006d6d696f006000d0000000000010000000000000010b00000002000000440600300300000001000000010000000001000018000000636f6e736f6c653a6d6963726f766d2d636f6e74726f6c301600000076697274696f2d636f6e74726f6c2d636f6e736f6c652100000076697274696f2d636f6e74726f6c2d636f6e736f6c652d333438393638393630300b00000076697274696f2d6d6d696f01000000040000006d6d696f007000d000000000001000000000000001030000000200000001000030030000000200000002000000000100000001000001000000070000006d6963726f766d0108000000706f727461626c655254000000025254000000010a000002180a00000101fd00000000000000000000000a00000278fd00000000000000000000000a0000010a000000616c6c6f772d6c69737407070707070707070707070707070707070707070707070707070707070707070104000000020000001100000069736f6c6174696f6e2e736563636f6d70060000007374726963740b0000006c696d6974732e70696473030000003531324e61bc000000000000002a36fe9c9717000e5fa31c0000000e0000006c696e75782d626f6f7474696d6501010101010101010101010101010101020202020202020202020202020202020300000001c800000000000000|af73b6bf51095c0c2ab84fb114bfa63d3e0dc1fd2b6f56f9a729391e19c762f9 # TEMPLATE on the host profile intel.host.v1. -KEY|HOST_PROFILE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d05000000696e74656c04000000686f7374010000005a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a58000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253545556570100000001000000020000000100000002000000000000000100000000752b7d0000000000c2eb0b00000000fa000000020000000000000000000020000000000000008000000000000000080000000001000000000000000000000000000020000000000100000000000020000000000000006000000000530100006561726c79636f6e3d78653920636f6e736f6c653d68766331207265626f6f743d742070616e69633d2d31206e76785f73616e64626f783d31206e76785f636f6e6669673d307864303031303030302c3635353336206e725f637075733d32206e76785f736e617073686f745f746965723d706c6174666f726d2076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030313030303a362076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030323030303a372076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030333030303a342076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030363030303a31312076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030373030303a33206e76785f636f6e74726f6c5f7474793d6876633217f80f8858431ca75fcc7ca694ea68637e017ede02db15e18d0ed4581067858d000000400000000000020000001000000000000400000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000001000000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000003000000767030040000007663707509000000706172746974696f6e00000000000000000000000000000000000000000006000000766d74696d6505000000636c6f636b06000000766d74696d65000000000000000000000000000000000000000000030000007069630300000070696303000000706963000000000200000004000000706d696f2000000000000000020000000000000004000000706d696fa00000000000000002000000000000000000000000000000000000000006000000696f6170696306000000696f6170696306000000696f617069630000000001000000040000006d6d696f0000c0fe00000000001000000000000000000000000000000000000000050000006c61706963050000006c6170696309000000706172746974696f6e0000000001000000040000006d6d696f0000e0fe00000000001000000000000000000000000000000000000000030000007069740300000070697403000000706974000000000100000004000000706d696f400000000000000004000000000000000100000000000000000000000000000000030000007274630300000072746303000000727463000000000100000004000000706d696f7000000000000000020000000000000001080000000000000000000000000000000d0000006d6963726f766d2d706f72746205000000706f7274620d0000006d6963726f766d2d706f727462000000000100000004000000706d696fe900000000000000020000000000000000000000000000000000000000100000006d6963726f766d2d73687574646f776e0800000073687574646f776e100000006d6963726f766d2d73687574646f776e000000000100000004000000706d696f0406000000000000010000000000000000000000000000000000000000180000006d6963726f766d2d736e617073686f742d7265717565737410000000736e617073686f742d72657175657374180000006d6963726f766d2d736e617073686f742d72657175657374000000000100000004000000706d696f05060000000000000100000000000000000000000000000000000000000b00000066733a6d6963726f766d300900000076697274696f2d66731300000076697274696f66732d333438393636353032340b00000076697274696f2d6d6d696f01000000040000006d6d696f001000d000000000001000000000000001060000000200000000000030030000000200000002000000000100000001000017000000636f6e736f6c653a6d6963726f766d2d76697274696f300e00000076697274696f2d636f6e736f6c651900000076697274696f2d636f6e736f6c652d333438393636393132300b00000076697274696f2d6d6d696f01000000040000006d6d696f002000d000000000001000000000000001070000000200000001000030030000000200000002000000000100000001000012000000626c6b3a73616e64626f783a64697374726f0a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393637333231360b00000076697274696f2d6d6d696f01000000040000006d6d696f003000d0000000000010000000000000010400000002000000640600300300000001000000010000000001000013000000626c6b3a73616e64626f783a736372617463680a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393638353530340b00000076697274696f2d6d6d696f01000000040000006d6d696f006000d0000000000010000000000000010b00000002000000440600300300000001000000010000000001000018000000636f6e736f6c653a6d6963726f766d2d636f6e74726f6c301600000076697274696f2d636f6e74726f6c2d636f6e736f6c652100000076697274696f2d636f6e74726f6c2d636f6e736f6c652d333438393638393630300b00000076697274696f2d6d6d696f01000000040000006d6d696f007000d000000000001000000000000001030000000200000001000030030000000200000002000000000100000001000001000000070000006d6963726f766d0000000000|35f14fb1b596b299a43751b7827b7e7b88bc5cd330ff0d8eae2ee40b3d7e0fc2 +KEY|HOST_PROFILE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d05000000696e74656c04000000686f7374010000005a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a58000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253545556570100000001000000020000000100000002000000000000000100000000752b7d0000000000c2eb0b00000000fa000000020000000000000000000020000000000000008000000000000000080000000001000000000000000000000000000020000000000100000000000020000000000000006000000000530100006561726c79636f6e3d78653920636f6e736f6c653d68766331207265626f6f743d742070616e69633d2d31206e76785f73616e64626f783d31206e76785f636f6e6669673d307864303031303030302c3635353336206e725f637075733d32206e76785f736e617073686f745f746965723d706c6174666f726d2076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030313030303a362076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030323030303a372076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030333030303a342076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030363030303a31312076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030373030303a33206e76785f636f6e74726f6c5f7474793d6876633217f80f8858431ca75fcc7ca694ea68637e017ede02db15e18d0ed4581067858d000000400000000000020000001000000000000400000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000001000000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000003000000767030040000007663707509000000706172746974696f6e00000000000000000000000000000000000000000006000000766d74696d6505000000636c6f636b06000000766d74696d65000000000000000000000000000000000000000000030000007069630300000070696303000000706963000000000200000004000000706d696f2000000000000000020000000000000004000000706d696fa00000000000000002000000000000000000000000000000000000000006000000696f6170696306000000696f6170696306000000696f617069630000000001000000040000006d6d696f0000c0fe00000000001000000000000000000000000000000000000000050000006c61706963050000006c6170696309000000706172746974696f6e0000000001000000040000006d6d696f0000e0fe00000000001000000000000000000000000000000000000000030000007069740300000070697403000000706974000000000100000004000000706d696f400000000000000004000000000000000100000000000000000000000000000000030000007274630300000072746303000000727463000000000100000004000000706d696f7000000000000000020000000000000001080000000000000000000000000000000d0000006d6963726f766d2d706f72746205000000706f7274620d0000006d6963726f766d2d706f727462000000000100000004000000706d696fe900000000000000020000000000000000000000000000000000000000100000006d6963726f766d2d73687574646f776e0800000073687574646f776e100000006d6963726f766d2d73687574646f776e000000000100000004000000706d696f0406000000000000010000000000000000000000000000000000000000180000006d6963726f766d2d736e617073686f742d7265717565737410000000736e617073686f742d72657175657374180000006d6963726f766d2d736e617073686f742d72657175657374000000000100000004000000706d696f05060000000000000100000000000000000000000000000000000000000b00000066733a6d6963726f766d300900000076697274696f2d66731300000076697274696f66732d333438393636353032340b00000076697274696f2d6d6d696f01000000040000006d6d696f001000d000000000001000000000000001060000000200000000000030030000000200000002000000000100000001000017000000636f6e736f6c653a6d6963726f766d2d76697274696f300e00000076697274696f2d636f6e736f6c651900000076697274696f2d636f6e736f6c652d333438393636393132300b00000076697274696f2d6d6d696f01000000040000006d6d696f002000d000000000001000000000000001070000000200000001000030030000000200000002000000000100000001000012000000626c6b3a73616e64626f783a64697374726f0a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393637333231360b00000076697274696f2d6d6d696f01000000040000006d6d696f003000d0000000000010000000000000010400000002000000640600300300000001000000010000000001000013000000626c6b3a73616e64626f783a736372617463680a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393638353530340b00000076697274696f2d6d6d696f01000000040000006d6d696f006000d0000000000010000000000000010b00000002000000440600300300000001000000010000000001000018000000636f6e736f6c653a6d6963726f766d2d636f6e74726f6c301600000076697274696f2d636f6e74726f6c2d636f6e736f6c652100000076697274696f2d636f6e74726f6c2d636f6e736f6c652d333438393638393630300b00000076697274696f2d6d6d696f01000000040000006d6d696f007000d000000000001000000000000001030000000200000001000030030000000200000002000000000100000001000001000000070000006d6963726f766d00000000004e61bc000000000000002a36fe9c9717000e5fa31c0000000e0000006c696e75782d626f6f7474696d6501010101010101010101010101010101020202020202020202020202020202020300000001c800000000000000|35f14fb1b596b299a43751b7827b7e7b88bc5cd330ff0d8eae2ee40b3d7e0fc2 # A machine of one processor and one device, and no NIC, which the invalid vectors change. -KEY|MINIMAL|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000|9adf5b7b4c5509968914b0cae4664263c768bab0e8e010616b562b0023331d64 +KEY|MINIMAL|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000|9adf5b7b4c5509968914b0cae4664263c768bab0e8e010616b562b0023331d64 # The class of the capture of TEMPLATE: the CPU profile auto, and 2 processors, all online at # boot. @@ -48,53 +52,53 @@ CLASS|TEMPLATE|0100470000007368613235363a616161616161616161616161616161616161616 CLASS|NETWORK|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d040000006175746f02000000020000000000002000000000000000800000000000000040000000000002000000100000000000040000000000020000000200000a000000050000002d2d6e65740b00000031302e302e302e322f3234110000002d2d6e6574776f726b2d70726f66696c6508000000706f727461626c65100000002d2d6e6574776f726b2d6567726573730400000064656e79110000002d2d6e6574776f726b2d696e67726573730400000064656e790f0000002d2d686f73742d6c6f6f706261636b0400000064656e79020000001100000069736f6c6174696f6e2e736563636f6d70060000007374726963740b0000006c696d6974732e7069647303000000353132|2f32edae7e9eb532625af600232c68aa78b92e02c8116cb49f857f13ff5fcc7c # MINIMAL in other versions of the encoding. -INVALID_KEY_VERSION|VERSION_0|0000470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 -INVALID_KEY_VERSION|VERSION_2|0200470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 -INVALID_KEY_VERSION|VERSION_65535|ffff470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY_VERSION|VERSION_0|0000470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 +INVALID_KEY_VERSION|VERSION_2|0200470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 +INVALID_KEY_VERSION|VERSION_65535|ffff470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # No bytes. INVALID_KEY|EMPTY| # MINIMAL without its last byte. -INVALID_KEY|TRUNCATED|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000 +INVALID_KEY|TRUNCATED|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d65000000000000000000000000000000000000000000000000000000000000000000000000 # MINIMAL and a byte after it. -INVALID_KEY|TRAILING_BYTE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d000000000000 +INVALID_KEY|TRAILING_BYTE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d650000000000000000000000000000000000000000000000000000000000000000000000000000 # A command line whose digest does not match it. -INVALID_KEY|COMMAND_LINE_DIGEST|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663300000000000000000000000000000000000000000000000000000000000000000001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY|COMMAND_LINE_DIGEST|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663300000000000000000000000000000000000000000000000000000000000000000001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # A backend that does not exist. -INVALID_KEY|UNKNOWN_BACKEND|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d76320100000000000000060000000000000001000000000000000100000000000000040000000000000001000000000000000300000078656e03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY|UNKNOWN_BACKEND|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d76320100000000000000060000000000000001000000000000000100000000000000040000000000000001000000000000000300000078656e03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # A device range of an address space that does not exist. -INVALID_KEY|UNKNOWN_ADDRESS_SPACE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e000000000100000004000000706f7274200000000000000002000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY|UNKNOWN_ADDRESS_SPACE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e000000000100000004000000706f7274200000000000000002000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # An interrupt delivery mode that is not UTF-8. -INVALID_KEY|INTERRUPT_MODE_NOT_UTF8|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000013000000656467652d7368617265642dff737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY|INTERRUPT_MODE_NOT_UTF8|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000013000000656467652d7368617265642dff737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # A NIC whose presence tag is 2. -INVALID_KEY|PRESENCE_TAG_2|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0200000000 +INVALID_KEY|PRESENCE_TAG_2|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d02000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # A NIC whose policy's enforcement is 2. -INVALID_KEY|BOOLEAN_2|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0108000000706f727461626c655254000000025254000000010a000002180a00000101fd00000000000000000000000a00000278fd00000000000000000000000a0000010a000000616c6c6f772d6c6973740707070707070707070707070707070707070707070707070707070707070707020400000000000000 +INVALID_KEY|BOOLEAN_2|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0108000000706f727461626c655254000000025254000000010a000002180a00000101fd00000000000000000000000a00000278fd00000000000000000000000a0000010a000000616c6c6f772d6c69737407070707070707070707070707070707070707070707070707070707070707070204000000000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # Invariant settings out of the order of their names. -INVALID_KEY|UNSORTED_SETTINGS|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00020000000b0000006c696d6974732e70696473030000003531321100000069736f6c6174696f6e2e736563636f6d7006000000737472696374 +INVALID_KEY|UNSORTED_SETTINGS|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00020000000b0000006c696d6974732e70696473030000003531321100000069736f6c6174696f6e2e736563636f6d70060000007374726963740000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # An invariant setting named twice. -INVALID_KEY|DUPLICATE_SETTINGS|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00020000000b0000006c696d6974732e70696473030000003531320b0000006c696d6974732e7069647303000000353132 +INVALID_KEY|DUPLICATE_SETTINGS|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00020000000b0000006c696d6974732e70696473030000003531320b0000006c696d6974732e70696473030000003531320000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # An invariant setting whose name holds an uppercase letter. -INVALID_KEY|UPPERCASE_SETTING|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00010000000b0000004c696d6974732e7069647303000000353132 +INVALID_KEY|UPPERCASE_SETTING|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00010000000b0000004c696d6974732e70696473030000003531320000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # More APIC IDs than a microVM has processors. -INVALID_KEY|TOO_MANY_APIC_IDS|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b010000000100000001000000010000000900000000000000010000000200000003000000040000000500000006000000070000000800000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY|TOO_MANY_APIC_IDS|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b010000000100000001000000010000000900000000000000010000000200000003000000040000000500000006000000070000000800000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # 3 processors, which a microVM cannot have. -INVALID_KEY|THREE_PROCESSORS|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b010000000100000003000000010000000300000000000000010000000200000000ca9a3b0000000000e1f505000000000000000003000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY|THREE_PROCESSORS|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b010000000100000003000000010000000300000000000000010000000200000000ca9a3b0000000000e1f505000000000000000003000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # RAM ranges that do not hold the RAM. -INVALID_KEY|MEMORY_RANGES|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000400000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY|MEMORY_RANGES|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000400000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # A machine without devices. -INVALID_KEY|NO_DEVICES|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY|NO_DEVICES|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # A device ID twice. -INVALID_KEY|DUPLICATE_DEVICE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000200000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000009000000706172746974696f6e05000000636c6f636b06000000766d74696d6500000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY|DUPLICATE_DEVICE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000200000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000009000000706172746974696f6e05000000636c6f636b06000000766d74696d6500000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # A filesystem slot that is not the dormant slot of version 1. -INVALID_KEY|SLOT_VERSION_0|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000000000000070000006d6963726f766d0000000000 +INVALID_KEY|SLOT_VERSION_0|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000000000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # A filesystem slot without a tag. -INVALID_KEY|EMPTY_TAG|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000000000000000000000 +INVALID_KEY|EMPTY_TAG|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # A distro whose logical block size is 256. -INVALID_KEY|DISTRO_BLOCK_SIZE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000010000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY|DISTRO_BLOCK_SIZE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000010000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # An OpenVMM identity that holds a space. -INVALID_KEY|ARTIFACT_WITH_SPACE|0100470000007368613235363a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY|ARTIFACT_WITH_SPACE|0100470000007368613235363a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # A CPU profile of revision 0. -INVALID_KEY|CPU_PROFILE_REVISION_0|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610000000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY|CPU_PROFILE_REVISION_0|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610000000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 # An effective CPUID that is not whole entries of 44 bytes. -INVALID_KEY|CPUID_LENGTH|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112b000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY|CPUID_LENGTH|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d7632010000000000000006000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112b000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000000000000000000000000000000000000000000000000000000e0000006c696e75782d626f6f7474696d6500000000000000000000000000000000000000000000000000000000000000000000000000 From d0cf3f6381446ee6daaaf32d0b68ca883de07188 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Tue, 10 Nov 2026 01:07:55 -0800 Subject: [PATCH 09/15] host: hand owned cold-launch commands to callers Move checked launch values and owned resources into VmmCommand without reopening them. Add standard owned-handle conversions and the compiled external cold-launch consumer, retaining the current merged preparation and spawn behavior. Expose the existing status-bearing launch wait for host-only callers and retain timeout, actual status and repeated-wait coverage. Public API snapshots will be generated during remote qualification. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6789d491-7d13-4929-bc64-4edfec93ba60 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6a9302c-f126-4231-832e-22426190bd6b Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 89997040-ff41-4516-a86a-d81422d5a79f --- nvx-rs/Cargo.toml | 5 + nvx-rs/README.md | 25 +++- nvx-rs/examples/external-launch.rs | 174 ++++++++++++++++++++++ nvx-rs/src/host/platform/windows/spawn.rs | 10 +- nvx-rs/src/host/spawn.rs | 24 ++- nvx-rs/src/host/vm.rs | 134 ++++++++++++++++- nvx-rs/src/host/vm/tests.rs | 99 +++++++++++- nvx-rs/tests/checked_handoff.rs | 49 ++++++ 8 files changed, 499 insertions(+), 21 deletions(-) create mode 100644 nvx-rs/examples/external-launch.rs diff --git a/nvx-rs/Cargo.toml b/nvx-rs/Cargo.toml index 804ce2173..131b63937 100644 --- a/nvx-rs/Cargo.toml +++ b/nvx-rs/Cargo.toml @@ -132,6 +132,11 @@ path = "src/bin/nvx-rs.rs" required-features = ["cli"] doc = false +[[example]] +name = "external-launch" +path = "examples/external-launch.rs" +required-features = ["host"] + # The guest binaries. Their unit tests live in the library, so the binaries have no test harness. [[bin]] name = "nvx-console-pending" diff --git a/nvx-rs/README.md b/nvx-rs/README.md index dfc6e2b97..1cb537081 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -124,7 +124,7 @@ changes only when the module's purpose does. | [`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::vm`](#nvxhostvm) | A private VM directory, cold preparation, verified detached launch, confirmed stop, and caller-saved reattachment | +| [`nvx::host::vm`](#nvxhostvm) | A private VM directory, owned cold-launch command, verified detached launch, confirmed stop, and caller-saved reattachment | | [`nvx::host::snapshot`](#nvxhostsnapshot) | Strict private snapshot directories, verified metadata, VM compatibility, captured NIC addresses, and time eligibility | | [`nvx::host::config`](#nvxhostconfig) | `VmConfig` and `CaptureConfig`: pure configuration, cold-boot kernel/OpenVMM arguments, portable length checks, and capture identity | | [`nvx::host::restore`](#nvxhostrestore) | A caller-driven restore to Ready without I/O or a clock, and its peer-verified restore-ready listener | @@ -528,7 +528,7 @@ these names before creating any output, including the log. `Prepared::capability()` and `outputs()` return what the caller may journal before a spawn. `log()` returns the same opened log, without reopening its -path. `inherit(file)` moves an already opened resource into the preparation's +path. `inherit(resource)` moves an already opened resource into the preparation's inheritance allowlist and keeps it open. `arguments()` returns the whole configuration's OpenVMM arguments. Creation makes caller-relative runtime and block-image paths absolute without resolving links or `..` components, @@ -557,6 +557,27 @@ blockless root or memory that cannot fit the record's byte count fails with `ErrorKind::Unsupported`. These projections do not enable the restore caller; `create` still accepts cold launches only. +`Prepared::into_command()` moves the complete launch into `VmmCommand`: its +program, arguments, working directory, capability, outputs, inherited resources, +opened log and Windows breakaway setting. It neither reopens nor duplicates +resources, renders again nor rediscovers paths. `InheritedHandle` owns its +resource, converts from and back to `OwnedFd` on Unix or `OwnedHandle` on Windows, +and also accepts an opened `File`. The command documents the caller's spawn +contract, including standard streams, allowlisted inheritance, Linux detachment +and umask, Windows log job and breakaway, and cleanup of a failed child. +Read the resolved NIC or build an activation record before consuming +`Prepared` into the command. + +The compiled [external-launch example](examples/external-launch.rs) consumes +this command, uses the existing public low-level spawn primitive without a +sandbox store, checks the cold guest's boot marker and shell response, then asks +it to exit cleanly. It requires the VMM's actual exit status 0 and a successful +outcome with complete teardown. Build with +`cargo build --locked --example external-launch` and supply the backend, OpenVMM, +kernel, initramfs, new VM directory, and the three artifact SHA-256 digests from +their installation metadata, in that order. Artifact paths may be relative to +the caller's directory. + `vm::spawn(prepared)` consumes that preparation and returns `Launched` with its verified `process`, the same `outputs`, and the same `capability`. The log and inherited resources are passed as already opened objects, never reopened diff --git a/nvx-rs/examples/external-launch.rs b/nvx-rs/examples/external-launch.rs new file mode 100644 index 000000000..3a10480c1 --- /dev/null +++ b/nvx-rs/examples/external-launch.rs @@ -0,0 +1,174 @@ +//! A caller-owned cold launch, using the existing low-level spawn primitive without a store. +//! +//! Run the compiled example with a backend, OpenVMM, kernel, initramfs, new VM directory, and +//! the three artifact SHA-256 digests from their installation metadata, in that order. + +use std::error::Error; +use std::io::{self, Write}; +use std::path::Path; +use std::time::{Duration, Instant}; + +use nvx::Hypervisor; +use nvx::activation::{MachineIdMode, View}; +use nvx::host::config::{Boot, ConsoleAttachment, RootConfig, VmConfig}; +use nvx::host::platform::Transport; +use nvx::host::runtime::{ArtifactIdentity, LaunchOptions, Runtime, RuntimeIdentity}; +use nvx::host::supervisor::{self, Inherited}; +use nvx::host::{artifacts, report, session, vm}; + +fn main() -> Result<(), Box> { + let args: Vec<_> = std::env::args_os().collect(); + if args.len() != 9 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "usage: external-launch BACKEND OPENVMM KERNEL INITRAMFS VM-DIR \ + OPENVMM-SHA256 KERNEL-SHA256 INITRAMFS-SHA256", + ) + .into()); + } + let hypervisor = match args[1].to_str() { + Some("kvm") => Hypervisor::Kvm, + Some("mshv") => Hypervisor::Mshv, + Some("whp") => Hypervisor::Whp, + _ => return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid backend").into()), + }; + nvx::host::runtime::probe_hypervisor(hypervisor)?; + let identity = |index: usize| { + ArtifactIdentity::sha256(args[index].to_str().ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "artifact digest is not Unicode", + ) + })?) + .map_err(Box::::from) + }; + let runtime = Runtime { + openvmm: artifacts::absolute(Path::new(&args[2]))?, + kernel: artifacts::absolute(Path::new(&args[3]))?, + initramfs: artifacts::absolute(Path::new(&args[4]))?, + identity: RuntimeIdentity::new(identity(6)?, identity(7)?, identity(8)?), + hypervisor, + cpu_profile: Some("auto".to_owned()), + windows_breakaway: false, + options: LaunchOptions::default(), + }; + let config = VmConfig { + processors: 1, + memory_mib: 256, + root: RootConfig::Blockless, + view: View { + hostname: b"external".to_vec(), + machine_id: MachineIdMode::Image, + }, + network: None, + filesystem: None, + console: ConsoleAttachment::Listener, + boot: Some(Boot { + command_line: "quiet loglevel=0".to_owned(), + }), + }; + vm::create_private_dir(Path::new(&args[5]))?; + let dir = vm::VmDir::open(Path::new(&args[5]))?; + let vm::VmmCommand { + program, + arguments, + working_dir, + capability, + outputs, + inherit, + log, + breakaway_from_job, + } = vm::create(&runtime, &config, None, &dir)?.into_command(); + let command = supervisor::VmmCommand { + program, + arguments, + working_dir, + breakaway_from_job, + inherit: inherit.iter().map(Inherited::new).collect(), + }; + let mut launched = supervisor::spawn(&command, capability.as_bytes(), log)?; + let result = (|| -> Result<(), Box> { + let process = launched.process()?; + let deadline = Instant::now() + Duration::from_secs(60); + let console = outputs.console.as_deref().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "no prepared boot console") + })?; + let mut console = session::connect( + console, + process.reference().pid, + process.reference().start_time, + deadline, + )?; + read_until(console.as_mut(), b"NVX-GUEST-BOOT-OK:", deadline)?; + console.write_all( + b"printf 'NVX-EXTERNAL-LAUNCH-OK\\n'; /sbin/nvx-exit 0\n", + Some(deadline.saturating_duration_since(Instant::now())), + )?; + read_until(console.as_mut(), b"\nNVX-EXTERNAL-LAUNCH-OK", deadline)?; + if !process.wait(deadline)? { + return Err(io::Error::new(io::ErrorKind::TimedOut, "VM did not exit").into()); + } + match launched.wait_timeout(Duration::ZERO)? { + Some(0) => {} + Some(status) => { + return Err(io::Error::other(format!("VMM exited with status {status}")).into()); + } + None => return Err(io::Error::other("VMM exit status is unavailable").into()), + } + let outcome = report::read(&outputs.outcome) + .ok_or_else(|| io::Error::other("missing or invalid VM outcome"))?; + if outcome.category.as_deref() != Some("success") + || outcome.status_code != Some(0) + || outcome.teardown_complete != Some(true) + { + return Err( + io::Error::other(format!("VM did not shut down cleanly: {outcome:?}")).into(), + ); + } + Ok(()) + })(); + if let Err(error) = result { + if !launched.kill_and_wait() { + return Err( + io::Error::other(format!("{error}; could not reap the launched VM")).into(), + ); + } + return Err(error); + } + launched.detach(); + println!( + "external cold launch completed; outcome={}", + outputs.outcome.display() + ); + Ok(()) +} + +/// Prints console output through a marker, retaining only enough bytes to match across reads. +fn read_until(console: &mut dyn Transport, marker: &[u8], deadline: Instant) -> io::Result<()> { + let mut pending = Vec::new(); + let mut buffer = [0; 4096]; + loop { + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Err(io::Error::new( + io::ErrorKind::TimedOut, + "console marker not received", + )); + } + let count = console.read(&mut buffer, Some(remaining))?; + if count == 0 { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "boot console closed", + )); + } + io::stdout().write_all(&buffer[..count])?; + pending.extend_from_slice(&buffer[..count]); + if pending.windows(marker.len()).any(|bytes| bytes == marker) { + return Ok(()); + } + if pending.len() > marker.len() { + pending.drain(..pending.len() - marker.len()); + } + } +} diff --git a/nvx-rs/src/host/platform/windows/spawn.rs b/nvx-rs/src/host/platform/windows/spawn.rs index e89348406..dfe398380 100644 --- a/nvx-rs/src/host/platform/windows/spawn.rs +++ b/nvx-rs/src/host/platform/windows/spawn.rs @@ -11,26 +11,27 @@ use std::{mem, ptr}; #[cfg(feature = "cli")] use windows_sys::Win32::Foundation::{ - DUPLICATE_SAME_ACCESS, DuplicateHandle, INVALID_HANDLE_VALUE, WAIT_TIMEOUT, + DUPLICATE_SAME_ACCESS, DuplicateHandle, INVALID_HANDLE_VALUE, }; use windows_sys::Win32::Foundation::{ GetHandleInformation, HANDLE, HANDLE_FLAG_INHERIT, SetHandleInformation, WAIT_OBJECT_0, + WAIT_TIMEOUT, }; #[cfg(feature = "cli")] use windows_sys::Win32::System::Console::{ GetStdHandle, STD_ERROR_HANDLE, STD_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, }; +#[cfg(feature = "cli")] +use windows_sys::Win32::System::Threading::GetCurrentProcess; use windows_sys::Win32::System::Threading::{ CREATE_BREAKAWAY_FROM_JOB, CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, CREATE_UNICODE_ENVIRONMENT, CreateProcessW, DeleteProcThreadAttributeList, - EXTENDED_STARTUPINFO_PRESENT, INFINITE, InitializeProcThreadAttributeList, + EXTENDED_STARTUPINFO_PRESENT, GetExitCodeProcess, INFINITE, InitializeProcThreadAttributeList, LPPROC_THREAD_ATTRIBUTE_LIST, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, PROC_THREAD_ATTRIBUTE_JOB_LIST, PROCESS_CREATION_FLAGS, PROCESS_INFORMATION, STARTF_USESTDHANDLES, STARTUPINFOEXW, TerminateProcess, UpdateProcThreadAttribute, WaitForSingleObject, }; -#[cfg(feature = "cli")] -use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetExitCodeProcess}; #[cfg(feature = "cli")] use super::process::refused_termination; @@ -99,7 +100,6 @@ impl LaunchedProcess { /// Waits up to `timeout` for the process to exit, and returns its exit status if it has, /// as `Popen.wait` reads it with `GetExitCodeProcess`. - #[cfg(feature = "cli")] pub(crate) fn wait_timeout(&self, timeout: Duration) -> io::Result> { let milliseconds = u32::try_from(timeout.as_millis()).unwrap_or(INFINITE - 1); // SAFETY: CreateProcessW returned the handle with full access. diff --git a/nvx-rs/src/host/spawn.rs b/nvx-rs/src/host/spawn.rs index fd71827ab..f767b756a 100644 --- a/nvx-rs/src/host/spawn.rs +++ b/nvx-rs/src/host/spawn.rs @@ -10,7 +10,6 @@ use std::path::PathBuf; use std::process::{Child, Command, Stdio}; #[cfg(not(windows))] use std::thread; -#[cfg(feature = "cli")] use std::time::Duration; #[cfg(not(windows))] use std::time::Instant; @@ -173,8 +172,9 @@ fn spawn_inner( } /// A VMM that [`spawn`] started, which the caller has not released yet: on Unix, its child -/// process, which the caller has not reaped, so that its ID names no other process; on Windows, -/// its process handle. +/// process; on Windows, its process handle. On Unix the child's ID names no other process until +/// [`wait_timeout`](Self::wait_timeout) reaps it; hold it with [`process`](Self::process) before +/// waiting to keep a verified handle after it exits. /// /// Record it with [`Launch::record_process`](crate::host::store::Launch::record_process), or hold /// it with [`process`](Self::process). Then release it with [`detach`](Self::detach), or end it @@ -311,8 +311,10 @@ impl Launched { /// Waits up to `timeout` for the process to exit, and returns its status if it has, as /// `Popen.wait` and `Popen.poll` report it: the exit code, or on Unix the negated number of /// the signal that ended the process. - #[cfg(feature = "cli")] - pub(crate) fn wait_timeout(&mut self, timeout: Duration) -> io::Result> { + /// + /// On Unix a successful wait reaps the child; call [`process`](Self::process) before it to + /// retain a verified process handle. Later waits return the same status. + pub fn wait_timeout(&mut self, timeout: Duration) -> io::Result> { #[cfg(windows)] return Ok(self.process.wait_timeout(timeout)?.map(i64::from)); #[cfg(not(windows))] @@ -333,7 +335,7 @@ impl Launched { } /// Reaps the child if it has exited, and returns its status if it has. - #[cfg(all(feature = "cli", not(windows)))] + #[cfg(not(windows))] fn try_reap(&mut self) -> io::Result> { let status = self.child.try_wait()?; self.reaped |= status.is_some(); @@ -376,11 +378,17 @@ impl Launched { } /// Returns `Popen.returncode` for `status`: the exit code, or the negated signal number. -#[cfg(all(feature = "cli", not(windows)))] +#[cfg(not(windows))] fn status_value(status: std::process::ExitStatus) -> i64 { + #[cfg(unix)] use std::os::unix::process::ExitStatusExt; - match (status.code(), status.signal()) { + #[cfg(unix)] + let signal = status.signal(); + #[cfg(not(unix))] + let signal: Option = None; + + match (status.code(), signal) { (Some(code), _) => i64::from(code), (None, Some(signal)) => -i64::from(signal), (None, None) => 0, diff --git a/nvx-rs/src/host/vm.rs b/nvx-rs/src/host/vm.rs index 06e0565a8..2c7900340 100644 --- a/nvx-rs/src/host/vm.rs +++ b/nvx-rs/src/host/vm.rs @@ -36,7 +36,7 @@ use crate::host::platform::{check_vm_dir as check_dir, create_vm_dir as create_d use crate::host::report::{self, Report}; use crate::host::runtime::{Runtime, RuntimeIdentity}; use crate::host::snapshot::Snapshot; -use crate::host::spawn::{self, VmmCommand}; +use crate::host::spawn; use crate::host::transport::{self, HostConnection}; use crate::host::{ platform::inherit::Inherited, @@ -301,6 +301,110 @@ pub fn reattach( }) } +/// An owned descriptor on Unix. +#[cfg(unix)] +type Owned = std::os::fd::OwnedFd; +/// An owned handle on Windows. +#[cfg(windows)] +type Owned = std::os::windows::io::OwnedHandle; +/// An owned file on hosts without descriptors or handles. +#[cfg(not(any(unix, windows)))] +type Owned = File; + +/// An already opened resource that a VMM inherits, owned until dropped or transferred. +/// +/// Conversions move the same descriptor or handle; they neither duplicate nor reopen it. +/// Borrowed raw descriptors and handles cannot transfer ownership into this type. +#[derive(Debug)] +pub struct InheritedHandle { + /// The owned platform resource. + handle: Owned, +} + +impl From for InheritedHandle { + fn from(file: File) -> Self { + Self { + handle: file.into(), + } + } +} + +#[cfg(unix)] +impl From for InheritedHandle { + fn from(handle: std::os::fd::OwnedFd) -> Self { + Self { handle } + } +} + +#[cfg(unix)] +impl From for std::os::fd::OwnedFd { + fn from(resource: InheritedHandle) -> Self { + resource.handle + } +} + +#[cfg(unix)] +impl std::os::fd::AsFd for InheritedHandle { + fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> { + std::os::fd::AsFd::as_fd(&self.handle) + } +} + +#[cfg(windows)] +impl From for InheritedHandle { + fn from(handle: std::os::windows::io::OwnedHandle) -> Self { + Self { handle } + } +} + +#[cfg(windows)] +impl From for std::os::windows::io::OwnedHandle { + fn from(resource: InheritedHandle) -> Self { + resource.handle + } +} + +#[cfg(windows)] +impl std::os::windows::io::AsHandle for InheritedHandle { + fn as_handle(&self) -> std::os::windows::io::BorrowedHandle<'_> { + std::os::windows::io::AsHandle::as_handle(&self.handle) + } +} + +/// A complete cold launch handed to the caller's launcher, with all its resources owned. +/// +/// The launcher supplies the capability on stdin through a pipe closed to writers before +/// spawning, and uses the opened log for stdout and stderr. It allows only the standard streams +/// and `inherit`, under their original descriptor or handle numbers, into the child. +/// +/// Follow [`spawn`]'s launch contract: detach on Linux, clear only the child's owner umask bits, +/// and keep allowed descriptors close-on-exec in the caller; on Windows, use an explicit handle +/// list, restore temporary inheritability, start without a console in the log-named job, +/// refuse a taken or untrusted job name, and honor `breakaway_from_job`. Windows callers +/// coordinate their other spawns with the inheritance window. If setup fails after spawning, +/// terminate and reap only that child before returning an error. The caller journals recovery +/// data and keeps the VM directory and runtime inputs stable; this value starts no process or +/// listener. +#[derive(Debug)] +pub struct VmmCommand { + /// The checked OpenVMM executable. + pub program: PathBuf, + /// The complete, checked arguments, without the executable. + pub arguments: Vec, + /// The VM directory in which OpenVMM runs. + pub working_dir: PathBuf, + /// The control credential, never written to disk by the library. + pub capability: Capability, + /// The already chosen output files and endpoints. + pub outputs: VmOutputs, + /// Owned resources allowed into the child besides its standard streams. + pub inherit: Vec, + /// The same private log that preparation opened. + pub log: File, + /// Whether Windows leaves the caller's job, which must permit breakaway. + pub breakaway_from_job: bool, +} + /// A checked cold launch, with its capability, chosen outputs, and already opened log. /// /// Creation starts no process and binds no listener. The caller keeps the VM directory and @@ -327,7 +431,7 @@ pub struct Prepared { /// The effective NIC, whose presence and address a snapshot fixes for a restore. nic: Option, /// Already opened resources added by the caller, retained until the preparation is dropped. - inherit: Vec, + inherit: Vec, } impl Prepared { @@ -438,8 +542,8 @@ impl Prepared { /// Adds an already opened resource to the inheritance allowlist, transferring ownership. /// /// The resource is not reopened, and remains open for the lifetime of this preparation. - pub fn inherit(&mut self, file: File) { - self.inherit.push(file); + pub fn inherit(&mut self, resource: impl Into) { + self.inherit.push(resource.into()); } /// Returns the same opened log that creation made, for example to take a liveness claim. @@ -451,6 +555,26 @@ impl Prepared { pub fn arguments(&self) -> &[OsString] { &self.arguments } + + /// Transfers the complete checked launch to the caller's launcher. + /// + /// Moves the same opened log and inherited resources, without cloning, reopening, rendering + /// arguments again or rediscovering paths. Starts no process or listener. The caller follows + /// [`VmmCommand`]'s launch contract and may retain its capability and outputs for recovery. + /// Read [`nic`](Self::nic) or build an [`activation_record`](Self::activation_record) before + /// consuming the preparation. + pub fn into_command(self) -> VmmCommand { + VmmCommand { + program: self.program, + arguments: self.arguments, + working_dir: self.working_dir, + capability: self.capability, + outputs: self.outputs, + inherit: self.inherit, + log: self.log, + breakaway_from_job: self.windows_breakaway, + } + } } /// Creates an inspectable cold launch in `dir`, without starting a process or binding endpoints. @@ -731,7 +855,7 @@ fn spawn_verified( inherit, .. } = prepared; - let command = VmmCommand { + let command = spawn::VmmCommand { program, arguments, working_dir, diff --git a/nvx-rs/src/host/vm/tests.rs b/nvx-rs/src/host/vm/tests.rs index 6ba7058f1..2c8280ae7 100644 --- a/nvx-rs/src/host/vm/tests.rs +++ b/nvx-rs/src/host/vm/tests.rs @@ -1009,11 +1009,108 @@ fn the_log_and_inherited_resources_stay_open_without_reopening_their_paths() { prepared.inherit(file); fs::rename(&input, &moved_input).unwrap(); fs::write(&input, b"replacement input").unwrap(); - prepared.inherit[0].write_all(b"original input").unwrap(); + inherited_file(prepared.inherit.pop().unwrap()) + .write_all(b"original input") + .unwrap(); assert_eq!(fs::read(&moved_input).unwrap(), b"original input"); assert_eq!(fs::read(&input).unwrap(), b"replacement input"); } +fn inherited_file(resource: InheritedHandle) -> File { + #[cfg(any(unix, windows))] + return File::from(Owned::from(resource)); + #[cfg(not(any(unix, windows)))] + resource.handle +} + +#[test] +fn a_command_moves_the_checked_launch_and_the_same_opened_resources() { + use std::io::Write; + + let (_directory, vm) = vm_dir(); + let mut runtime = cold_runtime(); + runtime.windows_breakaway = true; + let mut prepared = create(&runtime, &cold_config(), None, &vm).unwrap(); + let program = absolute(&runtime.openvmm).unwrap(); + let arguments = prepared.arguments().as_ptr(); + let capability = *prepared.capability().as_bytes(); + let outputs = prepared.outputs().clone(); + let original_log = vm.path().join("original-log"); + fs::rename(&outputs.log, &original_log).unwrap(); + fs::write(&outputs.log, b"replacement log").unwrap(); + let input = vm.path().join("input"); + let original_input = vm.path().join("original-input"); + prepared.inherit(File::create(&input).unwrap()); + let resources = prepared.inherit.as_ptr(); + fs::rename(&input, &original_input).unwrap(); + fs::write(&input, b"replacement input").unwrap(); + #[cfg(unix)] + let log_handle = std::os::fd::AsRawFd::as_raw_fd(prepared.log()); + #[cfg(windows)] + let log_handle = std::os::windows::io::AsRawHandle::as_raw_handle(prepared.log()); + runtime.openvmm = "different-program".into(); + runtime.windows_breakaway = false; + + let mut command = prepared.into_command(); + assert_eq!(command.program, program); + assert_eq!(command.working_dir, vm.path()); + assert!(command.breakaway_from_job); + assert_eq!(command.arguments.as_ptr(), arguments); + assert_eq!(command.inherit.as_ptr(), resources); + assert_eq!(command.capability.as_bytes(), &capability); + assert_eq!(command.outputs, outputs); + #[cfg(unix)] + assert_eq!(std::os::fd::AsRawFd::as_raw_fd(&command.log), log_handle); + #[cfg(windows)] + assert_eq!( + std::os::windows::io::AsRawHandle::as_raw_handle(&command.log), + log_handle + ); + command.log.write_all(b"original log").unwrap(); + inherited_file(command.inherit.pop().unwrap()) + .write_all(b"original input") + .unwrap(); + assert_eq!(fs::read(original_log).unwrap(), b"original log"); + assert_eq!(fs::read(original_input).unwrap(), b"original input"); + assert_eq!(fs::read(&outputs.log).unwrap(), b"replacement log"); + assert_eq!(fs::read(input).unwrap(), b"replacement input"); + drop(command); + assert!(outputs.log.exists()); + assert!(!outputs.outcome.exists()); +} + +#[cfg(unix)] +#[test] +fn an_owned_descriptor_moves_into_and_out_of_an_inherited_handle() { + use std::io::Write; + use std::os::fd::{AsFd, AsRawFd, OwnedFd}; + + let file = tempfile::tempfile().unwrap(); + let number = file.as_raw_fd(); + let descriptor: OwnedFd = file.into(); + let resource: InheritedHandle = descriptor.into(); + assert_eq!(resource.as_fd().as_raw_fd(), number); + let descriptor: OwnedFd = resource.into(); + assert_eq!(descriptor.as_raw_fd(), number); + File::from(descriptor).write_all(b"still open").unwrap(); +} + +#[cfg(windows)] +#[test] +fn an_owned_windows_handle_moves_into_and_out_of_an_inherited_handle() { + use std::io::Write; + use std::os::windows::io::{AsHandle, AsRawHandle, OwnedHandle}; + + let file = tempfile::tempfile().unwrap(); + let number = file.as_raw_handle(); + let handle: OwnedHandle = file.into(); + let resource: InheritedHandle = handle.into(); + assert_eq!(resource.as_handle().as_raw_handle(), number); + let handle: OwnedHandle = resource.into(); + assert_eq!(handle.as_raw_handle(), number); + File::from(handle).write_all(b"still open").unwrap(); +} + #[test] fn creation_checks_the_complete_actual_command_line_at_the_windows_boundary() { let (_directory, vm) = vm_dir(); diff --git a/nvx-rs/tests/checked_handoff.rs b/nvx-rs/tests/checked_handoff.rs index 8f1c7b07b..0d21eba1a 100644 --- a/nvx-rs/tests/checked_handoff.rs +++ b/nvx-rs/tests/checked_handoff.rs @@ -179,6 +179,55 @@ fn fixture_arguments(name: &str) -> Vec { .to_vec() } +/// A VMM fixture that exits with the requested status once the caller lets it go. +#[test] +#[ignore = "fixture that the exit-status test runs as a VMM"] +fn exit_status_fixture() { + let directory = env::current_dir().unwrap(); + if !directory.join(VMM_MARKER).exists() { + return; + } + let mut capability = [0; CAPABILITY_LEN]; + io::stdin().read_exact(&mut capability).unwrap(); + assert_eq!(capability, CAPABILITY); + let deadline = Instant::now() + VMM_LIFETIME; + while !directory.join(GO_MARKER).exists() { + if directory.join(EXIT_MARKER).exists() || Instant::now() >= deadline { + return; + } + thread::sleep(Duration::from_millis(5)); + } + let status = fs::read_to_string(directory.join("exit-status")).unwrap(); + std::process::exit(status.parse().unwrap()); +} + +#[test] +fn launched_waits_return_and_retain_the_actual_exit_status() { + for status in [0, 17] { + let mut vmm = VmmDirectory::new(); + fs::write(vmm.path().join("exit-status"), status.to_string()).unwrap(); + let command = VmmCommand::new( + env::current_exe().unwrap(), + fixture_arguments("exit_status_fixture"), + vmm.path(), + ); + let log = File::create(vmm.path().join("vmm.log")).unwrap(); + vmm.launched = Some(supervisor::spawn(&command, &CAPABILITY, log).unwrap()); + assert_eq!( + vmm.launched + .as_mut() + .unwrap() + .wait_timeout(Duration::ZERO) + .unwrap(), + None + ); + File::create(vmm.path().join(GO_MARKER)).unwrap(); + let launched = vmm.launched.as_mut().unwrap(); + assert_eq!(launched.wait_timeout(TIMEOUT).unwrap(), Some(status)); + assert_eq!(launched.wait_timeout(Duration::ZERO).unwrap(), Some(status)); + } +} + /// Returns the arguments that pass a VMM fixture the `--microvm-sandbox-block` values of /// `prepared`. fn block_arguments(prepared: &PreparedLaunch) -> Vec { From 56791c1a0628d610e81155566f0c249171877112 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Tue, 10 Nov 2026 01:29:06 -0800 Subject: [PATCH 10/15] host: reconcile the external example and API snapshots Initialize the merged cold-boot capture option explicitly for the external launcher. Record the platform public API snapshots generated from the private source checkpoint. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6789d491-7d13-4929-bc64-4edfec93ba60 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6a9302c-f126-4231-832e-22426190bd6b Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 89997040-ff41-4516-a86a-d81422d5a79f --- nvx-rs/examples/external-launch.rs | 1 + nvx-rs/public-api/linux.txt | 12 ++++++++++++ nvx-rs/public-api/windows.txt | 12 ++++++++++++ 3 files changed, 25 insertions(+) diff --git a/nvx-rs/examples/external-launch.rs b/nvx-rs/examples/external-launch.rs index 3a10480c1..b62daeb04 100644 --- a/nvx-rs/examples/external-launch.rs +++ b/nvx-rs/examples/external-launch.rs @@ -65,6 +65,7 @@ fn main() -> Result<(), Box> { console: ConsoleAttachment::Listener, boot: Some(Boot { command_line: "quiet loglevel=0".to_owned(), + capture: None, }), }; vm::create_private_dir(Path::new(&args[5]))?; diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 0e1a12245..c8e7cd6b5 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -3275,6 +3275,7 @@ method nvx::host::supervisor::Launched::detach method nvx::host::supervisor::Launched::id method nvx::host::supervisor::Launched::kill_and_wait method nvx::host::supervisor::Launched::process +method nvx::host::supervisor::Launched::wait_timeout enum nvx::host::supervisor::Reattached variant nvx::host::supervisor::Reattached::Exited field nvx::host::supervisor::Reattached::Exited::record @@ -3765,6 +3766,7 @@ fn nvx::host::transport::connect_verified mod nvx::host::vm struct nvx::host::vm::Capability method nvx::host::vm::Capability::as_bytes +struct nvx::host::vm::InheritedHandle struct nvx::host::vm::Launched field nvx::host::vm::Launched::capability field nvx::host::vm::Launched::outputs @@ -3774,6 +3776,7 @@ method nvx::host::vm::Prepared::activation_record method nvx::host::vm::Prepared::arguments method nvx::host::vm::Prepared::capability method nvx::host::vm::Prepared::inherit +method nvx::host::vm::Prepared::into_command method nvx::host::vm::Prepared::log method nvx::host::vm::Prepared::nic method nvx::host::vm::Prepared::outputs @@ -3798,6 +3801,15 @@ field nvx::host::vm::VmOutputs::console field nvx::host::vm::VmOutputs::endpoint field nvx::host::vm::VmOutputs::log field nvx::host::vm::VmOutputs::outcome +struct nvx::host::vm::VmmCommand +field nvx::host::vm::VmmCommand::arguments +field nvx::host::vm::VmmCommand::breakaway_from_job +field nvx::host::vm::VmmCommand::capability +field nvx::host::vm::VmmCommand::inherit +field nvx::host::vm::VmmCommand::log +field nvx::host::vm::VmmCommand::outputs +field nvx::host::vm::VmmCommand::program +field nvx::host::vm::VmmCommand::working_dir fn nvx::host::vm::create fn nvx::host::vm::create_private_dir fn nvx::host::vm::reattach diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index b849938fa..a511d9e4b 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -1860,6 +1860,7 @@ method nvx::host::supervisor::Launched::detach method nvx::host::supervisor::Launched::id method nvx::host::supervisor::Launched::kill_and_wait method nvx::host::supervisor::Launched::process +method nvx::host::supervisor::Launched::wait_timeout enum nvx::host::supervisor::Reattached variant nvx::host::supervisor::Reattached::Exited field nvx::host::supervisor::Reattached::Exited::record @@ -2350,6 +2351,7 @@ fn nvx::host::transport::connect_verified mod nvx::host::vm struct nvx::host::vm::Capability method nvx::host::vm::Capability::as_bytes +struct nvx::host::vm::InheritedHandle struct nvx::host::vm::Launched field nvx::host::vm::Launched::capability field nvx::host::vm::Launched::outputs @@ -2359,6 +2361,7 @@ method nvx::host::vm::Prepared::activation_record method nvx::host::vm::Prepared::arguments method nvx::host::vm::Prepared::capability method nvx::host::vm::Prepared::inherit +method nvx::host::vm::Prepared::into_command method nvx::host::vm::Prepared::log method nvx::host::vm::Prepared::nic method nvx::host::vm::Prepared::outputs @@ -2383,6 +2386,15 @@ field nvx::host::vm::VmOutputs::console field nvx::host::vm::VmOutputs::endpoint field nvx::host::vm::VmOutputs::log field nvx::host::vm::VmOutputs::outcome +struct nvx::host::vm::VmmCommand +field nvx::host::vm::VmmCommand::arguments +field nvx::host::vm::VmmCommand::breakaway_from_job +field nvx::host::vm::VmmCommand::capability +field nvx::host::vm::VmmCommand::inherit +field nvx::host::vm::VmmCommand::log +field nvx::host::vm::VmmCommand::outputs +field nvx::host::vm::VmmCommand::program +field nvx::host::vm::VmmCommand::working_dir fn nvx::host::vm::create fn nvx::host::vm::create_private_dir fn nvx::host::vm::reattach From e7a5c4fe05bb23788c0d696d81fbf486605e54ca Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Tue, 10 Nov 2026 06:46:12 -0800 Subject: [PATCH 11/15] host: require trusted log jobs for external VM launches Expose the existing strict spawn path for caller-owned VM commands and use it in the external launcher, preserving the legacy spawn fallback. Exercise real Windows ACL, object-type and object-manager-link collisions before the process-creation window, and retain the permissive non-disk-log behavior only for ordinary spawns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5cb71c8-5f7f-4248-836b-4aae842c213d Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6a9302c-f126-4231-832e-22426190bd6b Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 89997040-ff41-4516-a86a-d81422d5a79f --- nvx-rs/README.md | 12 +++- nvx-rs/examples/external-launch.rs | 4 +- nvx-rs/public-api/linux.txt | 1 + nvx-rs/public-api/windows.txt | 1 + nvx-rs/src/host/platform/windows/tests.rs | 86 +++++++++++++++++++++++ nvx-rs/src/host/spawn.rs | 20 ++++-- nvx-rs/src/host/supervisor/mod.rs | 6 +- 7 files changed, 119 insertions(+), 11 deletions(-) diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 1cb537081..d07b62b88 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -569,9 +569,9 @@ Read the resolved NIC or build an activation record before consuming `Prepared` into the command. The compiled [external-launch example](examples/external-launch.rs) consumes -this command, uses the existing public low-level spawn primitive without a -sandbox store, checks the cold guest's boot marker and shell response, then asks -it to exit cleanly. It requires the VMM's actual exit status 0 and a successful +this command, uses `supervisor::spawn_vm` without a sandbox store, checks the +cold guest's boot marker and shell response, then asks it to exit cleanly. +It requires the VMM's actual exit status 0 and a successful outcome with complete teardown. Build with `cargo build --locked --example external-launch` and supply the backend, OpenVMM, kernel, initramfs, new VM directory, and the three artifact SHA-256 digests from @@ -1095,6 +1095,12 @@ after, and returns a `Starting` launch that the caller commits once the VMM serves its control endpoint, or abandons. A launch that fails never leaves an unrecorded VMM running. +`spawn_vm` starts a caller-owned command without a store, with the same strict +Windows log-job requirement as `vm::spawn`: an untrusted job name or a log that +cannot name a job is refused before child creation. The legacy `spawn` keeps +its fallback without a job for those logs and names. Other spawn behavior is +shared. + `VmmCommand::inherit` lists the descriptors (Linux) or handles (Windows) that the VMM inherits under their numbers, besides its standard streams, and it inherits nothing else of the caller's: on Linux, they must be close-on-exec, a diff --git a/nvx-rs/examples/external-launch.rs b/nvx-rs/examples/external-launch.rs index b62daeb04..593617f00 100644 --- a/nvx-rs/examples/external-launch.rs +++ b/nvx-rs/examples/external-launch.rs @@ -1,4 +1,4 @@ -//! A caller-owned cold launch, using the existing low-level spawn primitive without a store. +//! A caller-owned cold launch, using the strict low-level spawn primitive without a store. //! //! Run the compiled example with a backend, OpenVMM, kernel, initramfs, new VM directory, and //! the three artifact SHA-256 digests from their installation metadata, in that order. @@ -87,7 +87,7 @@ fn main() -> Result<(), Box> { breakaway_from_job, inherit: inherit.iter().map(Inherited::new).collect(), }; - let mut launched = supervisor::spawn(&command, capability.as_bytes(), log)?; + let mut launched = supervisor::spawn_vm(&command, capability.as_bytes(), log)?; let result = (|| -> Result<(), Box> { let process = launched.process()?; let deadline = Instant::now() + Duration::from_secs(60); diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index c8e7cd6b5..fe3c19e1e 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -3322,6 +3322,7 @@ fn nvx::host::supervisor::lock_spawns fn nvx::host::supervisor::reattach fn nvx::host::supervisor::spawn fn nvx::host::supervisor::spawn_command +fn nvx::host::supervisor::spawn_vm mod nvx::host::template struct nvx::host::template::Acquired field nvx::host::template::Acquired::lease diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index a511d9e4b..5026d099b 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -1907,6 +1907,7 @@ fn nvx::host::supervisor::lock_spawns fn nvx::host::supervisor::reattach fn nvx::host::supervisor::spawn fn nvx::host::supervisor::spawn_command +fn nvx::host::supervisor::spawn_vm mod nvx::host::template struct nvx::host::template::Acquired field nvx::host::template::Acquired::lease diff --git a/nvx-rs/src/host/platform/windows/tests.rs b/nvx-rs/src/host/platform/windows/tests.rs index 7da84f781..54440c3ad 100644 --- a/nvx-rs/src/host/platform/windows/tests.rs +++ b/nvx-rs/src/host/platform/windows/tests.rs @@ -1,3 +1,4 @@ +use std::cell::Cell; use std::ffi::OsString; use std::fs::{self, OpenOptions}; use std::io::{self, Read, Write}; @@ -5,6 +6,7 @@ use std::mem; use std::os::windows::fs::OpenOptionsExt; use std::os::windows::io::AsHandle; use std::path::Path; +use std::rc::Rc; use std::thread; use std::time::{Duration, Instant}; @@ -940,6 +942,81 @@ fn local_link(name: &[u16], target: &[u16]) -> OwnedHandle { owned(link).unwrap() } +#[test] +fn vm_spawns_refuse_untrusted_log_job_names_before_creating_a_child() { + use crate::host::supervisor::{self, VmmCommand}; + use crate::protocol::CAPABILITY_LEN; + + let directory = tempfile::tempdir().unwrap(); + let log = fs::File::create(directory.path().join("vmm.log")).unwrap(); + let id = log_job_id(&log).unwrap().unwrap(); + let name = log_job_name(id); + let system = std::env::var_os("SystemRoot").unwrap(); + let command = VmmCommand::new( + Path::new(&system).join("System32").join("cmd.exe"), + ["/c", "exit", "0"].map(OsString::from).to_vec(), + directory.path(), + ); + let refused = || { + let entered = Rc::new(Cell::new(false)); + let observed = Rc::clone(&entered); + let result = with_spawn_window( + move || observed.set(true), + || supervisor::spawn_vm(&command, &[7; CAPABILITY_LEN], log.try_clone().unwrap()), + ); + let error = match result { + Err(error) => error, + Ok(child) => { + assert!(child.kill_and_wait(), "the unexpected child did not exit"); + panic!("an untrusted log-job name started a child"); + } + }; + assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}"); + assert!( + !entered.get(), + "a refused spawn entered the creation window" + ); + }; + + for sddl in ["D:P", "D:P(A;;GA;;;WD)"] { + let other = job_with_descriptor(&name, sddl); + refused(); + drop(other); + } + // SAFETY: the name is NUL-terminated; this event occupies only this test's fresh log name. + let event = owned(unsafe { CreateEventW(ptr::null(), 1, 0, name.as_ptr()) }).unwrap(); + refused(); + drop(event); + + let other_log = fs::File::create(directory.path().join("other.log")).unwrap(); + let other = create_log_job(&other_log).unwrap().unwrap(); + let link = local_link(&name, &object_name(raw(&other)).unwrap()); + refused(); + drop(link); + drop(other); + let elsewhere: Vec = local_directory() + .unwrap() + .into_iter() + .chain(format!(r"\nvx-vm-spawn-{:032x}", id.1).encode_utf16()) + .collect(); + let link = local_link(&name, &elsewhere); + refused(); + drop(link); + + let entered = Rc::new(Cell::new(false)); + let observed = Rc::clone(&entered); + let child = with_spawn_window( + move || observed.set(true), + || supervisor::spawn_vm(&command, &[7; CAPABILITY_LEN], log), + ) + .unwrap(); + assert!(child.kill_and_wait(), "the trusted child did not exit"); + assert!( + entered.get(), + "the successful spawn bypassed the observation" + ); +} + #[test] fn what_a_link_under_the_name_of_a_logs_job_leads_to_is_not_the_logs() { let directory = tempfile::tempdir().unwrap(); @@ -1033,6 +1110,15 @@ fn a_vmm_whose_log_is_no_file_of_a_disk_volume_starts_without_a_job() { std::env::temp_dir(), ); for log in [null, pipe] { + let error = + match supervisor::spawn_vm(&command, &[7; CAPABILITY_LEN], log.try_clone().unwrap()) { + Err(error) => error, + Ok(child) => { + assert!(child.kill_and_wait(), "the unexpected child did not exit"); + panic!("a strict spawn accepted a log without a job"); + } + }; + assert_eq!(error.kind(), io::ErrorKind::PermissionDenied, "{error}"); let launched = supervisor::spawn(&command, &[7; CAPABILITY_LEN], log).unwrap(); assert!(launched.kill_and_wait(), "the stand-in did not exit"); } diff --git a/nvx-rs/src/host/spawn.rs b/nvx-rs/src/host/spawn.rs index f767b756a..e3ba23a12 100644 --- a/nvx-rs/src/host/spawn.rs +++ b/nvx-rs/src/host/spawn.rs @@ -21,9 +21,9 @@ use crate::host::process::POLL_INTERVAL; use crate::host::process::{BootId, KILL_TIMEOUT, Process, ProcessRef}; use crate::protocol::CAPABILITY_LEN; -/// How [`spawn`] starts a VMM: its executable, arguments, and working directory, whether it -/// leaves the caller's job object on Windows, and the descriptors or handles of the caller that it -/// inherits besides its standard streams. +/// How [`spawn`] or [`spawn_vm`] starts a VMM: its executable, arguments, and working directory, +/// whether it leaves the caller's job object on Windows, and the descriptors or handles of the +/// caller that it inherits besides its standard streams. #[derive(Debug, Clone)] pub struct VmmCommand<'a> { /// The VMM's executable. @@ -109,8 +109,18 @@ pub fn spawn( spawn_inner(command, capability, log, false) } -/// Starts a library VM only with a trusted log-named job on Windows. -pub(crate) fn spawn_vm( +/// Starts a VMM with the strict launch contract of [`crate::host::vm::VmmCommand`]. +/// +/// Uses [`spawn`]'s standard streams, inheritance allowlist, detachment, and process lifetime. +/// Unlike [`spawn`], on Windows this requires a trusted log-named job: a log that is not a file +/// of a disk volume, or an untrusted object or object-manager link under its job name, fails with +/// [`io::ErrorKind::PermissionDenied`] before any child is created. A job of the user already +/// holding that name fails with [`io::ErrorKind::AlreadyExists`], as in [`spawn`]. +/// +/// On other hosts this has the same behavior and errors as [`spawn`]. The caller retains its +/// owned inherited resources until the spawn returns, and keeps or ends the returned child as +/// [`Launched`] describes. +pub fn spawn_vm( command: &VmmCommand<'_>, capability: &[u8; CAPABILITY_LEN], log: File, diff --git a/nvx-rs/src/host/supervisor/mod.rs b/nvx-rs/src/host/supervisor/mod.rs index de970be30..5596cce43 100644 --- a/nvx-rs/src/host/supervisor/mod.rs +++ b/nvx-rs/src/host/supervisor/mod.rs @@ -26,6 +26,10 @@ //! A launch that fails at any step ends as [`Launch::abandon`] ends it, and never leaves an //! unrecorded VMM running: a VMM whose identity is not recorded is ended through its handle first. //! +//! A caller launching an owned [`crate::host::vm::VmmCommand`] without a store uses [`spawn_vm`], +//! which refuses an untrusted Windows log-job name rather than taking [`spawn`]'s fallback +//! without a job. +//! //! ```no_run //! use std::time::{Duration, Instant}; //! @@ -234,4 +238,4 @@ pub(crate) use self::reattach::reattach_reporting; pub use self::reattach::{Reattached, reattach}; pub use self::vmm::{StopOutcome, Stopped, Vmm}; pub use super::platform::inherit::{Inherited, SpawnLock, lock_spawns, spawn_command}; -pub use super::spawn::{Launched, VmmCommand, spawn}; +pub use super::spawn::{Launched, VmmCommand, spawn, spawn_vm}; From 13f83ff65fe48923c5abdf6c3bab107f5be39e50 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Tue, 10 Nov 2026 07:40:49 -0800 Subject: [PATCH 12/15] host: reconcile the external launcher with verified exit results Preserve the complete owned-command and strict-log-job handoff while replaying the PR onto the current feature input. Treat a missing exit proof as a timeout and propagate the outcome reader error before checking for a missing report. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5cb71c8-5f7f-4248-836b-4aae842c213d Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6a9302c-f126-4231-832e-22426190bd6b Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 89997040-ff41-4516-a86a-d81422d5a79f --- nvx-rs/examples/external-launch.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nvx-rs/examples/external-launch.rs b/nvx-rs/examples/external-launch.rs index 593617f00..ef12e4d0b 100644 --- a/nvx-rs/examples/external-launch.rs +++ b/nvx-rs/examples/external-launch.rs @@ -106,7 +106,7 @@ fn main() -> Result<(), Box> { Some(deadline.saturating_duration_since(Instant::now())), )?; read_until(console.as_mut(), b"\nNVX-EXTERNAL-LAUNCH-OK", deadline)?; - if !process.wait(deadline)? { + if process.wait(deadline)?.is_none() { return Err(io::Error::new(io::ErrorKind::TimedOut, "VM did not exit").into()); } match launched.wait_timeout(Duration::ZERO)? { @@ -116,7 +116,7 @@ fn main() -> Result<(), Box> { } None => return Err(io::Error::other("VMM exit status is unavailable").into()), } - let outcome = report::read(&outputs.outcome) + let outcome = report::read(&outputs.outcome)? .ok_or_else(|| io::Error::other("missing or invalid VM outcome"))?; if outcome.category.as_deref() != Some("success") || outcome.status_code != Some(0) From 555744ed97096070380a3c80c9e9c1369d59cbe0 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Tue, 10 Nov 2026 10:05:11 -0800 Subject: [PATCH 13/15] host: confirm external-launch cleanup after failed setup Expose the existing strict child termination and confirmed-wait operation for external callers. Use it on example setup errors, preserve both failure messages, and cover running and already-reaped children through the public host-only API. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5cb71c8-5f7f-4248-836b-4aae842c213d Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6a9302c-f126-4231-832e-22426190bd6b Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 89997040-ff41-4516-a86a-d81422d5a79f --- nvx-rs/README.md | 5 ++++- nvx-rs/examples/external-launch.rs | 7 +++++-- nvx-rs/public-api/linux.txt | 1 + nvx-rs/public-api/windows.txt | 1 + nvx-rs/src/host/spawn.rs | 27 +++++++++++++++++---------- nvx-rs/tests/checked_handoff.rs | 18 ++++++++++++++++++ 6 files changed, 46 insertions(+), 13 deletions(-) diff --git a/nvx-rs/README.md b/nvx-rs/README.md index d07b62b88..89b22dcc7 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -1099,7 +1099,10 @@ unrecorded VMM running. Windows log-job requirement as `vm::spawn`: an untrusted job name or a log that cannot name a job is refused before child creation. The legacy `spawn` keeps its fallback without a job for those logs and names. Other spawn behavior is -shared. +shared. If setup fails after a child is created, including failed process +verification, `Launched::kill_and_confirm_exit` waits without a deadline for +its exit and reaps it on Unix. The caller reports any cleanup error; the +legacy `kill_and_wait` remains time-bounded and may release a running child. `VmmCommand::inherit` lists the descriptors (Linux) or handles (Windows) that the VMM inherits under their numbers, besides its standard streams, and it diff --git a/nvx-rs/examples/external-launch.rs b/nvx-rs/examples/external-launch.rs index ef12e4d0b..7a41676ef 100644 --- a/nvx-rs/examples/external-launch.rs +++ b/nvx-rs/examples/external-launch.rs @@ -129,9 +129,12 @@ fn main() -> Result<(), Box> { Ok(()) })(); if let Err(error) = result { - if !launched.kill_and_wait() { + if let Err(cleanup) = launched.kill_and_confirm_exit() { return Err( - io::Error::other(format!("{error}; could not reap the launched VM")).into(), + io::Error::other(format!( + "{error}; could not confirm exit of the launched VM: {cleanup}" + )) + .into(), ); } return Err(error); diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index fe3c19e1e..c8ad384b7 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -3273,6 +3273,7 @@ method nvx::host::supervisor::Inherited::proc_path struct nvx::host::supervisor::Launched method nvx::host::supervisor::Launched::detach method nvx::host::supervisor::Launched::id +method nvx::host::supervisor::Launched::kill_and_confirm_exit method nvx::host::supervisor::Launched::kill_and_wait method nvx::host::supervisor::Launched::process method nvx::host::supervisor::Launched::wait_timeout diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index 5026d099b..7d19e53f9 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -1858,6 +1858,7 @@ method nvx::host::supervisor::Inherited::new struct nvx::host::supervisor::Launched method nvx::host::supervisor::Launched::detach method nvx::host::supervisor::Launched::id +method nvx::host::supervisor::Launched::kill_and_confirm_exit method nvx::host::supervisor::Launched::kill_and_wait method nvx::host::supervisor::Launched::process method nvx::host::supervisor::Launched::wait_timeout diff --git a/nvx-rs/src/host/spawn.rs b/nvx-rs/src/host/spawn.rs index e3ba23a12..11561d7be 100644 --- a/nvx-rs/src/host/spawn.rs +++ b/nvx-rs/src/host/spawn.rs @@ -181,14 +181,15 @@ fn spawn_inner( } } -/// A VMM that [`spawn`] started, which the caller has not released yet: on Unix, its child -/// process; on Windows, its process handle. On Unix the child's ID names no other process until -/// [`wait_timeout`](Self::wait_timeout) reaps it; hold it with [`process`](Self::process) before -/// waiting to keep a verified handle after it exits. +/// A VMM that [`spawn`] or [`spawn_vm`] started, which the caller has not released yet: on Unix, +/// its child process; on Windows, its process handle. On Unix the child's ID names no other +/// process until [`wait_timeout`](Self::wait_timeout) reaps it; hold it with +/// [`process`](Self::process) before waiting to keep a verified handle after it exits. /// /// Record it with [`Launch::record_process`](crate::host::store::Launch::record_process), or hold /// it with [`process`](Self::process). Then release it with [`detach`](Self::detach), or end it -/// with [`kill_and_wait`](Self::kill_and_wait); the [`Process`] stays valid either way. +/// with [`kill_and_confirm_exit`](Self::kill_and_confirm_exit); the [`Process`] stays valid +/// either way. [`kill_and_wait`](Self::kill_and_wait) provides legacy, time-bounded cleanup. pub struct Launched { #[cfg(not(windows))] /// Child process retained until it is detached or terminated. @@ -224,9 +225,9 @@ impl Launched { /// The caller has not reaped the process, so its ID and start time still identify it, and /// the reference of the `Process` is the one to record: a successor of the caller can find /// the process again with [`Process::open`]. The `Process` stays valid after - /// [`detach`](Self::detach) or [`kill_and_wait`](Self::kill_and_wait) releases this - /// `Launched`. A process that exited already keeps its identity, and the `Process` reports - /// that it exited. + /// [`detach`](Self::detach), [`kill_and_confirm_exit`](Self::kill_and_confirm_exit), or + /// [`kill_and_wait`](Self::kill_and_wait) releases this `Launched`. A process that exited + /// already keeps its identity, and the `Process` reports that it exited. /// /// Fails if the identity of the process or of the current boot cannot be read, and with /// [`io::ErrorKind::Unsupported`] on hosts other than Linux and Windows. @@ -300,8 +301,14 @@ impl Launched { /// Terminates this owned child and waits without a deadline for proof of its exit. /// - /// Unlike legacy cleanup's timeout, this never detaches a child after a failed verification. - pub(crate) fn kill_and_confirm_exit(self) -> io::Result<()> { + /// Unlike [`kill_and_wait`](Self::kill_and_wait), this has no timeout and never hands off a + /// child to a background reaper. On Unix a successful call reaps the child. Even if + /// termination fails, it waits for the child's exit; a child that exited already succeeds. + /// Use this when setup fails after [`spawn_vm`] created a child, including when + /// [`process`](Self::process) cannot verify its identity. + /// + /// Fails if waiting cannot confirm exit, retaining both failures if termination also failed. + pub fn kill_and_confirm_exit(self) -> io::Result<()> { #[cfg(windows)] return self.process.terminate_and_wait(); #[cfg(not(windows))] diff --git a/nvx-rs/tests/checked_handoff.rs b/nvx-rs/tests/checked_handoff.rs index 0d21eba1a..f8ff09199 100644 --- a/nvx-rs/tests/checked_handoff.rs +++ b/nvx-rs/tests/checked_handoff.rs @@ -225,9 +225,27 @@ fn launched_waits_return_and_retain_the_actual_exit_status() { let launched = vmm.launched.as_mut().unwrap(); assert_eq!(launched.wait_timeout(TIMEOUT).unwrap(), Some(status)); assert_eq!(launched.wait_timeout(Duration::ZERO).unwrap(), Some(status)); + vmm.launched.take().unwrap().kill_and_confirm_exit().unwrap(); } } +#[test] +fn strict_cleanup_terminates_and_confirms_a_running_child() { + let mut vmm = VmmDirectory::new(); + let command = VmmCommand::new( + env::current_exe().unwrap(), + fixture_arguments("vmm_fixture"), + vmm.path(), + ); + let log = File::create(vmm.path().join("vmm.log")).unwrap(); + vmm.launched = Some(supervisor::spawn_vm(&command, &CAPABILITY, log).unwrap()); + vmm.report(STARTED_REPORT); + let process = vmm.launched.as_ref().unwrap().process().unwrap(); + assert!(!process.has_exited().unwrap()); + vmm.launched.take().unwrap().kill_and_confirm_exit().unwrap(); + assert!(process.has_exited().unwrap()); +} + /// Returns the arguments that pass a VMM fixture the `--microvm-sandbox-block` values of /// `prepared`. fn block_arguments(prepared: &PreparedLaunch) -> Vec { From 4fdf55db0384354980a1267f64b5701d500969b8 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Tue, 10 Nov 2026 10:13:02 -0800 Subject: [PATCH 14/15] style: format confirmed external-launch cleanup Apply the repository rustfmt output from the pre-test formatting check without changing cleanup behavior or assertions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d5cb71c8-5f7f-4248-836b-4aae842c213d Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c6a9302c-f126-4231-832e-22426190bd6b Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 89997040-ff41-4516-a86a-d81422d5a79f --- nvx-rs/examples/external-launch.rs | 10 ++++------ nvx-rs/tests/checked_handoff.rs | 12 ++++++++++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/nvx-rs/examples/external-launch.rs b/nvx-rs/examples/external-launch.rs index 7a41676ef..634d5e548 100644 --- a/nvx-rs/examples/external-launch.rs +++ b/nvx-rs/examples/external-launch.rs @@ -130,12 +130,10 @@ fn main() -> Result<(), Box> { })(); if let Err(error) = result { if let Err(cleanup) = launched.kill_and_confirm_exit() { - return Err( - io::Error::other(format!( - "{error}; could not confirm exit of the launched VM: {cleanup}" - )) - .into(), - ); + return Err(io::Error::other(format!( + "{error}; could not confirm exit of the launched VM: {cleanup}" + )) + .into()); } return Err(error); } diff --git a/nvx-rs/tests/checked_handoff.rs b/nvx-rs/tests/checked_handoff.rs index f8ff09199..4f7f7f03c 100644 --- a/nvx-rs/tests/checked_handoff.rs +++ b/nvx-rs/tests/checked_handoff.rs @@ -225,7 +225,11 @@ fn launched_waits_return_and_retain_the_actual_exit_status() { let launched = vmm.launched.as_mut().unwrap(); assert_eq!(launched.wait_timeout(TIMEOUT).unwrap(), Some(status)); assert_eq!(launched.wait_timeout(Duration::ZERO).unwrap(), Some(status)); - vmm.launched.take().unwrap().kill_and_confirm_exit().unwrap(); + vmm.launched + .take() + .unwrap() + .kill_and_confirm_exit() + .unwrap(); } } @@ -242,7 +246,11 @@ fn strict_cleanup_terminates_and_confirms_a_running_child() { vmm.report(STARTED_REPORT); let process = vmm.launched.as_ref().unwrap().process().unwrap(); assert!(!process.has_exited().unwrap()); - vmm.launched.take().unwrap().kill_and_confirm_exit().unwrap(); + vmm.launched + .take() + .unwrap() + .kill_and_confirm_exit() + .unwrap(); assert!(process.has_exited().unwrap()); } From 0eaadb9c51d74e4de200710dc64df1f822bb9aa0 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Tue, 10 Nov 2026 15:14:59 -0800 Subject: [PATCH 15/15] style: format the composed configuration imports Apply the pinned formatter's import wrapping only. No producer body, test expectation or API changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 411c6e97-5b7a-47c8-8c79-4317cf344d63 --- nvx-rs/src/host/vm.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nvx-rs/src/host/vm.rs b/nvx-rs/src/host/vm.rs index 247398c3e..97331d384 100644 --- a/nvx-rs/src/host/vm.rs +++ b/nvx-rs/src/host/vm.rs @@ -23,8 +23,9 @@ use crate::activation::{ TransactionId, Upper, }; use crate::host::config::{ - BlockGeometry, CaptureConfig, ConsoleAttachment, DEFAULT_GUEST_NETWORK, DiskCapture, NetworkConfig, - NicConfig, RootConfig, UpperConfig, VmConfig, fit_process_command_line, reject_disk_path, + BlockGeometry, CaptureConfig, ConsoleAttachment, DEFAULT_GUEST_NETWORK, DiskCapture, + NetworkConfig, NicConfig, RootConfig, UpperConfig, VmConfig, fit_process_command_line, + reject_disk_path, }; use crate::host::filesystem; use crate::host::network;