diff --git a/guest/common/init b/guest/common/init index 322333176..38e818f7e 100644 --- a/guest/common/init +++ b/guest/common/init @@ -35,6 +35,13 @@ case "$cmdline" in *" nvx_sandbox=1 "*) sandbox_mode=true ;; *) sandbox_mode=false ;; esac +new_init=false +if [ "$sandbox_mode" = true ]; then + case "$cmdline" in + *" nvx_lifecycle="*) ;; + *) new_init=true ;; + esac +fi fatal() { echo "$*" >&2 @@ -189,7 +196,7 @@ for token in $cmdline; do esac done set +f -if [ -n "$nvx_exec" ]; then +if [ -n "$nvx_exec" ] && [ "$new_init" = false ]; then case "$nvx_exec" in /*) ;; *) fatal "nvx_exec must be an absolute initramfs path" ;; @@ -206,6 +213,7 @@ if [ "$sandbox_mode" = false ]; then mount_virtfs fi # virt-net: if --net attached a NIC, configure the only non-loopback interface. +announcer_pid= nip=$(sed -n 's/.*virtnet_ip=\([^ ]*\).*/\1/p' /proc/cmdline) if [ -n "$nip" ]; then nmask=$(sed -n 's/.*virtnet_mask=\([^ ]*\).*/\1/p' /proc/cmdline) @@ -289,6 +297,7 @@ if [ -n "$nip" ]; then allow | host-loopback) if [ -n "$ngw" ] && [ -x /sbin/nvx-ingress ]; then /sbin/nvx-ingress "$ngw" /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, @@ -569,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. @@ -579,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 @@ -593,6 +611,20 @@ 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. + +`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 @@ -670,12 +702,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 @@ -3227,7 +3262,15 @@ disk's block device. Each binary's command line, exit statuses, and byte output are compatibility contracts; the [`nvx::guest`](src/guest/mod.rs) documentation describes each -program. Guests run them as static `x86_64-unknown-linux-musl` executables. On +program. After a restored platform capture, `nvx-init` keeps one activation +across control-session replacements, with its deadline anchored to the repair +acknowledgement. It applies the restore's root and network, mounts the runtime, +starts and retains an announcer only for published listeners, switches once, +then completes and reports Ready. The host supplies the complete mapping table +before the first valid EXEC starts the restored workload; later executions and +byte-identical activation retries reuse the retained state. + +Guests run them as static `x86_64-unknown-linux-musl` executables. On Linux, with the musl target and a C compiler driver, `cc`, `nvx.py` builds them: ```bash diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index e52582295..913b6ffe4 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -3037,6 +3037,9 @@ 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::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 @@ -3112,6 +3115,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 @@ -3135,6 +3139,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 @@ -3689,6 +3694,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 @@ -3770,6 +3776,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..e60eb3617 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -1623,6 +1623,9 @@ 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::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 @@ -1698,6 +1701,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 @@ -1721,6 +1725,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 @@ -2274,6 +2279,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 @@ -2355,6 +2361,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/src/guest/init.rs b/nvx-rs/src/guest/init.rs index 75d3e3738..cc203229d 100644 --- a/nvx-rs/src/guest/init.rs +++ b/nvx-rs/src/guest/init.rs @@ -37,6 +37,7 @@ use std::ffi::{CStr, CString, OsStr}; use std::io::{self, Read}; +use std::net::Ipv4Addr; use std::os::fd::BorrowedFd; use std::os::unix::ffi::OsStrExt; use std::os::unix::process::ExitStatusExt; @@ -44,6 +45,7 @@ use std::path::{Path, PathBuf}; use std::process::{ExitCode, ExitStatus as ProcessStatus}; use std::time::{Duration, Instant}; +use crate::activation::Publication; use crate::protocol::cmdline; use crate::protocol::root::{DISTRO_MMIO_BASE, SCRATCH_MMIO_BASE}; @@ -56,6 +58,7 @@ use super::sys::{ }; mod boot; +pub(crate) use boot::{RestoreHandoff, Restored}; /// The process ID of the init process. const INIT_PROCESS: i32 = 1; @@ -188,7 +191,7 @@ pub(crate) struct Capture { /// Request ID, kill flag and the original pre-request deadline. pending: Option<(u64, bool, Instant)>, /// The repair whose RESET must be acknowledged before later activation. - repaired: Option, + pub(crate) restored: Option, /// A fatal capture disposition, whose status PID 1 must preserve. shutdown: Option, } @@ -218,14 +221,14 @@ impl Capture { helpers, fresh, pending: None, - repaired: None, + restored: None, shutdown: None, } } /// Keeps one pending request and anchors its deadline before cleanup. pub(crate) fn queue(&mut self, request_id: u64, kill: bool) -> bool { - if self.pending.is_some() || self.repaired.is_some() || self.shutdown.is_some() { + if self.pending.is_some() || self.restored.is_some() || self.shutdown.is_some() { return false; } self.pending = Some((request_id, kill, Instant::now() + CAPTURE_TIMEOUT)); @@ -244,12 +247,14 @@ impl Capture { /// Whether this session must admit no root-dependent operation. pub(crate) fn rootless(&self) -> bool { - self.fresh || self.repaired.is_some() + self.restored.as_ref().map_or(self.fresh, |restored| { + restored.activation.established().is_none() + }) } - /// Keeps the original repair for later activation, which this slice does not perform. + /// Starts one activation from the repair's original acknowledgement. pub(crate) fn restored(&mut self, repaired: Repaired) { - self.repaired = Some(repaired); + self.restored = Some(Restored::new(&repaired, Instant::now())); } /// Records the fatal status before the loop returns directly to PID 1. @@ -318,6 +323,14 @@ pub(crate) fn report_capture(failure: impl std::fmt::Display) { ); } +/// Reports an activation failure before PID 1 ends the VM. +pub(crate) fn report_activation(failure: impl std::fmt::Display) { + write_console( + kernel::CONSOLE_DEVICE, + format!("NVX-INIT-ACTIVATION: {failure}\n").as_bytes(), + ); +} + /// Whether execution requires an account in the shared root's databases. #[cfg_attr( not(test), @@ -1003,6 +1016,57 @@ impl Runtime { }) } + /// Starts ingress from the applied root's publication and gateway, before its root switch. + /// The fork keeps the initramfs root and PID 1's priority; the sole collector owns its exit. + pub(crate) fn restore_announcer( + &mut self, + publication: Publication, + gateway: Option, + ) -> crate::Result<()> { + if publication == Publication::Unpublished { + return Ok(()); + } + if self.helpers.announcer.is_some() { + return Err(crate::Error::unsupported( + "an announcer is already registered", + )); + } + let gateway = gateway + .ok_or_else(|| crate::Error::unsupported("published listeners require a gateway"))?; + let program = super::ingress::PROGRAM.to_owned(); + #[cfg(test)] + let program = tests::restore::announcer_program(program); + let arguments = [ + c"nvx-ingress".to_owned(), + CString::new(gateway.to_string()).expect("an IPv4 address has no NUL"), + ]; + sys::check_released().map_err(|error| { + crate::Error::io("cannot fork the restored announcer with port permission") + .with_source(error) + })?; + let fork = sys::fork().map_err(|error| { + crate::Error::io("cannot fork the restored announcer") + .with_source(io::Error::from(error)) + })?; + if let sys::ForkResult::Parent(process) = fork { + self.helpers.announcer = Some(process); + return Ok(()); + } + if let Err(error) = sys::reset_signals() { + write_console( + kernel::CONSOLE_DEVICE, + format!("NVX-INIT-ERROR: cannot reset announcer signals: {error}\n").as_bytes(), + ); + sys::exit_immediately(FAILURE); + } + let error = sys::exec(&program, &arguments, None); + write_console( + kernel::CONSOLE_DEVICE, + format!("NVX-INIT-ERROR: cannot exec /sbin/nvx-ingress: {error}\n").as_bytes(), + ); + sys::exit_immediately(FAILURE); + } + /// Poll alongside control input so a blocked peer cannot hide a helper's end. pub(crate) fn wake(&self) -> Pollable<'_> { Pollable::Signals(self.system.signals.as_ref().expect("listening")) diff --git a/nvx-rs/src/guest/init/boot.rs b/nvx-rs/src/guest/init/boot.rs index 2e66abc53..62c427890 100644 --- a/nvx-rs/src/guest/init/boot.rs +++ b/nvx-rs/src/guest/init/boot.rs @@ -1,13 +1,17 @@ -//! The fixed cold-boot sequence of the VM's shared layered root. +//! The fixed cold-boot and restored-space setup of the VM's shared layered root. use std::ffi::{CString, OsStr}; use std::net::IpAddr; use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; -use std::time::Duration; +use std::time::{Duration, Instant}; use crate::activation::{self, View}; use crate::gpt::Expected; +use crate::guest::activation::{ + self as guest_activation, Activation, ActivationAction, ActivationInput, ActivationState, + Applied, RootTemplate, +}; use crate::guest::mappings::{Access, Mapper, MappingTable, Reserved, SourceRoot, TargetRoot}; use crate::guest::root::{self, Assembly, Device, Root, ShareChild, ShareKind}; use crate::guest::sys::FileDescriptor; @@ -323,5 +327,187 @@ fn share_child(value: &[u8]) -> Result { }) } +/// One repaired activation and the space it owns across control epochs. +pub(crate) struct Restored { + pub(crate) activation: Activation, + payload: Option>, + _applied: Option, +} + +/// The account and open mapping handles handed to the shared dispatcher after activation. +pub(crate) struct RestoreHandoff { + pub(crate) workload: protocol::workload::WorkloadIdentity, + pub(crate) user: CString, + pub(crate) home: CString, + pub(crate) hostname: CString, + pub(crate) table: MappingTable, + pub(crate) mapper: Mapper, + pub(crate) export: Option, +} + +impl Restored { + /// Anchors the activation once, even when the next session attaches later. + pub(crate) fn new(repaired: &crate::guest::snapshot::Repaired, now: Instant) -> Self { + Self { + activation: Activation::new(repaired, Duration::from_secs(30), now), + payload: None, + _applied: None, + } + } + + /// Takes the first bootstrap reset or checks the same deadline on a replacement session. + pub(crate) fn reset(&mut self, now: Instant) -> Result<()> { + if self.activation.state() == ActivationState::Bootstrap { + self.activation + .input(now, ActivationInput::Reset) + .map_err(activation_error)?; + } else { + self.activation.tick(now).map_err(activation_error)?; + } + Ok(()) + } + + /// Compares the complete retry payload before delegating record replay to the state machine. + pub(crate) fn accept( + &mut self, + payload: &[u8], + now: Instant, + ) -> Result<(ActivationAction, protocol::workload::WorkloadIdentity)> { + if self + .payload + .as_deref() + .is_some_and(|accepted| accepted != payload) + { + return Err(Error::unsupported( + "the activate request changed after acceptance", + )); + } + let (_, workload) = protocol::decode_activate_payload(payload).map_err(protocol_error)?; + let action = self + .activation + .input(now, ActivationInput::Activate(&payload[9..])) + .map_err(activation_error)?; + if matches!(action, ActivationAction::Apply(_)) { + self.payload = Some(payload.to_vec()); + } + Ok((action, workload)) + } + + /// Applies and switches one restored space, retaining its root and handing over its mapper. + pub(crate) fn apply( + &mut self, + record: &activation::ActivationRecord, + workload: protocol::workload::WorkloadIdentity, + mut announce: impl FnMut(activation::Publication, Option) -> Result<()>, + ) -> Result { + let command_line = std::fs::read("/proc/cmdline").map_err(activation_error)?; + let template = RootTemplate { + runtime: "/run/nvx".into(), + distro: Device { + mmio: DISTRO_MMIO_BASE, + sectors: None, + }, + scratch: Device { + mmio: SCRATCH_MMIO_BASE, + sectors: None, + }, + timeout: Duration::from_secs(30), + filesystem_tag: "microvm".into(), + gateway: cmdline::gateway(&command_line).map_err(protocol_error)?, + }; + let (applied, handoff) = restore_in_order( + self.activation.deadline(), + || { + guest_activation::apply(record, &template, self.activation.deadline()) + .map_err(activation_error) + }, + |applied| { + applied + .root_mut() + .mount_runtime(Some(Path::new("/run/nvx/time"))) + .map_err(activation_error)?; + let mut target = + TargetRoot::open(applied.root().path()).map_err(activation_error)?; + target + .add_mount(&applied.root().path().join("run")) + .map_err(activation_error)?; + let (user, home) = account::resolve( + applied.root().path(), + (workload.uid, workload.gid), + workload.create_account, + )?; + Ok(RestoreHandoff { + workload, + user, + home, + hostname: CString::new(applied.hostname()).map_err(activation_error)?, + table: u32::try_from(record.configuration().mappings) + .ok() + .and_then(MappingTable::new) + .ok_or_else(|| { + Error::unsupported("the restored mapping count is invalid") + })?, + mapper: Mapper::new(target, Reserved::container_root()), + export: applied.take_export(), + }) + }, + |applied| { + if applied.publication() == activation::Publication::Published { + announce(applied.publication(), applied.gateway())?; + } + Ok(()) + }, + |applied| applied.root_mut().switch().map_err(activation_error), + |applied| applied.teardown().map_err(activation_error), + )?; + self._applied = Some(applied); + Ok(handoff) + } +} + +/// Closes prepared handles before tearing down a space whose setup failed. +fn restore_in_order( + deadline: Option, + apply: impl FnOnce() -> Result, + mount_runtime: impl FnOnce(&mut T) -> Result, + announce: impl FnOnce(&T) -> Result<()>, + switch: impl FnOnce(&mut T) -> Result<()>, + teardown: impl FnOnce(T) -> Result<()>, +) -> Result<(T, R)> { + let check = || { + if deadline.is_some_and(|deadline| Instant::now() >= deadline) { + Err(Error::io( + "the activation deadline expired before the next space step", + )) + } else { + Ok(()) + } + }; + check()?; + let mut applied = apply()?; + let result = check() + .and_then(|()| mount_runtime(&mut applied)) + .and_then(|retained| { + check()?; + announce(&applied)?; + check()?; + switch(&mut applied)?; + Ok(retained) + }); + match result { + Ok(retained) => Ok((applied, retained)), + Err(error) => match teardown(applied) { + Ok(()) => Err(error), + Err(cleanup) => { + Err(Error::io(format!("{error}; cleanup failed: {cleanup}")).with_source(error)) + } + }, + } +} + +fn activation_error(error: impl std::error::Error + Send + Sync + 'static) -> Error { + Error::io(format!("failed to activate the restored root: {error}")).with_source(error) +} + #[cfg(test)] mod tests; diff --git a/nvx-rs/src/guest/init/boot/tests.rs b/nvx-rs/src/guest/init/boot/tests.rs index 8ad287155..6ec9fef60 100644 --- a/nvx-rs/src/guest/init/boot/tests.rs +++ b/nvx-rs/src/guest/init/boot/tests.rs @@ -9,6 +9,219 @@ const CLI: &str = "nvx_identity=1000:1001 nvx_control_tty=hvc2"; const EXPORT: &str = "virtfs_dir=/run/nvx/hostfs/root virtfs_tag=hostfs virtfs_mode=rw virtfs_aggregate=1"; +#[test] +fn restored_sessions_keep_the_deadline_of_the_actual_repair_acknowledgement() { + let repaired = crate::guest::snapshot::repair::tests::repaired_fixture(); + let acknowledged = repaired.acknowledged_at().unwrap(); + let mut restored = Restored::new(&repaired, acknowledged + Duration::from_secs(12)); + let deadline = acknowledged + Duration::from_secs(30); + assert_eq!(restored.activation.deadline(), Some(deadline)); + restored + .reset(acknowledged + Duration::from_secs(13)) + .unwrap(); + restored + .reset(acknowledged + Duration::from_secs(20)) + .unwrap(); + assert_eq!(restored.activation.deadline(), Some(deadline)); + let (_, payload) = super::super::Capture::activation_fixture(); + assert!(matches!( + restored + .accept(&payload, acknowledged + Duration::from_secs(21)) + .unwrap() + .0, + ActivationAction::Apply(_) + )); + restored + .reset(acknowledged + Duration::from_secs(29)) + .unwrap(); + assert!(matches!( + restored + .accept(&payload, acknowledged + Duration::from_secs(29)) + .unwrap() + .0, + ActivationAction::Join + )); + assert_eq!(restored.activation.deadline(), Some(deadline)); + assert!( + restored + .reset(deadline) + .unwrap_err() + .to_string() + .contains("deadline") + ); + assert_eq!(restored.activation.state(), ActivationState::Failed); +} + +#[test] +fn restored_resource_steps_finish_before_completion_and_ready() { + use crate::activation::{Network, Publication, ReadyActivation}; + use std::cell::RefCell; + let (mut capture, mut payload) = super::super::Capture::activation_fixture(); + let (record, _) = protocol::decode_activate_payload(&payload).unwrap(); + let mut configuration = record.configuration().clone(); + configuration.network = Some(Network { + dns: Some("10.88.0.53".parse().unwrap()), + publication: Publication::Published, + proxy: Some("http://10.88.0.1:3128".into()), + }); + let record = + activation::ActivationRecord::new(record.restore().copied(), configuration).unwrap(); + payload.truncate(9); + payload.extend(record.encode()); + let restored = capture.restored.as_mut().unwrap(); + let (ActivationAction::Apply(accepted), _) = restored.accept(&payload, Instant::now()).unwrap() + else { + panic!("one accepted activation"); + }; + let calls = RefCell::new(Vec::new()); + let (retained, ()) = restore_in_order( + None, + || { + calls.borrow_mut().push("apply"); + Ok(accepted.configuration().clone()) + }, + |space| { + calls.borrow_mut().push("mount-runtime"); + assert_eq!( + space.network.as_ref().unwrap().dns, + Some("10.88.0.53".parse().unwrap()) + ); + assert_eq!( + space.upper, + activation::Upper::Tmpfs { + max_memory_percent: 63 + } + ); + Ok(()) + }, + |space| { + assert_eq!( + space.network.as_ref().unwrap().publication, + Publication::Published + ); + calls.borrow_mut().push("owned-announcer"); + Ok(()) + }, + |_| { + calls.borrow_mut().push("switch"); + Ok(()) + }, + |_| panic!("a successful switched root is retained"), + ) + .unwrap(); + assert_eq!(retained.network, accepted.configuration().network); + let ready = restored + .activation + .complete(Instant::now(), Ok(())) + .unwrap(); + calls.borrow_mut().push("complete"); + assert_eq!(ready, ReadyActivation::of(&accepted)); + calls.borrow_mut().push("ready"); + assert_eq!( + *calls.borrow(), + [ + "apply", + "mount-runtime", + "owned-announcer", + "switch", + "complete", + "ready" + ] + ); + assert!(matches!( + restored.accept(&payload, Instant::now()).unwrap().0, + ActivationAction::Ready(_) + )); +} + +#[test] +fn restored_setup_drops_prepared_handles_before_cleanup_and_preserves_both_errors() { + use std::cell::RefCell; + struct Handle<'a>(&'a RefCell>); + impl Drop for Handle<'_> { + fn drop(&mut self) { + self.0.borrow_mut().push("close-mapper"); + } + } + let calls = RefCell::new(Vec::new()); + let error = restore_in_order( + None, + || { + calls.borrow_mut().push("apply"); + Ok(()) + }, + |_| { + calls.borrow_mut().push("mount-runtime"); + Ok(Handle(&calls)) + }, + |_| { + calls.borrow_mut().push("announcer-failed"); + Err(Error::io("announcer failed")) + }, + |_| panic!("failed setup cannot switch"), + |_| { + calls.borrow_mut().push("teardown"); + Err(Error::io("teardown failed")) + }, + ) + .err() + .unwrap(); + assert_eq!( + *calls.borrow(), + [ + "apply", + "mount-runtime", + "announcer-failed", + "close-mapper", + "teardown" + ] + ); + assert!(error.to_string().contains("announcer failed")); + assert!(error.to_string().contains("teardown failed")); + assert!( + std::error::Error::source(&error) + .unwrap() + .to_string() + .contains("announcer failed") + ); +} + +#[test] +fn an_expired_activation_cannot_begin_or_advance_to_another_space_step() { + assert!( + restore_in_order::<(), ()>( + Some(Instant::now()), + || panic!("an expired activation cannot apply"), + |_| panic!("cannot mount"), + |_| panic!("cannot announce"), + |_| panic!("cannot switch"), + |_| panic!("nothing was applied"), + ) + .is_err() + ); + let deadline = Instant::now() + Duration::from_secs(1); + let applied = std::cell::Cell::new(false); + let cleaned = std::cell::Cell::new(false); + let error = restore_in_order::<(), ()>( + Some(deadline), + || { + applied.set(true); + std::thread::sleep(deadline.saturating_duration_since(Instant::now())); + Ok(()) + }, + |_| panic!("an application that used its deadline cannot mount runtime"), + |_| panic!("cannot announce"), + |_| panic!("cannot switch"), + |_| { + cleaned.set(true); + Ok(()) + }, + ) + .unwrap_err(); + assert_eq!(cleaned.get(), applied.get()); + assert!(error.to_string().contains("deadline")); +} + fn tokens(extra: &str) -> Tokens { Tokens::parse(format!("{ROOT} {VIEW} {CLI} {extra}").as_bytes()).unwrap() } diff --git a/nvx-rs/src/guest/init/tests.rs b/nvx-rs/src/guest/init/tests.rs index 706bf18a7..66e525181 100644 --- a/nvx-rs/src/guest/init/tests.rs +++ b/nvx-rs/src/guest/init/tests.rs @@ -9,6 +9,7 @@ mod capture; mod exec; +pub(super) mod restore; pub(super) use exec::prepare_child; use std::collections::VecDeque; diff --git a/nvx-rs/src/guest/init/tests/capture.rs b/nvx-rs/src/guest/init/tests/capture.rs index 78aca2f9e..d4a05839c 100644 --- a/nvx-rs/src/guest/init/tests/capture.rs +++ b/nvx-rs/src/guest/init/tests/capture.rs @@ -14,6 +14,51 @@ impl Capture { fresh, ) } + + pub(crate) fn activation_fixture() -> (Self, Vec) { + use crate::activation::{ + ActivationRecord, Configuration, ExpectedResources, Image, MachineIdMode, OverlayMode, + RestoreSection, TransactionId, Upper, View, + }; + let repaired = crate::guest::snapshot::repair::tests::repaired_fixture(); + let record = ActivationRecord::new( + Some(RestoreSection { + transaction: TransactionId::from_bytes([0x22; TransactionId::LEN]), + }), + Configuration { + image: Image::Erofs { uuid: None }, + upper: Upper::Tmpfs { + max_memory_percent: 63, + }, + overlay: OverlayMode::UntrustedLowers, + export: None, + mappings: 0, + network: None, + view: View { + hostname: b"restored".to_vec(), + machine_id: MachineIdMode::Image, + }, + resources: ExpectedResources { + cpus: repaired.cpus(), + memory: repaired.memory(), + }, + }, + ) + .unwrap(); + let mut capture = Self::fixture(true); + capture.restored(repaired); + capture + .restored + .as_mut() + .unwrap() + .reset(Instant::now()) + .unwrap(); + let mut payload = 1000u32.to_le_bytes().to_vec(); + payload.extend_from_slice(&1001u32.to_le_bytes()); + payload.push(1); + payload.extend(record.encode()); + (capture, payload) + } } #[test] diff --git a/nvx-rs/src/guest/init/tests/restore.rs b/nvx-rs/src/guest/init/tests/restore.rs new file mode 100644 index 000000000..86cf3a741 --- /dev/null +++ b/nvx-rs/src/guest/init/tests/restore.rs @@ -0,0 +1,313 @@ +//! The restore-only announcer step and its existing helper supervision. + +use std::cell::RefCell; +use std::io::{Seek, SeekFrom, Write}; +use std::os::unix::fs::PermissionsExt; +use std::path::Path; + +use super::*; + +thread_local! { + static PROGRAM: RefCell> = const { RefCell::new(None) }; +} + +pub(crate) fn announcer_program(program: CString) -> CString { + PROGRAM.with_borrow(|replacement| replacement.clone().unwrap_or(program)) +} + +fn with_program(path: &Path, run: impl FnOnce() -> T) -> T { + struct Restore(Option); + impl Drop for Restore { + fn drop(&mut self) { + PROGRAM.replace(self.0.take()); + } + } + let _restore = Restore(PROGRAM.replace(Some(sys::path_c_string(path).unwrap()))); + run() +} + +fn runtime() -> Runtime { + Runtime { + system: RealSystem { signals: None }, + helpers: Helpers { + time: pid(i32::MAX), + announcer: None, + }, + terminating: false, + } +} + +struct Child { + process: ProcessId, + descriptor: FileDescriptor, + collected: bool, +} + +impl Child { + fn new(process: ProcessId) -> Self { + Self { + process, + descriptor: sys::open_pidfd(process).unwrap(), + collected: false, + } + } +} + +impl Drop for Child { + fn drop(&mut self) { + if !self.collected { + let _ = sys::kill_through_pidfd(self.descriptor.borrowed(), false); + let _ = sys::wait(self.process); + } + } +} + +fn script(directory: &Path, body: &str) -> std::path::PathBuf { + let path = directory.join("announcer"); + std::fs::write(&path, format!("#!/bin/sh\n{body}\n")).unwrap(); + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700)).unwrap(); + path +} + +fn until(mut ready: impl FnMut() -> bool) { + let deadline = Instant::now() + Duration::from_secs(5); + while !ready() { + assert!(Instant::now() < deadline, "the announcer did not advance"); + std::thread::sleep(Duration::from_millis(1)); + } +} + +fn collect_failure(runtime: &mut Runtime, child: &mut Child) -> String { + assert!(sys::readable_or_hung_up(&[child.descriptor.borrowed()], 5000).unwrap()[0]); + let error = runtime + .service(|_, _| panic!("the restored announcer was treated as a workload")) + .unwrap_err(); + child.collected = true; + error +} + +fn blocked(process: ProcessId) -> u64 { + let status = std::fs::read_to_string(format!("/proc/{}/status", process.raw())).unwrap(); + let mask = status + .lines() + .find_map(|line| line.strip_prefix("SigBlk:")) + .unwrap(); + u64::from_str_radix(mask.trim(), 16).unwrap() +} + +#[test] +fn unpublished_restores_start_nothing_even_without_a_gateway() { + let mut runtime = runtime(); + for gateway in [None, Some(Ipv4Addr::new(10, 88, 0, 1))] { + runtime + .restore_announcer(Publication::Unpublished, gateway) + .unwrap(); + assert_eq!(runtime.helpers.announcer, None); + } +} + +#[test] +fn a_missing_gateway_or_an_existing_announcer_starts_no_replacement() { + let mut runtime = runtime(); + assert_eq!( + runtime + .restore_announcer(Publication::Published, None) + .unwrap_err() + .kind(), + crate::ErrorKind::Unsupported + ); + assert_eq!(runtime.helpers.announcer, None); + runtime.helpers.announcer = Some(pid(7)); + assert_eq!( + runtime + .restore_announcer(Publication::Published, Some(Ipv4Addr::new(10, 88, 0, 1))) + .unwrap_err() + .kind(), + crate::ErrorKind::Unsupported + ); + assert_eq!(runtime.helpers.announcer, Some(pid(7))); +} + +#[test] +fn the_restored_child_gets_only_the_gateway_and_joins_fatal_helper_supervision() { + let name = "guest::init::tests::restore::the_restored_child_gets_only_the_gateway_and_joins_fatal_helper_supervision"; + if isolate_process_test("NVX_INIT_RESTORED_CHILD_TEST", name) { + return; + } + let directory = tempfile::tempdir().unwrap(); + in_child(|| { + let record = directory.path().join("arguments"); + let program = script( + directory.path(), + &format!( + "printf '%s\\n' \"$#\" \"$1\" \"$PPID\" > '{}'\nexec sleep 30", + record.display() + ), + ); + let mut runtime = runtime(); + runtime.system.listen().unwrap(); + with_program(&program, || { + runtime + .restore_announcer(Publication::Published, Some(Ipv4Addr::new(10, 88, 0, 1))) + .unwrap(); + }); + let mut child = Child::new(runtime.helpers.announcer.unwrap()); + let expected = format!("1\n10.88.0.1\n{}\n", sys::current_process().raw()); + until(|| std::fs::read_to_string(&record).is_ok_and(|record| record == expected)); + assert_eq!(nice_of(child.process), nice_of(sys::current_process())); + assert_eq!( + std::fs::read(format!("/proc/{}/oom_score_adj", child.process.raw())).unwrap(), + std::fs::read("/proc/self/oom_score_adj").unwrap() + ); + sys::kill_through_pidfd(child.descriptor.borrowed(), false).unwrap(); + let error = collect_failure(&mut runtime, &mut child); + assert_eq!( + error, + format!( + "the announcer, process {}, was killed by signal 9", + child.process.raw() + ) + ); + let mut system = Fake::default(); + finish(&mut system, Err(error.clone())); + assert_eq!(system.calls, [failure(&error), Call::ShutDown(FAILURE)]); + }); +} + +#[test] +fn a_failed_exec_remains_a_registered_fatal_helper() { + let name = "guest::init::tests::restore::a_failed_exec_remains_a_registered_fatal_helper"; + if isolate_process_test("NVX_INIT_RESTORED_EXEC_FAILURE_TEST", name) { + return; + } + in_child(|| { + let mut runtime = runtime(); + runtime.system.listen().unwrap(); + with_program(Path::new("/nonexistent/nvx-ingress"), || { + runtime + .restore_announcer(Publication::Published, Some(Ipv4Addr::new(10, 88, 0, 1))) + .unwrap(); + }); + let mut child = Child::new(runtime.helpers.announcer.unwrap()); + assert_eq!( + collect_failure(&mut runtime, &mut child), + format!( + "the announcer, process {}, exited with status 125", + child.process.raw() + ) + ); + }); +} + +#[test] +fn the_restored_announcer_unblocks_pid_1_signals_before_exec() { + let name = + "guest::init::tests::restore::the_restored_announcer_unblocks_pid_1_signals_before_exec"; + if isolate_process_test("NVX_INIT_RESTORED_SIGNALS_TEST", name) { + return; + } + in_child(|| { + let mut runtime = runtime(); + runtime.system.listen().unwrap(); + assert_ne!(blocked(sys::current_process()), 0); + with_program(Path::new("/bin/false"), || { + runtime + .restore_announcer(Publication::Published, Some(Ipv4Addr::new(10, 88, 0, 1))) + .unwrap(); + }); + let mut child = Child::new(runtime.helpers.announcer.unwrap()); + assert!(sys::readable_or_hung_up(&[child.descriptor.borrowed()], 5000).unwrap()[0]); + assert_eq!(blocked(child.process), 0); + assert_eq!( + collect_failure(&mut runtime, &mut child), + format!( + "the announcer, process {}, exited with status 1", + child.process.raw() + ) + ); + }); +} + +#[test] +fn a_refused_fork_keeps_the_helper_registry_unchanged() { + let mut runtime = runtime(); + let error = runtime + .restore_announcer(Publication::Published, Some(Ipv4Addr::new(10, 88, 0, 1))) + .unwrap_err(); + assert_eq!(error.kind(), crate::ErrorKind::Io); + assert_eq!(runtime.helpers.announcer, None); +} + +#[test] +#[ignore = "requires a guest's root, CAP_SYS_NICE, CAP_SYS_RESOURCE, and CAP_SYS_CHROOT"] +fn the_restored_helper_keeps_the_pre_switch_root_and_pid_1_priority() { + let name = "guest::init::tests::restore::the_restored_helper_keeps_the_pre_switch_root_and_pid_1_priority"; + if std::env::var_os("NVX_INIT_RESTORED_PRIORITY_TEST").is_none() { + let status = Command::new(std::env::current_exe().unwrap()) + .args(["--exact", name, "--ignored", "--nocapture"]) + .env("NVX_INIT_RESTORED_PRIORITY_TEST", "1") + .status() + .unwrap(); + assert!(status.success()); + return; + } + assert!(holds_capability(CAP_SYS_NICE).unwrap()); + assert!(holds_capability(CAP_SYS_RESOURCE).unwrap()); + let directory = tempfile::tempdir().unwrap(); + in_child(|| { + let root = directory.path().join("new-root"); + std::fs::create_dir(&root).unwrap(); + let marker = directory.path().join("old-root-marker"); + std::fs::write(&marker, b"the helper kept the initramfs root\n").unwrap(); + let ready = directory.path().join("ready"); + let gate = directory.path().join("gate"); + let output = directory.path().join("observed"); + let mut gate_file = std::fs::File::create(&gate).unwrap(); + let mut output_file = std::fs::OpenOptions::new() + .create(true) + .truncate(true) + .read(true) + .write(true) + .open(&output) + .unwrap(); + let program = script( + directory.path(), + &format!( + "printf ready > '{}'\nwhile test ! -s '{}'; do sleep 0.01; done\ncat '{}' > '{}'\nexec sleep 30", + ready.display(), + gate.display(), + marker.display(), + output.display() + ), + ); + let mut runtime = runtime(); + runtime.system.listen().unwrap(); + runtime.system.raise_priority(None).unwrap(); + with_program(&program, || { + runtime + .restore_announcer(Publication::Published, Some(Ipv4Addr::new(10, 88, 0, 1))) + .unwrap(); + }); + let mut child = Child::new(runtime.helpers.announcer.unwrap()); + until(|| ready.is_file()); + assert_eq!(nice_of(child.process), -10); + assert_eq!( + std::fs::read_to_string(format!("/proc/{}/oom_score_adj", child.process.raw())) + .unwrap() + .trim(), + "-1000" + ); + nix::unistd::chroot(&root).unwrap(); + std::env::set_current_dir("/").unwrap(); + gate_file.write_all(b"continue\n").unwrap(); + let mut observed = String::new(); + until(|| { + output_file.seek(SeekFrom::Start(0)).unwrap(); + observed.clear(); + output_file.read_to_string(&mut observed).unwrap(); + observed == "the helper kept the initramfs root\n" + }); + sys::kill_through_pidfd(child.descriptor.borrowed(), false).unwrap(); + assert!(collect_failure(&mut runtime, &mut child).contains("the announcer")); + }); +} diff --git a/nvx-rs/src/guest/managed_agent/dispatch.rs b/nvx-rs/src/guest/managed_agent/dispatch.rs index 53a071d65..ad13b8437 100644 --- a/nvx-rs/src/guest/managed_agent/dispatch.rs +++ b/nvx-rs/src/guest/managed_agent/dispatch.rs @@ -4,8 +4,9 @@ use crate::guest::init::Runtime; use crate::guest::mappings::{Mapper, MappingTable, SourceRoot}; use crate::guest::sys::{self, Errno, FileDescriptor}; use crate::protocol::{ - self, APP_CANCEL, APP_CAPTURE, APP_EXEC, APP_FEATURES, APP_MAPS, APP_PING, APP_READY, APP_STOP, - APP_STOPPED, GuestFeatures, INVALID_REQUEST, MAPPINGS_INCOMPLETE, UNSUPPORTED_OPERATION, + self, APP_ACTIVATE, APP_CANCEL, APP_CAPTURE, APP_EXEC, APP_FEATURES, APP_MAPS, APP_PING, + APP_READY, APP_STOP, APP_STOPPED, GuestFeatures, INVALID_REQUEST, MAPPINGS_INCOMPLETE, + UNSUPPORTED_OPERATION, }; use super::diagnostics; @@ -126,6 +127,7 @@ pub(crate) fn run( Err(error) }; } + let mut restored = None; loop { let payload = match session.next_event() { Ok(Event::Reset) => { @@ -146,7 +148,7 @@ pub(crate) fn run( // more specific stage. Its status is the error that ends the session, which // `control-session` reports too: `EPROTO` where C's `errno` was incidental, as after a // validation failure. - match handle_data(system, session, config, mappings, &payload) { + match handle_data(system, session, config, &mut restored, mappings, &payload) { Ok(Some(outcome)) => return Ok(outcome), Ok(None) => {} Err(_) if session.stopping() => return Ok(Outcome::Stopped), @@ -189,6 +191,7 @@ fn handle_data( system: &mut impl System, session: &mut Session, config: &AgentConfig, + restored: &mut Option, mappings: &mut HostMappings, payload: &[u8], ) -> Result, Errno> { @@ -199,7 +202,7 @@ fn handle_data( return Err(failure.into_error()); } }; - if session.rootless() && !matches!(request.kind, APP_CAPTURE | APP_STOP) { + if session.rootless() && !matches!(request.kind, APP_ACTIVATE | APP_CAPTURE | APP_STOP) { session.send_error( request.request_id, Errno::ENOTSUP as i32, @@ -207,7 +210,23 @@ fn handle_data( )?; return Ok(None); } + let config = restored.as_ref().unwrap_or(config); match request.kind { + APP_ACTIVATE if session.has_restored() => { + if let Some(handoff) = session.activate(request)? { + *mappings = HostMappings::layered(handoff.table, handoff.mapper, handoff.export)?; + *restored = Some(AgentConfig { + rootfs: c"-".to_owned(), + hostname: handoff.hostname, + uid: CString::new(handoff.workload.uid.to_string()).expect("decimal UID"), + gid: CString::new(handoff.workload.gid.to_string()).expect("decimal GID"), + user: handoff.user, + home: handoff.home, + direct: true, + entry: None, + }); + } + } APP_CAPTURE => { if session.queue_capture(request, false)? { return session.finish_capture(); @@ -245,6 +264,7 @@ fn handle_data( return Ok(None); } }; + session.start_activation()?; exec::run(system, session, config, request.request_id, &execution)?; if session.capture_pending() { return session.finish_capture(); diff --git a/nvx-rs/src/guest/managed_agent/dispatch/tests.rs b/nvx-rs/src/guest/managed_agent/dispatch/tests.rs index 9efb42f47..a94523509 100644 --- a/nvx-rs/src/guest/managed_agent/dispatch/tests.rs +++ b/nvx-rs/src/guest/managed_agent/dispatch/tests.rs @@ -440,6 +440,141 @@ fn mappings_mount_in_order_and_unblock_workloads_through_dispatch() { assert_eq!(system.spawn_calls, 0); } +#[test] +fn init_refuses_activation_without_a_restore_and_keeps_serving() { + for fresh in [true, false] { + let config = AgentConfig::for_test(None); + let mut mappings = HostMappings::announced_for(&config, 0); + let mut system = TestSystem::new(Paths::default()); + let (host, guest) = UnixStream::pair().unwrap(); + let thread = thread::spawn(move || { + let mut session = Session::new(FileDescriptor::from_owned(guest.into())) + .with_capture(crate::guest::init::Capture::fixture(fresh)); + let outcome = run(&mut system, &mut session, &config, &mut mappings); + (outcome, system) + }); + let mut harness = Harness { + host, + thread, + epoch: 0, + host_sequence: 0, + guest_sequence: 0, + }; + assert_eq!(harness.read_outer().0.record_type, OUTER_GUEST_ATTACH); + for epoch in [1, 2] { + harness.reset(epoch, 7); + assert_eq!( + harness.request(APP_ACTIVATE, b"misrouted"), + response( + APP_ERROR, + 42, + Errno::ENOTSUP as i32, + b"unsupported-operation" + ) + ); + } + if !fresh { + assert_eq!( + harness.request(APP_PING, &[]), + response(APP_READY, 42, 0, b"") + ); + } + let system = harness.stop(); + assert!(system.diagnostics.is_empty()); + assert_eq!(system.spawn_calls, 0); + } +} + +#[test] +fn restored_ready_maps_and_first_exec_share_the_existing_start_transition() -> TestResult { + use crate::guest::activation::{ActivationAction, ActivationState}; + use crate::guest::init::Capture; + if super::super::tests::isolate_process_test( + "NVX_RESTORED_DISPATCH_TEST", + "guest::managed_agent::dispatch::tests::restored_ready_maps_and_first_exec_share_the_existing_start_transition", + ) { + return Ok(()); + } + let _process = super::super::tests::PROCESS_TEST_LOCK.lock()?; + let (temporary, mut system) = TestSystem::with_host_export(); + system.direct_starter = Some(super::super::seam::tests::start_as_privileged); + let target = temporary.path().canonicalize()?.join("guest"); + let first = maps_payload( + 0, + &[(b"0/src", target.join("src").as_os_str().as_bytes(), 0)], + None, + ); + let second = maps_payload( + 1, + &[(b"1", target.join("out").as_os_str().as_bytes(), 0)], + None, + ); + let config = AgentConfig::for_test(None); + let mut mappings = HostMappings::announced_for(&config, 2); + let (mut capture, payload) = Capture::activation_fixture(); + let activation = capture.restored.as_mut().unwrap(); + assert!(matches!( + activation.accept(&payload, std::time::Instant::now())?.0, + ActivationAction::Apply(_) + )); + let ready = activation + .activation + .complete(std::time::Instant::now(), Ok(()))? + .encode(); + let (host, guest) = UnixStream::pair()?; + let thread = thread::spawn(move || { + let mut session = + Session::new(FileDescriptor::from_owned(guest.into())).with_capture(capture); + let outcome = run(&mut system, &mut session, &config, &mut mappings); + assert_eq!( + session.activation_state_for_test(), + Some(ActivationState::Started) + ); + (outcome, system) + }); + let mut harness = Harness { + host, + thread, + epoch: 0, + host_sequence: 0, + guest_sequence: 0, + }; + assert_eq!(harness.read_outer().0.record_type, OUTER_GUEST_ATTACH); + harness.reset(1, 7); + assert_eq!( + harness.request(APP_ACTIVATE, &payload), + response(APP_READY, 42, 0, &ready) + ); + let execution = workload_payload(&["/bin/sh", "-c", "printf restored"], None)?; + for maps in [&first, &second] { + assert_eq!( + harness.request(APP_EXEC, &execution), + response(APP_ERROR, 42, Errno::EAGAIN as i32, b"mappings-incomplete"), + ); + assert_eq!( + harness.request(APP_MAPS, maps), + response(APP_READY, 42, 0, b"") + ); + } + assert_eq!( + stdout_from(harness.request_all(43, APP_EXEC, &execution), 43, 0), + b"restored" + ); + harness.reset(2, 4); + assert_eq!( + harness.request(APP_ACTIVATE, &payload), + response(APP_READY, 42, 0, &ready) + ); + assert_eq!( + stdout_from(harness.request_all(44, APP_EXEC, &execution), 44, 0), + b"restored" + ); + let system = harness.stop(); + assert!(system.diagnostics.is_empty()); + assert_eq!(system.kernel.moves().len(), 2); + Ok(()) +} + #[test] fn mapping_sequence_errors_are_reported_through_dispatch() { let (temporary, system) = TestSystem::with_host_export(); diff --git a/nvx-rs/src/guest/managed_agent/session.rs b/nvx-rs/src/guest/managed_agent/session.rs index 2256e29fe..c4d618ed0 100644 --- a/nvx-rs/src/guest/managed_agent/session.rs +++ b/nvx-rs/src/guest/managed_agent/session.rs @@ -1,8 +1,10 @@ use std::cell::{Cell, RefCell}; use std::io::{self, Read, Write}; use std::process::ExitStatus; +use std::time::{Duration, Instant}; -use crate::guest::init::{self, Capture, CaptureStep, Runtime}; +use crate::guest::activation::{ActivationAction, ActivationState}; +use crate::guest::init::{self, Capture, CaptureStep, RestoreHandoff, Restored, Runtime}; use crate::guest::nvxs::{GuestConnection, GuestConnectionError}; use crate::guest::snapshot::{Captured, Disposition}; use crate::guest::sys::{self, Errno, FileDescriptor, Pollable}; @@ -28,9 +30,23 @@ struct Control<'r> { leader: Cell, status: Cell>, outcome: RefCell>, + /// Bounds partial I/O until the restored activation's Ready has been written. + deadline: Cell>, } impl Control<'_> { + fn remaining(&self) -> Result, Errno> { + self.deadline + .get() + .map(|deadline| { + deadline + .checked_duration_since(Instant::now()) + .filter(|remaining| !remaining.is_zero()) + .ok_or(Errno::ETIMEDOUT) + }) + .transpose() + } + fn service_leader(&self) -> Result<(), Errno> { if self.service(|id, status| { // Keep the first leader status across partial I/O, not unrelated orphans. @@ -65,24 +81,32 @@ impl Read for Control<'_> { if bytes.is_empty() { return Read::read(&mut self.descriptor, bytes); } - let Some(runtime) = &self.runtime else { + if self.runtime.is_none() && self.deadline.get().is_none() { return Read::read(&mut self.descriptor, bytes); - }; + } loop { - self.service_leader()?; - let signals = sys::poll( - [Pollable::File(&self.descriptor), runtime.borrow().wake()], - None, - )?[1]; - if signals.error { - return Err(Errno::EIO.into()); + if self.runtime.is_some() { + self.service_leader()?; } - if !signals.readable { - // A control-only wake includes HUP, which PollEvents does not expose. - match self.descriptor.read(bytes) { - Err(Errno::EAGAIN) => {} - result => return result.map_err(io::Error::from), + let timeout = self.remaining()?; + if let Some(runtime) = &self.runtime { + let signals = sys::poll( + [Pollable::File(&self.descriptor), runtime.borrow().wake()], + timeout, + )?[1]; + if signals.error { + return Err(Errno::EIO.into()); } + if signals.readable { + continue; + } + } else { + sys::poll([Pollable::File(&self.descriptor)], timeout)?; + } + self.remaining()?; + match self.descriptor.read(bytes) { + Err(Errno::EAGAIN) => {} + result => return result.map_err(io::Error::from), } } } @@ -90,31 +114,42 @@ impl Read for Control<'_> { impl Write for Control<'_> { fn write(&mut self, bytes: &[u8]) -> io::Result { - let Some(runtime) = &self.runtime else { + if self.runtime.is_none() && self.deadline.get().is_none() { return Write::write(&mut self.descriptor, bytes); - }; + } if bytes.is_empty() { return Write::write(&mut self.descriptor, bytes); } loop { - self.service_leader()?; + self.remaining()?; + if self.runtime.is_some() { + self.service_leader()?; + } match self.descriptor.write(bytes) { - Err(Errno::EAGAIN) => loop { - let events = sys::poll( - [ - Pollable::Writable(&self.descriptor), - runtime.borrow().wake(), - ], - None, - )?; - if events[1].error { - return Err(Errno::EIO.into()); - } - self.service_leader()?; - if events[0].writable || !events[1].readable { - break; + Err(Errno::EAGAIN) => { + if let Some(runtime) = &self.runtime { + loop { + let events = sys::poll( + [ + Pollable::Writable(&self.descriptor), + runtime.borrow().wake(), + ], + self.remaining()?, + )?; + self.remaining()?; + if events[1].error { + return Err(Errno::EIO.into()); + } + self.service_leader()?; + if events[0].writable || !events[1].readable { + break; + } + } + } else { + sys::poll([Pollable::Writable(&self.descriptor)], self.remaining()?)?; + self.remaining()?; } - }, + } result => return result.map_err(io::Error::from), } } @@ -206,6 +241,7 @@ impl<'r> Session<'r> { leader: Cell::new(0), status: Cell::new(None), outcome: RefCell::new(Ok(false)), + deadline: Cell::new(None), }), capture: None, last_error: None, @@ -223,6 +259,12 @@ impl<'r> Session<'r> { self.capture.is_some() } + pub(crate) fn has_restored(&self) -> bool { + self.capture + .as_ref() + .is_some_and(|capture| capture.restored.is_some()) + } + /// Whether root-dependent requests must remain refused. pub(crate) fn rootless(&self) -> bool { self.capture.as_ref().is_some_and(Capture::rootless) @@ -314,8 +356,17 @@ impl<'r> Session<'r> { } Ok(Captured::Restored(repaired)) => { capture.restored(repaired); + let restored = capture.restored.as_mut().expect("the repair was retained"); + self.descriptor().set_nonblocking()?; + self.connection + .get_ref() + .deadline + .set(restored.activation.deadline()); match self.next_event() { - Ok(Event::Reset) => self.acknowledge()?, + Ok(Event::Reset) => { + restored.reset(Instant::now()).map_err(activation_failure)?; + self.acknowledge()?; + } Ok(Event::Data(_)) => return Err(Errno::EPROTO), Err(failure) => { return Err(super::dispatch::report_failure_for_capture(failure)); @@ -342,6 +393,117 @@ impl<'r> Session<'r> { result } + /// Runs init's restored space setup or returns its cached activation result. + pub(crate) fn activate( + &mut self, + request: protocol::AppFrame<'_>, + ) -> Result, Errno> { + self.activate_with(request, |restored, session, record, workload| { + let runtime = session + .connection + .get_ref() + .runtime + .as_ref() + .ok_or_else(|| crate::Error::unsupported("activation needs the init runtime"))?; + restored.apply(record, workload, |publication, gateway| { + runtime.borrow_mut().restore_announcer(publication, gateway) + }) + }) + } + + /// Keeps one application and its result while replacement sessions join before Ready. + fn activate_with( + &mut self, + request: protocol::AppFrame<'_>, + apply: impl FnOnce( + &mut Restored, + &mut Self, + &crate::activation::ActivationRecord, + protocol::workload::WorkloadIdentity, + ) -> crate::Result, + ) -> Result, Errno> { + let mut capture = self.capture.take().ok_or(Errno::ENOTSUP)?; + let result = (|| { + let restored = capture.restored.as_mut().ok_or(Errno::EPROTO)?; + let (action, workload) = restored + .accept(request.payload, Instant::now()) + .map_err(activation_failure)?; + let (retained, ready, replies) = match action { + ActivationAction::Apply(record) => { + let retained = match apply(restored, self, &record, workload) { + Ok(retained) => retained, + Err(error) => { + init::report_activation(&error); + return Err(activation_failure( + restored + .activation + .complete(Instant::now(), Err(error.into())) + .expect_err("a failed application cannot become ready"), + )); + } + }; + let mut replies = vec![request.request_id]; + while sys::readable_or_hung_up(&[self.descriptor().borrowed()], 0)?[0] { + match self + .next_event() + .map_err(super::dispatch::report_failure_for_capture)? + { + Event::Reset => { + restored.reset(Instant::now()).map_err(activation_failure)?; + self.acknowledge()?; + replies.clear(); + } + Event::Data(bytes) => { + let retry = self + .accept_request(&bytes) + .map_err(DataFailure::into_error)?; + if retry.kind != protocol::APP_ACTIVATE + || !matches!( + restored + .accept(retry.payload, Instant::now()) + .map_err(activation_failure)? + .0, + ActivationAction::Join + ) + { + return Err(Errno::EPROTO); + } + replies.push(retry.request_id); + } + } + } + let ready = restored + .activation + .complete(Instant::now(), Ok(())) + .map_err(activation_failure)?; + (Some(retained), ready, replies) + } + ActivationAction::Ready(ready) => (None, ready, vec![request.request_id]), + _ => return Err(Errno::EPROTO), + }; + for id in replies { + self.send_app(protocol::APP_READY, id, 0, &ready.encode())?; + } + self.connection.get_ref().deadline.set(None); + Ok(retained) + })(); + self.capture = Some(capture); + result + } + + /// Takes the first valid mapped EXEC as the start transition, never starting twice. + pub(crate) fn start_activation(&mut self) -> Result<(), Errno> { + if let Some(restored) = self + .capture + .as_mut() + .and_then(|capture| capture.restored.as_mut()) + && restored.activation.state() != ActivationState::Started + { + restored.activation.start().map_err(activation_failure)?; + } + Ok(()) + } + /// Borrows init's sole collector for the lifetime of this control session. pub(crate) fn with_runtime( descriptor: FileDescriptor, @@ -419,6 +581,11 @@ impl<'r> Session<'r> { Ok(GuestEvent::Reset(_)) => { if let Some(capture) = &mut self.capture { capture.reset(); + if let Some(restored) = &mut capture.restored { + restored + .reset(Instant::now()) + .map_err(|error| ReceiveFailure::Read(activation_failure(error)))?; + } } Ok(Event::Reset) } @@ -517,6 +684,11 @@ impl<'r> Session<'r> { } } +fn activation_failure(error: impl std::fmt::Display) -> Errno { + init::report_activation(error); + Errno::EPROTO +} + /// Returns the stage at which the C agent reported a record that the session refused, or `None` /// for one that it reported as a failure to read. /// diff --git a/nvx-rs/src/guest/managed_agent/session/tests.rs b/nvx-rs/src/guest/managed_agent/session/tests.rs index ee79358d8..d4147b2de 100644 --- a/nvx-rs/src/guest/managed_agent/session/tests.rs +++ b/nvx-rs/src/guest/managed_agent/session/tests.rs @@ -12,6 +12,16 @@ use crate::protocol::{ const INSTANCE: [u8; 16] = [7; 16]; +impl Session<'_> { + pub(crate) fn activation_state_for_test(&self) -> Option { + self.capture + .as_ref()? + .restored + .as_ref() + .map(|state| state.activation.state()) + } +} + struct Peer { stream: UnixStream, epoch: u64, @@ -20,6 +30,15 @@ struct Peer { } impl Peer { + fn reset(&mut self, epoch: u64, sequence: u64) { + self.stream + .write_all(&encode_outer(OUTER_RESET, &INSTANCE, epoch, sequence, &[]).unwrap()) + .unwrap(); + self.epoch = epoch; + self.host_sequence = sequence + 1; + self.guest_sequence = 0; + } + fn read(&mut self) -> (OuterHeader, Vec) { let mut bytes = [0; OUTER_HEADER_LEN]; self.stream.read_exact(&mut bytes).unwrap(); @@ -95,6 +114,219 @@ fn connected(fresh: bool) -> (Session<'static>, Peer) { (session, peer) } +fn restored() -> (Session<'static>, Peer, Vec) { + let (mut session, peer) = connected(true); + let (capture, payload) = Capture::activation_fixture(); + let deadline = capture.restored.as_ref().unwrap().activation.deadline(); + session.capture = Some(capture); + session.descriptor().set_nonblocking().unwrap(); + session.connection.get_ref().deadline.set(deadline); + (session, peer, payload) +} + +fn activate_bytes(session: &mut Session<'_>, peer: &mut Peer, id: u64, payload: &[u8]) -> Vec { + peer.send(protocol::APP_ACTIVATE, id, payload); + let Event::Data(bytes) = session + .next_event() + .unwrap_or_else(|_| panic!("activate DATA")) + else { + panic!("the activate is DATA"); + }; + bytes +} + +#[test] +fn a_replacement_session_joins_one_activation_and_caches_ready_without_reapplying() { + let (mut session, mut peer, payload) = restored(); + let deadline = session.connection.get_ref().deadline.get(); + let (record, _) = protocol::decode_activate_payload(&payload).unwrap(); + let ready = crate::activation::ReadyActivation::of(&record).encode(); + let bytes = activate_bytes(&mut session, &mut peer, 61, &payload); + let request = session + .accept_request(&bytes) + .unwrap_or_else(|_| panic!("valid activate")); + peer.credit(); + let retained = session + .activate_with(request, |restored, _, accepted, workload| { + assert_eq!(restored.activation.state(), ActivationState::Activating); + assert_eq!(restored.activation.deadline(), deadline); + assert_eq!(accepted, &record); + assert_eq!( + (workload.uid, workload.gid, workload.create_account), + (1000, 1001, true) + ); + peer.reset(2, 4); + peer.send(protocol::APP_ACTIVATE, 62, &payload); + Ok("one applied space") + }) + .unwrap(); + assert_eq!(retained, Some("one applied space")); + assert_eq!(peer.read().0.record_type, OUTER_ACK); + peer.credit(); + peer.app(APP_READY, 62, 0, &ready); + assert!(!session.rootless()); + assert_eq!(session.connection.get_ref().deadline.get(), None); + let bytes = activate_bytes(&mut session, &mut peer, 63, &payload); + let request = session + .accept_request(&bytes) + .unwrap_or_else(|_| panic!("valid retry")); + peer.credit(); + assert!( + session + .activate_with::<()>(request, |_, _, _, _| panic!( + "must not apply or switch twice" + )) + .unwrap() + .is_none() + ); + peer.app(APP_READY, 63, 0, &ready); + session.start_activation().unwrap(); + session.start_activation().unwrap(); + assert_eq!( + session.activation_state_for_test(), + Some(ActivationState::Started) + ); +} + +#[test] +fn postactivation_capture_stays_refused_without_pending_cleanup() { + let (mut session, mut peer, payload) = restored(); + let state = session.capture.as_mut().unwrap().restored.as_mut().unwrap(); + assert!(matches!( + state.accept(&payload, Instant::now()).unwrap().0, + ActivationAction::Apply(_) + )); + state.activation.complete(Instant::now(), Ok(())).unwrap(); + session.start_activation().unwrap(); + for kill in [0, 1] { + assert!(!queue(&mut session, &mut peer, 71, &[kill], false)); + assert!(!session.capture_pending()); + assert!(session.finish_capture().unwrap().is_none()); + peer.app( + APP_ERROR, + 71, + Errno::ENOTSUP as i32, + b"unsupported-operation", + ); + } +} + +#[test] +fn changed_record_or_workload_bytes_during_application_fail_without_ready() { + for changed_identity in [false, true] { + let (mut session, mut peer, payload) = restored(); + let mut changed = payload.clone(); + if changed_identity { + changed[0] ^= 1; + } else { + let (record, _) = protocol::decode_activate_payload(&payload).unwrap(); + let mut configuration = record.configuration().clone(); + configuration.view.hostname = b"changed".to_vec(); + let record = + crate::activation::ActivationRecord::new(record.restore().copied(), configuration) + .unwrap(); + changed.truncate(9); + changed.extend(record.encode()); + } + let bytes = activate_bytes(&mut session, &mut peer, 64, &payload); + let request = session + .accept_request(&bytes) + .unwrap_or_else(|_| panic!("valid activate")); + peer.credit(); + let result = session.activate_with(request, |_, _, _, _| { + peer.send(protocol::APP_ACTIVATE, 65, &changed); + Ok(()) + }); + assert_eq!(result.err(), Some(Errno::EPROTO)); + peer.credit(); + peer.stream.set_nonblocking(true).unwrap(); + assert_eq!( + peer.stream.read(&mut [0]).unwrap_err().kind(), + io::ErrorKind::WouldBlock + ); + assert!(session.rootless()); + } +} + +#[test] +fn failed_application_never_reports_ready_or_admits_root_dependent_work() { + let (mut session, mut peer, payload) = restored(); + let bytes = activate_bytes(&mut session, &mut peer, 66, &payload); + let request = session + .accept_request(&bytes) + .unwrap_or_else(|_| panic!("valid activate")); + peer.credit(); + assert_eq!( + session + .activate_with::<()>(request, |_, _, _, _| Err(crate::Error::io( + "runtime mounts failed" + ))) + .err(), + Some(Errno::EPROTO) + ); + assert!(session.rootless()); + assert_eq!( + session.activation_state_for_test(), + Some(ActivationState::Failed) + ); + peer.stream.set_nonblocking(true).unwrap(); + assert_eq!( + peer.stream.read(&mut [0]).unwrap_err().kind(), + io::ErrorKind::WouldBlock + ); +} + +fn short_deadline(session: &mut Session<'_>) -> Instant { + let now = Instant::now(); + let repaired = crate::guest::snapshot::repair::tests::repaired_fixture(); + let state = session.capture.as_mut().unwrap().restored.as_mut().unwrap(); + state.activation = crate::guest::activation::Activation::with_resources( + crate::activation::ExpectedResources { + cpus: repaired.cpus(), + memory: repaired.memory(), + }, + Some(now - Duration::from_millis(100)), + Duration::from_millis(300), + now, + ); + state.reset(now).unwrap(); + let deadline = state.activation.deadline().unwrap(); + session.connection.get_ref().deadline.set(Some(deadline)); + deadline +} + +#[test] +fn reconnect_and_partial_input_wait_only_until_the_original_activation_deadline() { + let (mut session, mut peer, _) = restored(); + let deadline = short_deadline(&mut session); + peer.reset(2, 4); + assert!(matches!(session.next_event(), Ok(Event::Reset))); + session.acknowledge().unwrap(); + assert_eq!(peer.read().0.record_type, OUTER_ACK); + assert_eq!(session.connection.get_ref().deadline.get(), Some(deadline)); + let partial = encode_outer(OUTER_DATA, &INSTANCE, 2, 5, &[0; 24]).unwrap(); + peer.stream.write_all(&partial[..11]).unwrap(); + assert!(matches!( + session.next_event(), + Err(ReceiveFailure::Read(Errno::ETIMEDOUT)) + )); + assert!(Instant::now() >= deadline); +} + +#[test] +fn an_unread_output_stream_cannot_extend_the_activation_deadline() { + let (mut session, _peer, _) = restored(); + let deadline = short_deadline(&mut session); + let payload = vec![0; OUTER_MAX_PAYLOAD - protocol::APP_HEADER_LEN]; + let error = loop { + if let Err(error) = session.send_app(APP_READY, 67, 0, &payload) { + break error; + } + }; + assert_eq!(error, Errno::ETIMEDOUT); + assert!(Instant::now() >= deadline); +} + fn queue( session: &mut Session<'_>, peer: &mut Peer, diff --git a/nvx-rs/src/guest/sys/mod.rs b/nvx-rs/src/guest/sys/mod.rs index 328797ec7..6e0d5e80d 100644 --- a/nvx-rs/src/guest/sys/mod.rs +++ b/nvx-rs/src/guest/sys/mod.rs @@ -147,6 +147,8 @@ pub(crate) use sock_diag::{ INET_DIAG_BUFFER_LENGTH, InetDiagMessage, decode_inet_diag_dump, dump_ipv6_tcp_sockets, }; pub(crate) use socket::{UdpSocket, connect_datagram}; +#[cfg(target_arch = "x86_64")] +pub(crate) use spawn::reset_signals; pub(crate) use spawn::{ChildSpec, PreparedChild, REPORT_LENGTH, enter_root}; pub(crate) use terminal::{configure_raw, drain}; pub(crate) use timer::Timer; diff --git a/nvx-rs/src/guest/sys/spawn.rs b/nvx-rs/src/guest/sys/spawn.rs index 00116653c..7cf0590f0 100644 --- a/nvx-rs/src/guest/sys/spawn.rs +++ b/nvx-rs/src/guest/sys/spawn.rs @@ -985,7 +985,7 @@ struct KernelSigaction { /// /// The dispositions come first, so that a signal that arrived while blocked takes its default /// action once unblocked, and never runs a handler of the caller's. -fn reset_signals() -> Result<(), Errno> { +pub(crate) fn reset_signals() -> Result<(), Errno> { let default = KernelSigaction::default(); for signal in 1..=SIGNALS { if signal == nix::libc::SIGKILL || signal == nix::libc::SIGSTOP { diff --git a/nvx-rs/src/host/cli/mod.rs b/nvx-rs/src/host/cli/mod.rs index d72dd8d8a..4af6a3051 100644 --- a/nvx-rs/src/host/cli/mod.rs +++ b/nvx-rs/src/host/cli/mod.rs @@ -47,16 +47,55 @@ mod run; mod sandbox; mod session; mod shell; +mod snapshots; mod store; use std::borrow::Cow; use std::ffi::OsString; -use std::path::Path; +use std::path::{Path, PathBuf}; use self::args::{Parser, Receiver, Spec, Stop, Syntax}; use crate::ErrorCode; use crate::host::platform; +/// Returns the name of a temporary file next to `path`: `.NAME.UUID.tmp`, with a fresh UUID4 in +/// hexadecimal. +pub(crate) fn temporary_path(path: &Path) -> Result { + let Some(name) = path.file_name() else { + return Err(Exit::error( + ErrorCode::Other, + format!("{} has an empty name", path.display()), + )); + }; + let mut temporary = OsString::from("."); + temporary.push(name); + temporary.push(format!(".{}.tmp", uuid4_hex()?)); + Ok(path.with_file_name(temporary)) +} + +/// Returns 32 lowercase hexadecimal digits of 122 random bits, with the version and variant bits +/// of a version-4 UUID. +fn uuid4_hex() -> Result { + let mut bytes = random_bytes::<16>()?; + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + Ok(hex(&bytes)) +} + +/// Returns `N` random bytes from the operating system. +pub(crate) fn random_bytes() -> Result<[u8; N]> { + let mut bytes = [0u8; N]; + getrandom::fill(&mut bytes).map_err(|error| { + Exit::error(ErrorCode::Io, format!("cannot read random bytes: {error}")) + })?; + Ok(bytes) +} + +/// Returns `bytes` in lowercase hexadecimal. +pub(crate) fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|byte| format!("{byte:02x}")).collect() +} + /// The commands, in the order in which usage messages list them. const COMMANDS: [&str; 5] = ["artifacts", "download", "egress", "run", "sandbox"]; /// The options of the top-level parser. diff --git a/nvx-rs/src/host/cli/sandbox/state.rs b/nvx-rs/src/host/cli/sandbox/state.rs index d9fbe5b73..2dff2c014 100644 --- a/nvx-rs/src/host/cli/sandbox/state.rs +++ b/nvx-rs/src/host/cli/sandbox/state.rs @@ -30,10 +30,9 @@ //! [`Config::network`] reads back for `start` to render. It replaces the network options that //! `nvx.py` recorded, which no reader reads: `start` refuses a configuration without it. -use std::ffi::OsString; use std::fs::OpenOptions; use std::io::Write; -use std::path::{Path, PathBuf}; +use std::path::Path; use serde::de::DeserializeOwned; use serde::{Deserialize, Deserializer, Serialize}; @@ -44,6 +43,7 @@ use super::{MAX_MEMORY_MIB, path_string}; use crate::ErrorCode; use crate::host::cli::json::{Object, pretty_serde, read_object}; use crate::host::cli::output::LINE_SEPARATOR; +use crate::host::cli::snapshots::Name as SnapshotName; use crate::host::cli::{Exit, Result, io_error, io_message}; use crate::host::filesystem::ExportPlan; use crate::host::paths; @@ -53,6 +53,11 @@ use crate::host::sandbox::scratch::ScratchBinding; use crate::host::sandbox::{SandboxLaunch, SandboxNetwork, SandboxSettings, SandboxUpper}; use crate::protocol::root::RootUpper; +pub(crate) use crate::host::cli::{hex, random_bytes, temporary_path}; + +#[cfg(test)] +mod tests; + /// The configuration that earlier builds wrote for a sandbox without shares, whose `mount` is /// `null`, which `start` still reads. pub(crate) const CONFIG_FORMAT: u64 = 1; @@ -69,44 +74,6 @@ pub(crate) const CONFIG_FORMATS: [u64; 3] = /// What messages call a configuration. const DESCRIPTION: &str = "sandbox configuration"; -/// Returns the name of a temporary file next to `path`: `.NAME.UUID.tmp`, with a fresh UUID4 in -/// hexadecimal. -pub(crate) fn temporary_path(path: &Path) -> Result { - let Some(name) = path.file_name() else { - return Err(Exit::error( - ErrorCode::Other, - format!("{} has an empty name", path.display()), - )); - }; - let mut temporary = OsString::from("."); - temporary.push(name); - temporary.push(format!(".{}.tmp", uuid4_hex()?)); - Ok(path.with_file_name(temporary)) -} - -/// Returns 32 lowercase hexadecimal digits of 122 random bits, with the version and variant bits -/// of a version-4 UUID. -pub(crate) fn uuid4_hex() -> Result { - let mut bytes = random_bytes::<16>()?; - bytes[6] = (bytes[6] & 0x0f) | 0x40; - bytes[8] = (bytes[8] & 0x3f) | 0x80; - Ok(hex(&bytes)) -} - -/// Returns `N` random bytes from the operating system. -pub(crate) fn random_bytes() -> Result<[u8; N]> { - let mut bytes = [0u8; N]; - getrandom::fill(&mut bytes).map_err(|error| { - Exit::error(ErrorCode::Io, format!("cannot read random bytes: {error}")) - })?; - Ok(bytes) -} - -/// Returns `bytes` in lowercase hexadecimal. -pub(crate) fn hex(bytes: &[u8]) -> String { - bytes.iter().map(|byte| format!("{byte:02x}")).collect() -} - /// Atomically replaces `path` with the JSON `text` and a line end, with the line separator of the /// host for each `\n`, in a temporary file of mode 0600 that then replaces `path`. pub(crate) fn write_json(path: &Path, text: &str) -> Result { @@ -280,6 +247,9 @@ struct Record<'a> { scratch_binding: Option<&'a ScratchBinding>, /// The export plan of the shares, or `None` without shares. export: Option<&'a ExportPlan>, + /// The immutable named snapshot that supplies this sandbox's restore view. + #[serde(skip_serializing_if = "Option::is_none")] + source_snapshot: Option<&'a SnapshotName>, } /// Returns the JSON text of the configuration that `provision` records for `launch` and the other @@ -291,6 +261,17 @@ pub(crate) fn serialize_launch( distro: &RecordedDistro, scratch: Option<&RecordedScratch>, config: &ProvisionConfig<'_>, +) -> Result { + serialize_launch_with_snapshot(launch, distro, scratch, config, None) +} + +/// Records a restored sandbox's source alongside its own launch, without a snapshot index. +pub(crate) fn serialize_launch_with_snapshot( + launch: &SandboxLaunch, + distro: &RecordedDistro, + scratch: Option<&RecordedScratch>, + config: &ProvisionConfig<'_>, + source_snapshot: Option<&SnapshotName>, ) -> Result { let layer = &distro.layer; let settings = launch.settings(); @@ -314,6 +295,7 @@ pub(crate) fn serialize_launch( distro_binding: &distro.binding, scratch_binding: scratch.map(|scratch| &scratch.binding), export: launch.export(), + source_snapshot, }; pretty_serde(&record, DESCRIPTION) } @@ -375,9 +357,22 @@ pub(crate) struct Config { /// The sandbox's network, `null` for none, if recorded. #[serde(default, deserialize_with = "present")] network: Option, + /// The source of a restored sandbox, absent for a cold launch. + source_snapshot: Option, } impl Config { + #[cfg_attr( + not(test), + expect( + dead_code, + reason = "SL3.8a locks a restored sandbox's source snapshot" + ) + )] + pub(crate) fn source_snapshot(&self) -> Option<&SnapshotName> { + self.source_snapshot.as_ref() + } + /// Returns the fields of `object`, a configuration that [`read_config`] read, which fails as /// `sandbox configuration is malformed` without a field that it requires, or with a value of /// another type. @@ -403,6 +398,7 @@ impl Config { ), )); } + serde_json::from_value(Value::Object(object)).map_err(|_| malformed()) } diff --git a/nvx-rs/src/host/cli/sandbox/state/tests.rs b/nvx-rs/src/host/cli/sandbox/state/tests.rs new file mode 100644 index 000000000..6ec46cf9b --- /dev/null +++ b/nvx-rs/src/host/cli/sandbox/state/tests.rs @@ -0,0 +1,35 @@ +use super::*; +use crate::host::sandbox::layer::LayerRole; + +#[test] +fn restored_sandbox_records_round_trip_only_their_own_snapshot_name() { + let directory = tempfile::tempdir().unwrap(); + let image = directory.path().join("distro.erofs"); + std::fs::write(&image, b"distro layer\n").unwrap(); + let layer = SandboxLayer::new( + LayerRole::Distro, + image.to_str().unwrap(), + "11111111-1111-1111-1111-111111111111", + ); + let binding = DistroBinding::verify(&layer).unwrap(); + let launch = + SandboxLaunch::new(vec![layer], SandboxUpper::RAM, SandboxSettings::default()).unwrap(); + let distro = RecordedDistro::new(&launch, binding).unwrap(); + let options = ProvisionConfig { + hypervisor: "whp", + memory_mib: 128, + cmdline: "", + }; + for source in [None, Some("My.snapshot-1"), Some("other")] { + let name = source.map(|name| SnapshotName::try_from(name.to_owned()).unwrap()); + let text = serialize_launch_with_snapshot(&launch, &distro, None, &options, name.as_ref()) + .unwrap(); + let object: Object = serde_json::from_str(&text).unwrap(); + assert_eq!(object.contains_key("source_snapshot"), source.is_some()); + let record = Config::new(object.clone()).unwrap(); + assert_eq!(record.source_snapshot(), name.as_ref()); + let mut malformed = object; + malformed.insert("source_snapshot".to_owned(), Value::from("../other")); + assert!(Config::new(malformed).is_err()); + } +} diff --git a/nvx-rs/src/host/cli/snapshots.rs b/nvx-rs/src/host/cli/snapshots.rs new file mode 100644 index 000000000..044c8fd6c --- /dev/null +++ b/nvx-rs/src/host/cli/snapshots.rs @@ -0,0 +1,121 @@ +//! Immutable named snapshots and their external, exclusive locks. +//! +//! Names map to hexadecimal directory names under `snapshots/`. Each published directory +//! contains only the strict snapshot's three entries; its lock stays under `locks/`. +//! Completed private directories under `staging/` publish with one non-replacing rename. +#![cfg_attr( + not(test), + expect(dead_code, reason = "SL3.7b and SL3.8a use the named snapshot store") +)] + +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; + +use super::deadline::Deadline; +use super::store::{LockedState, StateDir}; +use super::{Exit, Result, hex, io_error, temporary_path}; +use crate::host::snapshot::Snapshot; +use crate::host::{platform, store}; +use crate::{Error, ErrorCode}; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "String")] +pub(crate) struct Name(String); + +impl TryFrom for Name { + type Error = Error; + + fn try_from(value: String) -> crate::Result { + if !(1..=64).contains(&value.len()) + || !value.as_bytes()[0].is_ascii_alphanumeric() + || !value + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte)) + { + return Err(Error::unsupported( + "a snapshot name must be 1 to 64 ASCII letters, digits, dots, underscores or \ + hyphens, starting with a letter or digit", + )); + } + Ok(Self(value)) + } +} + +#[derive(Debug)] +pub(crate) struct Store { + root: StateDir, + entries: StateDir, + locks: StateDir, + staging: StateDir, +} + +impl Store { + pub(crate) fn create(root: &Path) -> Result { + let root = StateDir::create(&std::path::absolute(root).map_err(Exit::from_os_error)?)?; + Ok(Self { + entries: StateDir::create(&root.dir().join("snapshots"))?, + locks: StateDir::create(&root.dir().join("locks"))?, + staging: StateDir::create(&root.dir().join("staging"))?, + root, + }) + } + + fn check(&self) -> Result { + for directory in [&self.root, &self.entries, &self.locks, &self.staging] { + StateDir::open(directory.dir())?; + } + Ok(()) + } + + pub(crate) fn lock(&self, name: &Name, deadline: Deadline) -> Result> { + self.check()?; + // Hex preserves case distinctions and avoids reserved host filenames. + let directory = hex(name.0.as_bytes()); + let lock = StateDir::create(&self.locks.dir().join(&directory))?.lock(deadline)?; + Ok(LockedSnapshot { + store: self, + dir: self.entries.dir().join(directory), + _lock: lock, + }) + } +} + +#[derive(Debug)] +pub(crate) struct LockedSnapshot<'a> { + store: &'a Store, + dir: PathBuf, + _lock: LockedState, +} + +impl LockedSnapshot<'_> { + pub(crate) fn stage(&self) -> Result { + self.store.check()?; + StateDir::create(&temporary_path(&self.store.staging.dir().join("snapshot"))?) + } + + pub(crate) fn open(&self) -> Result { + self.store.check()?; + Snapshot::open(&self.dir).map_err(Into::into) + } + + pub(crate) fn publish(&self, staged: StateDir) -> Result { + self.store.check()?; + if staged.dir().parent() != Some(self.store.staging.dir()) { + return Err(Exit::error( + ErrorCode::Io, + "only a private staging directory can publish a named snapshot", + )); + } + Snapshot::open(staged.dir())?; + store::files::sync_dir(staged.dir())?; + platform::rename_no_replace(staged.dir(), &self.dir) + .map_err(|error| io_error(ErrorCode::Io, &error, &self.dir))?; + store::files::sync_dir(self.store.entries.dir())?; + store::files::sync_dir(self.store.staging.dir())?; + self.open() + } +} + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/cli/snapshots/tests.rs b/nvx-rs/src/host/cli/snapshots/tests.rs new file mode 100644 index 000000000..4b75fa374 --- /dev/null +++ b/nvx-rs/src/host/cli/snapshots/tests.rs @@ -0,0 +1,238 @@ +use std::fs; +use std::path::Path; + +use super::*; +use crate::host::snapshot::tests::SnapshotFixture; +use crate::host::snapshot::{DESCRIPTION_FILE, KEY_FILE, MANIFEST_NAME, OPENVMM_DIR}; +use crate::host::tests::released; + +fn name(value: &str) -> Name { + Name::try_from(value.to_owned()).unwrap() +} + +fn staged(locked: &LockedSnapshot<'_>) -> StateDir { + let fixture = SnapshotFixture::new(); + let staged = locked.stage().unwrap(); + for entry in [KEY_FILE, DESCRIPTION_FILE, OPENVMM_DIR] { + fs::rename(fixture.dir.join(entry), staged.dir().join(entry)).unwrap(); + } + staged +} + +fn link_directory(link: &Path, target: &Path) { + #[cfg(unix)] + std::os::unix::fs::symlink(target, link).unwrap(); + #[cfg(windows)] + crate::host::tests::junction(link, target); +} + +#[test] +fn names_are_bounded_portable_components_with_distinct_mappings() { + for invalid in [ + "", + ".", + "..", + ".hidden", + "../escape", + "a/b", + "a\\b", + "a:b", + "a b", + "a\nb", + "snapshot\u{e9}", + ] { + assert!(Name::try_from(invalid.to_owned()).is_err(), "{invalid:?}"); + } + assert!(Name::try_from("a".repeat(65)).is_err()); + assert!(Name::try_from("a".repeat(64)).is_ok()); + let root = tempfile::tempdir().unwrap(); + let store = Store::create(&root.path().join("store")).unwrap(); + let mut mapped = Vec::new(); + for valid in ["a", "CON", "con.", "A", "name.1-2_3"] { + let held = store.lock(&name(valid), Deadline::after(0.0)).unwrap(); + assert_eq!(held.dir.parent(), Some(store.entries.dir())); + assert!(!held.dir.exists()); + assert!(!held._lock.dir().starts_with(&held.dir)); + mapped.push( + held.dir + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_ascii_lowercase(), + ); + } + mapped.sort(); + mapped.dedup(); + assert_eq!(mapped.len(), 5); +} + +#[test] +fn publication_is_strict_whole_and_cannot_replace_or_move_a_published_snapshot() { + let root = tempfile::tempdir().unwrap(); + let store = Store::create(&root.path().join("store")).unwrap(); + let held = store.lock(&name("first"), Deadline::after(0.0)).unwrap(); + let pending = staged(&held); + fs::write(pending.dir().join("log"), b"not a snapshot entry").unwrap(); + assert!(held.publish(pending.clone()).is_err()); + assert!(pending.dir().exists()); + assert!(!held.dir.exists()); + fs::remove_file(pending.dir().join("log")).unwrap(); + let expected = Snapshot::open(pending.dir()).unwrap(); + let key = fs::read(pending.dir().join(KEY_FILE)).unwrap(); + let published = held.publish(pending.clone()).unwrap(); + assert!(!pending.dir().exists()); + assert_eq!(published.key(), expected.key()); + assert_eq!(published.manifest(), expected.manifest()); + let mut entries: Vec<_> = fs::read_dir(&held.dir) + .unwrap() + .map(|entry| entry.unwrap().file_name()) + .collect(); + entries.sort(); + assert_eq!(entries, [KEY_FILE, OPENVMM_DIR, DESCRIPTION_FILE]); + let files = [ + held.dir.join(KEY_FILE), + held.dir.join(DESCRIPTION_FILE), + held.dir.join(OPENVMM_DIR).join(MANIFEST_NAME), + held.dir.join(OPENVMM_DIR).join("memory.bin"), + held.dir.join(OPENVMM_DIR).join("state.bin"), + ]; + let unchanged = || { + files + .iter() + .map(|path| { + let metadata = fs::metadata(path).unwrap(); + ( + platform::file_id(path).unwrap(), + metadata.len(), + metadata.modified().unwrap(), + ) + }) + .collect::>() + }; + let before = unchanged(); + let replacement = staged(&held); + assert!(held.publish(replacement.clone()).is_err()); + assert!(replacement.dir().exists()); + assert!(held.publish(StateDir::open(&held.dir).unwrap()).is_err()); + assert_eq!(fs::read(held.dir.join(KEY_FILE)).unwrap(), key); + assert_eq!( + fs::read(held.dir.join(DESCRIPTION_FILE)).unwrap(), + b"not JSON" + ); + assert_eq!(held.open().unwrap().key(), published.key()); + assert_eq!(unchanged(), before); +} + +#[test] +fn invalid_or_linked_directories_never_publish_or_open() { + let root = tempfile::tempdir().unwrap(); + let store = Store::create(&root.path().join("store")).unwrap(); + let held = store.lock(&name("first"), Deadline::after(0.0)).unwrap(); + let pending = staged(&held); + crate::host::store::files::write_private(&pending.dir().join(KEY_FILE), b"bad key").unwrap(); + assert!(held.publish(pending.clone()).is_err()); + assert!(!held.dir.exists()); + let valid = staged(&held); + let expected = Snapshot::open(valid.dir()).unwrap(); + let identity = platform::file_id(valid.dir()).unwrap(); + let key = fs::read(valid.dir().join(KEY_FILE)).unwrap(); + let linked = held.stage().unwrap(); + fs::remove_dir(linked.dir()).unwrap(); + link_directory(linked.dir(), valid.dir()); + assert!(held.publish(linked).is_err()); + assert!(!held.dir.exists()); + link_directory(&held.dir, valid.dir()); + assert!(held.open().is_err()); + assert!(held.publish(staged(&held)).is_err()); + assert_eq!(platform::file_id(valid.dir()).unwrap(), identity); + assert_eq!(fs::read(valid.dir().join(KEY_FILE)).unwrap(), key); + assert_eq!(Snapshot::open(valid.dir()).unwrap().key(), expected.key()); + let alias = root.path().join("alias"); + link_directory(&alias, store.root.dir()); + assert!(Store::create(&alias).is_err()); +} + +#[test] +fn exclusive_snapshot_locks_wait_and_other_names_remain_independent() { + let root = tempfile::tempdir().unwrap(); + let store = Store::create(&root.path().join("store")).unwrap(); + let first = store.lock(&name("first"), Deadline::after(0.0)).unwrap(); + let other_handle = Store::create(store.root.dir()).unwrap(); + assert!(matches!( + other_handle.lock(&name("first"), Deadline::after(0.0)), + Err(Exit::Error { + code: ErrorCode::SandboxBusy, + .. + }) + )); + let second = store.lock(&name("second"), Deadline::after(0.0)).unwrap(); + assert_ne!(first._lock.dir(), second._lock.dir()); + drop(first); + assert!(released(|| { + other_handle + .lock(&name("first"), Deadline::after(0.0)) + .is_ok() + })); + assert_eq!(fs::read_dir(store.entries.dir()).unwrap().count(), 0); +} + +#[cfg(target_os = "linux")] +#[test] +fn shared_directories_and_linked_locks_are_refused_without_restricting_them() { + use std::os::unix::fs::{PermissionsExt, symlink}; + + let root = tempfile::tempdir().unwrap(); + let shared = root.path().join("shared"); + fs::create_dir(&shared).unwrap(); + fs::set_permissions(&shared, fs::Permissions::from_mode(0o755)).unwrap(); + assert!(Store::create(&shared).is_err()); + assert_eq!( + fs::metadata(&shared).unwrap().permissions().mode() & 0o777, + 0o755 + ); + let store = Store::create(&root.path().join("store")).unwrap(); + let held = store.lock(&name("first"), Deadline::after(0.0)).unwrap(); + let pending = staged(&held); + fs::set_permissions(pending.dir(), fs::Permissions::from_mode(0o755)).unwrap(); + assert!(held.publish(pending).is_err()); + let lock = held._lock.path(super::super::store::StateFile::Lock); + drop(held); + fs::remove_file(&lock).unwrap(); + let target = root.path().join("target"); + fs::write(&target, b"unchanged").unwrap(); + symlink(&target, &lock).unwrap(); + assert!(store.lock(&name("first"), Deadline::after(0.0)).is_err()); + assert_eq!(fs::read(&target).unwrap(), b"unchanged"); +} + +#[cfg(windows)] +#[test] +fn shared_directory_acls_are_refused_before_publication() { + use std::process::{Command, Stdio}; + + let allow_everyone = |path: &Path| { + let status = crate::host::tests::spawn_locked( + Command::new("icacls") + .arg(path) + .args(["/grant", "*S-1-1-0:(OI)(CI)(F)"]) + .stdout(Stdio::null()), + ) + .unwrap() + .wait() + .unwrap(); + assert!(status.success()); + }; + let root = tempfile::tempdir().unwrap(); + let shared = StateDir::create(&root.path().join("shared")).unwrap(); + allow_everyone(shared.dir()); + assert!(Store::create(shared.dir()).is_err()); + assert!(platform::check_store_dir(shared.dir()).unwrap().is_some()); + let store = Store::create(&root.path().join("store")).unwrap(); + let held = store.lock(&name("first"), Deadline::after(0.0)).unwrap(); + let pending = staged(&held); + allow_everyone(&pending.dir().join(OPENVMM_DIR)); + assert!(held.publish(pending.clone()).is_err()); + assert!(pending.dir().exists()); + assert!(!held.dir.exists()); +} diff --git a/nvx-rs/src/host/cli/store/launch.rs b/nvx-rs/src/host/cli/store/launch.rs index d0a5c04e6..d3cdb0100 100644 --- a/nvx-rs/src/host/cli/store/launch.rs +++ b/nvx-rs/src/host/cli/store/launch.rs @@ -1,13 +1,17 @@ -//! Locked launch transitions, without spawning, recovery, or process termination. +//! Locked launch transitions and bounded recovery of an interrupted launch. + +use std::thread; +use std::time::{Duration, Instant}; use super::StateFile; use super::files; use super::locked::LockedState; use super::record::{LaunchJournal, RuntimeRecord}; -use crate::host::process::ProcessRef; +use crate::host::cli::deadline::RETRY_INTERVAL; +use crate::host::process::{BootId, KILL_TIMEOUT, Opened, Process, ProcessRef}; use crate::host::runtime::RuntimeIdentity; use crate::host::vm::{Prepared, SavedVm}; -use crate::{Error, Result}; +use crate::{Error, ErrorKind, Result}; /// A launch whose lock stays borrowed until commit or until its journal is left behind. #[derive(Debug)] @@ -17,6 +21,77 @@ pub(crate) struct Launch<'a> { } impl LockedState { + /// Resolves only an uncommitted journal, before the next command can mutate state. + pub(crate) fn recover(&mut self, deadline: Instant) -> Result<()> { + if self.runtime()?.is_some() { + return Ok(()); + } + let Some(journal) = self.journal()? else { + return Ok(()); + }; + if journal.vm_dir != self.dir() || journal.log != self.path(StateFile::Log) { + return Err(Error::io( + "the launch journal belongs to another sandbox directory", + )); + } + match journal.process() { + Some(reference) => self.end_recorded(&journal, reference, deadline)?, + None if !self.released_claim(&journal, deadline)? => { + let process = self.endpoint_owner(&journal)?; + end_process(&process, deadline)?; + } + None => {} + } + if self.capability()?.is_some() { + files::remove(self, StateFile::Capability)?; + } + // Keep the journal until cleanup has finished, including after a partial removal. + files::remove(self, StateFile::Journal) + } + + /// Ends only the recorded process, or accepts an independently proved release of its claim. + fn end_recorded( + &self, + journal: &LaunchJournal, + reference: &ProcessRef, + deadline: Instant, + ) -> Result<()> { + let boot = BootId::current().map_err(|error| { + Error::io("cannot identify the current host boot").with_source(error) + })?; + if boot != reference.boot { + return Ok(()); + } + let unheld = match Process::open(reference) { + Ok(Opened::Running(process)) => return end_process(&process, deadline), + Ok(Opened::Exited(_)) => Error::new( + ErrorKind::StillRunning, + "the recorded VM exited, but a process may still hold its launch log", + ), + Err(error) => Error::io("cannot verify the recorded VM process").with_source(error), + }; + if self.released_claim(journal, deadline)? { + Ok(()) + } else { + Err(unheld) + } + } + + /// Rechecks transient fork-inherited descriptors for at most one second. + fn released_claim(&self, journal: &LaunchJournal, deadline: Instant) -> Result { + let until = deadline.min(Instant::now() + Duration::from_secs(1)); + loop { + if !self.claim_held(journal)? { + return Ok(true); + } + let left = until.saturating_duration_since(Instant::now()); + if left.is_zero() { + return Ok(false); + } + thread::sleep(left.min(RETRY_INTERVAL)); + } + } + /// Durably publishes the capability and complete prepared journal before the caller spawns. pub(crate) fn begin_launch( &mut self, @@ -38,6 +113,48 @@ impl LockedState { } } +/// Ends a held, verified process and requires confirmation of its exit before the deadline. +fn end_process(process: &Process, deadline: Instant) -> Result<()> { + let failed = |message: &str| { + Error::new( + ErrorKind::StillRunning, + format!( + "cannot confirm exit of VM process {}: {message}", + process.reference().pid + ), + ) + }; + if process + .wait(Instant::now()) + .map_err(|error| failed("the exit check failed").with_source(error))? + .is_some() + { + return Ok(()); + } + let until = deadline.min(Instant::now() + KILL_TIMEOUT); + if until <= Instant::now() { + return Err(failed("the recovery deadline passed")); + } + let kill_error = process.kill().err(); + let exited = process.wait(until).map_err(|error| { + let mut failure = failed("the exit wait failed"); + if let Some(kill_error) = &kill_error { + failure = failed(&format!( + "termination failed: {kill_error}; the exit wait failed" + )); + } + failure.with_source(error) + })?; + if exited.is_some() { + return Ok(()); + } + let failure = failed("the verified process has not exited"); + Err(match kill_error { + Some(error) => failure.with_source(error), + None => failure, + }) +} + impl Launch<'_> { /// Records only the caller's spawned process, durably, before changing the in-memory state. pub(crate) fn record_process(&mut self, process: &ProcessRef) -> Result<()> { diff --git a/nvx-rs/src/host/cli/store/liveness.rs b/nvx-rs/src/host/cli/store/liveness.rs new file mode 100644 index 000000000..4b584f836 --- /dev/null +++ b/nvx-rs/src/host/cli/store/liveness.rs @@ -0,0 +1,86 @@ +//! Bounded launch proofs, without recovery, record removal, or process termination. + +use std::io; + +use super::StateFile; +use super::locked::LockedState; +use super::record::LaunchJournal; +use crate::host::platform; +use crate::host::process::{BootId, Opened, Process, ProcessRef}; +use crate::{Error, Result}; + +impl LockedState { + /// Returns whether a launch process may still run. Only a released claim proves exit; + /// failure to examine it proves neither, and preserves the original error. + pub(crate) fn claim_held(&self, journal: &LaunchJournal) -> Result { + let path = self.path(StateFile::Log); + if journal.vm_dir != self.dir() || journal.log != path { + return Err(Error::io( + "the launch journal belongs to another sandbox directory", + )); + } + platform::log_claim_held(&path, journal.log_identity) + .map_err(|error| Error::io("cannot check the launch log claim").with_source(error)) + } + + /// Holds the server of the recorded endpoint, rechecks its ownership, and on Linux proves + /// that this very process also holds the launch's log. An error is not proof of exit. + pub(crate) fn endpoint_owner(&self, journal: &LaunchJournal) -> Result { + if !self.claim_held(journal)? { + return Err(Error::io("the launch log claim is released")); + } + let process = + hold_endpoint_server(|| platform::endpoint_server_pid(&journal.endpoint), find)?; + let reference = process.reference(); + let matches = + platform::serves_launch(reference.pid, reference.start_time, journal.log_identity) + .map_err(|error| { + Error::io("cannot verify the endpoint owner's log").with_source(error) + })?; + if !matches { + return Err(Error::io( + "the endpoint owner does not hold the launch log as its standard output", + )); + } + Ok(process) + } +} + +/// Opens only the process whose start time was just read, never another that reused its ID. +fn find(pid: u32) -> io::Result> { + let Some(start_time) = platform::process_start_time(pid)? else { + return Ok(None); + }; + Ok( + match Process::open(&ProcessRef { + pid, + start_time, + boot: BootId::current()?, + })? { + Opened::Running(process) => Some(process), + Opened::Exited(_) => None, + }, + ) +} + +/// Adapted from the old store's owner check: the second endpoint lookup happens only after +/// the process is held. The callbacks keep replacement races testable without test-only hooks. +pub(super) fn hold_endpoint_server( + mut server_pid: impl FnMut() -> io::Result>, + hold: impl FnOnce(u32) -> io::Result>, +) -> Result { + let pid = server_pid() + .map_err(|error| Error::io("cannot examine the control endpoint").with_source(error))? + .ok_or_else(|| Error::io("no process serves the control endpoint"))?; + let process = hold(pid) + .map_err(|error| Error::io("cannot hold the control endpoint owner").with_source(error))? + .ok_or_else(|| Error::io("the control endpoint owner exited before it could be held"))?; + let again = server_pid() + .map_err(|error| Error::io("cannot recheck the control endpoint").with_source(error))?; + if again != Some(pid) { + return Err(Error::io( + "the control endpoint changed while its owner was held", + )); + } + Ok(process) +} diff --git a/nvx-rs/src/host/cli/store/locked.rs b/nvx-rs/src/host/cli/store/locked.rs index 68737e290..35bf46ffb 100644 --- a/nvx-rs/src/host/cli/store/locked.rs +++ b/nvx-rs/src/host/cli/store/locked.rs @@ -54,6 +54,9 @@ impl StateDir { /// access, who could hold its lock. A lock file that another transition removed or replaced /// after this opened it locks nothing, so the lock is taken again: at once the first time, /// and after a pause, until the deadline, when that happens again right after. + /// + /// Once held, the lock recovers an interrupted launch before returning. Recovery can end a + /// verified VM, but retains indeterminate records and reports the cause of its refusal. pub(crate) fn lock(&self, deadline: Deadline) -> Result { let path = self.path(StateFile::Lock); let mut retried_at_once = false; @@ -71,10 +74,14 @@ impl StateDir { } } if names_locked_file(&path, &file)? { - return Ok(LockedState { + let mut locked = LockedState { state: self.clone(), lock: file, - }); + }; + locked + .recover(deadline.instant()) + .map_err(crate::host::cli::sandbox::error_with_cause)?; + return Ok(locked); } if retried_at_once && !deadline.pause(RETRY_INTERVAL)? { return Err(busy()); diff --git a/nvx-rs/src/host/cli/store/mod.rs b/nvx-rs/src/host/cli/store/mod.rs index 16bcb2432..2d5f06ba9 100644 --- a/nvx-rs/src/host/cli/store/mod.rs +++ b/nvx-rs/src/host/cli/store/mod.rs @@ -30,6 +30,12 @@ //! A locked launch writes its prepared capability and outputs before spawn, records the spawned //! process durably, and publishes the caller's exact saved VM only at commit. Dropping an //! uncommitted launch keeps its journal; these transitions do not recover or signal a process. +//! Its journal also identifies the inherited log claim: held means a launch process may run, +//! and released proves exit. An endpoint owner is held and rechecked before it is returned; +//! Linux additionally verifies that this process holds that launch's log. +//! Taking the next lock recovers an interrupted, uncommitted launch before allowing mutation: +//! it ends only a verified process, removes its journal and capability only after proved exit, +//! and refuses indeterminate recovery without changing that evidence. #![cfg_attr( not(test), expect( @@ -40,6 +46,7 @@ mod files; mod launch; +mod liveness; mod locked; mod names; mod record; @@ -47,6 +54,7 @@ mod record; use std::io; use std::path::{Path, PathBuf}; +pub(crate) use self::locked::LockedState; pub(crate) use self::names::StateFile; use crate::ErrorCode; use crate::host::cli::{Exit, Result}; diff --git a/nvx-rs/src/host/cli/store/record.rs b/nvx-rs/src/host/cli/store/record.rs index d84718801..fa85fdf18 100644 --- a/nvx-rs/src/host/cli/store/record.rs +++ b/nvx-rs/src/host/cli/store/record.rs @@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize}; use super::StateFile; use super::files; use super::locked::LockedState; +use crate::host::platform; use crate::host::process::ProcessRef; use crate::host::runtime::RuntimeIdentity; use crate::host::vm::{Prepared, SavedVm, VmDir, VmOutputs}; @@ -21,6 +22,7 @@ pub(crate) struct LaunchJournal { pub(super) vm_dir: PathBuf, pub(super) capability: [u8; CAPABILITY_LEN], pub(super) log: PathBuf, + pub(super) log_identity: (u64, u64), pub(super) outcome: PathBuf, pub(super) endpoint: String, pub(super) console: Option, @@ -34,6 +36,7 @@ impl fmt::Debug for LaunchJournal { .debug_struct("LaunchJournal") .field("vm_dir", &self.vm_dir) .field("log", &self.log) + .field("log_identity", &self.log_identity) .field("outcome", &self.outcome) .field("endpoint", &self.endpoint) .field("console", &self.console) @@ -64,10 +67,15 @@ impl LaunchJournal { "the prepared launch belongs to another sandbox directory", )); } + platform::claim_log(prepared.log()) + .map_err(|error| Error::io("cannot claim the prepared VM log").with_source(error))?; + let log_identity = platform::opened_file_identity(prepared.log()) + .map_err(|error| Error::io("cannot identify the prepared VM log").with_source(error))?; Ok(Self { vm_dir: vm_dir.path().to_path_buf(), capability: *prepared.capability().as_bytes(), log: log.clone(), + log_identity, outcome: outcome.clone(), endpoint: endpoint.clone(), console: console.clone(), diff --git a/nvx-rs/src/host/cli/store/tests.rs b/nvx-rs/src/host/cli/store/tests.rs index f28a33df9..4dabc42a6 100644 --- a/nvx-rs/src/host/cli/store/tests.rs +++ b/nvx-rs/src/host/cli/store/tests.rs @@ -9,6 +9,8 @@ use crate::host::cli::deadline::Deadline; use crate::host::store::{JOURNAL_NAME, RUNTIME_NAME, SandboxStore}; mod launch; +mod liveness; +mod recovery; /// How long a test waits for a lock that it released to be free. /// diff --git a/nvx-rs/src/host/cli/store/tests/launch.rs b/nvx-rs/src/host/cli/store/tests/launch.rs index aff660e14..8712ea4ff 100644 --- a/nvx-rs/src/host/cli/store/tests/launch.rs +++ b/nvx-rs/src/host/cli/store/tests/launch.rs @@ -11,7 +11,10 @@ use crate::host::vm::{self, Prepared, SavedVm, VmDir}; use crate::hypervisor::Hypervisor; use crate::protocol::CAPABILITY_LEN; -fn prepared(state: &StateDir, console: ConsoleAttachment) -> (Prepared, RuntimeIdentity) { +pub(super) fn prepared( + state: &StateDir, + console: ConsoleAttachment, +) -> (Prepared, RuntimeIdentity) { let identity = |text| ArtifactIdentity::new(text).unwrap(); let runtime = Runtime { openvmm: "missing-openvmm".into(), @@ -172,16 +175,19 @@ fn an_interruption_keeps_the_journal_and_capability_without_publishing_runtime() launch.record_process(&saved.process).unwrap(); } } + let before = fs::read(state.path(StateFile::Journal)).unwrap(); + assert!(locked.begin_launch(&prepared, &runtime).is_err()); + assert_eq!(fs::read(state.path(StateFile::Journal)).unwrap(), before); drop(locked); - let mut reopened = state.lock(Deadline::after(10.0)).unwrap(); - let journal = reopened.journal().unwrap().unwrap(); + let journal: record::LaunchJournal = + serde_json::from_slice(&fs::read(state.path(StateFile::Journal)).unwrap()).unwrap(); assert_eq!(journal.process(), spawned.then_some(&saved.process)); assert_eq!(journal.capability, saved.capability); - assert_eq!(reopened.capability().unwrap(), Some(saved.capability)); - assert!(reopened.runtime().unwrap().is_none()); - let before = fs::read(state.path(StateFile::Journal)).unwrap(); - assert!(reopened.begin_launch(&prepared, &runtime).is_err()); - assert_eq!(fs::read(state.path(StateFile::Journal)).unwrap(), before); + assert_eq!( + fs::read(state.path(StateFile::Capability)).unwrap(), + saved.capability + ); + assert!(!state.path(StateFile::Runtime).exists()); } } diff --git a/nvx-rs/src/host/cli/store/tests/liveness.rs b/nvx-rs/src/host/cli/store/tests/liveness.rs new file mode 100644 index 000000000..e9bb3f683 --- /dev/null +++ b/nvx-rs/src/host/cli/store/tests/liveness.rs @@ -0,0 +1,271 @@ +use std::error::Error as _; +use std::io; +#[cfg(any(target_os = "linux", windows))] +use std::process::{Child, Command, Stdio}; + +use super::launch::prepared; +use super::*; +use crate::host::cli::store::liveness::hold_endpoint_server; +use crate::host::config::ConsoleAttachment; +use crate::host::process::{BootId, Opened, Process, ProcessRef}; +use crate::host::spawn::{self, VmmCommand}; + +const HELPER: &str = "host::cli::store::tests::liveness::log_holder_helper"; + +fn own_process() -> Process { + let pid = std::process::id(); + let Opened::Running(process) = Process::open(&ProcessRef { + pid, + start_time: platform::process_start_time(pid).unwrap().unwrap(), + boot: BootId::current().unwrap(), + }) + .unwrap() else { + panic!("this process runs"); + }; + process +} + +struct Running(Option); + +impl Drop for Running { + fn drop(&mut self) { + if let Some(child) = self.0.take() { + child.kill_and_confirm_exit().unwrap(); + } + } +} + +#[cfg(any(target_os = "linux", windows))] +struct Other(Child); + +#[cfg(any(target_os = "linux", windows))] +impl Drop for Other { + fn drop(&mut self) { + self.0.kill().unwrap(); + self.0.wait().unwrap(); + } +} + +#[cfg(any(target_os = "linux", windows))] +fn helper_command(state: &StateDir) -> Command { + let mut command = Command::new(std::env::current_exe().unwrap()); + command + .args(["--exact", HELPER, "--ignored", "--nocapture"]) + .current_dir(state.dir()) + .stdin(Stdio::null()); + command +} + +#[test] +fn a_claim_identifies_the_opened_log_and_last_close_proves_exit() { + let (_directory, state) = state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let identity = platform::opened_file_identity(prepared.log()).unwrap(); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + drop(locked.begin_launch(&prepared, &runtime).unwrap()); + let journal = locked.journal().unwrap().unwrap(); + assert_eq!(journal.log_identity, identity); + assert!(locked.claim_held(&journal).unwrap()); + let copy = prepared.log().try_clone().unwrap(); + drop(prepared); + assert!(locked.claim_held(&journal).unwrap()); + drop(copy); + assert!(released(|| !locked.claim_held(&journal).unwrap())); + assert!(locked.journal().unwrap().is_some()); + assert!(locked.capability().unwrap().is_some()); +} + +#[test] +fn replacing_the_log_or_sandbox_never_proves_the_launch_exited() { + let (_directory, state) = state(); + let (_other_directory, other) = super::state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + drop(locked.begin_launch(&prepared, &runtime).unwrap()); + let journal = locked.journal().unwrap().unwrap(); + let elsewhere = other.lock(Deadline::after(10.0)).unwrap(); + assert!(elsewhere.claim_held(&journal).is_err()); + drop(prepared); + fs::rename(state.path(StateFile::Log), state.dir().join("old-log")).unwrap(); + fs::write(state.path(StateFile::Log), b"another launch").unwrap(); + let error = locked.claim_held(&journal).unwrap_err(); + assert!(error.source().is_some()); + assert!( + error + .to_string() + .contains("cannot check the launch log claim") + ); + assert!(locked.endpoint_owner(&journal).is_err()); + assert!(locked.journal().unwrap().is_some()); +} + +#[test] +fn the_spawned_process_inherits_the_preparations_claim() { + let (_directory, state) = state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + drop(locked.begin_launch(&prepared, &runtime).unwrap()); + let journal = locked.journal().unwrap().unwrap(); + let command = VmmCommand::new( + std::env::current_exe().unwrap(), + ["--exact", HELPER, "--ignored", "--nocapture"] + .map(Into::into) + .to_vec(), + state.dir(), + ); + let child = Running(Some( + spawn::spawn_vm( + &command, + prepared.capability().as_bytes(), + prepared.log().try_clone().unwrap(), + ) + .unwrap(), + )); + drop(prepared); + assert!(released(|| state.dir().join("holder-ready").exists())); + assert!(locked.claim_held(&journal).unwrap()); + #[cfg(target_os = "linux")] + { + let owner = locked.endpoint_owner(&journal).unwrap(); + assert_eq!(owner.reference().pid, child.0.as_ref().unwrap().id()); + assert!(!owner.has_exited().unwrap()); + } + drop(child); + assert!(released(|| !locked.claim_held(&journal).unwrap())); +} + +#[cfg(target_os = "linux")] +#[test] +fn a_conflicting_lock_prevents_publishing_a_launch() { + let (_directory, state) = state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let other = fs::File::open(prepared.outputs().log.clone()).unwrap(); + other.try_lock().unwrap(); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + let error = locked.begin_launch(&prepared, &runtime).unwrap_err(); + assert!(error.source().is_some()); + assert!(locked.journal().unwrap().is_none()); + assert!(locked.capability().unwrap().is_none()); +} + +#[cfg(target_os = "linux")] +#[test] +fn an_endpoint_owner_without_the_claimed_log_is_rejected_without_signaling_it() { + let (_directory, state) = state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + drop(locked.begin_launch(&prepared, &runtime).unwrap()); + let journal = locked.journal().unwrap().unwrap(); + let mut other = Other( + helper_command(&state) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn() + .unwrap(), + ); + assert!(released(|| state.dir().join("holder-ready").exists())); + assert!(locked.claim_held(&journal).unwrap()); + let error = locked.endpoint_owner(&journal).err().unwrap(); + assert!(error.to_string().contains("does not hold the launch log")); + assert!(other.0.try_wait().unwrap().is_none()); + drop(prepared); + assert!(released(|| !locked.claim_held(&journal).unwrap())); + assert!(locked.endpoint_owner(&journal).is_err()); + assert!(other.0.try_wait().unwrap().is_none()); +} + +#[cfg(windows)] +#[test] +fn without_a_log_job_inherited_handles_remain_the_claim() { + let (_directory, state) = state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + drop(locked.begin_launch(&prepared, &runtime).unwrap()); + let journal = locked.journal().unwrap().unwrap(); + let mut command = helper_command(&state); + command + .stdout(Stdio::from(prepared.log().try_clone().unwrap())) + .stderr(Stdio::from(prepared.log().try_clone().unwrap())); + let child = Other(spawn_locked(&mut command).unwrap()); + drop(command); + drop(prepared); + assert!(released(|| state.dir().join("holder-ready").exists())); + assert!(locked.claim_held(&journal).unwrap()); + drop(child); + assert!(released(|| !locked.claim_held(&journal).unwrap())); +} + +#[test] +fn endpoint_ownership_is_rechecked_only_after_the_server_is_held() { + let pid = std::process::id(); + let held = std::cell::Cell::new(false); + let mut lookups = 0; + let owner = hold_endpoint_server( + || { + lookups += 1; + assert_eq!(held.get(), lookups == 2); + Ok(Some(pid)) + }, + |found| { + assert_eq!(found, pid); + held.set(true); + Ok(Some(own_process())) + }, + ) + .unwrap(); + assert_eq!(owner.reference().pid, pid); + assert_eq!(lookups, 2); + for replacement in [None, Some(pid.wrapping_add(1))] { + let mut lookups = 0; + let error = hold_endpoint_server( + || { + lookups += 1; + Ok(if lookups == 1 { Some(pid) } else { replacement }) + }, + |_| Ok(Some(own_process())), + ) + .err() + .unwrap(); + assert!(error.to_string().contains("endpoint changed")); + assert_eq!(lookups, 2); + } +} + +#[test] +fn missing_owners_and_lookup_or_hold_errors_are_not_exit_proofs() { + assert!(hold_endpoint_server(|| Ok(None), |_| panic!("no owner was found")).is_err()); + assert!(hold_endpoint_server(|| Ok(Some(1)), |_| Ok(None)).is_err()); + for phase in 0..3 { + let mut lookups = 0; + let error = hold_endpoint_server( + || { + lookups += 1; + if (phase == 0 && lookups == 1) || (phase == 2 && lookups == 2) { + Err(io::Error::from(io::ErrorKind::PermissionDenied)) + } else { + Ok(Some(std::process::id())) + } + }, + |_| { + if phase == 1 { + Err(io::Error::from(io::ErrorKind::PermissionDenied)) + } else { + Ok(Some(own_process())) + } + }, + ) + .err() + .unwrap(); + let source = error.source().unwrap().downcast_ref::().unwrap(); + assert_eq!(source.kind(), io::ErrorKind::PermissionDenied); + } +} + +#[test] +#[ignore = "invoked as an owned subprocess by the launch-claim tests"] +fn log_holder_helper() { + #[cfg(target_os = "linux")] + let _listener = std::os::unix::net::UnixListener::bind("control.sock").unwrap(); + fs::write("holder-ready", b"").unwrap(); + thread::sleep(Duration::from_secs(30)); +} diff --git a/nvx-rs/src/host/cli/store/tests/recovery.rs b/nvx-rs/src/host/cli/store/tests/recovery.rs new file mode 100644 index 000000000..fae235449 --- /dev/null +++ b/nvx-rs/src/host/cli/store/tests/recovery.rs @@ -0,0 +1,362 @@ +use super::launch::prepared; +use super::*; +use crate::host::cli::store::files; +use crate::host::config::ConsoleAttachment; + +#[test] +fn the_next_lock_cleans_a_pre_spawn_journal_only_after_its_claim_is_released() { + let (_directory, state) = state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + drop(locked.begin_launch(&prepared, &runtime).unwrap()); + drop(locked); + let journal = fs::read(state.path(StateFile::Journal)).unwrap(); + let capability = fs::read(state.path(StateFile::Capability)).unwrap(); + let started = Instant::now(); + assert!(state.lock(Deadline::after(0.05)).is_err()); + assert!(started.elapsed() < Duration::from_secs(2)); + assert_eq!(fs::read(state.path(StateFile::Journal)).unwrap(), journal); + assert_eq!( + fs::read(state.path(StateFile::Capability)).unwrap(), + capability + ); + drop(prepared); + let locked = state.lock(Deadline::after(10.0)).unwrap(); + assert!(locked.journal().unwrap().is_none()); + assert!(locked.capability().unwrap().is_none()); + assert!(state.path(StateFile::Log).exists()); +} + +#[test] +fn a_transient_inherited_log_descriptor_is_rechecked_before_refusing_recovery() { + let (_directory, state) = state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + drop(locked.begin_launch(&prepared, &runtime).unwrap()); + let inherited = prepared.log().try_clone().unwrap(); + drop(prepared); + drop(locked); + let release = thread::spawn(move || { + thread::sleep(Duration::from_millis(80)); + drop(inherited); + }); + let locked = state.lock(Deadline::after(10.0)).unwrap(); + release.join().unwrap(); + assert!(locked.journal().unwrap().is_none()); + assert!(locked.capability().unwrap().is_none()); +} + +#[test] +fn an_interrupted_cleanup_with_no_capability_can_finish_idempotently() { + let (_directory, state) = state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + drop(locked.begin_launch(&prepared, &runtime).unwrap()); + files::remove(&locked, StateFile::Capability).unwrap(); + drop(prepared); + locked + .recover(Instant::now() + Duration::from_secs(10)) + .unwrap(); + assert!(locked.journal().unwrap().is_none()); + locked.recover(Instant::now()).unwrap(); +} + +#[test] +fn replaced_logs_and_foreign_journal_paths_keep_all_evidence() { + for foreign in [false, true] { + let (_directory, state) = state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + drop(locked.begin_launch(&prepared, &runtime).unwrap()); + drop(prepared); + if foreign { + let mut journal = locked.journal().unwrap().unwrap(); + journal.vm_dir = state.dir().join("another-vm"); + locked.write_record(StateFile::Journal, &journal).unwrap(); + } else { + fs::rename(state.path(StateFile::Log), state.dir().join("old-log")).unwrap(); + fs::write(state.path(StateFile::Log), b"another log").unwrap(); + } + let journal = fs::read(state.path(StateFile::Journal)).unwrap(); + let capability = fs::read(state.path(StateFile::Capability)).unwrap(); + assert!(locked.recover(Instant::now()).is_err()); + assert_eq!(fs::read(state.path(StateFile::Journal)).unwrap(), journal); + assert_eq!( + fs::read(state.path(StateFile::Capability)).unwrap(), + capability + ); + drop(locked); + let (_, message) = error(state.lock(Deadline::after(0.0)).unwrap_err()); + let cause = if foreign { + "another sandbox directory" + } else { + "is not the claimed log" + }; + assert!(message.contains(cause), "{message}"); + } +} + +#[cfg(any(target_os = "linux", windows))] +mod process { + #[cfg(target_os = "linux")] + use std::process::{Child, Command, Stdio}; + + use super::*; + use crate::host::process::{BootId, Process, ProcessRef}; + use crate::host::spawn::{self, VmmCommand}; + use crate::host::vm::{Prepared, SavedVm, VmDir}; + + const HELPER: &str = "host::cli::store::tests::recovery::process::launch_helper"; + + struct Running(Option); + + impl Running { + fn process(&self) -> Process { + self.0.as_ref().unwrap().process().unwrap() + } + } + + impl Drop for Running { + fn drop(&mut self) { + if let Some(child) = self.0.take() { + child.kill_and_confirm_exit().unwrap(); + } + } + } + + #[cfg(target_os = "linux")] + struct Other(Child); + + #[cfg(target_os = "linux")] + impl Drop for Other { + fn drop(&mut self) { + if self.0.try_wait().unwrap().is_none() { + self.0.kill().unwrap(); + } + self.0.wait().unwrap(); + } + } + + fn running(state: &StateDir, prepared: &Prepared) -> Running { + fs::write( + state.dir().join("recovery-endpoint"), + &prepared.outputs().endpoint, + ) + .unwrap(); + let command = VmmCommand::new( + std::env::current_exe().unwrap(), + ["--exact", HELPER, "--ignored", "--nocapture"] + .map(Into::into) + .to_vec(), + state.dir(), + ); + let child = Running(Some( + spawn::spawn_vm( + &command, + prepared.capability().as_bytes(), + prepared.log().try_clone().unwrap(), + ) + .unwrap(), + )); + assert!(released(|| state.dir().join("recovery-ready").exists())); + child + } + + #[test] + fn an_unrecorded_vm_requires_both_endpoint_and_log_proofs_before_it_is_ended() { + let (_directory, state) = state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + drop(locked.begin_launch(&prepared, &runtime).unwrap()); + let child = running(&state, &prepared); + let process = child.process(); + drop(prepared); + drop(locked); + assert!(!process.has_exited().unwrap()); + let locked = state.lock(Deadline::after(10.0)).unwrap(); + assert!(process.wait(Instant::now()).unwrap().is_some()); + assert!(locked.journal().unwrap().is_none()); + assert!(locked.capability().unwrap().is_none()); + } + + #[cfg(target_os = "linux")] + #[test] + fn an_endpoint_owner_without_the_log_is_not_signaled_and_its_journal_stays() { + let (_directory, state) = state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + drop(locked.begin_launch(&prepared, &runtime).unwrap()); + fs::write( + state.dir().join("recovery-endpoint"), + &prepared.outputs().endpoint, + ) + .unwrap(); + let mut command = Command::new(std::env::current_exe().unwrap()); + command + .args(["--exact", HELPER, "--ignored", "--nocapture"]) + .current_dir(state.dir()) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + let mut other = Other(crate::host::tests::spawn_locked(&mut command).unwrap()); + assert!(released(|| state.dir().join("recovery-ready").exists())); + let journal = fs::read(state.path(StateFile::Journal)).unwrap(); + let capability = fs::read(state.path(StateFile::Capability)).unwrap(); + assert!( + locked + .recover(Instant::now() + Duration::from_secs(2)) + .is_err() + ); + assert!(other.0.try_wait().unwrap().is_none()); + assert_eq!(fs::read(state.path(StateFile::Journal)).unwrap(), journal); + assert_eq!( + fs::read(state.path(StateFile::Capability)).unwrap(), + capability + ); + } + + #[test] + fn a_recorded_process_is_ended_only_after_a_matching_identity_and_confirmed_exit() { + let (_directory, state) = state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + let mut launch = locked.begin_launch(&prepared, &runtime).unwrap(); + let child = running(&state, &prepared); + let process = child.process(); + launch.record_process(process.reference()).unwrap(); + drop(launch); + drop(prepared); + assert!(locked.recover(Instant::now()).is_err()); + assert!(!process.has_exited().unwrap()); + assert!(locked.journal().unwrap().is_some()); + assert!(locked.capability().unwrap().is_some()); + locked + .recover(Instant::now() + Duration::from_secs(10)) + .unwrap(); + assert!(process.wait(Instant::now()).unwrap().is_some()); + assert!(locked.journal().unwrap().is_none()); + assert!(locked.capability().unwrap().is_none()); + } + + #[test] + fn incorrect_recorded_ids_and_start_times_never_signal_a_live_process_or_drop_its_claim() { + for mismatch in 0..3 { + let (_directory, state) = state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + let mut launch = locked.begin_launch(&prepared, &runtime).unwrap(); + let child = running(&state, &prepared); + let process = child.process(); + let mut reference = process.reference().clone(); + match mismatch { + 0 => reference.pid = 0, + 1 => { + reference.pid = std::process::id(); + reference.start_time = platform::process_start_time(reference.pid) + .unwrap() + .unwrap() + + 1; + } + 2 => reference.start_time += 1, + _ => unreachable!(), + } + launch.record_process(&reference).unwrap(); + drop(launch); + drop(prepared); + let journal = fs::read(state.path(StateFile::Journal)).unwrap(); + let capability = fs::read(state.path(StateFile::Capability)).unwrap(); + assert!( + locked + .recover(Instant::now() + Duration::from_secs(2)) + .is_err() + ); + assert!(!process.has_exited().unwrap()); + assert_eq!(fs::read(state.path(StateFile::Journal)).unwrap(), journal); + assert_eq!( + fs::read(state.path(StateFile::Capability)).unwrap(), + capability + ); + } + } + + #[test] + fn an_earlier_boot_proves_exit_without_signaling_a_process_from_this_boot() { + let (_directory, state) = state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + let mut launch = locked.begin_launch(&prepared, &runtime).unwrap(); + let child = running(&state, &prepared); + let process = child.process(); + let mut boot = *BootId::current().unwrap().as_bytes(); + boot[0] ^= 1; + launch + .record_process(&ProcessRef { + boot: BootId::from_bytes(boot), + ..process.reference().clone() + }) + .unwrap(); + drop(launch); + drop(prepared); + locked.recover(Instant::now()).unwrap(); + assert!(!process.has_exited().unwrap()); + assert!(locked.journal().unwrap().is_none()); + assert!(locked.capability().unwrap().is_none()); + } + + #[test] + fn a_committed_runtime_is_not_recovered_through_its_leftover_journal() { + let (_directory, state) = state(); + let (prepared, runtime) = prepared(&state, ConsoleAttachment::Disconnected); + let mut locked = state.lock(Deadline::after(10.0)).unwrap(); + let mut launch = locked.begin_launch(&prepared, &runtime).unwrap(); + let child = running(&state, &prepared); + let process = child.process(); + launch.record_process(process.reference()).unwrap(); + let journal = fs::read(state.path(StateFile::Journal)).unwrap(); + launch + .commit(&SavedVm { + vm_dir: VmDir::open(state.dir()).unwrap(), + endpoint: prepared.outputs().endpoint.clone(), + capability: *prepared.capability().as_bytes(), + process: process.reference().clone(), + runtime, + }) + .unwrap(); + files::replace(&locked, StateFile::Journal, &journal).unwrap(); + let before = fs::read(state.path(StateFile::Runtime)).unwrap(); + drop(prepared); + drop(locked); + let locked = state.lock(Deadline::after(10.0)).unwrap(); + assert!(!process.has_exited().unwrap()); + assert_eq!(fs::read(state.path(StateFile::Runtime)).unwrap(), before); + assert_eq!(fs::read(state.path(StateFile::Journal)).unwrap(), journal); + assert!(locked.capability().unwrap().is_some()); + } + + #[test] + #[ignore = "invoked as an owned subprocess by interrupted-launch recovery tests"] + fn launch_helper() { + let endpoint = fs::read_to_string("recovery-endpoint").unwrap(); + #[cfg(target_os = "linux")] + let _server = std::os::unix::net::UnixListener::bind(endpoint).unwrap(); + #[cfg(windows)] + let server = platform::tests::control_server(&endpoint); + fs::write("recovery-ready", b"").unwrap(); + #[cfg(target_os = "linux")] + thread::sleep(Duration::from_secs(30)); + #[cfg(windows)] + { + use std::io::Read; + + loop { + platform::tests::accept_control(&server); + match (&server).read(&mut [0]) { + Ok(0) => {} + Err(error) if error.kind() == std::io::ErrorKind::BrokenPipe => {} + result => panic!("unexpected endpoint probe: {result:?}"), + } + platform::tests::disconnect_control(&server); + } + } + } +} 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..f668618ad 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,103 @@ 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 { + 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) + } + } + + 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), + openvmm::admits(token), + "{token}" + ); } } diff --git a/nvx-rs/src/host/platform/tests.rs b/nvx-rs/src/host/platform/tests.rs index be5b7967a..37392e1ba 100644 --- a/nvx-rs/src/host/platform/tests.rs +++ b/nvx-rs/src/host/platform/tests.rs @@ -2,6 +2,8 @@ pub(crate) use super::linux::tests::{ check_identity, owner_only_problem, start_time_from_stat, store_dir_problem, }; +#[cfg(all(windows, feature = "cli"))] +pub(crate) use super::windows::tests::disconnect_control; #[cfg(windows)] pub(crate) use super::windows::tests::set_access; #[cfg(windows)] diff --git a/nvx-rs/src/host/platform/windows/endpoint/tests.rs b/nvx-rs/src/host/platform/windows/endpoint/tests.rs index 42e283d69..4d882574a 100644 --- a/nvx-rs/src/host/platform/windows/endpoint/tests.rs +++ b/nvx-rs/src/host/platform/windows/endpoint/tests.rs @@ -36,6 +36,19 @@ pub(crate) fn accept_control(server: &File) -> i32 { connected } +#[cfg(feature = "cli")] +pub(crate) fn disconnect_control(server: &File) { + use std::os::windows::io::{AsHandle, AsRawHandle}; + + use windows_sys::Win32::System::Pipes::DisconnectNamedPipe; + + // SAFETY: the owned synchronous pipe finished its previous client. + assert_ne!( + unsafe { DisconnectNamedPipe(server.as_handle().as_raw_handle()) }, + 0 + ); +} + #[test] fn a_duplicated_transport_keeps_the_verified_connection_without_reconnecting() { let endpoint = format!( diff --git a/nvx-rs/src/host/platform/windows/tests.rs b/nvx-rs/src/host/platform/windows/tests.rs index 7da84f781..a3aa27b6e 100644 --- a/nvx-rs/src/host/platform/windows/tests.rs +++ b/nvx-rs/src/host/platform/windows/tests.rs @@ -20,6 +20,8 @@ use windows_sys::Win32::System::Threading::{ PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_SYNCHRONIZE, TerminateProcess, }; +#[cfg(feature = "cli")] +pub(crate) use super::endpoint::tests::disconnect_control; pub(crate) use super::endpoint::tests::{accept_control, control_server}; use super::identity::status_error; use super::log_claim::{local_directory, log_job_id, log_job_name, log_job_processes, object_name}; diff --git a/nvx-rs/src/host/restore/tests.rs b/nvx-rs/src/host/restore/tests.rs index f163d92da..918e04ae3 100644 --- a/nvx-rs/src/host/restore/tests.rs +++ b/nvx-rs/src/host/restore/tests.rs @@ -1,6 +1,8 @@ #![cfg(any(target_os = "linux", windows))] #![cfg_attr(windows, allow(unsafe_code))] +mod live_init; + use std::io::{BufRead, BufReader, Write}; use std::process::{Child, Command, Stdio}; use std::time::Instant; diff --git a/nvx-rs/src/host/restore/tests/live_init.rs b/nvx-rs/src/host/restore/tests/live_init.rs new file mode 100644 index 000000000..9b958ede6 --- /dev/null +++ b/nvx-rs/src/host/restore/tests/live_init.rs @@ -0,0 +1,522 @@ +use std::error::Error; +use std::ffi::OsString; +use std::fs::{self, OpenOptions}; +use std::io; +use std::net::{SocketAddr, TcpListener}; +use std::path::{Path, PathBuf}; +use std::thread; +use std::time::{Duration, Instant}; + +use crate::Hypervisor; +use crate::activation::{ + ActivationRecord, Configuration, ExpectedResources, Export, ExportKind, Image, MachineIdMode, + Network, OverlayMode, Publication, ReadyActivation, RestoreSection, TransactionId, Upper, View, +}; +use crate::host::instance::generate_capability; +use crate::host::nvxs::HostConnection; +use crate::host::session; +use crate::host::supervisor::{Launched, VmmCommand, spawn}; +use crate::host::template::TemplateManifest; +use crate::protocol::{ + APP_ERROR, APP_EXEC, APP_EXIT, APP_MAPS, APP_READY, APP_STDERR, APP_STDOUT, APP_STOP, + APP_STOPPED, HostMap, Workload, WorkloadEnvironment, decode_app, encode_app, + encode_exec_payload, encode_maps_payloads, +}; + +type AnyError = Box; +const ACTIVATE: u8 = 8; +const CAPTURE: u8 = 7; + +struct OwnedVm(Option); +impl Drop for OwnedVm { + fn drop(&mut self) { + if let Some(vm) = self.0.take() { + if vm + .process() + .and_then(|process| process.has_exited()) + .is_ok_and(|done| done) + { + vm.detach(); + } else if !vm.kill_and_wait() { + eprintln!("OWNED_RESTORE_TEARDOWN_FAILED"); + } + } + } +} + +fn require(ok: bool, message: impl Into) -> Result<(), AnyError> { + if ok { + Ok(()) + } else { + Err(message.into().into()) + } +} + +fn backend() -> Hypervisor { + if cfg!(windows) { + Hypervisor::Whp + } else { + Hypervisor::Mshv + } +} + +fn private_directory(path: &Path) -> io::Result<()> { + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt; + fs::DirBuilder::new().mode(0o700).create(path) + } + #[cfg(windows)] + { + fs::create_dir(path) + } +} + +fn ready( + connection: &mut HostConnection, + id: u64, + expected: &[u8], + by: Instant, +) -> Result<(), AnyError> { + let bytes = connection.receive(Some(by))?; + let frame = decode_app(&bytes)?; + require( + (frame.kind, frame.request_id, frame.status, frame.payload) == (APP_READY, id, 0, expected), + format!("unexpected Ready: {frame:?}"), + ) +} + +fn workload(script: &str) -> Result, AnyError> { + Ok(encode_exec_payload(&Workload { + argv: vec!["/bin/sh".into(), "-eu".into(), "-c".into(), script.into()], + timeout_ms: 10_000, + cwd: Some("/"), + environment: WorkloadEnvironment::Default, + })?) +} + +fn execute( + connection: &mut HostConnection, + id: u64, + script: &str, + by: Instant, +) -> Result { + connection.send(&encode_app(APP_EXEC, id, 0, &workload(script)?), Some(by))?; + let mut output = Vec::new(); + loop { + let bytes = connection.receive(Some(by))?; + let frame = decode_app(&bytes)?; + require( + frame.request_id == id, + format!("wrong execution response: {frame:?}"), + )?; + match frame.kind { + APP_STDOUT => output.extend_from_slice(frame.payload), + APP_STDERR => eprint!("{}", String::from_utf8_lossy(frame.payload)), + APP_EXIT => { + require( + frame.status == 0 && frame.payload == b"exit", + format!("execution failed: {frame:?}"), + )?; + return Ok(String::from_utf8(output)?); + } + _ => return Err(format!("unexpected execution frame: {frame:?}").into()), + } + } +} + +fn run() -> Result<(), AnyError> { + let grant = std::env::var("NVX_SL77B_LIVE_GRANT")?; + require( + !grant.is_empty(), + "live fixture requires explicit named host admission", + )?; + let mut args = vec![OsString::from("nvx-live-restored-init")]; + for key in [ + "NVX_SL77B_LIVE_NATIVE", + "NVX_SL77B_LIVE_SNAPSHOT", + "NVX_SL77B_LIVE_DISTRO", + "NVX_SL77B_LIVE_CONSOLE", + "NVX_SL77B_LIVE_ROOT", + "NVX_SL77B_LIVE_RESULT", + "NVX_SL77B_LIVE_CASE", + ] { + args.push( + std::env::var_os(key).ok_or_else(|| format!("missing live fixture input {key}"))?, + ); + } + require( + args.len() == 8, + "usage: restore VMM SNAPSHOT DISTRO ORIGINAL_CONSOLE ROOT RESULT CASE", + )?; + let vmm = PathBuf::from(&args[1]); + let snapshot = PathBuf::from(&args[2]); + let distro = PathBuf::from(&args[3]); + let original_console = PathBuf::from(&args[4]); + let root = PathBuf::from(&args[5]); + let result = PathBuf::from(&args[6]); + let case = args[7].to_str().ok_or("non-Unicode case")?; + require( + matches!(case, "published" | "unpublished" | "changed" | "deadline"), + "invalid fixture case", + )?; + for path in [&vmm, &snapshot, &distro, &original_console, &root, &result] { + require(path.is_absolute(), "all fixture paths must be absolute")?; + } + let manifest = TemplateManifest::read(&snapshot)?; + require( + manifest.hypervisor() == backend() && manifest.memory_len() == 128 << 20, + "wrong backend/memory snapshot", + )?; + require( + !root.exists() && !result.exists(), + "preserve existing fixture effects", + )?; + private_directory(&root)?; + let export = root.join("export"); + fs::create_dir(&export)?; + fs::write(export.join("proof"), b"CURRENT_573F_HOST_MAP\n")?; + let endpoint = if cfg!(windows) { + format!( + "//./pipe/openvmm-microvm-sl77b-live-{}-{case}", + std::process::id() + ) + } else { + snapshot + .parent() + .ok_or("snapshot has no private namespace parent")? + .join(format!("nvxs-{case}")) + .to_str() + .ok_or("non-Unicode endpoint")? + .to_owned() + }; + let address: SocketAddr = fs::read_to_string(&original_console)?.trim().parse()?; + require(address.ip().is_loopback(), "console must remain loopback")?; + let listener = TcpListener::bind(address)?; + listener.set_nonblocking(true)?; + let console = root.join("restored-console.log"); + let console_output = console.clone(); + let reader = thread::spawn(move || -> io::Result<()> { + let by = Instant::now() + Duration::from_secs(30); + let mut stream = loop { + match listener.accept() { + Ok((stream, _)) => break stream, + Err(error) if error.kind() == io::ErrorKind::WouldBlock && Instant::now() < by => { + thread::sleep(Duration::from_millis(25)); + } + Err(error) => return Err(error), + } + }; + stream.set_nonblocking(false)?; + let mut output = OpenOptions::new() + .write(true) + .create_new(true) + .open(console_output)?; + io::copy(&mut stream, &mut output)?; + Ok(()) + }); + let report = root.join("outcome.json"); + let (_temporary, directory) = super::vm_directory(); + let mut listener = super::ReadyListener::bind(&directory)?; + let acknowledgement = listener.endpoint().to_owned(); + let log_file = root.join("openvmm.log"); + let log = OpenOptions::new() + .write(true) + .create_new(true) + .open(&log_file)?; + let options: Vec = vec![ + "--machine".into(), + "microvm".into(), + "--microvm-sandbox-block".into(), + format!("distro:file:{},ro", distro.display()).into(), + "--restore-absent-scratch".into(), + "--single-process".into(), + "--hypervisor".into(), + if cfg!(windows) { "whp" } else { "mshv" }.into(), + "--cpu-profile".into(), + "auto".into(), + "--processors".into(), + "1".into(), + "--restore-snapshot".into(), + snapshot.as_os_str().to_owned(), + "--restore-memory".into(), + "128M".into(), + "--restore-ready-path".into(), + acknowledgement.into(), + "--restore-network-policy".into(), + "--network-profile".into(), + "portable".into(), + "--network-egress".into(), + "deny".into(), + "--network-ingress".into(), + "deny".into(), + "--host-loopback".into(), + "deny".into(), + "--mount-aggregate".into(), + "/run/nvx/hostfs/root".into(), + "--mount-child".into(), + format!("0,{},ro", export.display()).into(), + "--virtio-console".into(), + format!("connect=tcp:{address}").into(), + "--microvm-control-console".into(), + format!("listen={endpoint}").into(), + "--microvm-control-auth-stdin".into(), + "--microvm-report".into(), + report.as_os_str().to_owned(), + ]; + let capability = generate_capability()?; + let command = VmmCommand::new(vmm, options, &root); + let mut owned = OwnedVm(Some(spawn(&command, &capability, log)?)); + let running = owned.0.as_ref().ok_or("missing owned VM")?; + let process = running.process()?; + let pid = running.id(); + fs::write( + root.join("process.txt"), + format!( + "pid={pid}\nreference={:?}\ncase={case}\n", + process.reference() + ), + )?; + println!( + "OWNED_RESTORE_PID={pid} REFERENCE={:?} CASE={case}", + process.reference() + ); + let by = Instant::now() + Duration::from_secs(90); + let expected_event = b"OPENVMM_RESTORE_READY_V1\n"; + let mut event = Vec::new(); + while event.len() < expected_event.len() { + require(Instant::now() < by, "native readiness deadline expired")?; + let open = listener.receive(&process, &mut event, 64, Duration::from_millis(50))?; + require( + open || event.len() == expected_event.len(), + "native readiness closed before its event", + )?; + } + require( + event == expected_event, + "native restore-ready event differs", + )?; + super::parse_ready_event(&event)?; + fs::write(root.join("native-ready.receipt"), &event)?; + println!("NATIVE_READY_AUTHENTICATED_PID={pid}"); + let native_ready = Instant::now(); + if case == "deadline" { + thread::sleep(Duration::from_secs(8)); + } + let transport = session::connect(&endpoint, pid, process.reference().start_time, by)?; + let mut connection = HostConnection::attach(transport, &capability, by)?; + println!( + "RESTORED_HOST_SESSION_EPOCH={}", + connection.identity().epoch + ); + let attached = Instant::now(); + let expected_status; + if case == "deadline" { + require( + process + .wait(Instant::now() + Duration::from_secs(40))? + .is_some(), + "activation deadline did not end VM", + )?; + require( + native_ready.elapsed() >= Duration::from_secs(27) + && native_ready.elapsed() < Duration::from_secs(38) + && attached.elapsed() < Duration::from_secs(27), + "activation deadline renewed at the deliberately delayed host attachment", + )?; + println!( + "ORIGINAL_ACK_DEADLINE_ELAPSED_MS={}", + native_ready.elapsed().as_millis() + ); + expected_status = 125; + } else { + let published = case != "unpublished"; + let transaction = TransactionId::from_bytes([0x77; 16]); + let record = ActivationRecord::new( + Some(RestoreSection { transaction }), + Configuration { + image: Image::Erofs { uuid: None }, + upper: Upper::Tmpfs { + max_memory_percent: 50, + }, + overlay: OverlayMode::UntrustedLowers, + export: Some(Export { + kind: ExportKind::Aggregate, + }), + mappings: 1, + network: Some(Network { + dns: Some("10.88.0.53".parse()?), + publication: if published { + Publication::Published + } else { + Publication::Unpublished + }, + proxy: None, + }), + view: View { + hostname: b"restored-573f".to_vec(), + machine_id: MachineIdMode::Generated, + }, + resources: ExpectedResources { + cpus: 1, + memory: 128 << 20, + }, + }, + )?; + let mut activation = Vec::new(); + activation.extend_from_slice(&1000u32.to_le_bytes()); + activation.extend_from_slice(&1001u32.to_le_bytes()); + activation.push(1); + activation.extend(record.encode()); + fs::write(root.join("activate.bin"), &activation)?; + let expected_ready = ReadyActivation { + transaction: Some(transaction), + } + .encode(); + connection.send(&encode_app(ACTIVATE, 100, 0, &activation), Some(by))?; + ready(&mut connection, 100, &expected_ready, by)?; + println!("ACTIVATION_READY_PUBLICATION={published} OWN_DNS=10.88.0.53"); + if case == "changed" { + activation[0] ^= 1; + connection.send(&encode_app(ACTIVATE, 101, 0, &activation), Some(by))?; + require( + process + .wait(Instant::now() + Duration::from_secs(15))? + .is_some(), + "changed identity replay did not end VM", + )?; + expected_status = 125; + println!("CHANGED_IDENTITY_REPLAY_FATAL"); + } else { + connection.send(&encode_app(APP_EXEC, 102, 0, &workload("true")?), Some(by))?; + let bytes = connection.receive(Some(by))?; + let frame = decode_app(&bytes)?; + require( + frame.kind == APP_ERROR + && frame.status == 11 + && frame.payload == b"mappings-incomplete", + format!("EXEC bypassed MAPS gate: {frame:?}"), + )?; + let maps = [HostMap { + source: "0".into(), + target: "/mnt/restore-map".into(), + read_only: true, + }]; + for payload in encode_maps_payloads(&maps, 32768)? { + connection.send(&encode_app(APP_MAPS, 103, 0, &payload), Some(by))?; + ready(&mut connection, 103, &[], by)?; + } + let script = format!( + r#" +test "$(id -u)" = 1000 +test "$(id -g)" = 1001 +test "$(hostname)" = restored-573f +grep -q 'nameserver 10.88.0.53' /etc/resolv.conf +test "$(cat /mnt/restore-map/proof)" = CURRENT_573F_HOST_MAP +test ! -e /sbin/nvx-ingress +count=0 +for p in /proc/[0-9]*; do + test "$(cat "$p/comm" 2>/dev/null || true)" = nvx-ingress || continue + count=$((count + 1)) + grep -q '^PPid:[[:space:]]*1$' "$p/status" + test "$(cat "$p/oom_score_adj")" = -1000 + test "$(tr '\000' ' ' <"$p/cmdline")" = 'nvx-ingress 10.88.0.1 ' + test "$(awk '{{print $19}}' "$p/stat")" = -10 + printf 'REAL_RESTORED_ANNOUNCER pid=%s argv=' "${{p##*/}}" + tr '\000' ' ' <"$p/cmdline" + printf '\n' +done +test "$count" = {} +printf 'PRESERVED_573F_UPPER\n' >/tmp/restore-sentinel +printf 'MACHINE_ID=' +cat /etc/machine-id +printf '\n' +printf 'REAL_READY_MAPS_EXEC_OK\n' +"#, + u8::from(published) + ); + let first = execute(&mut connection, 104, &script, by)?; + print!("{first}"); + let machine = first + .lines() + .find(|line| line.starts_with("MACHINE_ID=")) + .ok_or("missing generated machine ID")? + .to_owned(); + for kill in [0, 1] { + connection.send( + &encode_app(CAPTURE, 105 + u64::from(kill), 0, &[kill]), + Some(by), + )?; + let bytes = connection.receive(Some(by))?; + let frame = decode_app(&bytes)?; + require( + frame.kind == APP_ERROR && frame.status == 95, + "activated restore accepted capture", + )?; + } + drop(connection); + let transport = session::connect(&endpoint, pid, process.reference().start_time, by)?; + connection = HostConnection::attach(transport, &capability, by)?; + connection.send(&encode_app(ACTIVATE, 107, 0, &activation), Some(by))?; + ready(&mut connection, 107, &expected_ready, by)?; + let replay = execute( + &mut connection, + 108, + r#" +test "$(cat /tmp/restore-sentinel)" = PRESERVED_573F_UPPER +test "$(cat /mnt/restore-map/proof)" = CURRENT_573F_HOST_MAP +printf 'MACHINE_ID=' +cat /etc/machine-id +printf '\n' +printf 'RECONNECT_REPLAY_PRESERVED_UPPER_AND_MAPS\n' +"#, + by, + )?; + require( + replay.lines().any(|line| line == machine), + "retry regenerated the machine ID", + )?; + print!("{replay}"); + connection.send(&encode_app(APP_STOP, 109, 0, &[]), Some(by))?; + let bytes = connection.receive(Some(by))?; + let frame = decode_app(&bytes)?; + require( + frame.kind == APP_STOPPED && frame.status == 0, + "STOP not acknowledged", + )?; + require( + process + .wait(Instant::now() + Duration::from_secs(30))? + .is_some(), + "STOP did not tear down restore", + )?; + expected_status = 0; + } + } + drop(connection); + reader.join().map_err(|_| "console reader panicked")??; + let outcome = crate::host::report::read(&report)?.ok_or("missing native outcome")?; + require( + outcome.status_code == Some(expected_status) && outcome.teardown_complete == Some(true), + format!("wrong restore outcome: {outcome:?}"), + )?; + let logs = fs::read_to_string(&console)?; + let switches = logs.matches("NVX-INIT-ROOT: switched").count(); + require( + switches == usize::from(case != "deadline"), + format!("unexpected root switch count {switches}"), + )?; + let text = format!( + "RESTORE_OK case={case} pid={pid} status={expected_status} switches={switches} teardown=true\n" + ); + fs::write(result, &text)?; + print!("{text}"); + owned.0.take().ok_or("missing owned restore")?.detach(); + Ok(()) +} + +#[test] +#[ignore = "requires explicit named host admission and current-source live restore fixture inputs"] +fn live_restored_init_activation() -> Result<(), AnyError> { + run() +} diff --git a/nvx-rs/src/host/snapshot.rs b/nvx-rs/src/host/snapshot.rs index 7fcb88f6e..55591aeb7 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`]. @@ -11,9 +14,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`. @@ -28,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; @@ -40,11 +45,17 @@ 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::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::{ @@ -58,6 +69,31 @@ 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 +/// 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 @@ -96,30 +132,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_openvmm(&dir.join(OPENVMM_DIR))?; let path = dir.join(KEY_FILE); let bytes = regular::open_regular(&path) .and_then(|file| { @@ -142,6 +158,15 @@ impl Snapshot { } } +/// 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 new file mode 100644 index 000000000..6d2132cc7 --- /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, check_openvmm, +}; +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_openvmm(&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(()) +} + +/// 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| { + 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..93b534f66 --- /dev/null +++ b/nvx-rs/src/host/snapshot/capture/tests.rs @@ -0,0 +1,469 @@ +//! 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.time(), manifest.time()); + 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 8ab84cc1d..313a8ae01 100644 --- a/nvx-rs/src/host/snapshot/key.rs +++ b/nvx-rs/src/host/snapshot/key.rs @@ -6,19 +6,21 @@ 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, - 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::check_setting; +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}; @@ -203,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 @@ -292,9 +295,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. @@ -463,12 +466,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, @@ -476,7 +479,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. @@ -489,15 +493,43 @@ 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; +/// 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. /// - /// 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. @@ -532,6 +564,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!( @@ -548,7 +639,7 @@ 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 = match class.cpu_profile.as_str() { + let selected = match class.cpu_profile { "auto" => true, "host" => machine.cpu.generation == HOST_GENERATION, requested => profile == requested, @@ -556,18 +647,23 @@ impl SnapshotKey { 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), ); } @@ -583,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( @@ -593,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 { @@ -614,12 +705,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, @@ -637,6 +734,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 { @@ -652,6 +750,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() @@ -732,8 +838,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: /// @@ -765,18 +873,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 @@ -812,12 +930,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)?; @@ -826,6 +946,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 d21fe338f..328b5b7f3 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 c73fe2710..e7ba35892 100644 --- a/nvx-rs/src/host/snapshot/tests.rs +++ b/nvx-rs/src/host/snapshot/tests.rs @@ -1207,6 +1207,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/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 142ca7c0c..89a38ddf0 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,20 +31,21 @@ 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}; /// 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. @@ -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..2ff425c72 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/nvx-rs/src/protocol/app.rs b/nvx-rs/src/protocol/app.rs index c6645486f..2feb2c3ff 100644 --- a/nvx-rs/src/protocol/app.rs +++ b/nvx-rs/src/protocol/app.rs @@ -62,7 +62,10 @@ pub const APP_MAPS: u8 = 6; pub(crate) const APP_CAPTURE: u8 = 7; /// Request that nvx-init activate a restored VM with its record and workload identity. #[cfg_attr( - not(test), + not(any( + test, + all(feature = "guest", target_os = "linux", target_arch = "x86_64") + )), expect( dead_code, reason = "SL4.3 sends activation requests to nvx-init in SL7.7b" @@ -204,7 +207,10 @@ pub(crate) fn encode_activate_payload( /// Decodes the persistent workload identity and the activation record that follows it. #[cfg_attr( - not(test), + not(any( + test, + all(feature = "guest", target_os = "linux", target_arch = "x86_64") + )), expect(dead_code, reason = "SL7.7b makes nvx-init decode activation requests") )] pub(crate) fn decode_activate_payload( diff --git a/nvx-rs/src/protocol/mod.rs b/nvx-rs/src/protocol/mod.rs index 907ff555c..c66dfd348 100644 --- a/nvx-rs/src/protocol/mod.rs +++ b/nvx-rs/src/protocol/mod.rs @@ -61,16 +61,21 @@ pub mod view; all(feature = "guest", target_os = "linux", target_arch = "x86_64") ))] pub(crate) use app::APP_CAPTURE; +#[cfg(feature = "host")] +pub(crate) use app::encode_capture_payload; +#[cfg(any( + feature = "host", + all(feature = "guest", target_os = "linux", target_arch = "x86_64") +))] +pub(crate) use app::workload; #[cfg(all(feature = "guest", target_os = "linux", target_arch = "x86_64"))] -pub(crate) use app::decode_capture_payload; +pub(crate) use app::{APP_ACTIVATE, decode_activate_payload, decode_capture_payload}; pub use app::{ APP_CANCEL, APP_ERROR, APP_EXEC, APP_EXIT, APP_FEATURES, APP_HEADER_LEN, APP_MAGIC, APP_MAPS, APP_PING, APP_READY, APP_STDERR, APP_STDOUT, APP_STOP, APP_STOPPED, APP_VERSION, AppFrame, BUSY, CWD_FAILED, ExitCategory, INVALID_REQUEST, LAUNCH_FAILED, MAPPING_FAILED, MAPPINGS_INCOMPLETE, UNSUPPORTED_OPERATION, decode_app, encode_app, }; -#[cfg(feature = "host")] -pub(crate) use app::{encode_capture_payload, workload}; pub use boot::{MAPS_TOKEN, WORKLOAD_ACCOUNT_TOKEN, parse_host_mapping_count}; pub use exec::{ EXEC_CWD_PRESENT, EXEC_ENVIRONMENT_PRESENT, EXEC_EXTENDED, EXEC_INHERIT_DEFAULT_ENV, 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 diff --git a/scripts/test-layout-allowlist.json b/scripts/test-layout-allowlist.json index 399c715f3..347e17ac4 100644 --- a/scripts/test-layout-allowlist.json +++ b/scripts/test-layout-allowlist.json @@ -5,12 +5,14 @@ "nvx-rs/src/guest/cgroup.rs": 2, "nvx-rs/src/guest/entry.rs": 1, "nvx-rs/src/guest/exec.rs": 4, - "nvx-rs/src/guest/init.rs": 6, + "nvx-rs/src/guest/init.rs": 7, "nvx-rs/src/guest/init/boot.rs": 1, "nvx-rs/src/guest/reseed.rs": 1, "nvx-rs/src/guest/sys/entry.rs": 3, "nvx-rs/src/guest/sys/spawn.rs": 7, "nvx-rs/src/guest/time/system.rs": 3, + "nvx-rs/src/host/cli/sandbox/state.rs": 1, + "nvx-rs/src/host/cli/snapshots.rs": 1, "nvx-rs/src/host/cli/store/mod.rs": 1, "nvx-rs/src/host/network/address.rs": 2, "nvx-rs/src/host/platform/windows/spawn.rs": 1, @@ -18,7 +20,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 } } diff --git a/scripts/test_nvx_tools.py b/scripts/test_nvx_tools.py index c653e4a84..c984b57b1 100644 --- a/scripts/test_nvx_tools.py +++ b/scripts/test_nvx_tools.py @@ -18440,6 +18440,168 @@ def _shell_function(source: str, name: str) -> str: return source[start : source.index("\n}\n", start) + 3] +class GuestInitRoutingTests(unittest.TestCase): + INIT = Path(__file__).parents[1] / "guest" / "common" / "init" + + def setUp(self): + shell = _posix_shell() + if shell is None: + self.skipTest("POSIX shell is unavailable") + self.shell = shell + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.pid = self.root / "daemon.pid" + self.announcer = self.root / "announcer.pid" + (self.root / "net" / "eth0").mkdir(parents=True) + (self.root / "os-release").write_text("ID=alpine\n", encoding="utf-8") + (self.root / "alpine-release").write_text("test\n", encoding="utf-8") + self.helpers: dict[str, Path] = {} + for name, script in { + "nvx-time": f"printf '4242\\n' >{shlex.quote(self.pid.as_posix())}\n", + "nvx-init": 'printf "new-init"; printf " <%s>" "$@"; echo\n', + "nvx-init-agent": 'printf "old-init"; printf " <%s>" "$@"; echo\n', + "nvx-ingress": ( + f'printf "%s\\n" "$$" >{shlex.quote(self.announcer.as_posix())}\n' + ), + "nvx-hostmount": "echo hostmount\n", + "shell": "echo blockless-shell\n", + }.items(): + path = self.root / name + path.write_text(f"#!/bin/sh\n{script}", encoding="utf-8") + path.chmod(0o755) + self.helpers[name] = path + + def _run(self, cmdline: str) -> subprocess.CompletedProcess[str]: + command_line = self.root / "cmdline" + command_line.write_text(cmdline, encoding="utf-8") + source = self.INIT.read_text(encoding="utf-8") + replacements = { + "/sbin/nvx-init-agent": self.helpers["nvx-init-agent"], + "/sbin/nvx-init": self.helpers["nvx-init"], + "/sbin/nvx-time": self.helpers["nvx-time"], + "/sbin/nvx-ingress": self.helpers["nvx-ingress"], + "/sbin/nvx-hostmount": self.helpers["nvx-hostmount"], + "/proc/cmdline": command_line, + "/proc/sys/kernel/modules_disabled": self.root / "modules_disabled", + "/sys/class/net": self.root / "net", + "/run/nvx/time/daemon.pid": self.pid, + "/etc/os-release": self.root / "os-release", + "/etc/alpine-release": self.root / "alpine-release", + "exec /bin/sh": self.helpers["shell"], + } + for original, replacement in replacements.items(): + target = shlex.quote(replacement.as_posix()) + if original == "exec /bin/sh": + target = f"exec {target}" + source = source.replace(original, target) + source = source.replace("/sbin/nvx-exit", "nvx_exit") + source = source.replace("/sbin/nvx-snapshot", "nvx_snapshot") + return subprocess.run( + [self.shell, "-s"], + input=( + "mount() { :; }\nifconfig() { :; }\nroute() { :; }\n" + 'nvx_exit() { exit "$1"; }\n' + "nvx_snapshot() { echo blockless-snapshot; }\n" + f"{source}" + ), + text=True, + capture_output=True, + check=False, + timeout=40, + ) + + def test_tokenless_layered_and_fresh_sandboxes_use_new_init(self): + for tokens in ( + "nvx_layer=distro,0xd0003000,erofs nvx_overlay_upper=ramfs", + "nvx_snapshot_tier=platform", + "nvx_snapshot_tier=platform nvx_mode=managed", + ): + with self.subTest(tokens=tokens): + result = self._run(f"console=hvc0 nvx_sandbox=1 {tokens}") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "new-init <4242>\n") + + def test_old_lifecycle_tokens_keep_the_old_layered_init(self): + for lifecycle in ("managed", "one-shot", ""): + with self.subTest(lifecycle=lifecycle): + result = self._run( + "nvx_sandbox=1 nvx_layer=distro,0xd0003000,erofs " + f"nvx_overlay_upper=ramfs nvx_lifecycle={lifecycle}" + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue(result.stdout.startswith("old-init"), result.stdout) + self.assertNotIn("new-init", result.stdout) + + def test_exec_override_cannot_bypass_the_new_sandbox_init(self): + result = self._run( + "nvx_sandbox=1 nvx_snapshot_tier=platform nvx_exec=/bin/true" + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "new-init <4242>\n") + for tokens in ("", "nvx_sandbox=1 nvx_lifecycle=managed"): + with self.subTest(tokens=tokens): + result = self._run(f"{tokens} nvx_exec=/bin/true") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "") + + def test_blockless_platform_and_snapshot_vms_keep_their_routes(self): + for tokens in ("", "nvx_snapshot_tier=platform", "shellsnap snap"): + with self.subTest(tokens=tokens): + result = self._run(f"console=hvc0 {tokens}") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("hostmount\n", result.stdout) + self.assertIn("blockless-shell\n", result.stdout) + self.assertNotIn("new-init", result.stdout) + self.assertNotIn("old-init", result.stdout) + if tokens == "shellsnap snap": + self.assertEqual(result.stdout.count("blockless-snapshot\n"), 2) + self.assertIn("SNAP-RESTORE-OK\n", result.stdout) + + def test_new_init_receives_the_announcers_process_id_when_present(self): + for ingress in ("allow", "host-loopback"): + with self.subTest(ingress=ingress): + result = self._run( + "nvx_sandbox=1 nvx_snapshot_tier=platform " + "virtnet_ip=172.16.0.2 virtnet_gw=172.16.0.1 " + f"virtnet_ingress={ingress}" + ) + self.assertEqual(result.returncode, 0, result.stderr) + pid = self.announcer.read_text(encoding="utf-8").strip() + self.assertGreater(int(pid), 1) + self.assertIn(f"new-init <4242> <{pid}>\n", result.stdout) + + def test_new_init_waits_for_the_time_daemons_deferred_publication(self): + temporary = self.pid.with_name("daemon.pid.tmp") + self.helpers["nvx-time"].write_text( + "#!/bin/sh\n" + f"(sleep 0.1; printf '4242\\n' >{shlex.quote(temporary.as_posix())}; " + f"mv {shlex.quote(temporary.as_posix())} " + f"{shlex.quote(self.pid.as_posix())}) &\n", + encoding="utf-8", + ) + result = self._run("nvx_sandbox=1 nvx_snapshot_tier=platform") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stdout, "new-init <4242>\n") + + def test_missing_helpers_or_selected_init_fail_without_fallback(self): + self.helpers["nvx-time"].write_text("#!/bin/sh\nexit 0\n", encoding="utf-8") + self.pid.mkdir() + result = self._run("nvx_sandbox=1 nvx_snapshot_tier=platform") + self.assertEqual(result.returncode, 125, result.stdout) + self.assertIn("time daemon process ID is unavailable", result.stderr) + self.assertNotIn("new-init", result.stdout) + + self.helpers["nvx-init"].unlink() + result = self._run("nvx_sandbox=1 nvx_snapshot_tier=platform") + self.assertEqual(result.returncode, 125, result.stdout) + self.assertIn("selected guest cannot act as sandbox control", result.stderr) + self.assertNotIn("old-init", result.stdout) + result = self._run("nvx_sandbox=1 nvx_lifecycle=managed") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertTrue(result.stdout.startswith("old-init"), result.stdout) + + class GuestInitWorkloadIdentityTests(unittest.TestCase): """The shell /init's workload account and identity checks, as a one-shot launch that asks for an account with nvx_workload_account=create runs diff --git a/tests/runtime-adapter/README.md b/tests/runtime-adapter/README.md index d700bda5f..d5517f86e 100644 --- a/tests/runtime-adapter/README.md +++ b/tests/runtime-adapter/README.md @@ -1116,8 +1116,9 @@ NVX's initramfs hands a VM of the managed lifecycle to NVX's managed agent: its `/init` runs `exec /sbin/nvx-managed-agent /dev/hvc2 - nvx-managed UID GID NAME HOME` as PID 1. A VM with a layered root, whose kernel command line has -`nvx_sandbox=1`, goes to NVX's init agent instead, which assembles the root: -`/init` runs `exec /sbin/nvx-init-agent`, without arguments. +`nvx_sandbox=1` and no old OpenVMM lifecycle token, goes to NVX's init instead: +`/init` runs `exec /sbin/nvx-init TIME_DAEMON [ANNOUNCER]`, with the helpers' +process IDs. Old-token layered launches keep `/sbin/nvx-init-agent`. `initramfs::write` copies that initramfs, and appends to the copy a second archive, in the `newc` format, which Linux unpacks over the first: @@ -1128,8 +1129,8 @@ archive, in the `newc` format, which Linux unpacks over the first: - a symbolic link from `/sbin/nvx-managed-agent` to that script; - a script, `/sbin/runtime-adapter-layered`, that runs `runtime-adapter-guest init /dev/hvc2 layered`, which names the control tty - itself, since `/init` passes NVX's init agent none; -- a symbolic link from `/sbin/nvx-init-agent` to that script, so that NVX's + itself, and ignores the helper arguments; +- a symbolic link from `/sbin/nvx-init` to that script, so that NVX's init agent never assembles the root in the adapter's VMs; - for the capture of a template whose clones delay their repair, for tests, the delay's file, `/etc/runtime-adapter-repair-delay`, in the `/etc` directory of diff --git a/tests/runtime-adapter/src/initramfs.rs b/tests/runtime-adapter/src/initramfs.rs index 67384d158..f79127902 100644 --- a/tests/runtime-adapter/src/initramfs.rs +++ b/tests/runtime-adapter/src/initramfs.rs @@ -7,11 +7,11 @@ //! exec /sbin/nvx-managed-agent /dev/hvc2 - nvx-managed UID GID NAME HOME //! ``` //! -//! and a VM with a layered root, whose kernel command line has `nvx_sandbox=1`, to NVX's init -//! agent, which assembles the root, without arguments: +//! and a VM with a layered root, whose kernel command line has `nvx_sandbox=1` and no old +//! OpenVMM lifecycle token, to NVX's init, with its helpers' process IDs: //! //! ```text -//! exec /sbin/nvx-init-agent +//! exec /sbin/nvx-init TIME_DAEMON [ANNOUNCER] //! ``` //! //! [`write`](fn@write) copies that initramfs, and appends to the copy a second archive, in the @@ -23,7 +23,7 @@ //! - a symbolic link from [`MANAGED_AGENT`] to that script; //! - a script, at [`LAYERED_LAUNCHER`], that runs the guest agent as //! `runtime-adapter-guest init /dev/hvc2 layered`, PID 1 of a VM with a layered root, which -//! names the control tty, [`CONTROL_TTY`], itself, since `/init` passes NVX's init agent none; +//! names the control tty, [`CONTROL_TTY`], itself, and ignores the helper arguments; //! - and a symbolic link from [`INIT_AGENT`] to that script, so that NVX's init agent never //! assembles the root in the adapter's VMs; //! - for the capture of a template whose clones delay their repair, for tests, the delay's file, @@ -53,7 +53,7 @@ pub const MANAGED_AGENT: &str = "/sbin/nvx-managed-agent"; /// NVX's init agent. pub const LAYERED_LAUNCHER: &str = "/sbin/runtime-adapter-layered"; /// Path of NVX's init agent, which `/init` runs in a VM with a layered root. -pub const INIT_AGENT: &str = "/sbin/nvx-init-agent"; +pub const INIT_AGENT: &str = "/sbin/nvx-init"; /// The VM's control tty, which `/init` requires of both lifecycles that NVX's agents serve. pub const CONTROL_TTY: &str = "/dev/hvc2"; diff --git a/tests/runtime-adapter/src/worker.rs b/tests/runtime-adapter/src/worker.rs index c2eab36c9..dd80fe88c 100644 --- a/tests/runtime-adapter/src/worker.rs +++ b/tests/runtime-adapter/src/worker.rs @@ -4,7 +4,7 @@ //! //! # PID 1 and the worker //! -//! In a VM with a layered root, NVX's `/init` runs `/sbin/nvx-init-agent`, which the adapter's +//! In a VM with a tokenless layered root, NVX's `/init` runs `/sbin/nvx-init`, which the adapter's //! initramfs replaces with `runtime-adapter-guest init /dev/hvc2 layered`; see the host side's //! `initramfs` module. That PID 1, [`supervise`], stays minimal: //! diff --git a/tests/runtime-adapter/tests/initramfs.rs b/tests/runtime-adapter/tests/initramfs.rs index 22eea4537..8ad14aed9 100644 --- a/tests/runtime-adapter/tests/initramfs.rs +++ b/tests/runtime-adapter/tests/initramfs.rs @@ -103,7 +103,7 @@ fn expected(agent: &[u8]) -> Vec { 0o100_755, initramfs::layered_launcher_script().as_bytes(), ), - entry(INIT_AGENT, 5, 0o120_777, LAYERED_LAUNCHER.as_bytes()), + entry("/sbin/nvx-init", 5, 0o120_777, LAYERED_LAUNCHER.as_bytes()), ] } @@ -151,15 +151,14 @@ fn the_launchers_run_the_guest_agent_on_the_control_tty() { ); assert_eq!(MANAGED_AGENT, "/sbin/nvx-managed-agent"); - // It passes NVX's init agent nothing, so the layered launcher names the control tty, which - // `/init` requires of both lifecycles. + // The layered launcher ignores the helper PIDs and names its control tty itself. let layered = runtime_adapter::GUEST_LAYERED_MODE; assert_eq!( initramfs::layered_launcher_script(), format!("#!/bin/sh\nexec {GUEST_AGENT} {init} /dev/hvc2 {layered}\n") ); assert_eq!(CONTROL_TTY, "/dev/hvc2"); - assert_eq!(INIT_AGENT, "/sbin/nvx-init-agent"); + assert_eq!(INIT_AGENT, "/sbin/nvx-init"); } #[test]