diff --git a/.gitattributes b/.gitattributes index 2f0e7ced0..4a372e1ba 100644 --- a/.gitattributes +++ b/.gitattributes @@ -20,5 +20,7 @@ scripts/setup/tool-versions.conf text eol=lf # of its commit only if they are checked out unchanged: check nvx-rs out with LF line endings on # every platform, whatever core.autocrlf says. nvx-rs/** text=auto eol=lf +# OpenVMM wrote these template manifests: they are bytes, never text. +nvx-rs/tests/fixtures/template/**/manifest.bin binary .github/workflows/*.lock.yml linguist-generated=true \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2fd7b9c37..482a3d133 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,7 +139,8 @@ jobs: # nvx-rs's protocol tests read copies of OpenVMM's control-session # protocol vectors, because the nvx-rs job cannot check out OpenVMM. - # This job can, so it compares the copies with the submodule's files. + # This job can, so it compares the copies with the submodule's files, + # and nvx::contract's versions of OpenVMM's contracts with OpenVMM's. - name: Compare OpenVMM's protocol vectors shell: bash run: >- diff --git a/doc/ci.md b/doc/ci.md index b09a1b0f9..b58abb28d 100644 --- a/doc/ci.md +++ b/doc/ci.md @@ -574,7 +574,9 @@ for the curl shim test and the `nvx-rs` build. Each job first builds the microVM test and platform jobs of its operating system, as described above. Each also runs the ignored `nvx-rs` test `tests/openvmm_vectors.rs`, which compares the submodule's control-session protocol vectors with the copies that -the protocol tests read in the `nvx-rs` job, which cannot check out OpenVMM; +the protocol tests read in the `nvx-rs` job, which cannot check out OpenVMM, +and the versions of OpenVMM's snapshot format, saved-state schema, time ABI, +and restore packet with those of `nvx::contract`; see [OpenVMM's vectors](../nvx-rs/tests/vectors/README.md#openvmms-vectors). The ignored test `tests/openvmm_cpu_profiles.rs` likewise compares the submodule's built-in CPU profiles with the copy of their identities from which diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 430677c3d..7e63dc63c 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -33,7 +33,8 @@ nvx-rs/ │ ├── activation/ the activation record of a layered root, its canonical encoding, and what │ │ Ready reports about it; no dependencies, shared by the host, the guest, and │ │ every adapter's framing -│ ├── contract.rs control-contract versions and release-manifest compatibility +│ ├── contract.rs control-contract versions, release-manifest compatibility, and the versions +│ │ of the contracts through which OpenVMM restores a snapshot │ ├── net/ CIDR parsing and subtraction, and hostname and guest-network forms │ ├── gpt/ strict reading of GPT disks whose first partition configures an adapter and │ │ whose other partitions hold a root's read-only layers; no dependencies, @@ -46,9 +47,10 @@ nvx-rs/ │ │ sides, without I/O, threads, or features, for any framing and host │ ├── host/ feature `host` (default): artifacts, launch, network and egress policy files, │ │ filesystem, NVXS connection, session, process, platform, VM instances, -│ │ sandbox stores, outcome reports, file seals, and the sandbox launch profile -│ │ of `nvx-rs sandbox`, its distro and scratch bindings, and its prepared -│ │ launches; with feature `cli`, the `nvx-rs` CLI +│ │ sandbox stores, templates' manifests and compatibility keys, outcome reports, +│ │ file seals, and the sandbox launch profile of `nvx-rs sandbox`, its distro +│ │ and scratch bindings, and its prepared launches; with feature `cli`, the +│ │ `nvx-rs` CLI │ ├── testing/ feature `testing`: the fake OpenVMM │ ├── guest/ feature `guest`: the programs that run in Linux guests, the guest's NVXS │ │ connection, the libraries through which a guest init or agent opens the @@ -62,9 +64,9 @@ nvx-rs/ ├── tests/ fake-OpenVMM, CLI, NVXS session, process-supervision, checked-handoff, store, │ host-module layering, and guest-binary tests, the CLI's scenario tests and │ their harness, in cli/, the store's record fixtures, the hand-written sandbox -│ configurations of every format, and the cases of the export and root boot -│ tokens, in fixtures/, and the conformance vectors of the wire formats, in -│ vectors/ +│ configurations of every format, the cases of the export and root boot tokens, +│ and the template manifests that OpenVMM wrote, in fixtures/, and the +│ conformance vectors of the wire formats and of a template's key, in vectors/ ├── build.rs bundled-release staging └── artifacts.json the NVX release that the `bundled` feature stages ``` @@ -78,7 +80,7 @@ nvx-rs/ | `nvx::protocol::root` | The boot tokens of a layered root, which the host renders and guests parse by the same rules: a `RootLayout` is a `RootImage` on the `distro` block device, an EROFS filesystem or a GPT disk with the `nvx::uuid::Uuid` that the guest expects of it, if any, over a `RootUpper`, the ext4 scratch disk or RAM. `RootLayout::tokens` renders `nvx_layer=distro,0xd0003000,FORMAT[:IDENTITY]` and `nvx_scratch=0xd0006000,ext4` or `nvx_overlay_upper=ramfs`, and `RootLayout::parse` reads them from a kernel command line, and refuses malformed, repeated, and conflicting tokens and the earlier form without a format, with no fallback that probes the devices. It reads the virtio-mmio devices of the command line with `nvx::protocol::cmdline`, to refuse a RAM upper beside one at the scratch base. `tests/fixtures/root-tokens.json` holds its cases, which the tests of the guest's shell init replay too | | `nvx::protocol::share` | The guest targets that a sandbox's live shares may not take, which the host's `sandbox::mount` and the guest's `root` both refuse: `RESERVED_MOUNT_TARGETS`, the trees where the container runtime mounts or binds, `RESERVED_EXACT_MOUNT_TARGETS`, the directories that it creates, `RESERVED_TARGET_CHARACTERS`, and `reservation`, which tells why a target is reserved | | `nvx::activation` | The activation record of a layered root: one bounded, versioned, non-secret record of the settings that its activation applies, which a clone restored from a template reads in place of its captured configuration. An `ActivationRecord` holds the schema's `VERSION`, a clone's `RestoreSection`, the restore's `TransactionId` and the `TemplateReference` of its template, the template's `StoreGeneration`, a nonzero publication ID that its store assigns in order, and the `Identity` of its key, and the `Configuration`: the `Image`, EROFS or GPT, with the `Identity` of its binding and the UUID or disk GUID that the guest expects; the `Upper`, a tmpfs or the scratch with its binding's `Identity` and its `Journal` policy; the `OverlayMode`; an `Export`, a directory or an aggregate with its plan's `Identity`, and its 1 to 4,096 mappings; the `Network`, its policy's `Identity`, a DNS address, the listeners' `Publication`, and a proxy endpoint without credentials; the `WorkloadView`, its hostname, `MachineIdMode`, and `WorkloadIdentity`, whose IDs are neither 0 nor `u32::MAX`; and the `ExpectedResources`. The other inputs of the root's assembly are invariants of the template. `ActivationRecord::new` refuses a configuration that `Configuration::check` finds past a public limit, `MAX_MAPPINGS`, `MAX_HOSTNAME_BYTES`, or `MAX_PROXY_ENDPOINT_BYTES`, which keep every record within `MAX_RECORD_BYTES`, or with an export without mappings, mappings without an export, a UID or GID of 0 or `u32::MAX`, a proxy endpoint with credentials, or a zero size. `ActivationRecord::encode` writes a record's canonical encoding, its version, then a tagged, length-prefixed field for each part in the order of their tags, 602 bytes at most, and `ActivationRecord::decode` reads exactly those bytes back and refuses any others, naming the `Field` at fault. `ReadyActivation` is what Ready reports once the activation applied: a clone's transaction ID, which `ReadyActivation::of` takes from a record, and nothing more for a cold boot, in a canonical encoding of `COLD_LEN` or `CLONE_LEN` bytes, 1 or 17, that `ReadyActivation::encode` writes and `ReadyActivation::decode` reads. The records and both encodings need no features | -| `nvx::contract` | The OpenVMM microVM ABI, boot contract, broker protocol, and control-contract revision that the crate speaks, and when each changes, and `manifest_is_compatible` for a release's `SOURCE-MANIFEST.json` | +| `nvx::contract` | The OpenVMM microVM ABI, boot contract, broker protocol, and control-contract revision that the crate speaks, and when each changes, and `manifest_is_compatible` for a release's `SOURCE-MANIFEST.json`; and the versions of OpenVMM's snapshot format, saved device state, time ABI, and restore packet, which a template's key holds and which an ignored test of `tests/openvmm_vectors.rs` checks against the OpenVMM submodule | | `nvx::net` | `Cidr`: IPv4 and IPv6 networks and their subtraction, which `Cidr::parse` reads without host bits and `Cidr::parse_masked` as OpenVMM reads the destination of an egress rule; `is_valid_hostname` and `is_valid_guest_network`, the forms of a VM's hostname and guest address, and `guest_gateway`, the gateway of a guest address | | `nvx::gpt` | `Disk::read`: the protective MBR, primary header, and primary partition entry array of a GPT disk, read through a `ReadAt` source that the caller gives and checked strictly, as UEFI specifies them and as Linux requires them, without ever reading the backup: 128-byte entries, at most 1 MiB of them, used entries first and without holes, a first partition and 1 to 254 layers, extents in the usable blocks without overlap, and unique GUIDs, with checked arithmetic in the disk's logical blocks. `Expected` adds the disk GUID and the partitions' unique GUIDs and sizes that a caller knows, and `Disk::read_first_partition` returns the first partition's opaque bytes, up to a bound and without trailing NUL bytes and whitespace. GUIDs are `nvx::uuid::Uuid`s. It has no dependencies, so the guest and the host's checks of the images that it stages share it | | `nvx::uuid` | `Uuid`: a UUID, such as the GUID of a GPT disk or partition or the UUID of a filesystem, in the order of its canonical text, the one text that it displays and parses: 32 lowercase hexadecimal digits in groups of 8, 4, 4, 4, and 12, joined by hyphens. `from_bytes_le` and `to_bytes_le` convert the layout in which GPT and other EFI GUIDs store their first three groups little-endian. It has no dependencies, so the host and the guest share it | @@ -94,6 +96,7 @@ nvx-rs/ | `nvx::host::nvxs` | `HostConnection`: the host's role of the NVXS session over a platform transport, on which `nvx::host::session` and other host applications run their protocols. It attaches with the launch capability, waiting while the broker asks it to, sends application bytes in as many DATA records as they take, and returns each DATA payload from the guest, with deadlines; a receive that times out keeps a partial record | | `nvx::host::process` | Verified process references. A `ProcessRef` names one process by its ID, its start time, and the `BootId` of the host boot during which it started. `Process::open` checks a reference and holds its process through a pidfd on Linux or a process handle on Windows, whether or not the caller started it, so that waits sleep until the process exits and termination never reaches a process that reuses its ID. A reference from an earlier boot names an exited process, and a process whose identity this user cannot check, such as another user's, makes `Process::open` fail rather than report an exit. Linux hosts without pidfds poll the reference instead, and cannot terminate the process | | `nvx::host::supervisor` | The supervisor of a sandbox's VMM, beneath the CLI, `Instance`, and external orchestrators, which keep the configuration, the authorization, and the application protocol. `launch` runs the store's launch steps under the sandbox's lock: it writes the journal's marker, spawns OpenVMM with the claimed log, records its `ProcessRef` right after, and returns a `Starting` launch that the caller commits once the VMM serves its control endpoint, or abandons. A launch that fails never leaves an unrecorded VMM running. `VmmCommand::inherit` lists the descriptors (Linux) or handles (Windows) that the VMM inherits under their numbers, besides its standard streams, and it inherits nothing else of the caller's: on Linux, they must be close-on-exec, a flag that only the VMM's own process clears; on Windows, they join the VMM's explicit handle list. `Vmm::stop` runs one graceful step of the caller's and waits for the VMM's exit event until a deadline, and takes a bounded forced step through the VMM's verified identity only on request. `Vmm::terminate` takes that forced step alone, within the caller's bound, for a VM whose guest the caller must not ask anything, such as one of another runtime identity. `reattach` recovers an interrupted launch, and holds the recorded VMM again after the orchestrator restarts within the same host boot: `Live`, `Mismatched` with another runtime identity, `Exited`, or `Indeterminate` | +| `nvx::host::template` | The metadata of templates, captures of a microVM from which independent clones are restored. `TemplateManifest::read` reads the `manifest.bin` that OpenVMM wrote for a capture, at most `MAX_MANIFEST_LEN` bytes of a regular file that is not a link or other reparse point, and refuses another snapshot format version, an unknown, mistyped, or repeated field, truncated bytes, and a capture that is not a template's: a platform-tier capture for clones, with a read-only `distro` whose identity is unbound, a fresh scratch, and the dormant filesystem slot. `CaptureClass` holds the invariant inputs of a capture, known before it: the runtime, the guest's build, the backend, the requested CPU profile, the processors and RAM, the geometries of the disks, the network, and the adapter's other `Invariants`. `TemplateKey::new` checks that the capture made the machine that its class requested, and derives the template's compatibility key, from metadata alone: the runtime and contract versions through which its clones are restored, and the backend, CPU profile and effective CPUID, topology, rates, resources, kernel command line, disks, and devices of its machine. A capture's own metadata, such as its time anchor and host, stays out of the key. A key and a class each have one canonical, versioned encoding, whose domain-separated SHA-256 digest is its identity, a `KeyIdentity`, which converts to and from the `nvx::activation::Identity` by which a clone's activation record names its template's key, or a `ClassIdentity`; the [conformance vectors](tests/vectors/README.md) of version 1, `tests/vectors/template-key-v1.txt`, pin the key's encoding and identity | | `nvx::host::seal` | `FileSeal`: what the host reports about an open file from its metadata alone, without reading it: the platform scheme, the device or volume serial number, the file ID, the kind, the length, the modification time, and on Linux the change time, with a versioned serde form. A seal detects accidental change, not tampering; see [Distro bindings](#distro-bindings) | | `nvx::host::session`, `platform`, `report` | The authenticated control session, which carries NVX's control protocol, `NVXC`, one frame per DATA record of a `HostConnection`; Linux and Windows primitives; and OpenVMM's outcome report | | `nvx::host::sandbox` | The launch profile of `nvx-rs sandbox`, with native input validation: `SandboxLaunch`, with the distro `SandboxLayer`, an EROFS layer or a GPT disk, the one layer that a sandbox takes, the ext4 scratch disk, the entrypoint, arguments, hostname, workload identity, and `SandboxSettings` resource limits (`Option` for memory and `Option` for processes), and the `SandboxPlan` that it makes of them. `plan`: `SandboxPlan`, the typed plan of a cold layered launch, which is persistable in a versioned serde form: one read-only `distro` block device, an EROFS filesystem or a GPT disk with the identity that the guest expects of it, if any, an upper, the ext4 scratch disk or RAM, the launch settings, the `ExportPlan` of its live shares, and optionally a `SandboxNetwork`, the guest network and its typed `NetworkPolicy`, whose proxy's URL as the guest sees it the plan reports. The plan of an adapter that boots its own guest, `SandboxPlan::adapter`, holds no launch settings and no export plan. A plan knows only the `distro` and `scratch` `BlockRole`s, and gives, for every combination, its `--microvm-sandbox-block` arguments, with `--microvm-workload-identity` when it has launch settings, with no scratch block device for a RAM upper, and those of its network, which `nvx::host::network` lowers, and its kernel command line, with the boot tokens of `nvx::protocol::root`, within the 1024-byte budget that OpenVMM leaves it, refusing one whose root tokens the guest's parser refuses. Both deliver the live shares as the managed lifecycle does: the arguments export them at `nvx::host::filesystem::GUEST_EXPORT`, the command line announces the number of their mappings with `nvx_maps=`, and `mapping_table` gives the ordered table that `start` sends once the guest agent is ready; the CLI's one-shot `run` still renders them as boot tokens. An adapter's plan renders the caller's tokens before those of its root, and refuses tokens that its root or OpenVMM's shares own. `mount`: `SandboxMount` requests of live shares of host directories and files, their guest targets and the reserved ones, their access policies and owner, and the checks of their host paths; `plan_mounts`, which plans them through `nvx::host::filesystem` for the container root, and `boot_shares`, which renders a plan as the OpenVMM arguments and boot tokens of one share or of the aggregate of several, as the one-shot lifecycle delivers them. `options`: the checks that `nvx-rs sandbox` and `nvx-rs run` apply to their options: the attribution of `--mount-deny`, `--mount-allow`, and `--mount-write` to shares, the systemd entrypoint check and the explicit verification of the distro layer, the targets of `nvx-rs run` shares, and the managed exec environment; see [Sandbox input contract](#sandbox-input-contract). `distro`: the `DistroBinding` of a distro layer, an EROFS layer or a GPT disk, which explicit verification makes or a staging caller imports, and its check from metadata alone, which returns the layer open; see [Distro bindings](#distro-bindings). `scratch`: the `ScratchBinding` of a scratch image, its identity, kind, and length, and its check from metadata alone, which claims the image for one sandbox at a time and returns it open. `prepared`: `PreparedLaunch`, the launch of a plan with the checked files of its block devices, which renders the plan's OpenVMM arguments and kernel command line, naming the checked files as `/proc/self/fd/N` on Linux and by their final paths on Windows, and lists the descriptors that the VMM inherits through `VmmCommand::inherit`; see [Checked handoff](#checked-handoff) | @@ -1170,8 +1173,10 @@ dispatch names; see `tests/openvmm_vectors.rs` is an ignored test that compares the copies of OpenVMM's vectors in `tests/vectors/openvmm/` with the files of the OpenVMM -submodule, which the `nvx-rs` job cannot check out. The NVX CLI jobs run it -once they check the submodule out, and so does an OpenVMM pin promotion; see +submodule, which the `nvx-rs` job cannot check out, and the versions of +OpenVMM's snapshot format, saved-state schema, time ABI, and restore packet in +`nvx::contract` with the submodule's. The NVX CLI jobs run it once they check +the submodule out, and so does an OpenVMM pin promotion; see [OpenVMM's vectors](tests/vectors/README.md#openvmms-vectors). `tests/openvmm_cpu_profiles.rs` is an ignored test that compares `src/host/cli/cpu_generations.json`, the identities of the built-in CPU diff --git a/nvx-rs/src/contract.rs b/nvx-rs/src/contract.rs index db5c57538..fa52b03bd 100644 --- a/nvx-rs/src/contract.rs +++ b/nvx-rs/src/contract.rs @@ -25,6 +25,26 @@ //! A release declares the microVM ABI version, the control session version, and the control //! contract revision of its artifacts in its `SOURCE-MANIFEST.json`. //! +//! The compatibility key of a template, `nvx::host::template::TemplateKey`, holds the boot and +//! control versions, and the versions of the contracts through which a template's clones are +//! restored, which no runtime identity records: +//! +//! - [`SNAPSHOT_FORMAT_VERSION`] is the version of OpenVMM's snapshot format: the manifest of a +//! capture, and the artifacts that it describes. It changes with the version of OpenVMM's +//! snapshot manifest, which refuses a snapshot of any other. +//! - [`DEVICE_STATE_SCHEMA_VERSION`] is the version of the schema of the saved state of a +//! snapshot's devices, which OpenVMM records in its manifest. It changes with it. +//! - [`TIME_ABI_VERSION`] is the version of the time ABI: the guest's clocks and timers, and their +//! repair after a restore. It changes with the time ABI version of OpenVMM's time contract. +//! - [`RESTORE_PACKET_VERSION`] is the version of the restore packet that OpenVMM gives a +//! restored guest, and that the guest decodes. It changes with the packet's layout or meaning. +//! +//! Each of these four must match the pinned OpenVMM's, which the ignored test +//! `tests/openvmm_vectors.rs` checks in the jobs that check out the OpenVMM submodule. The key +//! also holds the version of the activation contract, +//! [`activation::VERSION`](crate::activation::VERSION), through which each clone receives the +//! settings of its own launch. +//! //! The build script includes this file to check bundled artifacts, so it must not depend on the //! rest of the crate. @@ -40,6 +60,21 @@ pub const BOOT_CONTRACT_VERSION: u64 = 3; pub const CONTROL_SESSION_PROTOCOL_VERSION: u64 = 1; /// Control contract revision this crate speaks. pub const CONTROL_CONTRACT_REVISION: &str = "nvx-microvm-v2-control-v2"; +/// Version of OpenVMM's snapshot format that this crate reads: the manifest of a capture, and the +/// artifacts that it describes. It must match `MANIFEST_VERSION` in OpenVMM's +/// `openvmm/openvmm_helpers/src/snapshot.rs`. +pub const SNAPSHOT_FORMAT_VERSION: u64 = 6; +/// Version of the schema of a snapshot's saved device state that this crate reads. It must match +/// `SAVED_STATE_SCHEMA_VERSION` in OpenVMM's `openvmm/openvmm_helpers/src/snapshot/format.rs`. +pub const DEVICE_STATE_SCHEMA_VERSION: u64 = 1; +/// Version of the time ABI that this crate speaks: the guest's clocks and timers, and their repair +/// after a restore. It must match `TIME_ABI_VERSION` in OpenVMM's +/// `vmm_core/virt/src/time_abi/mod.rs`. +pub const TIME_ABI_VERSION: u64 = 1; +/// Version of the restore packet that OpenVMM gives a restored guest, and that this crate's guest +/// decodes. It must match `RESTORE_PACKET_VERSION` in OpenVMM's +/// `vm/devices/chipset_resources/src/microvm_time.rs`. +pub const RESTORE_PACKET_VERSION: u64 = 4; /// Returns whether the `openvmm` section of an NVX `SOURCE-MANIFEST.json` declares this contract. #[cfg(feature = "host")] diff --git a/nvx-rs/src/guest/time/decoders.rs b/nvx-rs/src/guest/time/decoders.rs index be79fe5fb..a96dc9247 100644 --- a/nvx-rs/src/guest/time/decoders.rs +++ b/nvx-rs/src/guest/time/decoders.rs @@ -26,8 +26,14 @@ pub(crate) const GENERATION_ID_SIZE: usize = 16; /// The magic number that starts every restore packet. const PACKET_MAGIC: [u8; 3] = *b"OVR"; -/// The restore-packet version of time ABI version one. -const PACKET_VERSION: u8 = 4; +/// The restore-packet version of time ABI version one, +/// [`RESTORE_PACKET_VERSION`](crate::contract::RESTORE_PACKET_VERSION), which a template's +/// compatibility key records. +const PACKET_VERSION: u8 = { + let version = crate::contract::RESTORE_PACKET_VERSION; + assert!(version <= u8::MAX as u64); + version as u8 +}; /// Restore flag indicating that the downtime came from the host's UTC clock. const PACKET_DOWNTIME_UTC: u8 = 0x01; /// Restore flag indicating that memory ranges follow. diff --git a/nvx-rs/src/host/mod.rs b/nvx-rs/src/host/mod.rs index 1703c659e..6b20a9d90 100644 --- a/nvx-rs/src/host/mod.rs +++ b/nvx-rs/src/host/mod.rs @@ -20,6 +20,10 @@ //! with a bounded forced step on request, and reattaches to it after the orchestrator restarts //! within the same host boot. The private `spawn` module, whose items `supervisor` re-exports, //! starts the VMM detached, with no other descriptor or handle of the caller's. +//! - [`template`] holds the metadata of templates, captures from which clones are restored: the +//! compatibility key of a template, [`TemplateKey`](template::TemplateKey), which it derives +//! from the [`CaptureClass`](template::CaptureClass) of the capture and the capture's +//! [`TemplateManifest`](template::TemplateManifest). //! - [`session`] speaks NVX's control protocol, `NVXC`, over [`nvxs`], which runs the host's role //! of the NVXS session for it and for any other application protocol, over a platform //! transport. [`platform`] holds the primitives of Linux and Windows hosts. Other hosts compile @@ -74,5 +78,6 @@ pub mod supervisor; #[cfg(unix)] #[allow(unsafe_code)] mod sys; +pub mod template; #[cfg(test)] mod test_support; diff --git a/nvx-rs/src/host/template/canonical.rs b/nvx-rs/src/host/template/canonical.rs new file mode 100644 index 000000000..064240dd4 --- /dev/null +++ b/nvx-rs/src/host/template/canonical.rs @@ -0,0 +1,353 @@ +//! The canonical encoding of a template's compatibility key and of a capture class: the bytes +//! whose SHA-256 digest is their identity. +//! +//! Every value has exactly one encoding, and the reader refuses any other: +//! +//! - 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`. +//! - A boolean is one byte, 0 or 1. +//! - A byte string of fixed length, such as a digest or an address, is its bytes. +//! - A byte string of variable length is its length, a `u32`, then its bytes. A text is the byte +//! string of its UTF-8 encoding. +//! - An optional value is one byte, 0 without the value and 1 before it. +//! - A list is its count, a `u32`, then its items in order. +//! +//! The reader bounds each length and count by the most that the value may hold, and refuses bytes +//! left after the value. + +use std::fmt; + +/// Why bytes are not the canonical encoding of a value. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct CanonicalError(String); + +impl CanonicalError { + /// Returns the error that `problem` describes. + pub(super) fn new(problem: impl Into) -> Self { + Self(problem.into()) + } +} + +impl fmt::Display for CanonicalError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.0) + } +} + +/// Writes values in their canonical encoding. +#[derive(Debug, Default)] +pub(super) struct Writer { + /// The bytes written so far. + bytes: Vec, +} + +impl Writer { + /// Returns the bytes written. + pub(super) fn finish(self) -> Vec { + self.bytes + } + + /// Writes a `u8`. + pub(super) fn u8(&mut self, value: u8) { + self.bytes.push(value); + } + + /// Writes a `u16`. + pub(super) fn u16(&mut self, value: u16) { + self.bytes.extend_from_slice(&value.to_le_bytes()); + } + + /// Writes a `u32`. + pub(super) fn u32(&mut self, value: u32) { + self.bytes.extend_from_slice(&value.to_le_bytes()); + } + + /// Writes a `u64`. + pub(super) fn u64(&mut self, value: u64) { + self.bytes.extend_from_slice(&value.to_le_bytes()); + } + + /// Writes a boolean. + pub(super) fn bool(&mut self, value: bool) { + self.u8(u8::from(value)); + } + + /// Writes a byte string of fixed length. + pub(super) fn fixed(&mut self, bytes: &[u8]) { + self.bytes.extend_from_slice(bytes); + } + + /// Writes a count or a length, which callers bound well below 2^32. + fn len(&mut self, len: usize) { + self.u32(u32::try_from(len).expect("canonical lengths are bounded")); + } + + /// Writes a byte string of variable length. + pub(super) fn bytes(&mut self, bytes: &[u8]) { + self.len(bytes.len()); + self.fixed(bytes); + } + + /// Writes a text. + pub(super) fn text(&mut self, text: &str) { + self.bytes(text.as_bytes()); + } + + /// Writes an optional value, with `write` for the value. + pub(super) fn option(&mut self, value: Option<&T>, write: impl FnOnce(&mut Self, &T)) { + match value { + None => self.u8(0), + Some(value) => { + self.u8(1); + write(self, value); + } + } + } + + /// Writes a list, with `write` for each item. + pub(super) fn list(&mut self, items: &[T], mut write: impl FnMut(&mut Self, &T)) { + self.len(items.len()); + for item in items { + write(self, item); + } + } +} + +/// Reads values from their canonical encoding, refusing any other. +#[derive(Debug)] +pub(super) struct Reader<'a> { + /// The bytes that remain. + bytes: &'a [u8], +} + +impl<'a> Reader<'a> { + /// Returns a reader of `bytes`. + pub(super) fn new(bytes: &'a [u8]) -> Self { + Self { bytes } + } + + /// Fails unless every byte was read. + pub(super) fn finish(self) -> Result<(), CanonicalError> { + match self.bytes.len() { + 0 => Ok(()), + 1 => Err(CanonicalError::new("a byte follows the value")), + count => Err(CanonicalError::new(format!( + "{count} bytes follow the value" + ))), + } + } + + /// Reads `count` bytes, which `what` names. + fn take(&mut self, count: usize, what: &str) -> Result<&'a [u8], CanonicalError> { + if count > self.bytes.len() { + return Err(CanonicalError::new(format!("{what} is truncated"))); + } + let (taken, rest) = self.bytes.split_at(count); + self.bytes = rest; + Ok(taken) + } + + /// Reads `N` bytes, which `what` names. + pub(super) fn fixed(&mut self, what: &str) -> Result<[u8; N], CanonicalError> { + Ok(self.take(N, what)?.try_into().expect("N bytes were taken")) + } + + /// Reads a `u8`, which `what` names. + pub(super) fn u8(&mut self, what: &str) -> Result { + Ok(self.fixed::<1>(what)?[0]) + } + + /// Reads a `u16`, which `what` names. + pub(super) fn u16(&mut self, what: &str) -> Result { + Ok(u16::from_le_bytes(self.fixed(what)?)) + } + + /// Reads a `u32`, which `what` names. + pub(super) fn u32(&mut self, what: &str) -> Result { + Ok(u32::from_le_bytes(self.fixed(what)?)) + } + + /// Reads a `u64`, which `what` names. + pub(super) fn u64(&mut self, what: &str) -> Result { + Ok(u64::from_le_bytes(self.fixed(what)?)) + } + + /// Reads a boolean, which `what` names. + pub(super) fn bool(&mut self, what: &str) -> Result { + match self.u8(what)? { + 0 => Ok(false), + 1 => Ok(true), + byte => Err(CanonicalError::new(format!( + "{what} is {byte}, not a boolean" + ))), + } + } + + /// Reads a count of at most `max` of the `unit`, bytes or items, of the value that `what` + /// names. + fn len(&mut self, max: usize, unit: &str, what: &str) -> Result { + let len = self.u32(what)?; + usize::try_from(len) + .ok() + .filter(|&len| len <= max) + .ok_or_else(|| { + CanonicalError::new(format!("{what} holds {len} {unit}, more than {max}")) + }) + } + + /// Reads a byte string of at most `max` bytes, which `what` names. + pub(super) fn bytes(&mut self, max: usize, what: &str) -> Result<&'a [u8], CanonicalError> { + let len = self.len(max, "bytes", what)?; + self.take(len, what) + } + + /// Reads a text of at most `max` bytes, which `what` names. + pub(super) fn text(&mut self, max: usize, what: &str) -> Result<&'a str, CanonicalError> { + std::str::from_utf8(self.bytes(max, what)?) + .map_err(|_| CanonicalError::new(format!("{what} is not UTF-8"))) + } + + /// Reads an optional value, which `what` names, with `read` for the value. + pub(super) fn option( + &mut self, + what: &str, + read: impl FnOnce(&mut Self) -> Result, + ) -> Result, CanonicalError> { + match self.u8(what)? { + 0 => Ok(None), + 1 => read(self).map(Some), + tag => Err(CanonicalError::new(format!( + "{what} has the presence tag {tag}, not 0 or 1" + ))), + } + } + + /// Reads a list of at most `max` items, which `what` names, with `read` for each item. + pub(super) fn list( + &mut self, + max: usize, + what: &str, + mut read: impl FnMut(&mut Self) -> Result, + ) -> Result, CanonicalError> { + let count = self.len(max, "items", what)?; + // Each item takes at least one byte, so a count beyond the bytes left is truncated. + if count > self.bytes.len() { + return Err(CanonicalError::new(format!("{what} is truncated"))); + } + (0..count).map(|_| read(self)).collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn values_round_trip_in_their_one_encoding() { + let mut writer = Writer::default(); + writer.u8(0xab); + writer.u16(0x0102); + writer.u32(0x0304_0506); + writer.u64(0x0708_090a_0b0c_0d0e); + writer.bool(true); + writer.fixed(&[9, 8]); + writer.text("héllo"); + writer.option(Some(&7u32), |writer, value| writer.u32(*value)); + writer.option(None::<&u32>, |writer, value| writer.u32(*value)); + writer.list(&[1u8, 2], |writer, item| writer.u8(*item)); + let bytes = writer.finish(); + assert_eq!( + bytes, + [ + 0xab, 0x02, 0x01, 0x06, 0x05, 0x04, 0x03, 0x0e, 0x0d, 0x0c, 0x0b, 0x0a, 0x09, 0x08, + 0x07, 1, 9, 8, 6, 0, 0, 0, b'h', 0xc3, 0xa9, b'l', b'l', b'o', 1, 7, 0, 0, 0, 0, 2, + 0, 0, 0, 1, 2 + ] + ); + let mut reader = Reader::new(&bytes); + assert_eq!(reader.u8("a").unwrap(), 0xab); + assert_eq!(reader.u16("b").unwrap(), 0x0102); + assert_eq!(reader.u32("c").unwrap(), 0x0304_0506); + assert_eq!(reader.u64("d").unwrap(), 0x0708_090a_0b0c_0d0e); + assert!(reader.bool("e").unwrap()); + assert_eq!(reader.fixed::<2>("f").unwrap(), [9, 8]); + assert_eq!(reader.text(6, "g").unwrap(), "héllo"); + assert_eq!( + reader.option("h", |reader| reader.u32("h")).unwrap(), + Some(7) + ); + assert_eq!(reader.option("i", |reader| reader.u32("i")).unwrap(), None); + assert_eq!( + reader.list(2, "j", |reader| reader.u8("j")).unwrap(), + [1, 2] + ); + reader.finish().unwrap(); + } + + #[test] + fn other_encodings_are_refused() { + let refused = |bytes: &[u8], read: fn(&mut Reader<'_>) -> Result<(), CanonicalError>| { + let mut reader = Reader::new(bytes); + read(&mut reader) + .and_then(|()| reader.finish()) + .unwrap_err() + .to_string() + }; + assert_eq!( + refused(&[2], |reader| reader.bool("flag").map(drop)), + "flag is 2, not a boolean" + ); + assert_eq!( + refused(&[2, 0], |reader| reader + .option("value", |reader| reader.u8("value")) + .map(drop)), + "value has the presence tag 2, not 0 or 1" + ); + assert_eq!( + refused(&[3, 0, 0, 0, b'a', b'b', b'c'], |reader| reader + .text(2, "name") + .map(drop)), + "name holds 3 bytes, more than 2" + ); + assert_eq!( + refused(&[2, 0, 0, 0, 0xff, 0xfe], |reader| reader + .text(2, "name") + .map(drop)), + "name is not UTF-8" + ); + assert_eq!( + refused(&[2, 0, 0, 0, b'a'], |reader| reader + .text(2, "name") + .map(drop)), + "name is truncated" + ); + assert_eq!( + refused(&[0xff, 0xff, 0, 0], |reader| { + reader + .list(usize::MAX, "items", |reader| reader.u8("item")) + .map(drop) + }), + "items is truncated" + ); + assert_eq!( + refused(&[3, 0, 0, 0], |reader| { + reader + .list(2, "items", |reader| reader.u8("item")) + .map(drop) + }), + "items holds 3 items, more than 2" + ); + assert_eq!( + refused(&[1, 0, 9], |reader| reader.u16("number").map(drop)), + "a byte follows the value" + ); + assert_eq!( + refused(&[1, 0, 9, 9], |reader| reader.u16("number").map(drop)), + "2 bytes follow the value" + ); + assert_eq!( + refused(&[1], |reader| reader.u32("number").map(drop)), + "number is truncated" + ); + } +} diff --git a/nvx-rs/src/host/template/key.rs b/nvx-rs/src/host/template/key.rs new file mode 100644 index 000000000..fcef996f5 --- /dev/null +++ b/nvx-rs/src/host/template/key.rs @@ -0,0 +1,1126 @@ +//! The compatibility key of a template, [`TemplateKey`], and the [`CaptureClass`] of the capture +//! that makes one. + +use std::collections::BTreeMap; +use std::fmt; +use std::net::{Ipv4Addr, Ipv6Addr}; + +use sha2::{Digest, Sha256}; + +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, + TemplateManifest, Topology, parse_profile_id, +}; +use crate::activation; +use crate::contract; +use crate::error::{Error, Result}; +use crate::host::sandbox::SandboxNetwork; +use crate::host::sandbox::mount::MOUNT_TAG; +use crate::host::store::{ArtifactIdentity, MAX_IDENTITY_LEN, RuntimeIdentity}; +use crate::hypervisor::Hypervisor; + +/// The domain of a key's identity: the bytes that precede its canonical encoding in the digest. +const KEY_DOMAIN: &[u8] = b"nvx template key\0"; + +/// The domain of a capture class's identity: the bytes that precede its canonical encoding in the +/// digest. +const CLASS_DOMAIN: &[u8] = b"nvx capture class\0"; + +/// The generation of a CPU profile that a host derives, rather than a built-in one. +const HOST_GENERATION: &str = "host"; + +/// The versions of the contracts of a template's clones that its runtime identity does not +/// record: the snapshot format, the saved device state, the time ABI, the restore packet, and the +/// activation record. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct Contracts { + /// [`contract::SNAPSHOT_FORMAT_VERSION`]. + snapshot: u64, + /// [`contract::DEVICE_STATE_SCHEMA_VERSION`]. + device_state: u64, + /// [`contract::TIME_ABI_VERSION`]. + time: u64, + /// [`contract::RESTORE_PACKET_VERSION`]. + restore_packet: u64, + /// [`activation::VERSION`], as a `u64`, as the other versions are. + activation: u64, +} + +impl Contracts { + /// The versions that this crate speaks. + const CURRENT: Self = Self { + snapshot: contract::SNAPSHOT_FORMAT_VERSION, + device_state: contract::DEVICE_STATE_SCHEMA_VERSION, + time: contract::TIME_ABI_VERSION, + restore_packet: contract::RESTORE_PACKET_VERSION, + // `u64::from` is not `const`, and widening a `u16` is lossless. + activation: activation::VERSION as u64, + }; + + /// Writes the versions. + fn write(&self, writer: &mut Writer) { + for version in [ + self.snapshot, + self.device_state, + self.time, + self.restore_packet, + self.activation, + ] { + writer.u64(version); + } + } + + /// Reads the versions. + fn read(reader: &mut Reader<'_>) -> Result { + Ok(Self { + snapshot: reader.u64("the snapshot format version")?, + device_state: reader.u64("the device-state schema version")?, + time: reader.u64("the time ABI version")?, + restore_packet: reader.u64("the restore packet version")?, + activation: reader.u64("the activation contract version")?, + }) + } +} + +/// Writes the runtime identity `runtime`: its artifacts, then its boot and control versions. +fn write_runtime(writer: &mut Writer, runtime: &RuntimeIdentity) { + for artifact in [&runtime.openvmm, &runtime.kernel, &runtime.initramfs] { + writer.text(artifact.as_str()); + } + writer.u64(runtime.boot_contract); + writer.text(&runtime.control_contract); + writer.u64(runtime.control_session); +} + +/// Reads an artifact identity, which `what` names. +fn read_artifact(reader: &mut Reader<'_>, what: &str) -> Result { + let text = reader.text(MAX_IDENTITY_LEN, what)?; + ArtifactIdentity::new(text).map_err(|error| CanonicalError::new(error.message())) +} + +/// Reads a runtime identity, as [`write_runtime`] writes it. +fn read_runtime(reader: &mut Reader<'_>) -> Result { + let runtime = RuntimeIdentity { + openvmm: read_artifact(reader, "the OpenVMM identity")?, + kernel: read_artifact(reader, "the kernel identity")?, + initramfs: read_artifact(reader, "the initramfs identity")?, + boot_contract: reader.u64("the boot contract version")?, + control_contract: reader + .text(MAX_IDENTITY_LEN, "the control contract revision")? + .to_owned(), + control_session: reader.u64("the control session version")?, + }; + runtime + .check() + .map_err(|error| CanonicalError::new(error.message()))?; + Ok(runtime) +} + +/// Writes a block geometry. +fn write_geometry(writer: &mut Writer, geometry: &BlockGeometry) { + writer.u64(geometry.length); + writer.u32(geometry.logical_block_size); + writer.u32(geometry.physical_block_size); +} + +/// Reads a block geometry, which `what` names. +fn read_geometry(reader: &mut Reader<'_>, what: &str) -> Result { + Ok(BlockGeometry { + length: reader.u64(what)?, + logical_block_size: reader.u32(what)?, + physical_block_size: reader.u32(what)?, + }) +} + +/// Returns the SHA-256 digest of `bytes` after `domain`. +fn digest(domain: &[u8], bytes: &[u8]) -> [u8; 32] { + let mut digest = Sha256::new(); + digest.update(domain); + digest.update(bytes); + digest.finalize().into() +} + +/// Writes `bytes` as lowercase hexadecimal digits. +fn write_hex(formatter: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result { + bytes + .iter() + .try_for_each(|byte| write!(formatter, "{byte:02x}")) +} + +/// Parses 64 hexadecimal digits, in either case, into the 32 bytes that they spell. +fn parse_hex32(text: &str) -> Option<[u8; 32]> { + let digits = text.as_bytes(); + if digits.len() != 64 { + return None; + } + let mut bytes = [0; 32]; + for (byte, pair) in bytes.iter_mut().zip(digits.chunks_exact(2)) { + let nibble = |digit: u8| char::from(digit).to_digit(16); + *byte = u8::try_from(nibble(pair[0])? * 16 + nibble(pair[1])?).ok()?; + } + Some(bytes) +} + +/// Declares a 32-byte identity: the domain-separated SHA-256 digest of a canonical encoding. +macro_rules! identity { + ($(#[$attribute:meta])* $name:ident, $what:literal) => { + $(#[$attribute])* + #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub struct $name([u8; 32]); + + impl $name { + /// Returns the identity whose bytes are `bytes`. + pub const fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) + } + + /// Returns the bytes of the identity. + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + } + + impl fmt::Display for $name { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write_hex(formatter, &self.0) + } + } + + impl fmt::Debug for $name { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(formatter, concat!(stringify!($name), "({})"), self) + } + } + + impl std::str::FromStr for $name { + type Err = Error; + + /// Parses the text form of the identity, 64 hexadecimal digits, failing with + /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for anything else. + fn from_str(text: &str) -> Result { + parse_hex32(text).map(Self).ok_or_else(|| { + Error::unsupported(format!( + concat!("{:?} is not ", $what, ", which is 64 hexadecimal digits"), + text + )) + }) + } + } + }; +} + +identity!( + /// The identity of a [`TemplateKey`]: the SHA-256 digest of its canonical encoding, after the + /// domain `nvx template key` and a NUL, so that it names a key of one encoding version alone. + /// + /// 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 + /// [`activation::Identity`] by which a clone's activation record names the key of its + /// template, in its [`TemplateReference`](activation::TemplateReference), without loss. + KeyIdentity, + "a template key's identity" +); + +impl From for activation::Identity { + /// Returns the identity whose bytes are the key identity's. + fn from(identity: KeyIdentity) -> Self { + Self::from_bytes(identity.0) + } +} + +impl From for KeyIdentity { + /// Returns the key identity whose bytes are the identity's. + fn from(identity: activation::Identity) -> Self { + Self(*identity.as_bytes()) + } +} + +identity!( + /// The identity of a [`CaptureClass`]: the SHA-256 digest of its canonical encoding, after the + /// domain `nvx capture class` and a NUL. + /// + /// Its text form, which `Display` writes and `FromStr` parses, is 64 lowercase hexadecimal + /// digits; parsing also accepts uppercase ones. + ClassIdentity, + "a capture class's identity" +); + +/// The invariant settings of a template that its key holds beyond its machine, such as the +/// isolation and resource settings of its guest, by name: what its adapter fixes for the +/// template and every clone of it, and no restore changes. +/// +/// A name is 1 to [`MAX_NAME_LEN`](Self::MAX_NAME_LEN) bytes of lowercase ASCII letters, digits, +/// `.`, `-`, and `_`, which starts with a letter, and a value 0 to +/// [`MAX_VALUE_LEN`](Self::MAX_VALUE_LEN) printable ASCII characters, spaces included. A template +/// holds at most [`MAX_SETTINGS`](Self::MAX_SETTINGS) of them, in the order of their names. +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] +pub struct Invariants(BTreeMap); + +impl Invariants { + /// Most settings of a template. + pub const MAX_SETTINGS: usize = 64; + /// Longest name of a setting, in bytes. + pub const MAX_NAME_LEN: usize = 64; + /// Longest value of a setting, in bytes. + pub const MAX_VALUE_LEN: usize = 256; + + /// Returns no settings. + pub fn new() -> Self { + Self::default() + } + + /// Returns the settings with `name` set to `value`, which replaces any earlier value of + /// `name`. + /// + /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a name or a value + /// of another form than the [type](Self) allows, and for a new setting beyond + /// [`MAX_SETTINGS`](Self::MAX_SETTINGS). + pub fn with(mut self, name: &str, value: &str) -> Result { + check_setting(name, value).map_err(|error| Error::unsupported(error.to_string()))?; + if !self.0.contains_key(name) && self.0.len() == Self::MAX_SETTINGS { + return Err(Error::unsupported(format!( + "a template holds at most {} invariant settings", + Self::MAX_SETTINGS + ))); + } + self.0.insert(name.to_owned(), value.to_owned()); + Ok(self) + } + + /// Returns the value of the setting `name`, if the settings hold it. + pub fn get(&self, name: &str) -> Option<&str> { + self.0.get(name).map(String::as_str) + } + + /// Returns the settings, in the order of their names. + pub fn iter(&self) -> impl Iterator { + self.0 + .iter() + .map(|(name, value)| (name.as_str(), value.as_str())) + } + + /// Returns the number of settings. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns whether there are no settings. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Writes the settings: a list of names and values. + fn write(&self, writer: &mut Writer) { + let settings: Vec<(&str, &str)> = self.iter().collect(); + writer.list(&settings, |writer, (name, value)| { + writer.text(name); + writer.text(value); + }); + } + + /// Reads the settings, which must be in the strict order of their names. + fn read(reader: &mut Reader<'_>) -> Result { + let settings = reader.list(Self::MAX_SETTINGS, "the invariant settings", |reader| { + let name = reader.text(Self::MAX_NAME_LEN, "the name of a setting")?; + let value = reader.text(Self::MAX_VALUE_LEN, "the value of a setting")?; + check_setting(name, value)?; + Ok((name.to_owned(), value.to_owned())) + })?; + if settings.windows(2).any(|pair| pair[0].0 >= pair[1].0) { + return Err(CanonicalError::new( + "the invariant settings are not in the strict order of their names", + )); + } + Ok(Self(settings.into_iter().collect())) + } +} + +/// Explains why `name` and `value` are not an invariant setting, if they are not. +fn check_setting(name: &str, value: &str) -> Result<(), CanonicalError> { + let name_valid = (1..=Invariants::MAX_NAME_LEN).contains(&name.len()) + && name.starts_with(|first: char| first.is_ascii_lowercase()) + && name.bytes().all(|byte| { + byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'.' | b'-' | b'_') + }); + if !name_valid { + return Err(CanonicalError::new(format!( + "invariant setting {name:?} must be named by 1 to {} lowercase ASCII letters, digits, \ + '.', '-', or '_', starting with a letter", + Invariants::MAX_NAME_LEN + ))); + } + if value.len() > Invariants::MAX_VALUE_LEN + || !value.bytes().all(|byte| (0x20..0x7f).contains(&byte)) + { + return Err(CanonicalError::new(format!( + "the value of invariant setting {name:?} must hold at most {} printable ASCII \ + characters", + Invariants::MAX_VALUE_LEN + ))); + } + Ok(()) +} + +/// The CPU profile that a capture requests: OpenVMM's `--cpu-profile`. +fn cpu_profile_problem(profile: &str) -> Option { + (profile != "auto" && profile != "host" && parse_profile_id(profile).is_none()).then(|| { + format!( + "CPU profile {profile:?} must be \"auto\", \"host\", or a profile ID, \ + VENDOR.GENERATION.vREVISION" + ) + }) +} + +/// The invariant inputs of a template's capture, known before it: what a template's adapter +/// requests, and the class of the templates that the capture makes. +/// +/// A capture whose manifest matches its class makes a template whose [`TemplateKey`] combines +/// them: [`TemplateKey::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. +/// +/// 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. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CaptureClass { + /// The runtime: the identities of OpenVMM, the guest kernel, and the guest initramfs, and the + /// versions of the boot and control contracts. + pub runtime: RuntimeIdentity, + /// The build of the guest's agent, which the adapter supplies. + pub guest_build: ArtifactIdentity, + /// The backend. + pub hypervisor: Hypervisor, + /// The CPU profile that the capture requests, as OpenVMM's `--cpu-profile` takes it: `auto`, + /// for the built-in profile of the host's CPU, `host`, for a profile that the host derives, or + /// a profile ID, `VENDOR.GENERATION.vREVISION`. + pub cpu_profile: String, + /// The processor capacity: 1, 2, 4, or 8. + pub processors: u32, + /// The processors online at boot: 1, 2, 4, or 8, at most [`processors`](Self::processors). + pub boot_processors: u32, + /// The RAM at boot, in bytes. + pub memory: u64, + /// The RAM capacity, in bytes, to which a restore may expand the RAM: at least + /// [`memory`](Self::memory). + pub memory_capacity: u64, + /// The placeholder `distro` that the capture attaches: its length is the capacity of every + /// clone's image. + pub distro: BlockGeometry, + /// The dormant scratch device. + pub scratch: BlockGeometry, + /// The network: `None` or a network that OpenVMM attaches no device for, as + /// [`SandboxNetwork::openvmm_arguments`] lowers it, for a template without a NIC, and otherwise + /// the network of its fixed NIC and the policy that OpenVMM enforces there. + pub network: Option, + /// The invariant settings beyond the machine. + pub invariants: Invariants, +} + +impl CaptureClass { + /// Version of the canonical encoding of a class, its first two bytes. + pub const VERSION: u16 = 1; + + /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) unless every field + /// has the form that its documentation gives, the runtime identity passes + /// [`RuntimeIdentity::check`], and the network lowers to OpenVMM's arguments. + pub fn check(&self) -> Result<()> { + self.runtime.check()?; + if let Some(problem) = cpu_profile_problem(&self.cpu_profile) { + return Err(Error::unsupported(problem)); + } + if !PROCESSOR_COUNTS.contains(&self.processors) + || !PROCESSOR_COUNTS.contains(&self.boot_processors) + || self.boot_processors > self.processors + { + return Err(Error::unsupported(format!( + "a capture of {} processors with {} online at boot needs 1, 2, 4, or 8 of each, \ + and no more online than in all", + self.processors, self.boot_processors + ))); + } + if self.memory == 0 || self.memory_capacity < self.memory { + return Err(Error::unsupported(format!( + "a capture of {} bytes of RAM needs a nonzero RAM and a capacity of at least it, \ + not {}", + self.memory, self.memory_capacity + ))); + } + for (role, geometry) in [("distro", &self.distro), ("scratch", &self.scratch)] { + if let Some(problem) = geometry.problem() { + return Err(Error::unsupported(format!( + "the {role} device of a capture is invalid: its {problem}" + ))); + } + } + self.network_arguments()?; + Ok(()) + } + + /// Returns OpenVMM's arguments of the network, none without a NIC. + fn network_arguments(&self) -> Result> { + match &self.network { + None => Ok(Vec::new()), + Some(network) => network.openvmm_arguments(), + } + } + + /// Returns the canonical encoding of the class, which its [`identity`](Self::identity) + /// digests. + /// + /// Version 1 holds, in order: [`VERSION`](Self::VERSION) as a `u16`; the runtime identity's + /// OpenVMM, kernel, and initramfs identities as texts, its boot contract version as a `u64`, + /// its control contract revision as a text, and its control session version as a `u64`; the + /// guest build as a text; the snapshot format, device-state schema, time ABI, restore packet, + /// and activation contract versions as `u64`s; the backend, as OpenVMM spells it, and the + /// CPU profile, as texts; the processors and those online at boot as `u32`s; the RAM and its + /// capacity as `u64`s; the `distro` and scratch geometries, each a `u64` length and two + /// `u32` block sizes; the network, as the list of the texts of OpenVMM's arguments that + /// [`SandboxNetwork::openvmm_arguments`] lowers it to, empty without a NIC; and the invariant + /// settings, as a list of names and values, each a text, in the order of their names. The + /// [module](super) describes the encoding of each kind of value. + /// + /// Fails as [`check`](Self::check) does. + pub fn to_bytes(&self) -> Result> { + self.encode(&Contracts::CURRENT) + } + + /// Returns the canonical encoding of the class, with the contract versions `contracts`. + fn encode(&self, contracts: &Contracts) -> Result> { + self.check()?; + let mut writer = Writer::default(); + writer.u16(Self::VERSION); + write_runtime(&mut writer, &self.runtime); + writer.text(self.guest_build.as_str()); + contracts.write(&mut writer); + writer.text(self.hypervisor.as_str()); + writer.text(&self.cpu_profile); + writer.u32(self.processors); + writer.u32(self.boot_processors); + writer.u64(self.memory); + writer.u64(self.memory_capacity); + write_geometry(&mut writer, &self.distro); + write_geometry(&mut writer, &self.scratch); + writer.list(&self.network_arguments()?, |writer, argument| { + writer.text(argument) + }); + self.invariants.write(&mut writer); + Ok(writer.finish()) + } + + /// Returns the identity of the class. + /// + /// Fails as [`check`](Self::check) does. + pub fn identity(&self) -> Result { + Ok(ClassIdentity(digest(CLASS_DOMAIN, &self.to_bytes()?))) + } +} + +/// The compatibility key of a template: a canonical, versioned projection of the small metadata +/// that decides which clones a template can serve, and on which hosts. +/// +/// A key holds, and nothing else: +/// +/// - **Bundle and contracts:** the identities of OpenVMM, the guest kernel, and the guest +/// initramfs, and the build of the guest's agent, which the adapter supplies; and the versions +/// of the key's encoding and of the boot, snapshot, device-state, time, restore-packet, +/// control, and activation contracts. +/// - **CPU and time:** the backend; the CPU profile's vendor, generation, revision, and the digest +/// of its canonical document, and the effective CPUID; the processor topology; and the TSC and +/// LAPIC timer rates that the time contract declares, and its rate policy, the tolerance of a +/// restoring host's TSC rate. The time ABI version is the time contract's. +/// - **Resources:** the processor capacity and the processors online at boot; the RAM at boot, +/// its capacity, the size of the blocks in which a restore expands it, and the ranges of both. +/// - **Boot:** the exact effective kernel command line, and its digest. +/// - **Disks:** the length and block sizes of the `distro`, which is the capacity of every clone's +/// image, and of the scratch. +/// - **Devices and isolation:** each device's ID, kind, state unit, transport, address ranges, +/// interrupt, features, and queues, and the interrupt delivery of the virtio devices; the +/// filesystem slot's version and tag; whether there is a NIC, and the fixed NIC's profile, MAC +/// addresses, addresses, prefixes, and gateways, with the identity of the network policy that +/// OpenVMM enforces there; 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. [`TemplateManifest`] keeps them. Nothing of a clone is part of it +/// either: its image, upper, export, network settings, identity, and activation targets. +/// +/// 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, +/// an image, the RAM, or the saved state. A bundle that changes any of its artifacts therefore +/// 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. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TemplateKey { + /// The runtime: OpenVMM, the guest kernel and initramfs, and the boot and control contracts. + runtime: RuntimeIdentity, + /// The build of the guest's agent. + guest_build: ArtifactIdentity, + /// The versions of the other contracts. + contracts: Contracts, + /// The machine, as the manifest describes it. + machine: Machine, + /// The tag of the filesystem slot. + filesystem_tag: String, + /// The invariant settings beyond the machine. + invariants: Invariants, +} + +impl TemplateKey { + /// Version of the canonical encoding of a key, its first two bytes. + pub const VERSION: u16 = 1; + + /// Most bytes of a key's canonical encoding: what the largest key that a manifest of at most + /// [`MAX_MANIFEST_LEN`](super::MAX_MANIFEST_LEN) bytes can describe takes, with room to spare. + pub const MAX_LEN: usize = 256 * 1024; + + /// Returns the key of the template that a capture of `class` made, whose manifest is + /// `manifest`. + /// + /// The manifest's machine must be the class's: the backend, the processors and those online + /// at boot, the RAM and its capacity, the `distro` and scratch geometries, the presence of a + /// NIC, and the NIC's address and prefix, and a CPU profile that the class's request selects. + /// A manifest whose machine has no processors online at boot activates all of them at boot, + /// which a class whose processors are all online at boot requests. The key takes the class's + /// runtime, guest build, and invariant settings, and this crate's versions of the other + /// contracts. + /// + /// The key also takes the identity of the network policy that OpenVMM enforced on the NIC, + /// which this check leaves to OpenVMM: OpenVMM derives it from the network arguments of the + /// class by an encoding of its own, which this crate does not reproduce. A capture that + /// enforced another policy than its class requested makes a key of its own, which no restore + /// with the class's network arguments serves, since OpenVMM refuses a restore whose network, + /// its policy included, is not the one that the snapshot records. + /// + /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) as + /// [`CaptureClass::check`] does, and with [`ErrorKind::Io`](crate::ErrorKind::Io) for a + /// manifest whose machine is not the class's, as a capture that did not do what its class + /// requested. + pub fn new(class: &CaptureClass, manifest: &TemplateManifest) -> Result { + class.check()?; + let machine = &manifest.machine; + let mismatch = |found: String, requested: String| { + Err(Error::io(format!( + "the capture does not match its class: {found}, but the class requests \ + {requested}" + ))) + }; + if machine.hypervisor != class.hypervisor { + return mismatch( + format!("its backend is {}", machine.hypervisor), + class.hypervisor.to_string(), + ); + } + 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() { + "auto" => true, + "host" => machine.cpu.generation == HOST_GENERATION, + requested => profile == requested, + }; + if !selected { + return mismatch( + format!("its CPU profile is {profile}"), + class.cpu_profile.clone(), + ); + } + let boot_processors = match machine.boot_processors { + 0 => machine.processors, + count => count, + }; + if (machine.processors, 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)), + describe(class.processors, class.boot_processors), + ); + } + if (machine.memory.boot, machine.memory.capacity) != (class.memory, class.memory_capacity) { + let describe = + |memory, capacity| format!("{memory} bytes of RAM, of a capacity of {capacity}"); + return mismatch( + format!( + "it has {}", + describe(machine.memory.boot, machine.memory.capacity) + ), + describe(class.memory, class.memory_capacity), + ); + } + for (role, found, requested) in [ + ("distro", &machine.distro, &class.distro), + ("scratch", &machine.scratch, &class.scratch), + ] { + if found != requested { + return mismatch( + format!("its {role} geometry is {found:?}"), + format!("{requested:?}"), + ); + } + } + let attached = !class.network_arguments()?.is_empty(); + let requested_network = class + .network + .as_ref() + .filter(|_| attached) + .map(|network| network.guest_network().to_owned()); + // A network that `SandboxNetwork` accepts has the canonical form that this writes. + let found_network = machine + .nic + .as_ref() + .map(|nic| format!("{}/{}", nic.ipv4, nic.prefix_length)); + if found_network != requested_network { + let describe = |network: Option| network.unwrap_or_else(|| "none".to_owned()); + return mismatch( + format!("its NIC network is {}", describe(found_network)), + describe(requested_network), + ); + } + Ok(Self { + runtime: class.runtime.clone(), + guest_build: class.guest_build.clone(), + contracts: Contracts::CURRENT, + machine: machine.clone(), + filesystem_tag: MOUNT_TAG.to_owned(), + invariants: class.invariants.clone(), + }) + } + + /// Returns the identity of the key. + pub fn identity(&self) -> KeyIdentity { + KeyIdentity(digest(KEY_DOMAIN, &self.to_bytes())) + } + + /// Returns the runtime: the identities of OpenVMM, the guest kernel, and the guest initramfs, + /// and the versions of the boot and control contracts. + pub fn runtime(&self) -> &RuntimeIdentity { + &self.runtime + } + + /// Returns the build of the guest's agent. + pub fn guest_build(&self) -> &ArtifactIdentity { + &self.guest_build + } + + /// Returns whether the versions of the snapshot, device-state, time, restore-packet, and + /// activation contracts in the key are those that this crate speaks. + pub fn speaks_current_contracts(&self) -> bool { + self.contracts == Contracts::CURRENT + } + + /// Returns the backend. + pub fn hypervisor(&self) -> Hypervisor { + self.machine.hypervisor + } + + /// Returns the ID of the CPU profile, `VENDOR.GENERATION.vREVISION`. + pub fn cpu_profile(&self) -> String { + self.machine.cpu.id() + } + + /// Returns the processor capacity. + pub fn processors(&self) -> u32 { + self.machine.processors + } + + /// Returns the processors online at boot, or 0 when a restore activates none, as when they + /// are all online at boot. + pub fn boot_processors(&self) -> u32 { + self.machine.boot_processors + } + + /// Returns the RAM at boot, in bytes. + pub fn memory(&self) -> u64 { + self.machine.memory.boot + } + + /// Returns the RAM capacity, in bytes. + pub fn memory_capacity(&self) -> u64 { + self.machine.memory.capacity + } + + /// Returns the size of the blocks in which a restore expands the RAM, in bytes. + pub fn memory_block_size(&self) -> u64 { + self.machine.memory.block_size + } + + /// Returns the exact effective kernel command line. + pub fn command_line(&self) -> &str { + &self.machine.command_line + } + + /// Returns the geometry of the `distro`, whose length is the capacity of every clone's image. + pub fn distro(&self) -> BlockGeometry { + self.machine.distro + } + + /// Returns the geometry of the scratch. + pub fn scratch(&self) -> BlockGeometry { + self.machine.scratch + } + + /// Returns whether the template has a NIC. + pub fn has_network(&self) -> bool { + self.machine.nic.is_some() + } + + /// Returns the tag of the filesystem slot, on which a clone's export is bound. + pub fn filesystem_tag(&self) -> &str { + &self.filesystem_tag + } + + /// Returns the invariant settings beyond the machine. + pub fn invariants(&self) -> &Invariants { + &self.invariants + } + + /// Returns the canonical encoding of the key, which its [`identity`](Self::identity) digests, + /// and [`from_bytes`](Self::from_bytes) reads back. + /// + /// Version 1 holds, in order, each value encoded as the [module](super) describes: + /// + /// 1. [`VERSION`](Self::VERSION), a `u16`. + /// 2. The bundle and contracts: the OpenVMM, kernel, and initramfs identities, texts; the boot + /// contract version, a `u64`; the control contract revision, a text; the control session + /// version, a `u64`; the guest build, a text; and the snapshot format, device-state schema, + /// time ABI, restore packet, and activation contract versions, `u64`s. + /// 3. CPU and time: the backend, a text, as OpenVMM spells it; the CPU profile's vendor and + /// generation, texts, its revision, a `u32`, and its digest, 32 bytes; the effective + /// CPUID, a byte string, as OpenVMM encodes it; the sockets, the dies of each socket, the + /// cores of each die, and the threads of each core, `u32`s, and the APIC IDs, a list of + /// `u32`s; the TSC rate and the LAPIC timer rate in Hz, `u64`s, and the rate tolerance in + /// parts per million, a `u32`. + /// 4. Resources: the processor capacity and the processors online at boot, `u32`s; the RAM at + /// boot, its capacity, and its block size, `u64`s; and the ranges of the RAM at boot, then + /// those that a restore may add, each a list of ranges of a `u64` start and a `u64` length. + /// 5. Boot: the effective command line, a text, and its SHA-256 digest, 32 bytes. + /// 6. Disks: the `distro`'s length, a `u64`, and its logical and physical block sizes, `u32`s; + /// then the scratch's. + /// 7. Devices and isolation: the interrupt delivery mode, a text, and the start and length of + /// the shared interrupt-status page, `u64`s; the devices, a list, each with its stable ID, + /// kind, state unit, and transport, texts, its address ranges, a list of an address space, + /// `mmio` or `pmio`, a text, a start, and a length, `u64`s, its optional interrupt, a + /// `u32`, its feature banks, a list of `u32`s, its queue count, a `u32`, and the largest + /// size of each queue, a list of `u32`s; the filesystem slot's version, a `u32`, and tag, + /// a text; the optional NIC, with its profile, a text, the guest's and the gateway's MAC + /// addresses, 6 bytes each, the guest's IPv4 address, 4 bytes, its prefix length, a `u8`, + /// 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. + pub fn to_bytes(&self) -> Vec { + let mut writer = Writer::default(); + writer.u16(Self::VERSION); + write_runtime(&mut writer, &self.runtime); + writer.text(self.guest_build.as_str()); + self.contracts.write(&mut writer); + write_machine(&mut writer, &self.machine, &self.filesystem_tag); + self.invariants.write(&mut writer); + writer.finish() + } + + /// 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 + /// value of another form than a key derived from a manifest has, and no byte left over. + /// + /// Fails with [`ErrorCode::FormatUnsupported`](crate::ErrorCode::FormatUnsupported) for a key + /// of another version, and with [`ErrorKind::Io`](crate::ErrorKind::Io) for bytes that are + /// not such a key. + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() > Self::MAX_LEN { + return Err(Error::io(format!( + "a template key of {} bytes is longer than the {} bytes that a key may hold", + bytes.len(), + Self::MAX_LEN + ))); + } + let mut reader = Reader::new(bytes); + let malformed = + |error: CanonicalError| Error::io(format!("the template key is malformed: {error}")); + let version = reader.u16("the version").map_err(malformed)?; + if version != Self::VERSION { + return Err(Error::new( + crate::ErrorCode::FormatUnsupported, + format!( + "the template key has encoding version {version}; this build reads version {} \ + only", + Self::VERSION + ), + )); + } + let read = |reader: &mut Reader<'_>| -> Result { + let runtime = read_runtime(reader)?; + let guest_build = read_artifact(reader, "the guest build")?; + let contracts = Contracts::read(reader)?; + let (machine, filesystem_tag) = read_machine(reader)?; + let invariants = Invariants::read(reader)?; + Ok(Self { + runtime, + guest_build, + contracts, + machine, + filesystem_tag, + invariants, + }) + }; + let key = read(&mut reader).map_err(malformed)?; + reader.finish().map_err(malformed)?; + Ok(key) + } +} + +/// Writes the machine `machine`, whose filesystem slot has the tag `tag`, as +/// [`TemplateKey::to_bytes`] describes it. +fn write_machine(writer: &mut Writer, machine: &Machine, tag: &str) { + writer.text(machine.hypervisor.as_str()); + let cpu = &machine.cpu; + writer.text(&cpu.vendor); + writer.text(&cpu.generation); + writer.u32(cpu.revision); + writer.fixed(&cpu.digest); + writer.bytes(&cpu.effective_cpuid); + let topology = &machine.topology; + for count in [ + topology.sockets, + topology.dies_per_socket, + topology.cores_per_die, + topology.threads_per_core, + ] { + writer.u32(count); + } + writer.list(&topology.apic_ids, |writer, id| writer.u32(*id)); + writer.u64(machine.rates.tsc_hz); + writer.u64(machine.rates.apic_hz); + writer.u32(machine.rates.tolerance_ppm); + writer.u32(machine.processors); + writer.u32(machine.boot_processors); + let memory = &machine.memory; + writer.u64(memory.boot); + writer.u64(memory.capacity); + writer.u64(memory.block_size); + for ranges in [&memory.ranges, &memory.expansion] { + writer.list(ranges, |writer, range| { + writer.u64(range.start); + writer.u64(range.length); + }); + } + writer.text(&machine.command_line); + writer.fixed(&machine.command_line_digest); + write_geometry(writer, &machine.distro); + write_geometry(writer, &machine.scratch); + writer.text(&machine.interrupts.mode); + writer.u64(machine.interrupts.status_page_start); + writer.u64(machine.interrupts.status_page_length); + writer.list(&machine.devices, |writer, device| { + for text in [ + &device.stable_id, + &device.kind, + &device.state_unit, + &device.transport, + ] { + writer.text(text); + } + writer.list(&device.ranges, |writer, range| { + writer.text(range.space.as_str()); + writer.u64(range.start); + writer.u64(range.length); + }); + writer.option(device.irq.as_ref(), |writer, irq| writer.u32(*irq)); + writer.list(&device.feature_banks, |writer, bank| writer.u32(*bank)); + writer.u32(device.queue_count); + writer.list(&device.queue_max_sizes, |writer, size| writer.u32(*size)); + }); + writer.u32(machine.filesystem_slot); + writer.text(tag); + writer.option(machine.nic.as_ref(), |writer, nic| { + writer.text(&nic.profile); + writer.fixed(&nic.guest_mac); + writer.fixed(&nic.gateway_mac); + writer.fixed(&nic.ipv4.octets()); + writer.u8(nic.prefix_length); + writer.fixed(&nic.gateway.octets()); + writer.option(nic.ipv6.as_ref(), |writer, ipv6| { + writer.fixed(&ipv6.address.octets()); + writer.u8(ipv6.prefix_length); + writer.fixed(&ipv6.gateway.octets()); + }); + writer.text(&nic.policy.mode); + writer.fixed(&nic.policy.digest); + writer.bool(nic.policy.required); + writer.u32(nic.policy.encoding_version); + }); +} + +/// Reads a machine and the tag of its filesystem slot, as [`write_machine`] writes them, and +/// checks the machine as a manifest's is checked. +fn read_machine(reader: &mut Reader<'_>) -> Result<(Machine, String), CanonicalError> { + let short = |reader: &mut Reader<'_>, what: &str| -> Result { + Ok(reader.text(MAX_TEXT_LEN, what)?.to_owned()) + }; + let backend = short(reader, "the backend")?; + let hypervisor = backend + .parse::() + .map_err(|_| CanonicalError::new(format!("the backend {backend:?} is unknown")))?; + let cpu = CpuProfile { + vendor: short(reader, "the CPU vendor")?, + generation: short(reader, "the CPU generation")?, + revision: reader.u32("the CPU profile revision")?, + digest: reader.fixed("the CPU profile digest")?, + effective_cpuid: reader + .bytes(MAX_CPUID_ENTRIES * CPUID_ENTRY_LEN, "the effective CPUID")? + .to_vec(), + }; + let topology = Topology { + sockets: reader.u32("the sockets")?, + dies_per_socket: reader.u32("the dies of each socket")?, + cores_per_die: reader.u32("the cores of each die")?, + threads_per_core: reader.u32("the threads of each core")?, + apic_ids: reader.list(MAX_PROCESSORS, "the APIC IDs", |reader| { + reader.u32("an APIC ID") + })?, + }; + let rates = Rates { + tsc_hz: reader.u64("the TSC rate")?, + apic_hz: reader.u64("the LAPIC timer rate")?, + tolerance_ppm: reader.u32("the rate tolerance")?, + }; + let processors = reader.u32("the processor capacity")?; + let boot_processors = reader.u32("the processors online at boot")?; + let boot = reader.u64("the RAM")?; + let capacity = reader.u64("the RAM capacity")?; + let block_size = reader.u64("the RAM block size")?; + let mut range_lists = Vec::with_capacity(2); + for what in ["the RAM ranges", "the RAM expansion ranges"] { + range_lists.push(reader.list(MAX_MEMORY_RANGES, what, |reader| { + Ok(MemoryRange { + start: reader.u64(what)?, + length: reader.u64(what)?, + }) + })?); + } + let expansion = range_lists.pop().expect("two lists were read"); + let ranges = range_lists.pop().expect("two lists were read"); + let command_line = reader + .text(MAX_COMMAND_LINE_LEN, "the command line")? + .to_owned(); + let command_line_digest = reader.fixed("the command line's digest")?; + let distro = read_geometry(reader, "the distro geometry")?; + let scratch = read_geometry(reader, "the scratch geometry")?; + let interrupts = Interrupts { + mode: short(reader, "the interrupt delivery mode")?, + status_page_start: reader.u64("the interrupt-status page")?, + status_page_length: reader.u64("the interrupt-status page")?, + }; + let devices = reader.list(MAX_DEVICES, "the devices", |reader| { + let stable_id = short(reader, "a device ID")?; + let kind = short(reader, "a device kind")?; + let state_unit = short(reader, "a device's state unit")?; + let transport = short(reader, "a device's transport")?; + let ranges = reader.list(MAX_DEVICE_RANGES, "a device's ranges", |reader| { + let space = reader.text(MAX_TEXT_LEN, "an address space")?; + let space = AddressSpace::parse(space).ok_or_else(|| { + CanonicalError::new(format!("the address space {space:?} is unknown")) + })?; + Ok(DeviceRange { + space, + start: reader.u64("a device range")?, + length: reader.u64("a device range")?, + }) + })?; + Ok(Device { + stable_id, + kind, + state_unit, + transport, + ranges, + irq: reader.option("a device's interrupt", |reader| { + reader.u32("a device's interrupt") + })?, + feature_banks: reader.list(MAX_DEVICE_WORDS, "a device's features", |reader| { + reader.u32("a feature bank") + })?, + queue_count: reader.u32("a device's queue count")?, + queue_max_sizes: reader.list(MAX_DEVICE_WORDS, "a device's queue sizes", |reader| { + reader.u32("a queue size") + })?, + }) + })?; + let filesystem_slot = reader.u32("the filesystem slot version")?; + let tag = short(reader, "the filesystem tag")?; + let nic = reader.option("the NIC", |reader| { + let profile = short(reader, "the network profile")?; + let guest_mac = reader.fixed("the guest's MAC address")?; + let gateway_mac = reader.fixed("the gateway's MAC address")?; + let ipv4 = Ipv4Addr::from(reader.fixed::<4>("the guest's IPv4 address")?); + let prefix_length = reader.u8("the IPv4 prefix length")?; + let gateway = Ipv4Addr::from(reader.fixed::<4>("the IPv4 gateway")?); + let ipv6 = reader.option("the IPv6 identity", |reader| { + Ok(Ipv6Identity { + address: Ipv6Addr::from(reader.fixed::<16>("the guest's IPv6 address")?), + prefix_length: reader.u8("the IPv6 prefix length")?, + gateway: Ipv6Addr::from(reader.fixed::<16>("the IPv6 gateway")?), + }) + })?; + let policy = PolicyIdentity { + mode: short(reader, "the network policy's mode")?, + digest: reader.fixed("the network policy's digest")?, + required: reader.bool("whether the network policy is required")?, + encoding_version: reader.u32("the network policy's encoding version")?, + }; + Ok(Nic { + profile, + guest_mac, + gateway_mac, + ipv4, + prefix_length, + gateway, + ipv6, + policy, + }) + })?; + let machine = Machine { + hypervisor, + cpu, + topology, + rates, + processors, + boot_processors, + memory: Memory { + boot, + capacity, + block_size, + ranges, + expansion, + }, + command_line, + command_line_digest, + distro, + scratch, + interrupts, + devices, + filesystem_slot, + nic, + }; + if let Some(problem) = machine.problem() { + return Err(CanonicalError::new(format!("the machine {problem}"))); + } + if machine.filesystem_slot != FILESYSTEM_SLOT_VERSION || tag.is_empty() { + return Err(CanonicalError::new( + "the filesystem slot is not the dormant slot of version 1, with a tag", + )); + } + Ok((machine, tag)) +} + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/template/key/tests.rs b/nvx-rs/src/host/template/key/tests.rs new file mode 100644 index 000000000..5203c4292 --- /dev/null +++ b/nvx-rs/src/host/template/key/tests.rs @@ -0,0 +1,968 @@ +//! Tests of [`TemplateKey`] and [`CaptureClass`]: what a key holds, what it leaves out, and its +//! canonical encoding. + +use std::collections::HashSet; + +use super::*; +use crate::ErrorCode; +use crate::host::network::{Access, EgressRule, NetworkPolicy}; +use crate::host::template::test_support::{ + self as support, MIB, ManifestSpec, artifact, network_class, +}; + +/// Returns the manifest that `spec` encodes. +fn manifest(spec: &ManifestSpec) -> TemplateManifest { + TemplateManifest::parse(&spec.encode()).unwrap() +} + +/// Returns the key of the template that a capture of `class` made, whose manifest `spec` +/// encodes. +fn key(class: &CaptureClass, spec: &ManifestSpec) -> TemplateKey { + TemplateKey::new(class, &manifest(spec)).unwrap() +} + +/// Returns the key of the template with a NIC of the tests. +fn network_key() -> TemplateKey { + key(&network_class(), &ManifestSpec::with_network()) +} + +/// A change to the class and the manifest of the template with a NIC of the tests. +type Change = fn(&mut CaptureClass, &mut ManifestSpec); + +/// Returns the identity of the key of the template with a NIC of the tests once `change` changes +/// it. +fn identity_after(change: Change) -> KeyIdentity { + let (mut class, mut spec) = (network_class(), ManifestSpec::with_network()); + change(&mut class, &mut spec); + key(&class, &spec).identity() +} + +/// Changes the RAM of a capture to `memory` bytes, below 3 GiB, of the same capacity. +fn set_memory(class: &mut CaptureClass, spec: &mut ManifestSpec, memory: u64) { + class.memory = memory; + spec.memory_size = memory; + let contract = spec.contract(); + contract.memory_ranges = vec![(0, memory, 0)]; + contract.expansion_ranges = vec![(memory, contract.memory_capacity - memory)]; +} + +#[test] +fn each_part_of_the_key_changes_it() { + let changes: &[(&str, Change)] = &[ + // Bundle and contracts. + ("OpenVMM", |class, _| class.runtime.openvmm = artifact('d')), + ("kernel", |class, _| class.runtime.kernel = artifact('d')), + ("initramfs", |class, _| { + class.runtime.initramfs = artifact('d') + }), + ("boot contract", |class, _| class.runtime.boot_contract += 1), + ("control contract", |class, _| { + class.runtime.control_contract = "other-revision".to_owned(); + }), + ("control session", |class, _| { + class.runtime.control_session += 1 + }), + ("guest build", |class, _| { + class.guest_build = ArtifactIdentity::new("guest-build-2").unwrap(); + }), + // CPU and time. + ("backend", |class, spec| { + class.hypervisor = Hypervisor::Mshv; + spec.contract().source_hypervisor = "mshv".to_owned(); + }), + ("CPU vendor", |_, spec| { + spec.cpu().id = "amd.icelake-sp.v1".to_owned(); + }), + ("CPU generation", |_, spec| { + spec.cpu().id = "intel.sapphirerapids.v1".to_owned(); + }), + ("CPU profile revision", |_, spec| { + spec.cpu().id = "intel.icelake-sp.v2".to_owned(); + }), + ("CPU profile digest", |_, spec| spec.cpu().digest[31] ^= 1), + ("effective CPUID", |_, spec| { + spec.cpu().effective_cpuid[87] ^= 1 + }), + ("sockets", |_, spec| spec.contract().topology.0 = 2), + ("dies", |_, spec| spec.contract().topology.1 = 2), + ("cores", |_, spec| spec.contract().topology.2 = 1), + ("threads", |_, spec| spec.contract().topology.3 = 2), + ("APIC IDs", |_, spec| { + spec.contract().topology.4 = vec![0, 2] + }), + ("TSC rate", |_, spec| spec.time().tsc_hz += 1), + ("LAPIC timer rate", |_, spec| spec.time().apic_hz += 1), + ("rate policy", |_, spec| spec.time().tolerance_ppm += 1), + // Resources. + ("processors", |class, spec| { + class.processors = 4; + class.boot_processors = 4; + spec.vp_count = 4; + spec.contract().topology = (1, 1, 4, 1, vec![0, 1, 2, 3]); + }), + ("processors online at boot", |_, spec| { + spec.contract().boot_online_vp_count = 2; + }), + ("RAM", |class, spec| set_memory(class, spec, 640 * MIB)), + ("RAM capacity", |class, spec| { + class.memory_capacity = 1024 * MIB; + let contract = spec.contract(); + contract.memory_capacity = 1024 * MIB; + contract.expansion_ranges = vec![(512 * MIB, 512 * MIB)]; + }), + ("RAM block size", |_, spec| { + spec.contract().memory_block_size = 64 * MIB; + }), + ("RAM ranges", |_, spec| { + spec.contract().memory_ranges = + vec![(0, 256 * MIB, 0), (4096 * MIB, 256 * MIB, 256 * MIB)]; + }), + ("RAM expansion ranges", |_, spec| { + spec.contract().expansion_ranges = + vec![(512 * MIB, 512 * MIB), (4096 * MIB, 1024 * MIB)]; + }), + // Boot. + ("command line", |_, spec| { + spec.contract().command_line.push_str(" quiet"); + }), + // Disks. + ("distro capacity", |class, spec| { + class.distro.length *= 2; + spec.contract().blocks[0].length *= 2; + }), + ("distro block sizes", |class, spec| { + class.distro.physical_block_size = 512; + spec.contract().blocks[0].physical_block_size = 512; + }), + ("scratch length", |class, spec| { + class.scratch.length *= 2; + spec.contract().blocks[1].length *= 2; + }), + ("scratch geometry", |class, spec| { + class.scratch.logical_block_size = 4096; + class.scratch.physical_block_size = 4096; + let scratch = &mut spec.contract().blocks[1]; + scratch.logical_block_size = 4096; + scratch.physical_block_size = 4096; + }), + // Devices and isolation. + ("device ID", |_, spec| { + spec.contract().devices[2].stable_id = "clock".to_owned(); + }), + ("device kind", |_, spec| { + spec.contract().devices[2].kind = "timer".to_owned(); + }), + ("state unit", |_, spec| { + spec.contract().devices[2].state_unit = "time".to_owned(); + }), + ("device transport", |_, spec| { + spec.contract().devices[14].transport = "virtio-pci".to_owned(); + }), + ("MMIO", |_, spec| { + spec.contract().devices[4].ranges[0].1 += 0x1000 + }), + ("address space", |_, spec| { + spec.contract().devices[8].ranges[0].0 = "mmio".to_owned(); + }), + ("device ranges", |_, spec| { + spec.contract().devices[3].ranges.pop(); + }), + ("IRQ", |_, spec| spec.contract().devices[14].irq = Some(5)), + ("an IRQ of 0", |_, spec| { + spec.contract().devices[2].irq = Some(0) + }), + ("features", |_, spec| { + spec.contract().devices[14].feature_banks[0] ^= 1; + }), + ("queue count", |_, spec| { + let device = &mut spec.contract().devices[14]; + device.queue_count = 2; + device.queue_max_sizes = vec![256, 256]; + }), + ("queue sizes", |_, spec| { + spec.contract().devices[14].queue_max_sizes[0] = 128; + }), + ("device order", |_, spec| { + let contract = spec.contract(); + contract.devices.swap(3, 4); + contract.renumber(); + }), + ("interrupt delivery", |_, spec| { + spec.contract().interrupt_mode = "edge".to_owned(); + }), + ("interrupt-status page", |_, spec| { + spec.contract().status_page.0 += 0x1000; + }), + ("NIC presence", |class, spec| { + *class = support::class(); + *spec = ManifestSpec::template(); + }), + ("network profile", |_, spec| { + spec.network().profile = "strict".to_owned(); + }), + ("guest MAC", |_, spec| spec.network().guest_mac[5] = 3), + ("gateway MAC", |_, spec| spec.network().gateway_mac[5] = 3), + ("IPv4 address", |class, spec| { + class.network = Some( + SandboxNetwork::new("10.0.0.3/24", NetworkPolicy::new(Access::Allow)).unwrap(), + ); + spec.network().ipv4 = [10, 0, 0, 3]; + }), + ("IPv4 prefix", |class, spec| { + class.network = Some( + SandboxNetwork::new("10.0.0.2/16", NetworkPolicy::new(Access::Allow)).unwrap(), + ); + spec.network().prefix_length = 16; + }), + ("IPv4 gateway", |_, spec| { + spec.network().gateway = [10, 0, 0, 9] + }), + ("IPv6 address", |_, spec| spec.network().ipv6[15] = 3), + ("IPv6 prefix", |_, spec| { + spec.network().ipv6_prefix_length = 64 + }), + ("IPv6 gateway", |_, spec| { + spec.network().ipv6_gateway[15] = 9 + }), + ("IPv6 presence", |_, spec| { + let network = spec.network(); + network.ipv6.clear(); + network.ipv6_prefix_length = 0; + network.ipv6_gateway.clear(); + }), + ("policy mode", |_, spec| { + spec.network().policy_mode = "deny-all".to_owned(); + }), + ("policy digest", |_, spec| { + spec.network().policy_digest[0] ^= 1 + }), + ("policy enforcement", |_, spec| { + spec.network().policy_required = false; + }), + ("policy encoding", |_, spec| { + spec.network().policy_encoding_version = 3; + }), + ("invariant setting", |class, _| { + class.invariants = Invariants::new() + .with("isolation.seccomp", "strict") + .unwrap(); + }), + ]; + let base = network_key().identity(); + let mut identities = HashSet::from([base]); + for (part, change) in changes { + assert!( + identities.insert(identity_after(*change)), + "a change of the {part} leaves the key as it is, or as another change makes it" + ); + } + // Two values of one setting make two keys. + let mut class = network_class(); + class.invariants = Invariants::new() + .with("isolation.seccomp", "relaxed") + .unwrap(); + assert!(identities.insert(key(&class, &ManifestSpec::with_network()).identity())); +} + +#[test] +fn each_contract_version_changes_the_key() { + let base = network_key(); + assert!(base.speaks_current_contracts()); + let changes: [fn(&mut Contracts); 5] = [ + |contracts| contracts.snapshot += 1, + |contracts| contracts.device_state += 1, + |contracts| contracts.time += 1, + |contracts| contracts.restore_packet += 1, + |contracts| contracts.activation += 1, + ]; + let mut identities = HashSet::from([base.identity()]); + for change in changes { + let mut other = base.clone(); + change(&mut other.contracts); + assert!(!other.speaks_current_contracts()); + assert!(identities.insert(other.identity())); + // A key of other versions reads back as it is, so that a store can name what it holds. + assert_eq!(TemplateKey::from_bytes(&other.to_bytes()).unwrap(), other); + } +} + +#[test] +fn a_generations_own_metadata_leaves_the_key_as_it_is() { + let changes: &[(&str, Change)] = &[ + ("capture time", |_, spec| spec.created_at.0 += 3600), + ("OpenVMM version", |_, spec| { + spec.openvmm_version = "1.2.3".to_owned(); + }), + ("saved state length", |_, spec| spec.state_size *= 2), + ("TSC anchor", |_, spec| spec.time().capture_tsc += 1), + ("UTC anchor", |_, spec| spec.time().capture_utc_ns += 1), + ("monotonic anchor", |_, spec| { + spec.time().capture_monotonic_ns += 1 + }), + ("host clock", |_, spec| { + spec.time().host_clock = "windows-interrupt-time".to_owned(); + }), + ("host ID", |_, spec| spec.time().host_id = vec![9; 16]), + ("boot ID", |_, spec| spec.time().boot_id = vec![9; 16]), + ("lineage counter", |_, spec| spec.time().lineage += 1), + ("cut to the anchor", |_, spec| { + spec.time().cut_to_anchor_ns = None + }), + ("a cut of 0", |_, spec| { + spec.time().cut_to_anchor_ns = Some(0) + }), + ("attachments", |_, spec| spec.contract().attachments.clear()), + ("state units", |_, spec| { + spec.contract().state_units.push("extra".to_owned()); + }), + ("CPU profile document", |_, spec| { + spec.cpu().document = b"another document".to_vec(); + }), + ("capture CPU signature", |_, spec| spec.cpu().signature += 1), + // The class requests a profile, which the key names as OpenVMM selected it. + ("requested CPU profile", |class, _| { + class.cpu_profile = "intel.icelake-sp.v1".to_owned(); + }), + ]; + let base = network_key(); + for (part, change) in changes { + assert_eq!( + identity_after(*change), + base.identity(), + "a change of the {part} changes the key" + ); + } +} + +#[test] +fn a_key_holds_what_its_capture_made() { + let made = network_key(); + let class = network_class(); + assert_eq!(made.runtime(), &class.runtime); + assert_eq!(made.guest_build(), &class.guest_build); + assert_eq!(made.hypervisor(), Hypervisor::Kvm); + assert_eq!(made.cpu_profile(), "intel.icelake-sp.v1"); + assert_eq!((made.processors(), made.boot_processors()), (2, 0)); + assert_eq!( + ( + made.memory(), + made.memory_capacity(), + made.memory_block_size() + ), + (512 * MIB, 2048 * MIB, 128 * MIB) + ); + assert_eq!( + made.command_line(), + ManifestSpec::with_network().contract().command_line + ); + assert_eq!( + (made.distro(), made.scratch()), + (class.distro, class.scratch) + ); + assert!(made.has_network()); + assert!(!key(&support::class(), &ManifestSpec::template()).has_network()); + assert_eq!(made.filesystem_tag(), "microvm"); + assert_eq!(made.invariants(), &Invariants::new()); +} + +/// Asserts that the capture of `class` whose manifest is `spec` does not match its class, with +/// a message that holds `expected`. +fn assert_mismatched(class: &CaptureClass, spec: &ManifestSpec, expected: &str) { + let error = TemplateKey::new(class, &manifest(spec)).unwrap_err(); + assert_eq!(error.code(), ErrorCode::Io, "{error}"); + assert!( + error + .message() + .starts_with("the capture does not match its class: ") + && error.message().contains(expected), + "{:?} lacks {expected:?}", + error.message() + ); +} + +#[test] +fn a_capture_that_does_not_match_its_class_is_refused() { + let (template, with_network) = (ManifestSpec::template(), ManifestSpec::with_network()); + let base = support::class(); + let mut class = base.clone(); + class.hypervisor = Hypervisor::Mshv; + assert_mismatched( + &class, + &template, + "its backend is kvm, but the class requests mshv", + ); + for requested in ["amd.genoa.v1", "intel.icelake-sp.v2", "host"] { + class = base.clone(); + class.cpu_profile = requested.to_owned(); + assert_mismatched( + &class, + &template, + &format!("its CPU profile is intel.icelake-sp.v1, but the class requests {requested}"), + ); + } + class = base.clone(); + class.processors = 4; + class.boot_processors = 4; + assert_mismatched( + &class, + &template, + "it has 2 processors, 2 online at boot, but the class requests 4 processors, 4 online at \ + boot", + ); + // A capture whose processors are all online at boot does not serve a class that keeps some + // offline. + class = base.clone(); + class.boot_processors = 1; + assert_mismatched( + &class, + &template, + "it has 2 processors, 2 online at boot, but the class requests 2 processors, 1 online", + ); + let mut spec = template.clone(); + spec.contract().boot_online_vp_count = 1; + assert_mismatched( + &base, + &spec, + "it has 2 processors, 1 online at boot, but the class requests 2 processors, 2 online", + ); + assert!(TemplateKey::new(&class, &manifest(&spec)).is_ok()); + class = base.clone(); + class.memory = 1024 * MIB; + assert_mismatched( + &class, + &template, + "it has 536870912 bytes of RAM, of a capacity of 2147483648, but the class requests \ + 1073741824 bytes of RAM", + ); + class = base.clone(); + class.memory_capacity = 4096 * MIB; + assert_mismatched(&class, &template, "of a capacity of 4294967296"); + class = base.clone(); + class.distro.length *= 2; + assert_mismatched(&class, &template, "its distro geometry is"); + class = base.clone(); + class.scratch.physical_block_size = 4096; + assert_mismatched(&class, &template, "its scratch geometry is"); + assert_mismatched( + &base, + &with_network, + "its NIC network is 10.0.0.2/24, but the class requests none", + ); + assert_mismatched( + &network_class(), + &template, + "its NIC network is none, but the class requests 10.0.0.2/24", + ); + // A network whose policy lets nothing through attaches no NIC. + class = base.clone(); + class.network = + Some(SandboxNetwork::new("10.0.0.2/24", NetworkPolicy::new(Access::Deny)).unwrap()); + assert_mismatched(&class, &with_network, "but the class requests none"); + assert_eq!(key(&class, &template), key(&base, &template)); + class.network = + Some(SandboxNetwork::new("10.0.0.3/24", NetworkPolicy::new(Access::Allow)).unwrap()); + assert_mismatched( + &class, + &with_network, + "its NIC network is 10.0.0.2/24, but the class requests 10.0.0.3/24", + ); +} + +#[test] +fn a_class_selects_a_profile_as_openvmm_does() { + let mut spec = ManifestSpec::template(); + spec.cpu().id = "intel.host.v1".to_owned(); + let mut class = support::class(); + // `auto` falls back to a profile that the host derives when no built-in profile serves the + // host's CPU. + for requested in ["auto", "host", "intel.host.v1"] { + class.cpu_profile = requested.to_owned(); + assert_eq!(key(&class, &spec).cpu_profile(), "intel.host.v1"); + } + class.cpu_profile = "intel.icelake-sp.v1".to_owned(); + assert_mismatched(&class, &spec, "its CPU profile is intel.host.v1"); +} + +#[test] +fn invalid_classes_are_refused() { + type ClassChange = fn(&mut CaptureClass); + let cases: &[(ClassChange, &str)] = &[ + ( + |class| class.runtime.control_contract = "two words".to_owned(), + "control contract revision \"two words\" must hold 1 to 128 printable ASCII \ + characters and no space", + ), + ( + |class| class.cpu_profile = "fastest".to_owned(), + "CPU profile \"fastest\" must be \"auto\", \"host\", or a profile ID", + ), + ( + |class| class.processors = 3, + "a capture of 3 processors with 2 online at boot needs 1, 2, 4, or 8 of each", + ), + ( + |class| class.boot_processors = 0, + "a capture of 2 processors with 0 online at boot", + ), + ( + |class| class.boot_processors = 4, + "a capture of 2 processors with 4 online at boot", + ), + ( + |class| class.memory = 0, + "a capture of 0 bytes of RAM needs a nonzero RAM", + ), + ( + |class| class.memory_capacity = class.memory - 1, + "needs a nonzero RAM and a capacity of at least it", + ), + ( + |class| class.distro.logical_block_size = 256, + "the distro device of a capture is invalid: its logical block size 256 is not a \ + power of two of at least 512", + ), + ( + |class| class.scratch.length = 0, + "the scratch device of a capture is invalid: its length 0", + ), + ]; + for (change, expected) in cases { + let mut class = network_class(); + change(&mut class); + for error in [ + class.check().unwrap_err(), + class.to_bytes().unwrap_err(), + class.identity().unwrap_err(), + TemplateKey::new(&class, &manifest(&ManifestSpec::with_network())).unwrap_err(), + ] { + assert_eq!(error.code(), ErrorCode::Unsupported, "{error}"); + assert!( + error.message().contains(expected), + "{:?} lacks {expected:?}", + error.message() + ); + } + } +} + +#[test] +fn each_field_of_a_class_changes_its_identity() { + type ClassChange = fn(&mut CaptureClass); + let changes: &[ClassChange] = &[ + |class| class.runtime.openvmm = artifact('d'), + |class| class.runtime.kernel = artifact('d'), + |class| class.runtime.initramfs = artifact('d'), + |class| class.runtime.boot_contract += 1, + |class| class.runtime.control_contract = "other".to_owned(), + |class| class.runtime.control_session += 1, + |class| class.guest_build = artifact('e'), + |class| class.hypervisor = Hypervisor::Whp, + |class| class.cpu_profile = "host".to_owned(), + |class| class.processors = 4, + |class| class.boot_processors = 1, + |class| class.memory = 1024 * MIB, + |class| class.memory_capacity = 4096 * MIB, + |class| class.distro.length *= 2, + |class| class.distro.physical_block_size = 512, + |class| class.scratch.length *= 2, + |class| { + class.scratch.logical_block_size = 4096; + class.scratch.physical_block_size = 4096; + }, + |class| class.network = None, + |class| { + let policy = + NetworkPolicy::new(Access::Allow).with_deny(EgressRule::to("192.0.2.0/24")); + class.network = Some(SandboxNetwork::new("10.0.0.2/24", policy).unwrap()); + }, + |class| { + class.invariants = Invariants::new() + .with("isolation.seccomp", "strict") + .unwrap(); + }, + ]; + let base = network_class().identity().unwrap(); + let mut identities = HashSet::from([base]); + for (index, change) in changes.iter().enumerate() { + let mut class = network_class(); + change(&mut class); + assert!( + identities.insert(class.identity().unwrap()), + "change {index} leaves the class's identity as it is" + ); + } + // A network that attaches no NIC is no network. + let mut denied = network_class(); + denied.network = + Some(SandboxNetwork::new("10.0.0.2/24", NetworkPolicy::new(Access::Deny)).unwrap()); + let mut none = network_class(); + none.network = None; + assert_eq!(denied.identity().unwrap(), none.identity().unwrap()); + // The identity digests the canonical encoding after the class's domain. + let class = network_class(); + let mut digest = Sha256::new(); + digest.update(b"nvx capture class\0"); + digest.update(class.to_bytes().unwrap()); + assert_eq!( + class.identity().unwrap().as_bytes(), + &<[u8; 32]>::from(digest.finalize()) + ); +} + +#[test] +fn keys_read_back_from_their_canonical_encoding() { + for made in [ + network_key(), + key(&support::class(), &ManifestSpec::template()), + ] { + let bytes = made.to_bytes(); + assert_eq!(&bytes[..2], &[1, 0]); + let read = TemplateKey::from_bytes(&bytes).unwrap(); + assert_eq!(read, made); + assert_eq!(read.to_bytes(), bytes); + let mut digest = Sha256::new(); + digest.update(b"nvx template key\0"); + digest.update(&bytes); + assert_eq!( + made.identity().as_bytes(), + &<[u8; 32]>::from(digest.finalize()) + ); + } +} + +/// Returns the message of the refusal of the key bytes `bytes`, which must have the code `code`. +fn key_refusal(bytes: &[u8], code: ErrorCode) -> String { + let error = TemplateKey::from_bytes(bytes).unwrap_err(); + assert_eq!(error.code(), code, "{error}"); + error.message().to_owned() +} + +#[test] +fn other_encodings_of_a_key_are_refused() { + let key = network_key(); + let bytes = key.to_bytes(); + for version in [0u16, 2, u16::MAX] { + let mut other = bytes.clone(); + other[..2].copy_from_slice(&version.to_le_bytes()); + assert_eq!( + key_refusal(&other, ErrorCode::FormatUnsupported), + format!( + "the template key has encoding version {version}; this build reads version 1 \ + only" + ) + ); + } + let mut longer = bytes.clone(); + longer.push(0); + assert_eq!( + key_refusal(&longer, ErrorCode::Io), + "the template key is malformed: a byte follows the value" + ); + for len in 0..bytes.len() { + key_refusal(&bytes[..len], ErrorCode::Io); + } + assert_eq!( + key_refusal(&[], ErrorCode::Io), + "the template key is malformed: the version is truncated" + ); + let oversized = vec![0; TemplateKey::MAX_LEN + 1]; + assert!(key_refusal(&oversized, ErrorCode::Io).contains("is longer than the")); +} + +#[test] +fn keys_of_machines_that_no_manifest_describes_are_refused() { + type KeyChange = fn(&mut TemplateKey); + let cases: &[(KeyChange, &str)] = &[ + ( + |key| key.machine.command_line.push_str(" quiet"), + "the machine has an effective command line whose digest does not match it", + ), + ( + |key| key.machine.devices.clear(), + "the machine has 0 devices, not 1 to 64", + ), + ( + |key| key.machine.cpu.effective_cpuid.pop().map(drop).unwrap(), + "the machine has an effective CPUID of 87 bytes", + ), + ( + |key| key.machine.processors = 3, + "the machine has 3 processors", + ), + ( + |key| key.machine.distro.logical_block_size = 256, + "the machine has a distro block whose logical block size 256", + ), + ( + |key| key.machine.filesystem_slot = 0, + "the filesystem slot is not the dormant slot of version 1, with a tag", + ), + ( + |key| key.filesystem_tag.clear(), + "the filesystem slot is not the dormant slot of version 1, with a tag", + ), + ( + |key| key.machine.cpu.revision = 0, + "the machine has the invalid CPU profile vendor", + ), + ]; + for (change, expected) in cases { + let mut changed = network_key(); + change(&mut changed); + let message = key_refusal(&changed.to_bytes(), ErrorCode::Io); + assert!( + message.starts_with(&format!("the template key is malformed: {expected}")), + "{message:?} lacks {expected:?}" + ); + } + // Invariant settings out of the order of their names, which a key never writes. + let mut class = network_class(); + class.invariants = Invariants::new() + .with("a", "1") + .unwrap() + .with("b", "2") + .unwrap(); + let mut bytes = key(&class, &ManifestSpec::with_network()).to_bytes(); + let len = bytes.len(); + 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); + assert_eq!( + key_refusal(&bytes, ErrorCode::Io), + "the template key is malformed: the invariant settings are not in the strict order of \ + their names" + ); + // An artifact identity that a runtime identity refuses. + let mut bytes = network_key().to_bytes(); + bytes[2 + 4 + "sha256:".len()] = b' '; + assert!( + key_refusal(&bytes, ErrorCode::Io).contains("artifact identity"), + "{bytes:?}" + ); +} + +#[test] +fn invariant_settings_have_one_form() { + let settings = Invariants::new() + .with("isolation.seccomp", "strict") + .unwrap() + .with("limits.pids", "512") + .unwrap() + .with("isolation.seccomp", "relaxed") + .unwrap(); + assert_eq!(settings.len(), 2); + assert!(!settings.is_empty()); + assert_eq!(settings.get("isolation.seccomp"), Some("relaxed")); + assert_eq!(settings.get("limits.cpu"), None); + assert_eq!( + settings.iter().collect::>(), + [("isolation.seccomp", "relaxed"), ("limits.pids", "512")] + ); + let long_name = "a".repeat(Invariants::MAX_NAME_LEN + 1); + let long_value = "v".repeat(Invariants::MAX_VALUE_LEN + 1); + for (name, value) in [ + ("", "value"), + ("Isolation", "value"), + ("1st", "value"), + ("with space", "value"), + (long_name.as_str(), "value"), + ("name", long_value.as_str()), + ("name", "line\nbreak"), + ("name", "tab\there"), + ("name", "é"), + ] { + let error = Invariants::new().with(name, value).unwrap_err(); + assert_eq!(error.code(), ErrorCode::Unsupported, "{name:?} {value:?}"); + } + assert!(Invariants::new().with("name", "").is_ok()); + let mut full = Invariants::new(); + for index in 0..Invariants::MAX_SETTINGS { + full = full.with(&format!("setting{index}"), "1").unwrap(); + } + assert!(full.clone().with("setting0", "2").is_ok()); + assert_eq!( + full.with("another", "1").unwrap_err().message(), + "a template holds at most 64 invariant settings" + ); +} + +#[test] +fn a_key_identity_is_the_identity_that_names_a_clones_template() { + let identity = network_key().identity(); + let named = activation::Identity::from(identity); + assert_eq!(named.as_bytes(), identity.as_bytes()); + assert_eq!(named.to_string(), identity.to_string()); + assert_eq!(KeyIdentity::from(named), identity); + // A clone's activation record names the template by its generation and this identity. + let reference = activation::TemplateReference { + generation: activation::StoreGeneration::new(std::num::NonZeroU64::MIN), + key: identity.into(), + }; + assert_eq!(KeyIdentity::from(reference.key), identity); +} + +#[test] +fn identities_have_a_text_form() { + let identity = network_key().identity(); + let text = identity.to_string(); + assert_eq!(text.len(), 64); + assert!( + text.bytes() + .all(|byte| byte.is_ascii_digit() || byte.is_ascii_lowercase()) + ); + assert_eq!(text.parse::().unwrap(), identity); + assert_eq!( + text.to_uppercase().parse::().unwrap(), + identity + ); + assert_eq!(format!("{identity:?}"), format!("KeyIdentity({text})")); + assert_eq!(KeyIdentity::from_bytes(*identity.as_bytes()), identity); + let invalid = [ + text[1..].to_owned(), + format!("{text}0"), + format!("g{}", &text[1..]), + String::new(), + "é".to_owned(), + ]; + for invalid in &invalid { + let error = invalid.parse::().unwrap_err(); + assert_eq!(error.code(), ErrorCode::Unsupported); + assert_eq!( + error.message(), + format!("{invalid:?} is not a template key's identity, which is 64 hexadecimal digits") + ); + } + let class = network_class().identity().unwrap(); + assert_eq!(class.to_string().parse::().unwrap(), class); + assert!( + "x".parse::() + .unwrap_err() + .message() + .contains("is not a capture class's identity") + ); +} + +/// The conformance vectors of version 1 of the canonical encodings of keys and classes. +const VECTORS: &str = include_str!("../../../../tests/vectors/template-key-v1.txt"); + +/// The versions of the contracts that the vectors hold. +const VECTOR_CONTRACTS: Contracts = Contracts { + snapshot: 6, + device_state: 1, + time: 1, + restore_packet: 4, + activation: 1, +}; + +/// Returns each vector of `kind`: its name, its bytes, and its values. +fn vectors(kind: &str) -> Vec<(&'static str, Vec, Vec<&'static str>)> { + VECTORS + .lines() + .filter(|line| !line.is_empty() && !line.starts_with('#')) + .filter_map(|line| { + let mut fields = line.split('|'); + (fields.next() == Some(kind)).then(|| { + let name = fields.next().expect("a vector has a name"); + let hex = fields.next().expect("a vector has bytes"); + let bytes = (0..hex.len()) + .step_by(2) + .map(|index| u8::from_str_radix(&hex[index..index + 2], 16).unwrap()) + .collect(); + (name, bytes, fields.collect()) + }) + }) + .collect() +} + +/// Returns the class of the capture that the vectors call `name`, with the runtime that they +/// hold. +fn vector_class(name: &str) -> CaptureClass { + let mut class = if name == "NETWORK" { + let class = network_class(); + CaptureClass { + invariants: Invariants::new() + .with("isolation.seccomp", "strict") + .unwrap() + .with("limits.pids", "512") + .unwrap(), + ..class + } + } else { + support::class() + }; + class.runtime = RuntimeIdentity { + openvmm: artifact('a'), + kernel: artifact('b'), + initramfs: artifact('c'), + boot_contract: 3, + control_contract: "nvx-microvm-v2-control-v2".to_owned(), + control_session: 1, + }; + class +} + +/// Returns the manifest of the capture that the vectors call `name`. +fn vector_spec(name: &str) -> ManifestSpec { + match name { + "TEMPLATE" => ManifestSpec::template(), + "NETWORK" => ManifestSpec::with_network(), + "HOST_PROFILE" => { + let mut spec = ManifestSpec::template(); + spec.cpu().id = "intel.host.v1".to_owned(); + spec + } + _ => panic!("no capture is named {name}"), + } +} + +#[test] +fn the_conformance_vectors_decode_and_encode() { + let keys = vectors("KEY"); + let names: Vec<&str> = keys.iter().map(|(name, ..)| *name).collect(); + assert_eq!(names, ["TEMPLATE", "NETWORK", "HOST_PROFILE", "MINIMAL"]); + for (name, bytes, values) in keys { + let key = TemplateKey::from_bytes(&bytes).unwrap_or_else(|error| panic!("{name}: {error}")); + assert_eq!(key.to_bytes(), bytes, "{name}"); + assert_eq!(values, [key.identity().to_string()], "{name}"); + if name != "MINIMAL" { + // The key that this crate derives from the capture is the vector's, but for the + // versions of the contracts, which are this crate's. + let derived = TemplateKey::new(&vector_class(name), &manifest(&vector_spec(name))) + .unwrap_or_else(|error| panic!("{name}: {error}")); + let derived = TemplateKey { + contracts: VECTOR_CONTRACTS, + ..derived + }; + assert_eq!(derived, key, "{name}"); + } + } + let classes = vectors("CLASS"); + let names: Vec<&str> = classes.iter().map(|(name, ..)| *name).collect(); + assert_eq!(names, ["TEMPLATE", "NETWORK"]); + for (name, bytes, values) in classes { + let class = vector_class(name); + assert_eq!(class.encode(&VECTOR_CONTRACTS).unwrap(), bytes, "{name}"); + assert_eq!( + values, + [ClassIdentity(digest(CLASS_DOMAIN, &bytes)).to_string()], + "{name}" + ); + } + let versions = vectors("INVALID_KEY_VERSION"); + assert_eq!(versions.len(), 3); + for (name, bytes, _) in versions { + let error = TemplateKey::from_bytes(&bytes).expect_err(name); + assert_eq!( + error.code(), + ErrorCode::FormatUnsupported, + "{name}: {error}" + ); + } + let invalid = vectors("INVALID_KEY"); + assert_eq!(invalid.len(), 23); + for (name, bytes, _) in invalid { + let error = TemplateKey::from_bytes(&bytes).expect_err(name); + assert_eq!(error.code(), ErrorCode::Io, "{name}: {error}"); + } +} diff --git a/nvx-rs/src/host/template/manifest.rs b/nvx-rs/src/host/template/manifest.rs new file mode 100644 index 000000000..1388443e5 --- /dev/null +++ b/nvx-rs/src/host/template/manifest.rs @@ -0,0 +1,1825 @@ +//! The manifest of a template's capture, which [`TemplateManifest::read`] reads. +//! +//! OpenVMM writes `manifest.bin` into the directory of a snapshot that it publishes: one message +//! of the protobuf wire format, as its `mesh` encodes `openvmm.snapshot.SnapshotManifest`, of at +//! most [`MAX_MANIFEST_LEN`] bytes. It describes the machine that the snapshot restores, its time +//! contract and CPU profile, its sandbox blocks, and the lengths of its artifacts. Restore treats +//! it as authoritative, and so does a template's [compatibility key](super::TemplateKey), which +//! projects its machine. [`TemplateManifest`] describes what the reader refuses. + +use std::fs::{File, OpenOptions}; +use std::io::{self, Read}; +use std::net::{Ipv4Addr, Ipv6Addr}; +use std::path::Path; +use std::time::{Duration, SystemTime}; + +use sha2::{Digest, Sha256}; + +use super::wire::{Field, Message, WireError}; +use crate::contract; +use crate::error::{Error, ErrorCode, Result}; +use crate::host::filesystem; +use crate::host::process::BootId; +use crate::hypervisor::Hypervisor; + +/// Name of the manifest in a snapshot's directory. +pub const MANIFEST_NAME: &str = "manifest.bin"; + +/// Longest manifest that [`TemplateManifest::read`] reads, in bytes: 1 MiB, the limit of +/// OpenVMM's own reader. +pub const MAX_MANIFEST_LEN: u64 = 1024 * 1024; + +/// The magic that identifies a manifest of version 6 of OpenVMM's snapshot format. +const FORMAT_MAGIC: &[u8] = b"OPENVMM_SNAPSHOT_V6\0"; + +/// The type of the root of a snapshot's saved state. +const SAVED_STATE_ROOT_TYPE: &str = "openvmm.SavedState"; + +/// The only architecture of a microVM. +const ARCHITECTURE: &str = "x86_64"; + +/// The page size of a microVM, in bytes. +const PAGE_SIZE: u32 = 4096; + +/// The machine profile of a microVM. +const MACHINE_PROFILE: &str = "microvm"; + +/// Version of the boot layout of a microVM: OpenVMM's Linux-direct boot structures. +const BOOT_LAYOUT_VERSION: u32 = 2; + +/// Version of the dormant filesystem slot of a microVM. +pub(super) const FILESYSTEM_SLOT_VERSION: u32 = 1; + +/// Version of the restore-time memory expansion of a microVM. +const MEMORY_EXPANSION_VERSION: u32 = 1; + +/// The tier of a template's capture. +const TIER_PLATFORM: &str = "platform"; + +/// The restore policy of a template, which restores independent clones. +const RESTORE_POLICY_CLONE: &str = "clone"; + +/// The configuration sections that a platform capture consumed: its invariants only. +const CONSUMED_INVARIANTS: u32 = 1; + +/// Longest effective kernel command line of a microVM, in bytes, without its NUL. +pub(super) const MAX_COMMAND_LINE_LEN: usize = 2047; + +/// Most memory ranges, or memory expansion ranges, of a machine. +pub(super) const MAX_MEMORY_RANGES: usize = 128; + +/// Most devices of a machine. +pub(super) const MAX_DEVICES: usize = 64; + +/// Most address ranges of one device. +pub(super) const MAX_DEVICE_RANGES: usize = 16; + +/// Most feature banks, or queues, of one device. +pub(super) const MAX_DEVICE_WORDS: usize = 64; + +/// Longest text of a machine other than its command line, such as a device ID, in bytes. +pub(super) const MAX_TEXT_LEN: usize = 256; + +/// Most processors of a machine. +pub(super) const MAX_PROCESSORS: usize = 8; + +/// Most sandbox blocks of a machine: one of each role of OpenVMM. +const MAX_BLOCKS: usize = 4; + +/// Longest saved state of a snapshot, in bytes: 256 MiB, the limit of OpenVMM's restore. +const MAX_STATE_LEN: u64 = 256 * 1024 * 1024; + +/// Most names in a manifest's inventory of state units. +const MAX_STATE_UNITS: usize = 512; + +/// Most host attachments in a manifest. +const MAX_ATTACHMENTS: usize = 64; + +/// Longest identity of a host attachment, in bytes. +const MAX_ATTACHMENT_IDENTITY_LEN: usize = 4096; + +/// Bytes of one entry of a CPU profile's effective CPUID record. +pub(super) const CPUID_ENTRY_LEN: usize = 44; + +/// Most entries of a CPU profile's effective CPUID record. +pub(super) const MAX_CPUID_ENTRIES: usize = 1024; + +/// The processor counts of a microVM. +pub(super) const PROCESSOR_COUNTS: [u32; 4] = [1, 2, 4, 8]; + +/// Bytes of a SHA-256 digest. +const SHA256_LEN: usize = 32; + +/// Bytes of a host or boot identity. +const HOST_IDENTITY_LEN: usize = 16; + +/// Nanoseconds in a second. +const NANOS_PER_SECOND: i32 = 1_000_000_000; + +/// The host clock from which a capture took its monotonic anchor. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum HostClock { + /// Linux's `CLOCK_BOOTTIME`, `linux-boottime` in a manifest. + LinuxBoottime, + /// Windows' interrupt time, in nanoseconds, `windows-interrupt-time` in a manifest. + WindowsInterruptTime, +} + +impl HostClock { + /// Returns the clock's spelling in a manifest. + pub const fn as_str(self) -> &'static str { + match self { + Self::LinuxBoottime => "linux-boottime", + Self::WindowsInterruptTime => "windows-interrupt-time", + } + } + + /// Parses the spelling of a clock in a manifest. + fn parse(text: &str) -> Option { + match text { + "linux-boottime" => Some(Self::LinuxBoottime), + "windows-interrupt-time" => Some(Self::WindowsInterruptTime), + _ => None, + } + } +} + +/// 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. +/// +/// 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. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TimeContract { + /// The declared TSC rate of the snapshot's lineage, in Hz. + pub tsc_frequency_hz: u64, + /// The tolerance of the TSC rate of a restoring host, in parts per million. + pub tsc_tolerance_ppm: u32, + /// The rate of the LAPIC timer, in Hz. + pub apic_frequency_hz: u64, + /// The TSC of the first virtual processor at the capture anchor. + pub capture_tsc: u64, + /// The host's UTC at the capture anchor, in nanoseconds since the Unix epoch. + pub capture_utc_ns: u64, + /// The host's monotonic time at the capture anchor, in nanoseconds, on + /// [`host_clock`](Self::host_clock). + pub capture_monotonic_ns: u64, + /// The clock of [`capture_monotonic_ns`](Self::capture_monotonic_ns). + pub host_clock: HostClock, + /// The identity of the capturing host: its machine ID on Linux, and its machine GUID on + /// Windows. + pub host_id: [u8; 16], + /// The boot of the capturing host, which a [`BootId`] of the same host compares with. + pub boot_id: BootId, + /// The time lineage counter: the generation counter of the captured VM process, which each + /// restore of the snapshot increments. + pub lineage: u32, + /// The host's monotonic time from the cut of the saved VM time to the capture anchor, in + /// nanoseconds, if the capture recorded it. + pub cut_to_anchor_ns: Option, +} + +/// The CPU profile of a capture: its ID, `VENDOR.GENERATION.vREVISION`, the digest of its +/// canonical document, and the effective CPUID that the VM saw. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(super) struct CpuProfile { + /// The vendor, such as `amd` or `intel`. + pub(super) vendor: String, + /// The generation, such as `genoa`, or `host` for a profile that a host derived. + pub(super) generation: String, + /// The revision. + pub(super) revision: u32, + /// The SHA-256 digest of the profile's canonical document. + pub(super) digest: [u8; SHA256_LEN], + /// The effective CPUID, as OpenVMM encodes it: entries of [`CPUID_ENTRY_LEN`] bytes. + pub(super) effective_cpuid: Vec, +} + +impl CpuProfile { + /// Returns the profile's ID: `VENDOR.GENERATION.vREVISION`. + pub(super) fn id(&self) -> String { + format!("{}.{}.v{}", self.vendor, self.generation, self.revision) + } +} + +/// Returns whether `text` is one word of a CPU profile ID: 1 to 32 lowercase ASCII letters, +/// digits, or hyphens. +fn is_profile_word(text: &str) -> bool { + (1..=32).contains(&text.len()) + && text + .bytes() + .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') +} + +/// Parses a CPU profile ID, `VENDOR.GENERATION.vREVISION`, whose revision is a positive decimal +/// number without leading zeros, into its vendor, generation, and revision. +pub(super) fn parse_profile_id(id: &str) -> Option<(String, String, u32)> { + let mut words = id.split('.'); + let (vendor, generation, revision) = (words.next()?, words.next()?, words.next()?); + let digits = revision.strip_prefix('v')?; + if words.next().is_some() + || !is_profile_word(vendor) + || !is_profile_word(generation) + || digits.is_empty() + || !digits.bytes().all(|byte| byte.is_ascii_digit()) + || digits.starts_with('0') + { + return None; + } + Some(( + vendor.to_owned(), + generation.to_owned(), + digits.parse().ok()?, + )) +} + +/// The declared rates of a capture's time contract. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(super) struct Rates { + /// The declared TSC rate, in Hz. + pub(super) tsc_hz: u64, + /// The rate of the LAPIC timer, in Hz. + pub(super) apic_hz: u64, + /// The rate policy: the tolerance of a restoring host's TSC rate, in parts per million. + pub(super) tolerance_ppm: u32, +} + +/// The processor topology of a machine. +#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)] +pub(super) struct Topology { + /// Sockets. + pub(super) sockets: u32, + /// Dies in each socket. + pub(super) dies_per_socket: u32, + /// Cores in each die. + pub(super) cores_per_die: u32, + /// Threads in each core. + pub(super) threads_per_core: u32, + /// The APIC ID of each processor. + pub(super) apic_ids: Vec, +} + +/// A range of guest-physical memory. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(super) struct MemoryRange { + /// Its first guest-physical address. + pub(super) start: u64, + /// Its length, in bytes. + pub(super) length: u64, +} + +/// Returns the total length of `ranges`, unless a range is empty or ends beyond 2^64, or their +/// total does. +fn total_length(ranges: &[MemoryRange]) -> Option { + ranges.iter().try_fold(0u64, |total, range| { + if range.length == 0 { + return None; + } + range.start.checked_add(range.length)?; + total.checked_add(range.length) + }) +} + +/// The memory of a machine: the RAM that it boots with, and the capacity to which a restore may +/// expand it. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(super) struct Memory { + /// The RAM at boot, in bytes. + pub(super) boot: u64, + /// The RAM capacity, in bytes. + pub(super) capacity: u64, + /// The size of the blocks in which a restore expands RAM, in bytes. + pub(super) block_size: u64, + /// The ranges of the RAM at boot, in the order in which the memory file holds them. + pub(super) ranges: Vec, + /// The ranges that a restore may add, in their order. + pub(super) expansion: Vec, +} + +impl Memory { + /// Explains why the memory is invalid, if it is. + fn problem(&self) -> Option { + if self.ranges.is_empty() + || self.ranges.len() > MAX_MEMORY_RANGES + || total_length(&self.ranges) != Some(self.boot) + || self.boot == 0 + { + return Some(format!( + "has {} bytes of RAM in {} ranges that do not hold it", + self.boot, + self.ranges.len() + )); + } + if self.block_size == 0 + || self.capacity < self.boot + || !self.boot.is_multiple_of(self.block_size) + || !self.capacity.is_multiple_of(self.block_size) + { + return Some(format!( + "has a RAM capacity of {} bytes in blocks of {} bytes for {} bytes of RAM", + self.capacity, self.block_size, self.boot + )); + } + if self.expansion.len() > MAX_MEMORY_RANGES + || total_length(&self.expansion) != Some(self.capacity - self.boot) + { + return Some( + "has memory expansion ranges that do not span its RAM capacity".to_owned(), + ); + } + None + } +} + +/// The address space of a device's range. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(super) enum AddressSpace { + /// Memory-mapped I/O, `mmio`. + Mmio, + /// Port I/O, `pmio`. + Pmio, +} + +impl AddressSpace { + /// Returns the space's spelling in a manifest. + pub(super) const fn as_str(self) -> &'static str { + match self { + Self::Mmio => "mmio", + Self::Pmio => "pmio", + } + } + + /// Parses the spelling of a space in a manifest. + pub(super) fn parse(text: &str) -> Option { + match text { + "mmio" => Some(Self::Mmio), + "pmio" => Some(Self::Pmio), + _ => None, + } + } +} + +/// An address range of a device. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(super) struct DeviceRange { + /// Its address space. + pub(super) space: AddressSpace, + /// Its first address. + pub(super) start: u64, + /// Its length. + pub(super) length: u64, +} + +/// A guest-visible device of a machine, in the order of the machine's devices. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(super) struct Device { + /// Its stable ID, unique in the machine. + pub(super) stable_id: String, + /// Its kind, such as `virtio-blk`. + pub(super) kind: String, + /// The name of the unit of saved state that holds its state. + pub(super) state_unit: String, + /// Its transport, such as `virtio-mmio`, or empty for a platform device. + pub(super) transport: String, + /// Its address ranges. + pub(super) ranges: Vec, + /// Its interrupt, if it has one. + pub(super) irq: Option, + /// Its feature banks, 32 bits each. + pub(super) feature_banks: Vec, + /// Its queue count. + pub(super) queue_count: u32, + /// The largest size of each of its queues. + pub(super) queue_max_sizes: Vec, +} + +impl Device { + /// Explains why the device is invalid, if it is. + fn problem(&self) -> Option { + let texts = [&self.stable_id, &self.kind, &self.state_unit]; + if texts + .into_iter() + .chain([&self.transport]) + .any(|text| text.len() > MAX_TEXT_LEN) + || texts.into_iter().any(String::is_empty) + { + return Some(format!( + "has a device without an ID, a kind, or a state unit, or with one of more than \ + {MAX_TEXT_LEN} bytes: {:?}", + self.stable_id + )); + } + if self.ranges.len() > MAX_DEVICE_RANGES + || self.feature_banks.len() > MAX_DEVICE_WORDS + || self.queue_max_sizes.len() > MAX_DEVICE_WORDS + { + return Some(format!( + "has the device {:?} with more than {MAX_DEVICE_RANGES} ranges, or more than \ + {MAX_DEVICE_WORDS} feature banks or queues", + self.stable_id + )); + } + None + } +} + +/// How a machine's virtio devices deliver their interrupts. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(super) struct Interrupts { + /// The delivery mode, such as `edge-shared-status`. + pub(super) mode: String, + /// The guest-physical address of the shared interrupt-status page, or 0 without one. + pub(super) status_page_start: u64, + /// The length of the shared interrupt-status page, or 0 without one. + pub(super) status_page_length: u64, +} + +/// The length and block sizes of a sandbox block device. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct BlockGeometry { + /// The length that the device presents, in bytes: nonzero, and a multiple of the logical + /// block size. + pub length: u64, + /// The logical block size, in bytes: a power of two of at least 512. + pub logical_block_size: u32, + /// The physical block size, in bytes: a power of two of at least the logical block size. + pub physical_block_size: u32, +} + +impl BlockGeometry { + /// Explains why the geometry is invalid, if it is, as a phrase whose subject is a size, such + /// as "length 1000 is not a nonzero multiple of its logical block size". + pub(super) fn problem(&self) -> Option { + if self.logical_block_size < 512 || !self.logical_block_size.is_power_of_two() { + Some(format!( + "logical block size {} is not a power of two of at least 512", + self.logical_block_size + )) + } else if self.physical_block_size < self.logical_block_size + || !self.physical_block_size.is_power_of_two() + { + Some(format!( + "physical block size {} is not a power of two of at least its logical block size", + self.physical_block_size + )) + } else if self.length == 0 + || !self + .length + .is_multiple_of(u64::from(self.logical_block_size)) + { + Some(format!( + "length {} is not a nonzero multiple of its logical block size", + self.length + )) + } else { + None + } + } +} + +/// The IPv6 identity of a machine's fixed NIC. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(super) struct Ipv6Identity { + /// The guest's address. + pub(super) address: Ipv6Addr, + /// The prefix length of the guest's network. + pub(super) prefix_length: u8, + /// The gateway's address. + pub(super) gateway: Ipv6Addr, +} + +/// The identity of the network policy that OpenVMM enforces on a machine's fixed NIC. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(super) struct PolicyIdentity { + /// The policy's mode, as OpenVMM names it. + pub(super) mode: String, + /// The SHA-256 digest of the policy's canonical encoding, which OpenVMM computed. + pub(super) digest: [u8; SHA256_LEN], + /// Whether the policy is enforced. + pub(super) required: bool, + /// The version of the policy's canonical encoding, which 0 records for version 1. + pub(super) encoding_version: u32, +} + +/// The fixed NIC of a machine, and the network policy that OpenVMM enforces on it. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(super) struct Nic { + /// Its network profile, such as `portable`. + pub(super) profile: String, + /// The guest's MAC address. + pub(super) guest_mac: [u8; 6], + /// The gateway's MAC address. + pub(super) gateway_mac: [u8; 6], + /// The guest's IPv4 address. + pub(super) ipv4: Ipv4Addr, + /// The prefix length of the guest's IPv4 network. + pub(super) prefix_length: u8, + /// The gateway's IPv4 address. + pub(super) gateway: Ipv4Addr, + /// The guest's IPv6 identity, if it has one. + pub(super) ipv6: Option, + /// The network policy. + pub(super) policy: PolicyIdentity, +} + +impl Nic { + /// Explains why the NIC is invalid, if it is. + fn problem(&self) -> Option { + if [&self.profile, &self.policy.mode] + .into_iter() + .any(|text| text.is_empty() || text.len() > MAX_TEXT_LEN) + { + return Some("has a NIC without a network profile or a policy mode".to_owned()); + } + if !(1..=30).contains(&self.prefix_length) + || self + .ipv6 + .is_some_and(|ipv6| !(1..=128).contains(&ipv6.prefix_length)) + { + return Some("has a NIC with an invalid prefix length".to_owned()); + } + None + } +} + +/// The machine of a template's capture, as its manifest describes it: what its key projects. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct Machine { + /// The backend that captured it. + pub(super) hypervisor: Hypervisor, + /// The CPU profile. + pub(super) cpu: CpuProfile, + /// The processor topology. + pub(super) topology: Topology, + /// The declared rates of the time contract. + pub(super) rates: Rates, + /// The processor capacity. + pub(super) processors: u32, + /// The processors online at boot, or 0 when a restore activates none. + pub(super) boot_processors: u32, + /// The memory. + pub(super) memory: Memory, + /// The effective kernel command line. + pub(super) command_line: String, + /// The SHA-256 digest of the effective kernel command line. + pub(super) command_line_digest: [u8; SHA256_LEN], + /// The read-only `distro` block device. + pub(super) distro: BlockGeometry, + /// The scratch block device. + pub(super) scratch: BlockGeometry, + /// The interrupt delivery of the virtio devices. + pub(super) interrupts: Interrupts, + /// The devices, in their order. + pub(super) devices: Vec, + /// The version of the dormant filesystem slot. + pub(super) filesystem_slot: u32, + /// The fixed NIC, if the machine has one. + pub(super) nic: Option, +} + +impl Machine { + /// Explains why the machine is invalid, if it is, as a phrase whose subject is the machine. + /// + /// Both a manifest and a key's canonical encoding must describe a valid machine. + pub(super) fn problem(&self) -> Option { + let cpu = &self.cpu; + if !is_profile_word(&cpu.vendor) || !is_profile_word(&cpu.generation) || cpu.revision == 0 { + return Some(format!( + "has the invalid CPU profile vendor {:?}, generation {:?}, and revision {}", + cpu.vendor, cpu.generation, cpu.revision + )); + } + if cpu.effective_cpuid.is_empty() + || !cpu.effective_cpuid.len().is_multiple_of(CPUID_ENTRY_LEN) + || cpu.effective_cpuid.len() > MAX_CPUID_ENTRIES * CPUID_ENTRY_LEN + { + return Some(format!( + "has an effective CPUID of {} bytes, not 1 to {MAX_CPUID_ENTRIES} entries of \ + {CPUID_ENTRY_LEN} bytes", + cpu.effective_cpuid.len() + )); + } + if !PROCESSOR_COUNTS.contains(&self.processors) + || self.topology.apic_ids.len() != self.processors as usize + || self.boot_processors > self.processors + || (self.boot_processors != 0 && !PROCESSOR_COUNTS.contains(&self.boot_processors)) + { + return Some(format!( + "has {} processors, {} APIC IDs, and {} processors online at boot, which a \ + microVM cannot have", + self.processors, + self.topology.apic_ids.len(), + self.boot_processors + )); + } + if self.rates.tsc_hz == 0 || self.rates.apic_hz == 0 { + return Some("declares no TSC or LAPIC timer rate".to_owned()); + } + if let Some(problem) = self.memory.problem() { + return Some(problem); + } + if self.command_line.len() > MAX_COMMAND_LINE_LEN { + return Some(format!( + "has an effective command line of {} bytes, more than the {MAX_COMMAND_LINE_LEN} \ + that a microVM's command line holds", + self.command_line.len() + )); + } + if self.command_line_digest != <[u8; SHA256_LEN]>::from(Sha256::digest(&self.command_line)) + { + return Some("has an effective command line whose digest does not match it".to_owned()); + } + for (role, geometry) in [("distro", self.distro), ("scratch", self.scratch)] { + if let Some(problem) = geometry.problem() { + return Some(format!("has a {role} block whose {problem}")); + } + } + if self.interrupts.mode.is_empty() || self.interrupts.mode.len() > MAX_TEXT_LEN { + return Some("names no interrupt delivery mode".to_owned()); + } + if self.devices.is_empty() || self.devices.len() > MAX_DEVICES { + return Some(format!( + "has {} devices, not 1 to {MAX_DEVICES}", + self.devices.len() + )); + } + for (index, device) in self.devices.iter().enumerate() { + if let Some(problem) = device.problem() { + return Some(problem); + } + if self.devices[..index] + .iter() + .any(|other| other.stable_id == device.stable_id) + { + return Some(format!( + "has the device ID {:?} more than once", + device.stable_id + )); + } + } + self.nic.as_ref().and_then(Nic::problem) + } +} + +/// The manifest of a template's capture, which OpenVMM wrote as `manifest.bin` in the snapshot's +/// directory: the machine that its clones restore, and what the capture recorded of itself. +/// +/// [`read`](Self::read) reads it, bounded and strict: it reads at most [`MAX_MANIFEST_LEN`] bytes +/// of a manifest that is a regular file, not a link or other reparse point, and refuses a manifest +/// of any other format version than +/// [`SNAPSHOT_FORMAT_VERSION`](crate::contract::SNAPSHOT_FORMAT_VERSION), a field that this +/// format does not have, a field that is not of its type or that appears twice, truncated bytes, +/// and a manifest that is not a template's: a platform-tier capture of a microVM, for clones, with +/// a read-only `distro` whose identity is unbound, a fresh scratch, and the dormant filesystem +/// slot. It never reads another artifact of the snapshot. +/// +/// A template's [`TemplateKey`](super::TemplateKey) projects its machine. The rest is the +/// capture's own: when it was taken, its [`TimeContract`]'s anchor, host, boot, and lineage +/// counter, and the lengths of its artifacts, in which the generations of one key differ. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TemplateManifest { + /// When OpenVMM created the snapshot. + created_at: SystemTime, + /// The version of the OpenVMM that captured it. + openvmm_version: String, + /// The length of the snapshot's saved state, `state.bin`, in bytes. + state_len: u64, + /// The time contract. + time: TimeContract, + /// The machine. + pub(super) machine: Machine, +} + +impl TemplateManifest { + /// Reads the manifest of the snapshot in the directory `snapshot`: its [`MANIFEST_NAME`]. + /// + /// The file is opened without following a link, or on Windows any other reparse point, and + /// must be a regular file of at most [`MAX_MANIFEST_LEN`] bytes, which it reads without + /// reading more. It then checks the manifest as [`parse`](Self::parse) does. The manifest is + /// named by its path in `snapshot`, so `snapshot` and the directories above it must be ones + /// that only the user, or an administrator, may change. + /// + /// Fails with [`ErrorKind::NotFound`](crate::ErrorKind::NotFound) if the manifest does not + /// exist, with [`ErrorKind::Io`](crate::ErrorKind::Io) if it cannot be read, or is not a + /// regular file of at most [`MAX_MANIFEST_LEN`] bytes, and otherwise as + /// [`parse`](Self::parse) does. + pub fn read(snapshot: &Path) -> Result { + let path = snapshot.join(MANIFEST_NAME); + let description = format!("the snapshot manifest {}", path.display()); + let bytes = read_bounded(&path).map_err(|error| { + let code = match error.kind() { + io::ErrorKind::NotFound => ErrorCode::NotFound, + _ => ErrorCode::Io, + }; + Error::new(code, format!("cannot read {description}")).with_source(error) + })?; + Self::parse_described(&bytes, &description) + } + + /// Parses and checks the bytes of a template's manifest. + /// + /// Fails with [`ErrorCode::FormatUnsupported`] for a + /// manifest of another version of the snapshot format, saved-state schema, machine ABI, boot + /// layout, or time ABI than this build reads, and with + /// [`ErrorKind::Io`](crate::ErrorKind::Io) for more than [`MAX_MANIFEST_LEN`] bytes, bytes + /// that are not such a manifest, and a manifest that is not a template's, as + /// [`TemplateManifest`] describes. + pub fn parse(bytes: &[u8]) -> Result { + Self::parse_described(bytes, "the snapshot manifest") + } + + /// Implements [`parse`](Self::parse), with messages that call the manifest `description`. + fn parse_described(bytes: &[u8], description: &str) -> Result { + if bytes.len() as u64 > MAX_MANIFEST_LEN { + return Err(Error::io(format!( + "{description} holds {} bytes, more than the {MAX_MANIFEST_LEN} that a manifest \ + may hold", + bytes.len() + ))); + } + check_format(bytes).map_err(|problem| problem.into_error(description))?; + let raw = decode_manifest(bytes) + .map_err(|problem| Error::io(format!("{description} is malformed: {problem}")))?; + Self::from_raw(raw).map_err(|problem| problem.into_error(description)) + } + + /// Returns when OpenVMM created the snapshot. + pub fn created_at(&self) -> SystemTime { + self.created_at + } + + /// Returns the version of the OpenVMM that captured the snapshot, as it names itself. + pub fn openvmm_version(&self) -> &str { + &self.openvmm_version + } + + /// Returns the backend that captured the snapshot. + pub fn hypervisor(&self) -> Hypervisor { + self.machine.hypervisor + } + + /// Returns the time contract. + pub fn time(&self) -> &TimeContract { + &self.time + } + + /// Returns the length of the snapshot's saved state, `state.bin`, in bytes. + pub fn state_len(&self) -> u64 { + self.state_len + } + + /// Returns the length of the snapshot's RAM, `memory.bin`, in bytes: the RAM at boot. + pub fn memory_len(&self) -> u64 { + self.machine.memory.boot + } + + /// Checks the decoded manifest `raw`, and returns the manifest. + fn from_raw(raw: RawManifest) -> Result { + // The version and the magic were checked before the manifest was decoded. + if u64::from(raw.saved_state_schema_version) != contract::DEVICE_STATE_SCHEMA_VERSION + || raw.saved_state_root_type != SAVED_STATE_ROOT_TYPE + { + return Err(Problem::Format(format!( + "has saved-state schema version {} of root type {:?}", + raw.saved_state_schema_version, raw.saved_state_root_type + ))); + } + if raw.architecture != ARCHITECTURE || raw.page_size != PAGE_SIZE { + return Err(invalid(format!( + "describes a VM of architecture {:?} with {}-byte pages, not an x86-64 microVM", + raw.architecture, raw.page_size + ))); + } + if !(1..=MAX_STATE_LEN).contains(&raw.state_size) { + return Err(invalid(format!( + "has a saved state of {} bytes, not 1 to {MAX_STATE_LEN}", + raw.state_size + ))); + } + if ( + raw.tier.as_str(), + raw.restore_policy.as_str(), + raw.consumed_sections, + ) != (TIER_PLATFORM, RESTORE_POLICY_CLONE, CONSUMED_INVARIANTS) + { + return Err(invalid(format!( + "is not a template's: it was captured at tier {:?} with restore policy {:?} after \ + consuming configuration sections {:#x}, not at tier \"platform\" for clones after \ + consuming its invariants alone", + raw.tier, raw.restore_policy, raw.consumed_sections + ))); + } + let (seconds, nanos) = raw.created_at; + let created_at = u64::try_from(seconds) + .ok() + .filter(|_| (0..NANOS_PER_SECOND).contains(&nanos)) + .and_then(|seconds| { + SystemTime::UNIX_EPOCH.checked_add(Duration::new(seconds, nanos.unsigned_abs())) + }) + .ok_or_else(|| { + invalid(format!( + "has the invalid creation time of {seconds} seconds and {nanos} nanoseconds" + )) + })?; + let Some(contract) = raw.contract else { + return Err(invalid("has no machine contract")); + }; + let (machine, time) = check_contract(contract, raw.vp_count, raw.memory_size)?; + Ok(Self { + created_at, + openvmm_version: raw.openvmm_version, + state_len: raw.state_size, + time, + machine, + }) + } +} + +/// Opens the file at `path` without following a link, or on Windows any other reparse point, and +/// reads it whole, if it is a regular file of at most [`MAX_MANIFEST_LEN`] bytes. +fn read_bounded(path: &Path) -> io::Result> { + let not_regular = || io::Error::other("it is a link, or not a regular file"); + let mut options = OpenOptions::new(); + options.read(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + + // A FIFO is refused below, before anything is read from it, so the open must not wait for + // a writer. + options + .custom_flags((nix::fcntl::OFlag::O_NOFOLLOW | nix::fcntl::OFlag::O_NONBLOCK).bits()); + } + #[cfg(windows)] + { + use std::os::windows::fs::OpenOptionsExt; + + /// `FILE_FLAG_OPEN_REPARSE_POINT`, which opens a link as itself. + const OPEN_REPARSE_POINT: u32 = 0x0020_0000; + options.custom_flags(OPEN_REPARSE_POINT); + } + let file = match options.open(path) { + Ok(file) => file, + // `O_NOFOLLOW` refuses a link, and an open that does not wait refuses a socket. + #[cfg(unix)] + Err(error) + if [nix::errno::Errno::ELOOP, nix::errno::Errno::ENXIO] + .into_iter() + .any(|errno| error.raw_os_error() == Some(errno as i32)) => + { + return Err(not_regular()); + } + Err(error) => return Err(error), + }; + let metadata = file.metadata()?; + if !metadata.file_type().is_file() || filesystem::is_reparse_point(&metadata) { + return Err(not_regular()); + } + read_at_most(file, MAX_MANIFEST_LEN) +} + +/// Reads `file` to its end, failing if it holds more than `limit` bytes, without reading more +/// than one byte past the limit. +fn read_at_most(file: File, limit: u64) -> io::Result> { + let mut bytes = Vec::new(); + file.take(limit + 1).read_to_end(&mut bytes)?; + if bytes.len() as u64 > limit { + return Err(io::Error::other(format!( + "it holds more than the {limit} bytes that a manifest may hold" + ))); + } + Ok(bytes) +} + +/// Why a manifest is refused: one of another format, or one that is not a template's. +#[derive(Debug, Clone, PartialEq, Eq)] +enum Problem { + /// The manifest is of a format that this build does not read, which this describes. + Format(String), + /// The manifest is malformed, or not a template's, for the reason that this gives. + Invalid(String), +} + +impl Problem { + /// Returns the error of the manifest that `description` names. + fn into_error(self, description: &str) -> Error { + match self { + Self::Format(problem) => Error::new( + ErrorCode::FormatUnsupported, + format!( + "{description} {problem}, which this build does not read; capture the template \ + again with the runtime of this build" + ), + ), + Self::Invalid(problem) => Error::io(format!("{description} {problem}")), + } + } +} + +/// Returns an invalid manifest's [`Problem`], which `problem` describes. +fn invalid(problem: impl Into) -> Problem { + Problem::Invalid(problem.into()) +} + +/// Checks the format of the manifest `bytes`, its version and magic, from its top-level fields +/// alone, so that a manifest of another version, whose fields this build may not know, is reported +/// as such. +fn check_format(bytes: &[u8]) -> Result<(), Problem> { + let all: Vec = (1..64).collect(); + let mut message = Message::new(bytes, &[], &all); + let mut version = None; + let mut magic = None; + loop { + let field = match message.next_field() { + Ok(Some(field)) => field, + Ok(None) => break, + Err(error) => return Err(invalid(format!("is malformed: it {error}"))), + }; + // A field that appears twice is refused once the manifest is decoded. + match field.number { + 1 => version = field.uint64().ok(), + 12 => magic = field.bytes().ok(), + _ => {} + } + } + let version = version.unwrap_or(0); + if version != contract::SNAPSHOT_FORMAT_VERSION { + return Err(Problem::Format(format!("has format version {version}"))); + } + if magic != Some(FORMAT_MAGIC) { + return Err(Problem::Format( + "has another format magic than its version's".to_owned(), + )); + } + Ok(()) +} + +/// Decodes the fields of the message `data`, whose type knows the fields `known`, and `repeated`, +/// which may appear more than once, with `each`, and describes the first problem as one of `what`. +fn decode<'a>( + data: &'a [u8], + what: &str, + known: &[u32], + repeated: &[u32], + mut each: impl FnMut(Field<'a>) -> Result<(), String>, +) -> Result<(), String> { + let mut message = Message::new(data, known, repeated); + loop { + match message.next_field() { + Ok(Some(field)) => each(field)?, + Ok(None) => return Ok(()), + Err(error) => return Err(format!("{what} {error}")), + } + } +} + +/// Returns a mapper of a [`WireError`] of a field of the message `what`. +fn field_error(what: &str) -> impl Fn(WireError) -> String + '_ { + move |error| format!("{what} {error}") +} + +/// Appends `item` to `items`, which may hold at most `limit` items of the field that `what` +/// names. +fn push_bounded(items: &mut Vec, item: T, limit: usize, what: &str) -> Result<(), String> { + if items.len() == limit { + return Err(format!("{what} hold more than {limit} items")); + } + items.push(item); + Ok(()) +} + +/// The manifest, as its fields encode it. +#[derive(Default)] +struct RawManifest { + /// Field 2: seconds and nanoseconds. + created_at: (i64, i32), + /// Field 3. + openvmm_version: String, + /// Field 4. + memory_size: u64, + /// Field 5. + vp_count: u32, + /// Field 6. + page_size: u32, + /// Field 7. + architecture: String, + /// Field 8. + state_size: u64, + /// Field 11. + contract: Option, + /// Field 13. + saved_state_schema_version: u32, + /// Field 14. + saved_state_root_type: String, + /// Field 15. + tier: String, + /// Field 16. + restore_policy: String, + /// Field 17. + consumed_sections: u32, +} + +/// Decodes `openvmm.snapshot.SnapshotManifest`, whose version, field 1, and magic, field 12, +/// [`check_format`] checked. +fn decode_manifest(data: &[u8]) -> Result { + let what = "it"; + let wire = field_error(what); + let mut raw = RawManifest::default(); + decode( + data, + what, + &[1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17], + &[], + |field| { + match field.number { + 1 => { + field.uint32().map_err(&wire)?; + } + 2 => raw.created_at = decode_timestamp(field.bytes().map_err(&wire)?)?, + 3 => raw.openvmm_version = field.text().map_err(&wire)?.to_owned(), + 4 => raw.memory_size = field.uint64().map_err(&wire)?, + 5 => raw.vp_count = field.uint32().map_err(&wire)?, + 6 => raw.page_size = field.uint32().map_err(&wire)?, + 7 => raw.architecture = field.text().map_err(&wire)?.to_owned(), + 8 => raw.state_size = field.uint64().map_err(&wire)?, + 11 => raw.contract = Some(decode_contract(field.bytes().map_err(&wire)?)?), + 12 => { + field.bytes().map_err(&wire)?; + } + 13 => raw.saved_state_schema_version = field.uint32().map_err(&wire)?, + 14 => raw.saved_state_root_type = field.text().map_err(&wire)?.to_owned(), + 15 => raw.tier = field.text().map_err(&wire)?.to_owned(), + 16 => raw.restore_policy = field.text().map_err(&wire)?.to_owned(), + 17 => raw.consumed_sections = field.uint32().map_err(&wire)?, + _ => unreachable!("the message refuses unknown fields"), + } + Ok(()) + }, + )?; + Ok(raw) +} + +/// Decodes `google.protobuf.Timestamp` into its seconds and nanoseconds. +fn decode_timestamp(data: &[u8]) -> Result<(i64, i32), String> { + let what = "its creation time"; + let wire = field_error(what); + let mut timestamp = (0, 0); + decode(data, what, &[1, 2], &[], |field| { + match field.number { + 1 => timestamp.0 = field.int64().map_err(&wire)?, + 2 => timestamp.1 = field.int32().map_err(&wire)?, + _ => unreachable!("the message refuses unknown fields"), + } + Ok(()) + })?; + Ok(timestamp) +} + +/// The machine contract, as its fields encode it. +#[derive(Default)] +struct RawContract { + /// Field 1. + machine_profile: String, + /// Field 2. + abi_version: u32, + /// Field 3. + source_hypervisor: String, + /// Field 4. + command_line: String, + /// Field 5. + command_line_digest: Vec, + /// Field 6: start, length, and file offset. + memory_ranges: Vec<(u64, u64, u64)>, + /// Field 7. + topology: Topology, + /// Field 8: each device and its order. + devices: Vec<(Device, u32)>, + /// Field 9: how many names it holds. + state_units: usize, + /// Field 10: how many attachments it holds. + attachments: usize, + /// Field 16. + boot_layout_version: u32, + /// Field 18. + network: Option, + /// Field 19: whether it is present. + filesystem: bool, + /// Field 21. + blocks: Vec, + /// Field 22. + filesystem_slot_version: u32, + /// Field 23. + boot_online_vp_count: u32, + /// Field 24. + interrupt_mode: String, + /// Field 25. + status_page_start: u64, + /// Field 26. + status_page_length: u64, + /// Field 27. + expansion_version: u32, + /// Field 28. + memory_capacity: u64, + /// Field 29. + memory_block_size: u64, + /// Field 30. + expansion_ranges: Vec, + /// Field 31. + time: Option, + /// Field 32. + cpu_profile: Option, +} + +/// Decodes `openvmm.snapshot.SnapshotMachineContract`. +fn decode_contract(data: &[u8]) -> Result { + let what = "its machine contract"; + let wire = field_error(what); + let mut raw = RawContract::default(); + decode( + data, + what, + &[ + 1, 2, 3, 4, 5, 7, 16, 18, 19, 22, 23, 24, 25, 26, 27, 28, 29, 31, 32, + ], + &[6, 8, 9, 10, 21, 30], + |field| { + match field.number { + 1 => raw.machine_profile = field.text().map_err(&wire)?.to_owned(), + 2 => raw.abi_version = field.uint32().map_err(&wire)?, + 3 => raw.source_hypervisor = field.text().map_err(&wire)?.to_owned(), + 4 => raw.command_line = field.text().map_err(&wire)?.to_owned(), + 5 => raw.command_line_digest = field.bytes().map_err(&wire)?.to_vec(), + 6 => { + let range = decode_memory_range(field.bytes().map_err(&wire)?)?; + push_bounded( + &mut raw.memory_ranges, + range, + MAX_MEMORY_RANGES, + "its memory ranges", + )?; + } + 7 => raw.topology = decode_topology(field.bytes().map_err(&wire)?)?, + 8 => { + let index = raw.devices.len(); + let device = decode_device(field.bytes().map_err(&wire)?, index)?; + push_bounded(&mut raw.devices, device, MAX_DEVICES, "its devices")?; + } + 9 => { + field.text().map_err(&wire)?; + if raw.state_units == MAX_STATE_UNITS { + return Err(format!( + "its state units hold more than {MAX_STATE_UNITS} items" + )); + } + raw.state_units += 1; + } + 10 => { + decode_attachment(field.bytes().map_err(&wire)?, raw.attachments)?; + if raw.attachments == MAX_ATTACHMENTS { + return Err(format!( + "its attachments hold more than {MAX_ATTACHMENTS} items" + )); + } + raw.attachments += 1; + } + 16 => raw.boot_layout_version = field.uint32().map_err(&wire)?, + 18 => raw.network = Some(decode_network(field.bytes().map_err(&wire)?)?), + 19 => { + // A template's filesystem slot is dormant, so the policy of an attached + // filesystem is refused without its fields being read. + field.bytes().map_err(&wire)?; + raw.filesystem = true; + } + 21 => { + let block = decode_block(field.bytes().map_err(&wire)?)?; + push_bounded(&mut raw.blocks, block, MAX_BLOCKS, "its sandbox blocks")?; + } + 22 => raw.filesystem_slot_version = field.uint32().map_err(&wire)?, + 23 => raw.boot_online_vp_count = field.uint32().map_err(&wire)?, + 24 => raw.interrupt_mode = field.text().map_err(&wire)?.to_owned(), + 25 => raw.status_page_start = field.uint64().map_err(&wire)?, + 26 => raw.status_page_length = field.uint64().map_err(&wire)?, + 27 => raw.expansion_version = field.uint32().map_err(&wire)?, + 28 => raw.memory_capacity = field.uint64().map_err(&wire)?, + 29 => raw.memory_block_size = field.uint64().map_err(&wire)?, + 30 => { + let (start, length) = decode_expansion_range(field.bytes().map_err(&wire)?)?; + push_bounded( + &mut raw.expansion_ranges, + MemoryRange { start, length }, + MAX_MEMORY_RANGES, + "its memory expansion ranges", + )?; + } + 31 => raw.time = Some(decode_time(field.bytes().map_err(&wire)?)?), + 32 => raw.cpu_profile = Some(decode_cpu_profile(field.bytes().map_err(&wire)?)?), + _ => unreachable!("the message refuses unknown fields"), + } + Ok(()) + }, + )?; + Ok(raw) +} + +/// Decodes `openvmm.snapshot.SnapshotMemoryRange` into its start, length, and file offset. +fn decode_memory_range(data: &[u8]) -> Result<(u64, u64, u64), String> { + let what = "its memory range"; + let wire = field_error(what); + let mut range = (0, 0, 0); + decode(data, what, &[1, 2, 3], &[], |field| { + let value = field.uint64().map_err(&wire)?; + match field.number { + 1 => range.0 = value, + 2 => range.1 = value, + 3 => range.2 = value, + _ => unreachable!("the message refuses unknown fields"), + } + Ok(()) + })?; + Ok(range) +} + +/// Decodes `openvmm.snapshot.SnapshotMemoryExpansionRange`, a range that a restore may add, into +/// its start and length: it has no file offset. +fn decode_expansion_range(data: &[u8]) -> Result<(u64, u64), String> { + let what = "its memory expansion range"; + let wire = field_error(what); + let mut range = (0, 0); + decode(data, what, &[1, 2], &[], |field| { + let value = field.uint64().map_err(&wire)?; + match field.number { + 1 => range.0 = value, + 2 => range.1 = value, + _ => unreachable!("the message refuses unknown fields"), + } + Ok(()) + })?; + Ok(range) +} + +/// Decodes `openvmm.snapshot.SnapshotProcessorTopology`. +fn decode_topology(data: &[u8]) -> Result { + let what = "its processor topology"; + let wire = field_error(what); + let mut topology = Topology::default(); + decode(data, what, &[1, 2, 3, 4, 5], &[], |field| { + match field.number { + 1 => topology.sockets = field.uint32().map_err(&wire)?, + 2 => topology.dies_per_socket = field.uint32().map_err(&wire)?, + 3 => topology.cores_per_die = field.uint32().map_err(&wire)?, + 4 => topology.threads_per_core = field.uint32().map_err(&wire)?, + 5 => topology.apic_ids = field.packed_uint32().map_err(&wire)?, + _ => unreachable!("the message refuses unknown fields"), + } + Ok(()) + })?; + Ok(topology) +} + +/// Decodes `openvmm.snapshot.SnapshotDevice`, the device at `index` of the machine, and returns +/// it with its order. +fn decode_device(data: &[u8], index: usize) -> Result<(Device, u32), String> { + let what = format!("its device {index}"); + let wire = field_error(&what); + let mut device = Device { + stable_id: String::new(), + kind: String::new(), + state_unit: String::new(), + transport: String::new(), + ranges: Vec::new(), + irq: None, + feature_banks: Vec::new(), + queue_count: 0, + queue_max_sizes: Vec::new(), + }; + let mut order = 0; + decode(data, &what, &[1, 2, 3, 4, 6, 7, 8, 9, 10], &[5], |field| { + match field.number { + 1 => device.stable_id = field.text().map_err(&wire)?.to_owned(), + 2 => device.state_unit = field.text().map_err(&wire)?.to_owned(), + 3 => device.kind = field.text().map_err(&wire)?.to_owned(), + 4 => order = field.uint32().map_err(&wire)?, + 5 => { + let range = decode_device_range(field.bytes().map_err(&wire)?, index)?; + push_bounded( + &mut device.ranges, + range, + MAX_DEVICE_RANGES, + &format!("the ranges of {what}"), + )?; + } + 6 => device.irq = Some(field.uint32().map_err(&wire)?), + 7 => device.transport = field.text().map_err(&wire)?.to_owned(), + 8 => device.feature_banks = field.packed_uint32().map_err(&wire)?, + 9 => device.queue_count = field.uint32().map_err(&wire)?, + 10 => device.queue_max_sizes = field.packed_uint32().map_err(&wire)?, + _ => unreachable!("the message refuses unknown fields"), + } + Ok(()) + })?; + Ok((device, order)) +} + +/// Decodes `openvmm.snapshot.SnapshotDeviceRange`, a range of the device at `index`. +fn decode_device_range(data: &[u8], index: usize) -> Result { + let what = format!("a range of its device {index}"); + let wire = field_error(&what); + let mut space = String::new(); + let mut range = DeviceRange { + space: AddressSpace::Mmio, + start: 0, + length: 0, + }; + decode(data, &what, &[1, 2, 3], &[], |field| { + match field.number { + 1 => space = field.text().map_err(&wire)?.to_owned(), + 2 => range.start = field.uint64().map_err(&wire)?, + 3 => range.length = field.uint64().map_err(&wire)?, + _ => unreachable!("the message refuses unknown fields"), + } + Ok(()) + })?; + range.space = AddressSpace::parse(&space) + .ok_or_else(|| format!("{what} is in the unknown address space {space:?}"))?; + Ok(range) +} + +/// Decodes `openvmm.snapshot.SnapshotAttachment`, the host attachment at `index`, which a key +/// does not hold, so that only its form is checked. +fn decode_attachment(data: &[u8], index: usize) -> Result<(), String> { + let what = format!("its attachment {index}"); + let wire = field_error(&what); + decode(data, &what, &[1, 2, 3, 4, 5, 6, 7, 8], &[], |field| { + match field.number { + 1 | 2 | 4 | 5 => { + field.text().map_err(&wire)?; + } + 3 => { + field.boolean().map_err(&wire)?; + } + 6 => { + if field.bytes().map_err(&wire)?.len() > MAX_ATTACHMENT_IDENTITY_LEN { + return Err(format!( + "{what} has an identity of more than {MAX_ATTACHMENT_IDENTITY_LEN} bytes" + )); + } + } + 7 | 8 => { + field.uint64().map_err(&wire)?; + } + _ => unreachable!("the message refuses unknown fields"), + } + Ok(()) + }) +} + +/// The network of the machine contract, as its fields encode it. +#[derive(Default)] +struct RawNetwork { + /// Field 9. + profile: String, + /// Field 1. + ipv4: u32, + /// Field 2. + prefix_length: u32, + /// Field 3. + gateway: u32, + /// Field 4. + guest_mac: Vec, + /// Field 5. + gateway_mac: Vec, + /// Field 6. + policy_mode: String, + /// Field 7. + policy_digest: Vec, + /// Field 8. + policy_required: bool, + /// Field 10. + policy_encoding_version: u32, + /// Field 11. + ipv6: Vec, + /// Field 12. + ipv6_prefix_length: u32, + /// Field 13. + ipv6_gateway: Vec, +} + +/// Decodes `openvmm.snapshot.SnapshotMicrovmNetwork`. +fn decode_network(data: &[u8]) -> Result { + let what = "its network"; + let wire = field_error(what); + let mut raw = RawNetwork::default(); + decode( + data, + what, + &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], + &[], + |field| { + match field.number { + 1 => raw.ipv4 = field.uint32().map_err(&wire)?, + 2 => raw.prefix_length = field.uint32().map_err(&wire)?, + 3 => raw.gateway = field.uint32().map_err(&wire)?, + 4 => raw.guest_mac = field.bytes().map_err(&wire)?.to_vec(), + 5 => raw.gateway_mac = field.bytes().map_err(&wire)?.to_vec(), + 6 => raw.policy_mode = field.text().map_err(&wire)?.to_owned(), + 7 => raw.policy_digest = field.bytes().map_err(&wire)?.to_vec(), + 8 => raw.policy_required = field.boolean().map_err(&wire)?, + 9 => raw.profile = field.text().map_err(&wire)?.to_owned(), + 10 => raw.policy_encoding_version = field.uint32().map_err(&wire)?, + 11 => raw.ipv6 = field.bytes().map_err(&wire)?.to_vec(), + 12 => raw.ipv6_prefix_length = field.uint32().map_err(&wire)?, + 13 => raw.ipv6_gateway = field.bytes().map_err(&wire)?.to_vec(), + _ => unreachable!("the message refuses unknown fields"), + } + Ok(()) + }, + )?; + Ok(raw) +} + +/// A sandbox block of the machine contract, as its fields encode it. +#[derive(Default)] +struct RawBlock { + /// Field 1. + role: String, + /// Field 2. + read_only: bool, + /// Fields 3, 7, and 8. + geometry: (u64, u32, u32), + /// Field 4. + identity_kind: String, + /// Field 5. + identity: Vec, + /// Field 6. + artifact: String, + /// Field 9. + restore_mode: String, +} + +/// Decodes `openvmm.snapshot.SnapshotMicrovmSandboxBlock`. +fn decode_block(data: &[u8]) -> Result { + let what = "its sandbox block"; + let wire = field_error(what); + let mut raw = RawBlock::default(); + decode(data, what, &[1, 2, 3, 4, 5, 6, 7, 8, 9], &[], |field| { + match field.number { + 1 => raw.role = field.text().map_err(&wire)?.to_owned(), + 2 => raw.read_only = field.boolean().map_err(&wire)?, + 3 => raw.geometry.0 = field.uint64().map_err(&wire)?, + 4 => raw.identity_kind = field.text().map_err(&wire)?.to_owned(), + 5 => raw.identity = field.bytes().map_err(&wire)?.to_vec(), + 6 => raw.artifact = field.text().map_err(&wire)?.to_owned(), + 7 => raw.geometry.1 = field.uint32().map_err(&wire)?, + 8 => raw.geometry.2 = field.uint32().map_err(&wire)?, + 9 => raw.restore_mode = field.text().map_err(&wire)?.to_owned(), + _ => unreachable!("the message refuses unknown fields"), + } + Ok(()) + })?; + Ok(raw) +} + +/// The time contract, as its fields encode it. +#[derive(Default)] +struct RawTime { + /// Field 1. + abi_version: u32, + /// Field 2. + tsc_hz: u64, + /// Field 3. + tolerance_ppm: u32, + /// Field 4. + apic_hz: u64, + /// Field 5. + capture_tsc: u64, + /// Field 6. + capture_utc_ns: u64, + /// Field 7. + capture_monotonic_ns: u64, + /// Field 8. + host_clock: String, + /// Field 9. + host_id: Vec, + /// Field 10. + boot_id: Vec, + /// Field 11. + lineage: u32, + /// Field 12. + cut_to_anchor_ns: Option, +} + +/// Decodes `openvmm.snapshot.SnapshotTimeContract`. +fn decode_time(data: &[u8]) -> Result { + let what = "its time contract"; + let wire = field_error(what); + let mut raw = RawTime::default(); + decode( + data, + what, + &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + &[], + |field| { + match field.number { + 1 => raw.abi_version = field.uint32().map_err(&wire)?, + 2 => raw.tsc_hz = field.uint64().map_err(&wire)?, + 3 => raw.tolerance_ppm = field.uint32().map_err(&wire)?, + 4 => raw.apic_hz = field.uint64().map_err(&wire)?, + 5 => raw.capture_tsc = field.uint64().map_err(&wire)?, + 6 => raw.capture_utc_ns = field.uint64().map_err(&wire)?, + 7 => raw.capture_monotonic_ns = field.uint64().map_err(&wire)?, + 8 => raw.host_clock = field.text().map_err(&wire)?.to_owned(), + 9 => raw.host_id = field.bytes().map_err(&wire)?.to_vec(), + 10 => raw.boot_id = field.bytes().map_err(&wire)?.to_vec(), + 11 => raw.lineage = field.uint32().map_err(&wire)?, + 12 => raw.cut_to_anchor_ns = Some(field.uint64().map_err(&wire)?), + _ => unreachable!("the message refuses unknown fields"), + } + Ok(()) + }, + )?; + Ok(raw) +} + +/// The CPU profile record, as its fields encode it. +#[derive(Default)] +struct RawCpuProfile { + /// Field 1. + id: String, + /// Field 2. + digest: Vec, + /// Field 4. + effective_cpuid: Vec, +} + +/// Decodes `openvmm.snapshot.SnapshotCpuProfile`. Its canonical document, field 3, which its +/// digest names, and the capturing host's CPU signature, field 6, which is diagnostic, are not +/// kept. +fn decode_cpu_profile(data: &[u8]) -> Result { + let what = "its CPU profile"; + let wire = field_error(what); + let mut raw = RawCpuProfile::default(); + decode(data, what, &[1, 2, 3, 4, 6], &[], |field| { + match field.number { + 1 => raw.id = field.text().map_err(&wire)?.to_owned(), + 2 => raw.digest = field.bytes().map_err(&wire)?.to_vec(), + 3 => { + field.bytes().map_err(&wire)?; + } + 4 => raw.effective_cpuid = field.bytes().map_err(&wire)?.to_vec(), + 6 => { + field.uint32().map_err(&wire)?; + } + _ => unreachable!("the message refuses unknown fields"), + } + Ok(()) + })?; + Ok(raw) +} + +/// Checks the decoded machine contract `raw` of a manifest that records `vp_count` processors and +/// `memory_size` bytes of RAM, and returns the machine and the time contract. +fn check_contract( + raw: RawContract, + vp_count: u32, + memory_size: u64, +) -> Result<(Machine, TimeContract), Problem> { + if raw.machine_profile != MACHINE_PROFILE + || u64::from(raw.abi_version) != contract::MICROVM_ABI_VERSION + || raw.boot_layout_version != BOOT_LAYOUT_VERSION + { + return Err(Problem::Format(format!( + "describes a machine of profile {:?}, ABI version {}, and boot layout version {}", + raw.machine_profile, raw.abi_version, raw.boot_layout_version + ))); + } + let time = check_time(raw.time)?; + if raw.expansion_version != MEMORY_EXPANSION_VERSION { + return Err(invalid(format!( + "is not a template's: it declares memory expansion version {}, not version 1", + raw.expansion_version + ))); + } + if raw.filesystem_slot_version != FILESYSTEM_SLOT_VERSION || raw.filesystem { + return Err(invalid( + "is not a template's: its filesystem slot is not the dormant slot of version 1", + )); + } + let hypervisor = raw.source_hypervisor.parse::().map_err(|_| { + invalid(format!( + "was captured by the unknown backend {:?}", + raw.source_hypervisor + )) + })?; + let cpu = check_cpu_profile(raw.cpu_profile)?; + let command_line_digest = <[u8; SHA256_LEN]>::try_from(raw.command_line_digest.as_slice()) + .map_err(|_| invalid("has an effective command line whose digest is not 32 bytes"))?; + // The RAM at boot fills the memory file from its start, range after range. + let mut offset = 0u64; + let mut ranges = Vec::with_capacity(raw.memory_ranges.len()); + for &(start, length, file_offset) in &raw.memory_ranges { + if file_offset != offset { + return Err(invalid( + "has memory ranges that do not fill its memory file in order", + )); + } + offset = offset.saturating_add(length); + ranges.push(MemoryRange { start, length }); + } + let (distro, scratch) = check_blocks(raw.blocks)?; + let nic = raw.network.map(check_network).transpose()?; + let machine = Machine { + hypervisor, + cpu, + topology: raw.topology, + rates: Rates { + tsc_hz: time.tsc_frequency_hz, + apic_hz: time.apic_frequency_hz, + tolerance_ppm: time.tsc_tolerance_ppm, + }, + processors: vp_count, + boot_processors: raw.boot_online_vp_count, + memory: Memory { + boot: memory_size, + capacity: raw.memory_capacity, + block_size: raw.memory_block_size, + ranges, + expansion: raw.expansion_ranges, + }, + command_line: raw.command_line, + command_line_digest, + distro, + scratch, + interrupts: Interrupts { + mode: raw.interrupt_mode, + status_page_start: raw.status_page_start, + status_page_length: raw.status_page_length, + }, + devices: check_order(raw.devices)?, + filesystem_slot: raw.filesystem_slot_version, + nic, + }; + if let Some(problem) = machine.problem() { + return Err(invalid(problem)); + } + Ok((machine, time)) +} + +/// Checks the decoded time contract `raw`, and returns it. +fn check_time(raw: Option) -> Result { + let Some(raw) = raw else { + return Err(invalid("has no time contract")); + }; + if u64::from(raw.abi_version) != contract::TIME_ABI_VERSION { + return Err(Problem::Format(format!( + "has a time contract of time ABI version {}", + raw.abi_version + ))); + } + let host_clock = HostClock::parse(&raw.host_clock).ok_or_else(|| { + invalid(format!( + "has a time contract of the unknown host clock {:?}", + raw.host_clock + )) + })?; + let identity = |bytes: &[u8], what: &str| { + <[u8; HOST_IDENTITY_LEN]>::try_from(bytes).map_err(|_| { + invalid(format!( + "has a time contract whose {what} is {} bytes, not {HOST_IDENTITY_LEN}", + bytes.len() + )) + }) + }; + Ok(TimeContract { + tsc_frequency_hz: raw.tsc_hz, + tsc_tolerance_ppm: raw.tolerance_ppm, + apic_frequency_hz: raw.apic_hz, + capture_tsc: raw.capture_tsc, + capture_utc_ns: raw.capture_utc_ns, + capture_monotonic_ns: raw.capture_monotonic_ns, + host_clock, + host_id: identity(&raw.host_id, "host identity")?, + boot_id: BootId::from_bytes(identity(&raw.boot_id, "boot identity")?), + lineage: raw.lineage, + cut_to_anchor_ns: raw.cut_to_anchor_ns, + }) +} + +/// Checks the decoded CPU profile record `raw`, and returns the profile. +fn check_cpu_profile(raw: Option) -> Result { + let Some(raw) = raw else { + return Err(invalid("has no CPU profile")); + }; + let (vendor, generation, revision) = parse_profile_id(&raw.id).ok_or_else(|| { + invalid(format!( + "has the CPU profile {:?}, which is not VENDOR.GENERATION.vREVISION", + raw.id + )) + })?; + let digest = <[u8; SHA256_LEN]>::try_from(raw.digest.as_slice()) + .map_err(|_| invalid("has a CPU profile whose digest is not 32 bytes"))?; + Ok(CpuProfile { + vendor, + generation, + revision, + digest, + effective_cpuid: raw.effective_cpuid, + }) +} + +/// Checks that the decoded devices `raw` are in their order, and returns them. +fn check_order(raw: Vec<(Device, u32)>) -> Result, Problem> { + raw.into_iter() + .enumerate() + .map(|(index, (device, order))| { + if order as usize == index { + Ok(device) + } else { + Err(invalid(format!( + "lists device {index} with the order {order}" + ))) + } + }) + .collect() +} + +/// Checks the decoded sandbox blocks `raw`: a template's read-only `distro`, whose identity is +/// unbound, then its fresh scratch. Returns their geometries, which [`Machine::problem`] checks. +fn check_blocks(raw: Vec) -> Result<(BlockGeometry, BlockGeometry), Problem> { + let not_template = || { + invalid( + "is not a template's: its sandbox blocks are not a read-only distro whose identity is \ + unbound and a fresh scratch", + ) + }; + let [distro, scratch]: [RawBlock; 2] = raw.try_into().map_err(|_| not_template())?; + let is = |block: &RawBlock, role: &str, read_only: bool, identity_kind: &str| { + block.role == role + && block.read_only == read_only + && block.identity_kind == identity_kind + && block.identity.is_empty() + && block.artifact.is_empty() + && block.restore_mode.is_empty() + }; + if !is(&distro, "distro", true, "unbound") || !is(&scratch, "scratch", false, "fresh") { + return Err(not_template()); + } + let geometry = |block: &RawBlock| BlockGeometry { + length: block.geometry.0, + logical_block_size: block.geometry.1, + physical_block_size: block.geometry.2, + }; + Ok((geometry(&distro), geometry(&scratch))) +} + +/// Checks the decoded network `raw`, and returns the fixed NIC, which [`Machine::problem`] +/// checks. +fn check_network(raw: RawNetwork) -> Result { + let mac = |bytes: &[u8]| <[u8; 6]>::try_from(bytes).ok(); + let (Some(guest_mac), Some(gateway_mac)) = (mac(&raw.guest_mac), mac(&raw.gateway_mac)) else { + return Err(invalid("has a NIC whose MAC addresses are not 6 bytes")); + }; + let prefix_length = u8::try_from(raw.prefix_length) + .map_err(|_| invalid("has a NIC with an invalid prefix length"))?; + let ipv6 = match ( + raw.ipv6.as_slice(), + raw.ipv6_prefix_length, + raw.ipv6_gateway.as_slice(), + ) { + ([], 0, []) => None, + (address, prefix_length, gateway) => { + let (Ok(address), Ok(prefix_length), Ok(gateway)) = ( + <[u8; 16]>::try_from(address), + u8::try_from(prefix_length), + <[u8; 16]>::try_from(gateway), + ) else { + return Err(invalid("has a NIC with an invalid IPv6 identity")); + }; + Some(Ipv6Identity { + address: Ipv6Addr::from(address), + prefix_length, + gateway: Ipv6Addr::from(gateway), + }) + } + }; + let digest = <[u8; SHA256_LEN]>::try_from(raw.policy_digest.as_slice()) + .map_err(|_| invalid("has a NIC whose policy digest is not 32 bytes"))?; + Ok(Nic { + profile: raw.profile, + guest_mac, + gateway_mac, + ipv4: Ipv4Addr::from(raw.ipv4), + prefix_length, + gateway: Ipv4Addr::from(raw.gateway), + ipv6, + policy: PolicyIdentity { + mode: raw.policy_mode, + digest, + required: raw.policy_required, + encoding_version: raw.policy_encoding_version, + }, + }) +} + +#[cfg(test)] +mod tests; diff --git a/nvx-rs/src/host/template/manifest/tests.rs b/nvx-rs/src/host/template/manifest/tests.rs new file mode 100644 index 000000000..6cad9c827 --- /dev/null +++ b/nvx-rs/src/host/template/manifest/tests.rs @@ -0,0 +1,1034 @@ +//! Tests of [`TemplateManifest`]: what it reads of a template's manifest, and what it refuses. + +use std::fs; +use std::path::Path; +use std::time::{Duration, SystemTime}; + +use super::*; +use crate::host::template::test_support::{MIB, ManifestSpec, Proto}; + +/// Returns the manifest that the encoding of `spec` parses to. +fn parse(spec: &ManifestSpec) -> TemplateManifest { + TemplateManifest::parse(&spec.encode()).unwrap() +} + +/// Returns the message of the refusal of `bytes`, which must have the code `code`. +fn refusal(bytes: &[u8], code: ErrorCode) -> String { + let error = TemplateManifest::parse(bytes).expect_err("the manifest is refused"); + assert_eq!(error.code(), code, "{error}"); + error.message().to_owned() +} + +/// Asserts that the manifest of a template is refused with `code` and a message that holds +/// `expected` once `change` changes it. +fn assert_refused(change: impl FnOnce(&mut ManifestSpec), code: ErrorCode, expected: &str) { + let mut spec = ManifestSpec::template(); + change(&mut spec); + let message = refusal(&spec.encode(), code); + assert!(message.contains(expected), "{message:?} lacks {expected:?}"); +} + +#[test] +fn a_templates_manifest_is_read_whole() { + let mut spec = ManifestSpec::template(); + let manifest = parse(&spec); + assert_eq!( + manifest.created_at(), + SystemTime::UNIX_EPOCH + Duration::new(1_761_300_000, 123_456_789) + ); + assert_eq!(manifest.openvmm_version(), "0.0.0"); + assert_eq!(manifest.hypervisor(), Hypervisor::Kvm); + assert_eq!(manifest.state_len(), 65_536); + assert_eq!(manifest.memory_len(), 512 * MIB); + assert_eq!( + manifest.time(), + &TimeContract { + tsc_frequency_hz: 2_100_000_000, + tsc_tolerance_ppm: 250, + apic_frequency_hz: 200_000_000, + capture_tsc: 12_345_678, + capture_utc_ns: 1_700_000_000_000_000_000, + capture_monotonic_ns: 123_000_000_000, + host_clock: HostClock::LinuxBoottime, + host_id: [1; 16], + boot_id: BootId::from_bytes([2; 16]), + lineage: 3, + cut_to_anchor_ns: Some(200), + } + ); + let machine = &manifest.machine; + assert_eq!(machine.cpu.id(), "intel.icelake-sp.v1"); + assert_eq!(machine.cpu.digest, [0x5a; 32]); + assert_eq!(machine.cpu.effective_cpuid, (0..88).collect::>()); + assert_eq!( + machine.topology, + Topology { + sockets: 1, + dies_per_socket: 1, + cores_per_die: 2, + threads_per_core: 1, + apic_ids: vec![0, 1], + } + ); + assert_eq!( + machine.rates, + Rates { + tsc_hz: 2_100_000_000, + apic_hz: 200_000_000, + tolerance_ppm: 250, + } + ); + assert_eq!((machine.processors, machine.boot_processors), (2, 0)); + assert_eq!( + machine.memory, + Memory { + boot: 512 * MIB, + capacity: 2048 * MIB, + block_size: 128 * MIB, + ranges: vec![MemoryRange { + start: 0, + length: 512 * MIB, + }], + expansion: vec![MemoryRange { + start: 512 * MIB, + length: 1536 * MIB, + }], + } + ); + assert_eq!(machine.command_line, spec.contract().command_line); + assert_eq!( + machine.command_line_digest, + <[u8; 32]>::from(Sha256::digest(&machine.command_line)) + ); + assert_eq!( + (machine.distro, machine.scratch), + ( + BlockGeometry { + length: 1024 * MIB, + logical_block_size: 512, + physical_block_size: 4096, + }, + BlockGeometry { + length: 64 * MIB, + logical_block_size: 512, + physical_block_size: 512, + } + ) + ); + assert_eq!( + machine.interrupts, + Interrupts { + mode: "edge-shared-status".to_owned(), + status_page_start: 0x3_0000, + status_page_length: 0x1000, + } + ); + let ids: Vec<&str> = machine + .devices + .iter() + .map(|device| device.stable_id.as_str()) + .collect(); + assert_eq!( + ids, + [ + "partition", + "vp0", + "vmtime", + "pic", + "ioapic", + "lapic", + "pit", + "rtc", + "microvm-portb", + "microvm-shutdown", + "microvm-snapshot-request", + "fs:microvm0", + "console:microvm-virtio0", + "blk:sandbox:distro", + "blk:sandbox:scratch", + "console:microvm-control0", + ] + ); + assert_eq!( + machine.devices[3], + Device { + stable_id: "pic".to_owned(), + kind: "pic".to_owned(), + state_unit: "pic".to_owned(), + transport: String::new(), + ranges: vec![ + DeviceRange { + space: AddressSpace::Pmio, + start: 0x20, + length: 2, + }, + DeviceRange { + space: AddressSpace::Pmio, + start: 0xa0, + length: 2, + }, + ], + irq: None, + feature_banks: Vec::new(), + queue_count: 0, + queue_max_sizes: Vec::new(), + } + ); + // An interrupt of 0 is an interrupt, unlike none. + assert_eq!(machine.devices[6].irq, Some(0)); + assert_eq!( + machine.devices[13], + Device { + stable_id: "blk:sandbox:distro".to_owned(), + kind: "virtio-blk".to_owned(), + state_unit: "virtio-blk-3489673216".to_owned(), + transport: "virtio-mmio".to_owned(), + ranges: vec![DeviceRange { + space: AddressSpace::Mmio, + start: 0xd000_3000, + length: 0x1000, + }], + irq: Some(4), + feature_banks: vec![0x3000_0664, 3], + queue_count: 1, + queue_max_sizes: vec![256], + } + ); + assert_eq!(machine.filesystem_slot, 1); + assert_eq!(machine.nic, None); +} + +#[test] +fn a_fixed_nic_is_read_with_the_identity_of_its_policy() { + let manifest = parse(&ManifestSpec::with_network()); + assert_eq!( + manifest.machine.nic, + Some(Nic { + profile: "portable".to_owned(), + guest_mac: [0x52, 0x54, 0, 0, 0, 2], + gateway_mac: [0x52, 0x54, 0, 0, 0, 1], + ipv4: Ipv4Addr::new(10, 0, 0, 2), + prefix_length: 24, + gateway: Ipv4Addr::new(10, 0, 0, 1), + ipv6: Some(Ipv6Identity { + address: "fd00::a00:2".parse().unwrap(), + prefix_length: 120, + gateway: "fd00::a00:1".parse().unwrap(), + }), + policy: PolicyIdentity { + mode: "allow-list".to_owned(), + digest: [7; 32], + required: true, + encoding_version: 4, + }, + }) + ); + assert_eq!(manifest.machine.devices[11].stable_id, "net:microvm0"); +} + +#[test] +fn the_fields_of_a_manifest_need_not_be_written_in_their_order() { + // `mesh` writes a message's fields in the order of its type's declaration, which may differ + // from the order of their numbers, as the network's profile, field 9, comes first. + let mut spec = ManifestSpec::template(); + let reversed = Proto::default() + .varint(17, 1) + .text(16, "clone") + .text(15, "platform") + .text(14, "openvmm.SavedState") + .varint(13, 1) + .bytes(12, b"OPENVMM_SNAPSHOT_V6\0"); + spec.tier.clear(); + spec.restore_policy.clear(); + spec.root_type.clear(); + spec.schema_version = 0; + spec.consumed_sections = 0; + spec.magic.clear(); + let bytes = spec.encode_with(reversed); + assert_eq!( + TemplateManifest::parse(&bytes).unwrap(), + parse(&ManifestSpec::template()) + ); +} + +#[test] +fn other_versions_of_the_format_are_unsupported() { + let unsupported = ErrorCode::FormatUnsupported; + for version in [0, 1, 5, 7, u32::MAX] { + assert_refused( + |spec| spec.version = version, + unsupported, + &format!( + "the snapshot manifest has format version {version}, which this build does not \ + read; capture the template again" + ), + ); + } + assert_refused( + |spec| spec.magic = b"OPENVMM_SNAPSHOT_V5\0".to_vec(), + unsupported, + "has another format magic than its version's", + ); + assert_refused( + |spec| spec.magic.clear(), + unsupported, + "has another format magic", + ); + assert_refused( + |spec| spec.schema_version = 2, + unsupported, + "has saved-state schema version 2", + ); + assert_refused( + |spec| spec.root_type = "openvmm.OtherState".to_owned(), + unsupported, + "of root type \"openvmm.OtherState\"", + ); + assert_refused( + |spec| spec.contract().machine_profile = "standard".to_owned(), + unsupported, + "describes a machine of profile \"standard\"", + ); + for version in [1, 3] { + assert_refused( + |spec| spec.contract().abi_version = version, + unsupported, + &format!("ABI version {version}"), + ); + } + assert_refused( + |spec| spec.contract().boot_layout_version = 1, + unsupported, + "boot layout version 1", + ); + assert_refused( + |spec| spec.time().abi_version = 2, + unsupported, + "has a time contract of time ABI version 2", + ); + // A manifest of another version is reported as such, even with fields that this build does + // not know, such as a field that version 6 retired. + let mut spec = ManifestSpec::template(); + spec.version = 5; + let message = refusal( + &spec.encode_with(Proto::default().varint(9, 1).varint(40, 1)), + unsupported, + ); + assert!(message.contains("has format version 5"), "{message}"); +} + +#[test] +fn fields_that_the_format_does_not_have_are_refused() { + let template = ManifestSpec::template(); + // Field 9 is retired, and field 18 does not exist yet. + for number in [9, 10, 18, 63, 64, 1000] { + let message = refusal( + &template.encode_with(Proto::default().varint(number, 1)), + ErrorCode::Io, + ); + assert_eq!( + message, + format!("the snapshot manifest is malformed: it holds the unknown field {number}") + ); + } + let unknown = Proto::default().varint(40, 1).finish(); + for (decoded, what) in [ + (decode_contract(&unknown).map(drop), "its machine contract"), + (decode_time(&unknown).map(drop), "its time contract"), + (decode_cpu_profile(&unknown).map(drop), "its CPU profile"), + (decode_network(&unknown).map(drop), "its network"), + (decode_block(&unknown).map(drop), "its sandbox block"), + (decode_device(&unknown, 2).map(drop), "its device 2"), + ( + decode_device_range(&unknown, 2).map(drop), + "a range of its device 2", + ), + (decode_attachment(&unknown, 1), "its attachment 1"), + (decode_memory_range(&unknown).map(drop), "its memory range"), + ( + decode_expansion_range(&unknown).map(drop), + "its memory expansion range", + ), + ( + decode_topology(&unknown).map(drop), + "its processor topology", + ), + (decode_timestamp(&unknown).map(drop), "its creation time"), + ] { + assert_eq!(decoded, Err(format!("{what} holds the unknown field 40"))); + } + // The CPU signature, field 6, is known, but field 5 is not. + assert_eq!( + decode_cpu_profile(&Proto::default().varint(5, 1).finish()).map(drop), + Err("its CPU profile holds the unknown field 5".to_owned()) + ); +} + +#[test] +fn malformed_manifests_are_refused() { + let template = ManifestSpec::template(); + let malformed = |bytes: &[u8]| refusal(bytes, ErrorCode::Io); + // A field that is not repeated, twice. + assert_eq!( + malformed(&template.encode_with(Proto::default().varint(5, 2))), + "the snapshot manifest is malformed: it holds field 5 more than once" + ); + // A field of another type than its own. + assert_eq!( + decode_topology(&Proto::default().text(1, "one").finish()).map(drop), + Err("its processor topology holds field 1, which is not an integer".to_owned()) + ); + assert_eq!( + decode_contract(&Proto::default().varint(4, 1).finish()).map(drop), + Err("its machine contract holds field 4, which is not bytes".to_owned()) + ); + // A text that is not UTF-8, and a number that does not fit its type. + let mut spec = template.clone(); + spec.openvmm_version.clear(); + assert_eq!( + malformed(&spec.encode_with(Proto::default().bytes(3, &[0xff, 0xfe]))), + "the snapshot manifest is malformed: it holds field 3, which is not UTF-8" + ); + spec = template.clone(); + spec.vp_count = 0; + assert_eq!( + malformed(&spec.encode_with(Proto::default().varint(5, 1 << 32))), + "the snapshot manifest is malformed: it holds field 5, which does not fit 32 bits" + ); + assert_eq!( + decode_block(&Proto::default().varint(2, 2).finish()).map(drop), + Err("its sandbox block holds field 2, which is 2, not a boolean".to_owned()) + ); + // An address space that does not exist. + let range = Proto::default().text(1, "io").varint(3, 1).finish(); + assert_eq!( + decode_device_range(&range, 4), + Err("a range of its device 4 is in the unknown address space \"io\"".to_owned()) + ); + // An expansion range has no file offset, field 3, not even an explicit zero one. + for offset in [4096, 0] { + let expansion = Proto::default() + .varint(1, 4096) + .varint(2, 4096) + .some(3, offset) + .finish(); + assert_eq!( + decode_contract(&Proto::default().always_bytes(30, &expansion).finish()).map(drop), + Err("its memory expansion range holds the unknown field 3".to_owned()) + ); + } +} + +#[test] +fn truncated_manifests_are_refused() { + let bytes = ManifestSpec::template().encode(); + for len in 0..bytes.len() { + let error = + TemplateManifest::parse(&bytes[..len]).expect_err("a truncated manifest is refused"); + // A manifest that ends between its fields lacks some, such as its format magic. + assert!( + matches!(error.code(), ErrorCode::Io | ErrorCode::FormatUnsupported), + "{len}: {error}" + ); + } + // A value that runs past its message is truncated. + assert_eq!( + refusal(&bytes[..bytes.len() - 1], ErrorCode::Io), + "the snapshot manifest is malformed: it is truncated" + ); +} + +#[test] +fn bounded_lists_are_refused_past_their_bounds() { + let mut contract = Proto::default(); + for _ in 0..=MAX_DEVICES { + contract = contract.message( + 8, + Proto::default() + .text(1, "device") + .text(2, "unit") + .text(3, "kind"), + ); + } + assert_eq!( + decode_contract(&contract.finish()).map(drop), + Err(format!("its devices hold more than {MAX_DEVICES} items")) + ); + let mut contract = Proto::default(); + for _ in 0..=MAX_STATE_UNITS { + contract = contract.text(9, "unit"); + } + assert_eq!( + decode_contract(&contract.finish()).map(drop), + Err(format!( + "its state units hold more than {MAX_STATE_UNITS} items" + )) + ); + let mut contract = Proto::default(); + for _ in 0..=MAX_BLOCKS { + contract = contract.message(21, Proto::default().text(1, "distro")); + } + assert_eq!( + decode_contract(&contract.finish()).map(drop), + Err(format!( + "its sandbox blocks hold more than {MAX_BLOCKS} items" + )) + ); + let identity = vec![1; MAX_ATTACHMENT_IDENTITY_LEN + 1]; + assert_eq!( + decode_attachment(&Proto::default().bytes(6, &identity).finish(), 0), + Err(format!( + "its attachment 0 has an identity of more than {MAX_ATTACHMENT_IDENTITY_LEN} bytes" + )) + ); +} + +#[test] +fn oversized_manifests_are_refused() { + let bytes = vec![0; MAX_MANIFEST_LEN as usize + 1]; + assert_eq!( + refusal(&bytes, ErrorCode::Io), + format!( + "the snapshot manifest holds {} bytes, more than the {MAX_MANIFEST_LEN} that a \ + manifest may hold", + MAX_MANIFEST_LEN + 1 + ) + ); +} + +#[test] +fn manifests_that_are_not_a_templates_are_refused() { + let io = ErrorCode::Io; + for (tier, policy, sections) in [ + ("workload-start", "clone", 1), + ("instance-checkpoint", "resume", 7), + ("platform", "resume", 1), + ("platform", "clone", 7), + ("platform", "clone", 0), + ("", "", 0), + ] { + assert_refused( + |spec| { + spec.tier = tier.to_owned(); + spec.restore_policy = policy.to_owned(); + spec.consumed_sections = sections; + }, + io, + &format!( + "is not a template's: it was captured at tier {tier:?} with restore policy \ + {policy:?} after consuming configuration sections {sections:#x}" + ), + ); + } + let blocks = "is not a template's: its sandbox blocks are not a read-only distro whose \ + identity is unbound and a fresh scratch"; + let bound: [fn(&mut ManifestSpec); 8] = [ + // A distro bound to an image. + |spec| { + let distro = &mut spec.contract().blocks[0]; + distro.identity_kind = "sha256".to_owned(); + distro.identity = vec![1; 32]; + }, + |spec| spec.contract().blocks[0].read_only = false, + // A scratch paired with the capture. + |spec| { + let scratch = &mut spec.contract().blocks[1]; + scratch.identity_kind = "sha256".to_owned(); + scratch.identity = vec![1; 32]; + scratch.artifact = "scratch.img".to_owned(); + }, + |spec| spec.contract().blocks[1].restore_mode = "private-copy".to_owned(), + |spec| { + spec.contract().blocks.pop(); + }, + |spec| spec.contract().blocks.reverse(), + |spec| { + let mut runtime = spec.contract().blocks[0].clone(); + runtime.role = "runtime".to_owned(); + spec.contract().blocks.insert(1, runtime); + }, + |spec| spec.contract().blocks.clear(), + ]; + for change in bound { + assert_refused(change, io, blocks); + } + let slot = "is not a template's: its filesystem slot is not the dormant slot of version 1"; + assert_refused( + |spec| spec.contract().filesystem = Some(Proto::default().text(1, "/mnt/share").finish()), + io, + slot, + ); + assert_refused( + |spec| spec.contract().filesystem = Some(Vec::new()), + io, + slot, + ); + assert_refused(|spec| spec.contract().filesystem_slot_version = 0, io, slot); + assert_refused( + |spec| spec.contract().expansion_version = 0, + io, + "is not a template's: it declares memory expansion version 0", + ); + assert_refused(|spec| spec.contract = None, io, "has no machine contract"); + assert_refused( + |spec| spec.contract().time = None, + io, + "has no time contract", + ); + assert_refused(|spec| spec.contract().cpu = None, io, "has no CPU profile"); +} + +/// A change to the manifest of a template. +type Change = fn(&mut ManifestSpec); + +#[test] +fn manifests_of_invalid_machines_are_refused() { + let cases: &[(Change, &str)] = &[ + ( + |spec| spec.page_size = 8192, + "describes a VM of architecture \"x86_64\" with 8192-byte pages", + ), + ( + |spec| spec.architecture = "aarch64".to_owned(), + "describes a VM of architecture \"aarch64\"", + ), + ( + |spec| spec.state_size = 0, + "has a saved state of 0 bytes, not 1 to 268435456", + ), + ( + |spec| spec.state_size = 256 * MIB + 1, + "has a saved state of 268435457 bytes", + ), + ( + |spec| spec.created_at.1 = 1_000_000_000, + "has the invalid creation time of 1761300000 seconds and 1000000000 nanoseconds", + ), + ( + |spec| spec.created_at = (-1, 0), + "has the invalid creation time of -1 seconds", + ), + ( + |spec| spec.contract().source_hypervisor = "hyperv".to_owned(), + "was captured by the unknown backend \"hyperv\"", + ), + ( + |spec| spec.cpu().id = "intel.icelake-sp".to_owned(), + "has the CPU profile \"intel.icelake-sp\", which is not VENDOR.GENERATION.vREVISION", + ), + ( + |spec| spec.cpu().id = "Intel.icelake-sp.v1".to_owned(), + "which is not VENDOR.GENERATION.vREVISION", + ), + ( + |spec| spec.cpu().id = "intel.icelake-sp.v0".to_owned(), + "which is not VENDOR.GENERATION.vREVISION", + ), + ( + |spec| spec.cpu().id = "intel.icelake-sp.v01".to_owned(), + "which is not VENDOR.GENERATION.vREVISION", + ), + ( + |spec| spec.cpu().digest.truncate(31), + "has a CPU profile whose digest is not 32 bytes", + ), + ( + |spec| spec.cpu().effective_cpuid.push(0), + "has an effective CPUID of 89 bytes, not 1 to 1024 entries of 44 bytes", + ), + ( + |spec| spec.cpu().effective_cpuid.clear(), + "has an effective CPUID of 0 bytes", + ), + ( + |spec| spec.cpu().effective_cpuid = vec![0; 1025 * 44], + "has an effective CPUID of 45100 bytes", + ), + ( + |spec| spec.contract().command_line_digest = Some(vec![0; 32]), + "has an effective command line whose digest does not match it", + ), + ( + |spec| spec.contract().command_line_digest = Some(vec![0; 31]), + "has an effective command line whose digest is not 32 bytes", + ), + ( + |spec| spec.contract().command_line = "x".repeat(2048), + "has an effective command line of 2048 bytes, more than the 2047", + ), + ( + |spec| spec.contract().memory_ranges[0].1 -= 4096, + "has 536870912 bytes of RAM in 1 ranges that do not hold it", + ), + ( + |spec| spec.contract().memory_ranges.clear(), + "bytes of RAM in 0 ranges", + ), + ( + |spec| { + spec.contract().memory_ranges = vec![(0, 256 * MIB, 0), (4096 * MIB, 256 * MIB, 0)]; + }, + "has memory ranges that do not fill its memory file in order", + ), + ( + |spec| spec.contract().memory_capacity = 256 * MIB, + "has a RAM capacity of 268435456 bytes in blocks of 134217728 bytes", + ), + ( + |spec| spec.contract().memory_block_size = 0, + "in blocks of 0 bytes", + ), + ( + |spec| spec.contract().memory_block_size = 384 * MIB, + "in blocks of 402653184 bytes", + ), + ( + |spec| spec.contract().expansion_ranges[0].1 -= 128 * MIB, + "has memory expansion ranges that do not span its RAM capacity", + ), + ( + |spec| spec.contract().topology.4.push(2), + "has 2 processors, 3 APIC IDs, and 0 processors online at boot", + ), + ( + |spec| { + spec.vp_count = 3; + spec.contract().topology.4.push(2); + }, + "has 3 processors, 3 APIC IDs", + ), + ( + |spec| spec.contract().boot_online_vp_count = 4, + "and 4 processors online at boot, which a microVM cannot have", + ), + ( + |spec| spec.time().tsc_hz = 0, + "declares no TSC or LAPIC timer rate", + ), + ( + |spec| spec.time().apic_hz = 0, + "declares no TSC or LAPIC timer rate", + ), + ( + |spec| spec.contract().devices[2].order = 7, + "lists device 2 with the order 7", + ), + ( + |spec| spec.contract().devices[2].stable_id = "vp0".to_owned(), + "has the device ID \"vp0\" more than once", + ), + ( + |spec| spec.contract().devices[2].kind.clear(), + "has a device without an ID, a kind, or a state unit", + ), + ( + |spec| spec.contract().devices[2].transport = "t".repeat(257), + "or with one of more than 256 bytes: \"vmtime\"", + ), + ( + |spec| spec.contract().devices[4].feature_banks = vec![0; 65], + "has the device \"ioapic\" with more than 16 ranges, or more than 64 feature banks", + ), + ( + |spec| spec.contract().devices.clear(), + "has 0 devices, not 1 to 64", + ), + ( + |spec| spec.contract().interrupt_mode.clear(), + "names no interrupt delivery mode", + ), + ( + |spec| spec.contract().blocks[0].logical_block_size = 256, + "has a distro block whose logical block size 256 is not a power of two of at least \ + 512", + ), + ( + |spec| spec.contract().blocks[1].physical_block_size = 256, + "has a scratch block whose physical block size 256 is not a power of two", + ), + ( + |spec| spec.contract().blocks[1].length = 1000, + "has a scratch block whose length 1000 is not a nonzero multiple of its logical \ + block size", + ), + ( + |spec| spec.time().host_clock = "utc".to_owned(), + "has a time contract of the unknown host clock \"utc\"", + ), + ( + |spec| spec.time().host_id.truncate(15), + "has a time contract whose host identity is 15 bytes, not 16", + ), + ( + |spec| spec.time().boot_id.push(0), + "has a time contract whose boot identity is 17 bytes, not 16", + ), + ]; + for (change, expected) in cases { + assert_refused(change, ErrorCode::Io, expected); + } +} + +#[test] +fn manifests_of_invalid_nics_are_refused() { + let cases: &[(Change, &str)] = &[ + ( + |spec| spec.network().guest_mac.truncate(5), + "has a NIC whose MAC addresses are not 6 bytes", + ), + ( + |spec| spec.network().gateway_mac.clear(), + "has a NIC whose MAC addresses are not 6 bytes", + ), + ( + |spec| spec.network().prefix_length = 31, + "has a NIC with an invalid prefix length", + ), + ( + |spec| spec.network().prefix_length = 0, + "has a NIC with an invalid prefix length", + ), + ( + |spec| spec.network().ipv6_prefix_length = 129, + "has a NIC with an invalid prefix length", + ), + ( + |spec| spec.network().ipv6_gateway.clear(), + "has a NIC with an invalid IPv6 identity", + ), + ( + |spec| spec.network().policy_digest.truncate(31), + "has a NIC whose policy digest is not 32 bytes", + ), + ( + |spec| spec.network().profile.clear(), + "has a NIC without a network profile or a policy mode", + ), + ]; + for (change, expected) in cases { + let mut spec = ManifestSpec::with_network(); + change(&mut spec); + let message = refusal(&spec.encode(), ErrorCode::Io); + assert!(message.contains(expected), "{message:?} lacks {expected:?}"); + } + // A NIC without an IPv6 identity has none. + let mut spec = ManifestSpec::with_network(); + let network = spec.network(); + network.ipv6.clear(); + network.ipv6_prefix_length = 0; + network.ipv6_gateway.clear(); + assert_eq!(parse(&spec).machine.nic.unwrap().ipv6, None); +} + +#[test] +fn profile_ids_have_one_form() { + assert_eq!( + parse_profile_id("amd.genoa.v12"), + Some(("amd".to_owned(), "genoa".to_owned(), 12)) + ); + assert_eq!( + parse_profile_id("intel.host.v1"), + Some(("intel".to_owned(), "host".to_owned(), 1)) + ); + let long = format!("{}.genoa.v1", "a".repeat(33)); + for id in [ + "", + "amd", + "amd.genoa", + "amd.genoa.1", + "amd.genoa.v", + "amd.genoa.v0", + "amd.genoa.v01", + "amd.genoa.v1.extra", + "amd..v1", + ".genoa.v1", + "AMD.genoa.v1", + "amd.gen_oa.v1", + "amd.genoa.v99999999999", + &long, + ] { + assert_eq!(parse_profile_id(id), None, "{id:?}"); + } +} + +/// Writes `bytes` as the manifest of a new snapshot directory, and returns the directory. +fn snapshot(bytes: &[u8]) -> tempfile::TempDir { + let directory = tempfile::tempdir().unwrap(); + fs::write(directory.path().join(MANIFEST_NAME), bytes).unwrap(); + directory +} + +/// Returns the directory of the template fixture `name`, whose manifest OpenVMM wrote. +fn fixture(name: &str) -> std::path::PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/template") + .join(name) +} + +#[test] +fn the_manifests_that_openvmm_writes_are_read() { + // The fixtures describe the captures of the specs of these tests, which OpenVMM's own code + // encoded, but for the CPU profile's records, OpenVMM's test records of a profile, and the + // digest of the network policy, which OpenVMM computed. + for (name, mut spec) in [ + ("platform-kvm", ManifestSpec::template()), + ("platform-mshv-network", ManifestSpec::with_network()), + ] { + let manifest = TemplateManifest::read(&fixture(name)).unwrap(); + let machine = &manifest.machine; + assert_eq!(machine.cpu.id(), "intel.icelake-sp.v1", "{name}"); + assert_eq!(machine.cpu.effective_cpuid.len() % CPUID_ENTRY_LEN, 0); + let cpu = spec.cpu(); + cpu.digest = machine.cpu.digest.to_vec(); + cpu.effective_cpuid = machine.cpu.effective_cpuid.clone(); + if let Some(nic) = &machine.nic { + spec.network().policy_digest = nic.policy.digest.to_vec(); + spec.contract().source_hypervisor = "mshv".to_owned(); + } + assert_eq!(manifest, parse(&spec), "{name}"); + } +} + +/// Returns the message of the refusal of the manifest of the snapshot directory `directory`, +/// which must have the code `code`, and the error that caused it. +fn read_refusal(directory: &Path, code: ErrorCode) -> (String, String) { + let error = TemplateManifest::read(directory).expect_err("the manifest is refused"); + assert_eq!(error.code(), code, "{error}"); + let cause = std::error::Error::source(&error) + .map(ToString::to_string) + .unwrap_or_default(); + (error.message().to_owned(), cause) +} + +#[test] +fn read_reads_the_manifest_of_a_snapshot() { + let spec = ManifestSpec::with_network(); + let directory = snapshot(&spec.encode()); + assert_eq!( + TemplateManifest::read(directory.path()).unwrap(), + parse(&spec) + ); + // The messages of a manifest that is read name its path. + let mut invalid = spec; + invalid.tier = "workload-start".to_owned(); + let directory = snapshot(&invalid.encode()); + let (message, _) = read_refusal(directory.path(), ErrorCode::Io); + let path = directory.path().join(MANIFEST_NAME); + assert!( + message.starts_with(&format!( + "the snapshot manifest {} is not a template's", + path.display() + )), + "{message}" + ); +} + +#[test] +fn read_refuses_a_missing_manifest_as_not_found() { + let directory = tempfile::tempdir().unwrap(); + let (message, _) = read_refusal(directory.path(), ErrorCode::NotFound); + let path = directory.path().join(MANIFEST_NAME); + assert_eq!( + message, + format!("cannot read the snapshot manifest {}", path.display()) + ); +} + +#[test] +fn read_refuses_more_than_a_manifest_may_hold() { + let directory = snapshot(&vec![0; MAX_MANIFEST_LEN as usize + 1]); + let (_, cause) = read_refusal(directory.path(), ErrorCode::Io); + assert_eq!( + cause, + format!("it holds more than the {MAX_MANIFEST_LEN} bytes that a manifest may hold") + ); +} + +#[test] +fn read_refuses_a_directory() { + let directory = tempfile::tempdir().unwrap(); + fs::create_dir(directory.path().join(MANIFEST_NAME)).unwrap(); + read_refusal(directory.path(), ErrorCode::Io); +} + +#[test] +fn read_refuses_a_link() { + let directory = tempfile::tempdir().unwrap(); + let target = directory.path().join("target.bin"); + fs::write(&target, ManifestSpec::template().encode()).unwrap(); + let path = directory.path().join(MANIFEST_NAME); + #[cfg(unix)] + let linked = std::os::unix::fs::symlink(&target, &path); + #[cfg(windows)] + let linked = std::os::windows::fs::symlink_file(&target, &path); + if linked.is_err() { + // Windows lets only Developer Mode or a privilege create a symbolic link. + eprintln!("skipped: this host does not let the test create a symbolic link"); + return; + } + let (_, cause) = read_refusal(directory.path(), ErrorCode::Io); + assert_eq!(cause, "it is a link, or not a regular file"); +} + +#[cfg(windows)] +#[test] +fn read_refuses_a_junction() { + use std::os::windows::process::CommandExt; + use std::process::{Command, Stdio}; + + let directory = tempfile::tempdir().unwrap(); + let target = directory.path().join("target"); + fs::create_dir(&target).unwrap(); + fs::write( + target.join(MANIFEST_NAME), + ManifestSpec::template().encode(), + ) + .unwrap(); + let path = directory.path().join(MANIFEST_NAME); + let status = Command::new("cmd") + .raw_arg(format!( + "/C mklink /J \"{}\" \"{}\"", + path.display(), + target.display() + )) + .stdout(Stdio::null()) + .status() + .unwrap(); + assert!(status.success(), "cannot link {}", path.display()); + read_refusal(directory.path(), ErrorCode::Io); +} + +#[cfg(unix)] +#[test] +fn read_refuses_a_fifo_without_waiting_for_a_writer() { + use std::sync::mpsc; + use std::thread; + + use nix::sys::stat::Mode; + + let directory = tempfile::tempdir().unwrap(); + nix::unistd::mkfifo( + &directory.path().join(MANIFEST_NAME), + Mode::S_IRUSR | Mode::S_IWUSR, + ) + .unwrap(); + // A read that waited for a writer would never end, so it runs on a thread that the test can + // leave behind. + let (sender, receiver) = mpsc::channel(); + let path = directory.path().to_owned(); + thread::spawn(move || { + sender + .send(TemplateManifest::read(&path).map(drop)) + .unwrap() + }); + let error = receiver + .recv_timeout(Duration::from_secs(10)) + .expect("the read does not wait for a writer") + .unwrap_err(); + assert_eq!(error.code(), ErrorCode::Io, "{error}"); + let cause = std::error::Error::source(&error).map(ToString::to_string); + assert_eq!( + cause.as_deref(), + Some("it is a link, or not a regular file") + ); +} diff --git a/nvx-rs/src/host/template/mod.rs b/nvx-rs/src/host/template/mod.rs new file mode 100644 index 000000000..320462985 --- /dev/null +++ b/nvx-rs/src/host/template/mod.rs @@ -0,0 +1,48 @@ +//! Templates: captures of a microVM from which independent clones are restored, and the metadata +//! that decides which clones a template can serve, and on which hosts. +//! +//! A template is a snapshot that OpenVMM captured at its platform tier, after it consumed the +//! invariant configuration of a microVM alone, for clones: a VM booted with a placeholder +//! `distro`, a fresh scratch, and a dormant filesystem slot, which each clone binds to its own +//! image, scratch, and export when it is restored. Its compatibility key, [`TemplateKey`], is what +//! a clone and the host that restores it must match: the runtime and the contracts through which +//! the template is restored, and the machine that it restores. +//! +//! - [`CaptureClass`] holds the invariant inputs of a capture, known before it: the runtime, the +//! guest's build, the backend, the CPU profile, the resources, the geometries of the disks, the +//! network, and the other invariant settings. +//! - [`TemplateManifest::read`] reads the manifest that OpenVMM wrote for a capture, with its time +//! contract, and refuses one that is not a template's. +//! - [`TemplateKey::new`] derives the key of a template from its class and its manifest, after it +//! checks that the capture made the machine that its class requested. +//! +//! # Canonical encoding +//! +//! A key and a class each have one canonical encoding, which [`TemplateKey::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: +//! +//! - 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`. +//! - A boolean is one byte, 0 or 1. +//! - A byte string of fixed length, such as a digest or an address, is its bytes. +//! - A byte string of variable length is its length, a `u32`, then its bytes. A text is the byte +//! string of its UTF-8 encoding. +//! - An optional value is one byte: 0 without the value, and 1 before it. +//! - A list is its count, a `u32`, then its items in order. +//! +//! [`TemplateKey::from_bytes`] refuses any other encoding, and bytes left after it. The vectors in +//! `tests/vectors/template-key-v1.txt` fix version 1 of a key's encoding. + +mod canonical; +mod key; +mod manifest; +#[cfg(test)] +mod test_support; +mod wire; + +pub use self::key::{CaptureClass, ClassIdentity, Invariants, KeyIdentity, TemplateKey}; +pub use self::manifest::{ + BlockGeometry, HostClock, MANIFEST_NAME, MAX_MANIFEST_LEN, TemplateManifest, TimeContract, +}; diff --git a/nvx-rs/src/host/template/test_support.rs b/nvx-rs/src/host/template/test_support.rs new file mode 100644 index 000000000..4587466b1 --- /dev/null +++ b/nvx-rs/src/host/template/test_support.rs @@ -0,0 +1,988 @@ +//! Builders of the manifests and capture classes of templates, for their tests. +//! +//! [`Proto`] writes the protobuf wire format as OpenVMM's `mesh` does, independently of the reader +//! that the tests check, and [`ManifestSpec`] holds each field of a manifest, so that a test +//! changes one field and encodes the others as they were. + +use sha2::{Digest, Sha256}; + +use super::{BlockGeometry, CaptureClass, Invariants}; +use crate::host::network::{Access, NetworkPolicy}; +use crate::host::sandbox::SandboxNetwork; +use crate::host::store::{ArtifactIdentity, RuntimeIdentity}; +use crate::hypervisor::Hypervisor; + +/// Bytes in a MiB. +pub(super) const MIB: u64 = 1024 * 1024; + +/// A protobuf message under construction, whose fields are written in the order of the calls. +/// +/// Like `mesh`, the typed methods omit a field whose value is zero or empty, but for +/// [`message`](Self::message) and [`some`](Self::some), which write an `Option` that is `Some`. +#[derive(Debug, Clone, Default)] +pub(super) struct Proto(Vec); + +impl Proto { + /// Appends `value` to `bytes` as a varint. + pub(super) fn put_varint(bytes: &mut Vec, mut value: u64) { + loop { + let byte = (value & 0x7f) as u8; + value >>= 7; + if value == 0 { + bytes.push(byte); + return; + } + bytes.push(byte | 0x80); + } + } + + /// Appends the key of field `number` of wire type `wire_type`. + pub(super) fn key(mut self, number: u32, wire_type: u8) -> Self { + Self::put_varint(&mut self.0, u64::from(number) << 3 | u64::from(wire_type)); + self + } + + /// Appends field `number` as the varint `value`, even if it is 0. + pub(super) fn some(self, number: u32, value: u64) -> Self { + let mut message = self.key(number, 0); + Self::put_varint(&mut message.0, value); + message + } + + /// Appends field `number` as the varint `value`, unless it is 0. + pub(super) fn varint(self, number: u32, value: u64) -> Self { + if value == 0 { + self + } else { + self.some(number, value) + } + } + + /// Appends field `number` as the bytes `bytes`, even if they are empty. + pub(super) fn always_bytes(self, number: u32, bytes: &[u8]) -> Self { + let mut message = self.key(number, 2); + Self::put_varint(&mut message.0, bytes.len() as u64); + message.0.extend_from_slice(bytes); + message + } + + /// Appends field `number` as the bytes `bytes`, unless they are empty. + pub(super) fn bytes(self, number: u32, bytes: &[u8]) -> Self { + if bytes.is_empty() { + self + } else { + self.always_bytes(number, bytes) + } + } + + /// Appends field `number` as the text `text`, unless it is empty. + pub(super) fn text(self, number: u32, text: &str) -> Self { + self.bytes(number, text.as_bytes()) + } + + /// Appends field `number` as the message `message`, even if it is empty, as `mesh` writes an + /// `Option` that is `Some`, or an element of a repeated field. + pub(super) fn message(self, number: u32, message: Proto) -> Self { + self.always_bytes(number, &message.0) + } + + /// Appends field `number` as the packed varints `values`, unless there are none. + pub(super) fn packed(self, number: u32, values: &[u32]) -> Self { + let mut packed = Vec::new(); + for &value in values { + Self::put_varint(&mut packed, u64::from(value)); + } + self.bytes(number, &packed) + } + + /// Appends `bytes` as they are. + pub(super) fn raw(mut self, bytes: &[u8]) -> Self { + self.0.extend_from_slice(bytes); + self + } + + /// Returns the message's bytes. + pub(super) fn finish(self) -> Vec { + self.0 + } +} + +/// `openvmm.snapshot.SnapshotDevice`. +#[derive(Debug, Clone)] +pub(super) struct DeviceSpec { + /// Field 1. + pub(super) stable_id: String, + /// Field 2. + pub(super) state_unit: String, + /// Field 3. + pub(super) kind: String, + /// Field 4. + pub(super) order: u32, + /// Field 5: each range's address space, start, and length. + pub(super) ranges: Vec<(String, u64, u64)>, + /// Field 6. + pub(super) irq: Option, + /// Field 7. + pub(super) transport: String, + /// Field 8. + pub(super) feature_banks: Vec, + /// Field 9. + pub(super) queue_count: u32, + /// Field 10. + pub(super) queue_max_sizes: Vec, +} + +impl DeviceSpec { + /// Returns a platform device, which has no transport, features, or queues. + fn platform( + stable_id: &str, + state_unit: &str, + kind: &str, + ranges: &[(&str, u64, u64)], + irq: Option, + ) -> Self { + Self { + stable_id: stable_id.to_owned(), + state_unit: state_unit.to_owned(), + kind: kind.to_owned(), + order: 0, + ranges: ranges + .iter() + .map(|&(space, start, length)| (space.to_owned(), start, length)) + .collect(), + irq, + transport: String::new(), + feature_banks: Vec::new(), + queue_count: 0, + queue_max_sizes: Vec::new(), + } + } + + /// Returns a virtio-mmio device of `kind` at `base`, whose state unit `unit` names. + fn virtio( + stable_id: &str, + kind: &str, + unit: &str, + base: u64, + irq: u32, + features: u64, + queues: u32, + ) -> Self { + Self { + stable_id: stable_id.to_owned(), + state_unit: format!("{unit}-{base}"), + kind: kind.to_owned(), + order: 0, + ranges: vec![("mmio".to_owned(), base, 0x1000)], + irq: Some(irq), + transport: "virtio-mmio".to_owned(), + feature_banks: vec![features as u32, (features >> 32) as u32], + queue_count: queues, + queue_max_sizes: vec![256; queues as usize], + } + } + + /// Encodes the device. + fn encode(&self) -> Proto { + let mut message = Proto::default() + .text(1, &self.stable_id) + .text(2, &self.state_unit) + .text(3, &self.kind) + .varint(4, self.order.into()); + for (space, start, length) in &self.ranges { + message = message.message( + 5, + Proto::default() + .text(1, space) + .varint(2, *start) + .varint(3, *length), + ); + } + if let Some(irq) = self.irq { + message = message.some(6, irq.into()); + } + message + .text(7, &self.transport) + .packed(8, &self.feature_banks) + .varint(9, self.queue_count.into()) + .packed(10, &self.queue_max_sizes) + } +} + +/// `openvmm.snapshot.SnapshotAttachment`. +#[derive(Debug, Clone)] +pub(super) struct AttachmentSpec { + /// Field 1. + pub(super) stable_id: String, + /// Field 2. + pub(super) kind: String, + /// Field 3. + pub(super) required: bool, + /// Field 4. + pub(super) reconnect_policy: String, + /// Field 5. + pub(super) identity_kind: String, + /// Field 6. + pub(super) identity: Vec, + /// Field 7. + pub(super) length: u64, + /// Field 8. + pub(super) reconnect_timeout_ms: u64, +} + +impl AttachmentSpec { + /// Returns an attachment that is not required, which `policy` reconnects. + fn new( + stable_id: &str, + kind: &str, + policy: &str, + identity_kind: &str, + identity: &[u8], + ) -> Self { + Self { + stable_id: stable_id.to_owned(), + kind: kind.to_owned(), + required: false, + reconnect_policy: policy.to_owned(), + identity_kind: identity_kind.to_owned(), + identity: identity.to_vec(), + length: 0, + reconnect_timeout_ms: 0, + } + } + + /// Encodes the attachment. + fn encode(&self) -> Proto { + Proto::default() + .text(1, &self.stable_id) + .text(2, &self.kind) + .varint(3, self.required.into()) + .text(4, &self.reconnect_policy) + .text(5, &self.identity_kind) + .bytes(6, &self.identity) + .varint(7, self.length) + .varint(8, self.reconnect_timeout_ms) + } +} + +/// `openvmm.snapshot.SnapshotMicrovmNetwork`. +#[derive(Debug, Clone)] +pub(super) struct NetworkSpec { + /// Field 1. + pub(super) ipv4: [u8; 4], + /// Field 2. + pub(super) prefix_length: u32, + /// Field 3. + pub(super) gateway: [u8; 4], + /// Field 4. + pub(super) guest_mac: Vec, + /// Field 5. + pub(super) gateway_mac: Vec, + /// Field 6. + pub(super) policy_mode: String, + /// Field 7. + pub(super) policy_digest: Vec, + /// Field 8. + pub(super) policy_required: bool, + /// Field 9. + pub(super) profile: String, + /// Field 10. + pub(super) policy_encoding_version: u32, + /// Field 11. + pub(super) ipv6: Vec, + /// Field 12. + pub(super) ipv6_prefix_length: u32, + /// Field 13. + pub(super) ipv6_gateway: Vec, +} + +impl NetworkSpec { + /// Encodes the network, its profile first, as `mesh` writes it. + fn encode(&self) -> Proto { + Proto::default() + .text(9, &self.profile) + .varint(1, u32::from_be_bytes(self.ipv4).into()) + .varint(2, self.prefix_length.into()) + .varint(3, u32::from_be_bytes(self.gateway).into()) + .bytes(4, &self.guest_mac) + .bytes(5, &self.gateway_mac) + .text(6, &self.policy_mode) + .bytes(7, &self.policy_digest) + .varint(8, self.policy_required.into()) + .varint(10, self.policy_encoding_version.into()) + .bytes(11, &self.ipv6) + .varint(12, self.ipv6_prefix_length.into()) + .bytes(13, &self.ipv6_gateway) + } +} + +/// `openvmm.snapshot.SnapshotMicrovmSandboxBlock`. +#[derive(Debug, Clone)] +pub(super) struct BlockSpec { + /// Field 1. + pub(super) role: String, + /// Field 2. + pub(super) read_only: bool, + /// Field 3. + pub(super) length: u64, + /// Field 4. + pub(super) identity_kind: String, + /// Field 5. + pub(super) identity: Vec, + /// Field 6. + pub(super) artifact: String, + /// Field 7. + pub(super) logical_block_size: u32, + /// Field 8. + pub(super) physical_block_size: u32, + /// Field 9. + pub(super) restore_mode: String, +} + +impl BlockSpec { + /// Encodes the block. + fn encode(&self) -> Proto { + Proto::default() + .text(1, &self.role) + .varint(2, self.read_only.into()) + .varint(3, self.length) + .text(4, &self.identity_kind) + .bytes(5, &self.identity) + .text(6, &self.artifact) + .varint(7, self.logical_block_size.into()) + .varint(8, self.physical_block_size.into()) + .text(9, &self.restore_mode) + } +} + +/// `openvmm.snapshot.SnapshotTimeContract`. +#[derive(Debug, Clone)] +pub(super) struct TimeSpec { + /// Field 1. + pub(super) abi_version: u32, + /// Field 2. + pub(super) tsc_hz: u64, + /// Field 3. + pub(super) tolerance_ppm: u32, + /// Field 4. + pub(super) apic_hz: u64, + /// Field 5. + pub(super) capture_tsc: u64, + /// Field 6. + pub(super) capture_utc_ns: u64, + /// Field 7. + pub(super) capture_monotonic_ns: u64, + /// Field 8. + pub(super) host_clock: String, + /// Field 9. + pub(super) host_id: Vec, + /// Field 10. + pub(super) boot_id: Vec, + /// Field 11. + pub(super) lineage: u32, + /// Field 12. + pub(super) cut_to_anchor_ns: Option, +} + +impl TimeSpec { + /// Encodes the time contract. + fn encode(&self) -> Proto { + let message = Proto::default() + .varint(1, self.abi_version.into()) + .varint(2, self.tsc_hz) + .varint(3, self.tolerance_ppm.into()) + .varint(4, self.apic_hz) + .varint(5, self.capture_tsc) + .varint(6, self.capture_utc_ns) + .varint(7, self.capture_monotonic_ns) + .text(8, &self.host_clock) + .bytes(9, &self.host_id) + .bytes(10, &self.boot_id) + .varint(11, self.lineage.into()); + match self.cut_to_anchor_ns { + Some(cut) => message.some(12, cut), + None => message, + } + } +} + +/// `openvmm.snapshot.SnapshotCpuProfile`. +#[derive(Debug, Clone)] +pub(super) struct CpuSpec { + /// Field 1. + pub(super) id: String, + /// Field 2. + pub(super) digest: Vec, + /// Field 3. + pub(super) document: Vec, + /// Field 4. + pub(super) effective_cpuid: Vec, + /// Field 6. + pub(super) signature: u32, +} + +impl CpuSpec { + /// Encodes the CPU profile record. + fn encode(&self) -> Proto { + Proto::default() + .text(1, &self.id) + .bytes(2, &self.digest) + .bytes(3, &self.document) + .bytes(4, &self.effective_cpuid) + .varint(6, self.signature.into()) + } +} + +/// `openvmm.snapshot.SnapshotMachineContract`. +#[derive(Debug, Clone)] +pub(super) struct ContractSpec { + /// Field 1. + pub(super) machine_profile: String, + /// Field 2. + pub(super) abi_version: u32, + /// Field 3. + pub(super) source_hypervisor: String, + /// Field 4. + pub(super) command_line: String, + /// Field 5, or `None` for the SHA-256 digest of the command line. + pub(super) command_line_digest: Option>, + /// Field 6: each range's start, length, and file offset. + pub(super) memory_ranges: Vec<(u64, u64, u64)>, + /// Field 7: the sockets, the dies of each socket, the cores of each die, the threads of each + /// core, and the APIC IDs. + pub(super) topology: (u32, u32, u32, u32, Vec), + /// Field 8. + pub(super) devices: Vec, + /// Field 9. + pub(super) state_units: Vec, + /// Field 10. + pub(super) attachments: Vec, + /// Field 16. + pub(super) boot_layout_version: u32, + /// Field 18. + pub(super) network: Option, + /// Field 19: the encoded policy of an attached filesystem. + pub(super) filesystem: Option>, + /// Field 21. + pub(super) blocks: Vec, + /// Field 22. + pub(super) filesystem_slot_version: u32, + /// Field 23. + pub(super) boot_online_vp_count: u32, + /// Field 24. + pub(super) interrupt_mode: String, + /// Fields 25 and 26: the start and the length of the shared interrupt-status page. + pub(super) status_page: (u64, u64), + /// Field 27. + pub(super) expansion_version: u32, + /// Field 28. + pub(super) memory_capacity: u64, + /// Field 29. + pub(super) memory_block_size: u64, + /// Field 30: each range's start and length. + pub(super) expansion_ranges: Vec<(u64, u64)>, + /// Field 31. + pub(super) time: Option, + /// Field 32. + pub(super) cpu: Option, +} + +impl ContractSpec { + /// Gives each device its index as its order. + pub(super) fn renumber(&mut self) { + for (index, device) in self.devices.iter_mut().enumerate() { + device.order = index as u32; + } + } + + /// Encodes the contract. + fn encode(&self) -> Proto { + let digest = self + .command_line_digest + .clone() + .unwrap_or_else(|| Sha256::digest(&self.command_line).to_vec()); + let mut message = Proto::default() + .text(1, &self.machine_profile) + .varint(2, self.abi_version.into()) + .text(3, &self.source_hypervisor) + .text(4, &self.command_line) + .bytes(5, &digest); + for &(start, length, offset) in &self.memory_ranges { + message = message.message( + 6, + Proto::default() + .varint(1, start) + .varint(2, length) + .varint(3, offset), + ); + } + let (sockets, dies, cores, threads, apic_ids) = &self.topology; + message = message.message( + 7, + Proto::default() + .varint(1, (*sockets).into()) + .varint(2, (*dies).into()) + .varint(3, (*cores).into()) + .varint(4, (*threads).into()) + .packed(5, apic_ids), + ); + for device in &self.devices { + message = message.message(8, device.encode()); + } + for unit in &self.state_units { + message = message.always_bytes(9, unit.as_bytes()); + } + for attachment in &self.attachments { + message = message.message(10, attachment.encode()); + } + message = message.varint(16, self.boot_layout_version.into()); + if let Some(network) = &self.network { + message = message.message(18, network.encode()); + } + if let Some(filesystem) = &self.filesystem { + message = message.always_bytes(19, filesystem); + } + for block in &self.blocks { + message = message.message(21, block.encode()); + } + message = message + .varint(22, self.filesystem_slot_version.into()) + .varint(23, self.boot_online_vp_count.into()) + .text(24, &self.interrupt_mode) + .varint(25, self.status_page.0) + .varint(26, self.status_page.1) + .varint(27, self.expansion_version.into()) + .varint(28, self.memory_capacity) + .varint(29, self.memory_block_size); + for &(start, length) in &self.expansion_ranges { + message = message.message(30, Proto::default().varint(1, start).varint(2, length)); + } + if let Some(time) = &self.time { + message = message.message(31, time.encode()); + } + if let Some(cpu) = &self.cpu { + message = message.message(32, cpu.encode()); + } + message + } +} + +/// `openvmm.snapshot.SnapshotManifest`. +#[derive(Debug, Clone)] +pub(super) struct ManifestSpec { + /// Field 1. + pub(super) version: u32, + /// Field 2: seconds and nanoseconds. + pub(super) created_at: (i64, i32), + /// Field 3. + pub(super) openvmm_version: String, + /// Field 4. + pub(super) memory_size: u64, + /// Field 5. + pub(super) vp_count: u32, + /// Field 6. + pub(super) page_size: u32, + /// Field 7. + pub(super) architecture: String, + /// Field 8. + pub(super) state_size: u64, + /// Field 11. + pub(super) contract: Option, + /// Field 12. + pub(super) magic: Vec, + /// Field 13. + pub(super) schema_version: u32, + /// Field 14. + pub(super) root_type: String, + /// Field 15. + pub(super) tier: String, + /// Field 16. + pub(super) restore_policy: String, + /// Field 17. + pub(super) consumed_sections: u32, +} + +/// The kernel command line of the template of [`ManifestSpec::template`]. +const COMMAND_LINE: &str = "earlycon=xe9 console=hvc1 reboot=t panic=-1 nvx_sandbox=1 \ + nvx_config=0xd0010000,65536 nr_cpus=2 nvx_snapshot_tier=platform \ + virtio_mmio.device=0x1000@0xd0001000:6 \ + virtio_mmio.device=0x1000@0xd0002000:7 \ + virtio_mmio.device=0x1000@0xd0003000:4 \ + virtio_mmio.device=0x1000@0xd0006000:11 \ + virtio_mmio.device=0x1000@0xd0007000:3 nvx_control_tty=hvc2"; + +impl ManifestSpec { + /// Returns the manifest of a template: 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. + pub(super) fn template() -> Self { + let (pmio, mmio) = ("pmio", "mmio"); + let platform = DeviceSpec::platform; + let mut contract = ContractSpec { + machine_profile: "microvm".to_owned(), + abi_version: 2, + source_hypervisor: "kvm".to_owned(), + command_line: COMMAND_LINE.to_owned(), + command_line_digest: None, + memory_ranges: vec![(0, 512 * MIB, 0)], + topology: (1, 1, 2, 1, vec![0, 1]), + devices: vec![ + platform("partition", "partition", "partition", &[], None), + platform("vp0", "partition", "vcpu", &[], None), + platform("vmtime", "vmtime", "clock", &[], None), + platform( + "pic", + "pic", + "pic", + &[(pmio, 0x20, 2), (pmio, 0xa0, 2)], + None, + ), + platform( + "ioapic", + "ioapic", + "ioapic", + &[(mmio, 0xfec0_0000, 0x1000)], + None, + ), + platform( + "lapic", + "partition", + "lapic", + &[(mmio, 0xfee0_0000, 0x1000)], + None, + ), + platform("pit", "pit", "pit", &[(pmio, 0x40, 4)], Some(0)), + platform("rtc", "rtc", "rtc", &[(pmio, 0x70, 2)], Some(8)), + platform( + "microvm-portb", + "microvm-portb", + "portb", + &[(pmio, 0xe9, 2)], + None, + ), + platform( + "microvm-shutdown", + "microvm-shutdown", + "shutdown", + &[(pmio, 0x604, 1)], + None, + ), + platform( + "microvm-snapshot-request", + "microvm-snapshot-request", + "snapshot-request", + &[(pmio, 0x605, 1)], + None, + ), + DeviceSpec::virtio( + "fs:microvm0", + "virtio-fs", + "virtiofs", + 0xd000_1000, + 6, + 0x3_3000_0000, + 2, + ), + DeviceSpec::virtio( + "console:microvm-virtio0", + "virtio-console", + "virtio-console", + 0xd000_2000, + 7, + 0x3_3000_0001, + 2, + ), + DeviceSpec::virtio( + "blk:sandbox:distro", + "virtio-blk", + "virtio-blk", + 0xd000_3000, + 4, + 0x3_3000_0664, + 1, + ), + DeviceSpec::virtio( + "blk:sandbox:scratch", + "virtio-blk", + "virtio-blk", + 0xd000_6000, + 11, + 0x3_3000_0644, + 1, + ), + DeviceSpec::virtio( + "console:microvm-control0", + "virtio-control-console", + "virtio-control-console", + 0xd000_7000, + 3, + 0x3_3000_0001, + 2, + ), + ], + state_units: Vec::new(), + attachments: vec![ + AttachmentSpec::new( + "console:microvm-virtio0", + "virtio-console", + "discard-while-disconnected", + "disconnected", + b"discard", + ), + AttachmentSpec::new( + "console:microvm-control0", + "virtio-control-console", + "broker-authenticated-listener", + "unix-socket", + b"/run/nvx/control.sock", + ), + ], + boot_layout_version: 2, + network: None, + filesystem: None, + blocks: vec![ + BlockSpec { + role: "distro".to_owned(), + read_only: true, + length: 1024 * MIB, + identity_kind: "unbound".to_owned(), + identity: Vec::new(), + artifact: String::new(), + logical_block_size: 512, + physical_block_size: 4096, + restore_mode: String::new(), + }, + BlockSpec { + role: "scratch".to_owned(), + read_only: false, + length: 64 * MIB, + identity_kind: "fresh".to_owned(), + identity: Vec::new(), + artifact: String::new(), + logical_block_size: 512, + physical_block_size: 512, + restore_mode: String::new(), + }, + ], + filesystem_slot_version: 1, + boot_online_vp_count: 0, + interrupt_mode: "edge-shared-status".to_owned(), + status_page: (0x3_0000, 0x1000), + expansion_version: 1, + memory_capacity: 2048 * MIB, + memory_block_size: 128 * MIB, + expansion_ranges: vec![(512 * MIB, 1536 * MIB)], + time: Some(TimeSpec { + abi_version: 1, + tsc_hz: 2_100_000_000, + tolerance_ppm: 250, + apic_hz: 200_000_000, + capture_tsc: 12_345_678, + capture_utc_ns: 1_700_000_000_000_000_000, + capture_monotonic_ns: 123_000_000_000, + host_clock: "linux-boottime".to_owned(), + host_id: vec![1; 16], + boot_id: vec![2; 16], + lineage: 3, + cut_to_anchor_ns: Some(200), + }), + cpu: Some(CpuSpec { + id: "intel.icelake-sp.v1".to_owned(), + digest: vec![0x5a; 32], + document: b"canonical profile document".to_vec(), + effective_cpuid: (0..88).collect(), + signature: 0x0006_06a6, + }), + }; + contract.renumber(); + for index in 0..contract.devices.len() { + let unit = contract.devices[index].state_unit.clone(); + if !contract.state_units.contains(&unit) { + contract.state_units.push(unit); + } + } + Self { + version: 6, + created_at: (1_761_300_000, 123_456_789), + openvmm_version: "0.0.0".to_owned(), + memory_size: 512 * MIB, + vp_count: 2, + page_size: 4096, + architecture: "x86_64".to_owned(), + state_size: 65_536, + contract: Some(contract), + magic: b"OPENVMM_SNAPSHOT_V6\0".to_vec(), + schema_version: 1, + root_type: "openvmm.SavedState".to_owned(), + tier: "platform".to_owned(), + restore_policy: "clone".to_owned(), + consumed_sections: 1, + } + } + + /// Returns the manifest of [`template`](Self::template) with a fixed NIC at `10.0.0.2/24`, + /// with an IPv6 identity, under an allow-list policy. + pub(super) fn with_network() -> Self { + let mut spec = Self::template(); + let contract = spec.contract(); + contract.devices.insert( + 11, + DeviceSpec::virtio( + "net:microvm0", + "virtio-net", + "virtio-net", + 0xd000_0000, + 10, + 0x1_0000_0020, + 2, + ), + ); + contract.renumber(); + contract + .state_units + .push("virtio-net-3489660928".to_owned()); + contract.command_line = contract.command_line.replacen( + "virtio_mmio.device=0x1000@0xd0001000:6", + "virtio_mmio.device=0x1000@0xd0000000:10 virtio_mmio.device=0x1000@0xd0001000:6", + 1, + ) + " virtnet_ip=10.0.0.2 virtnet_mask=255.255.255.0 virtnet_gw=10.0.0.1 \ + virtnet_ip6=fd00::a00:2/120 virtnet_gw6=fd00::a00:1"; + contract.attachments.insert( + 0, + AttachmentSpec::new( + "net:microvm0", + "virtio-net", + "recreate-endpoint", + "user-mode-nat", + b"consomme", + ), + ); + let mut ipv6 = [0; 16]; + ipv6[0] = 0xfd; + ipv6[12] = 0x0a; + let mut ipv6_gateway = ipv6; + ipv6[15] = 2; + ipv6_gateway[15] = 1; + contract.network = Some(NetworkSpec { + ipv4: [10, 0, 0, 2], + prefix_length: 24, + gateway: [10, 0, 0, 1], + guest_mac: vec![0x52, 0x54, 0, 0, 0, 2], + gateway_mac: vec![0x52, 0x54, 0, 0, 0, 1], + policy_mode: "allow-list".to_owned(), + policy_digest: vec![7; 32], + policy_required: true, + profile: "portable".to_owned(), + policy_encoding_version: 4, + ipv6: ipv6.to_vec(), + ipv6_prefix_length: 120, + ipv6_gateway: ipv6_gateway.to_vec(), + }); + spec + } + + /// Returns the machine contract. + pub(super) fn contract(&mut self) -> &mut ContractSpec { + self.contract.as_mut().expect("the spec has a contract") + } + + /// Returns the time contract. + pub(super) fn time(&mut self) -> &mut TimeSpec { + self.contract() + .time + .as_mut() + .expect("the spec has a time contract") + } + + /// Returns the CPU profile record. + pub(super) fn cpu(&mut self) -> &mut CpuSpec { + self.contract() + .cpu + .as_mut() + .expect("the spec has a CPU profile") + } + + /// Returns the fixed NIC. + pub(super) fn network(&mut self) -> &mut NetworkSpec { + self.contract() + .network + .as_mut() + .expect("the spec has a network") + } + + /// Encodes the manifest, with the fields of `extra` after the others. + pub(super) fn encode_with(&self, extra: Proto) -> Vec { + let (seconds, nanos) = self.created_at; + // `mesh` writes an `i64` and an `i32` as varints of their two's complement in 64 bits. + let created_at = Proto::default() + .varint(1, seconds as u64) + .varint(2, i64::from(nanos) as u64); + let mut message = Proto::default() + .varint(1, self.version.into()) + .message(2, created_at) + .text(3, &self.openvmm_version) + .varint(4, self.memory_size) + .varint(5, self.vp_count.into()) + .varint(6, self.page_size.into()) + .text(7, &self.architecture) + .varint(8, self.state_size); + if let Some(contract) = &self.contract { + message = message.message(11, contract.encode()); + } + message + .bytes(12, &self.magic) + .varint(13, self.schema_version.into()) + .text(14, &self.root_type) + .text(15, &self.tier) + .text(16, &self.restore_policy) + .varint(17, self.consumed_sections.into()) + .raw(&extra.finish()) + .finish() + } + + /// Encodes the manifest. + pub(super) fn encode(&self) -> Vec { + self.encode_with(Proto::default()) + } +} + +/// Returns the artifact identity `sha256:` and 64 times `digit`. +pub(super) fn artifact(digit: char) -> ArtifactIdentity { + ArtifactIdentity::sha256(&digit.to_string().repeat(64)).unwrap() +} + +/// Returns the class of the capture of [`ManifestSpec::template`]. +pub(super) fn class() -> CaptureClass { + CaptureClass { + runtime: RuntimeIdentity::new(artifact('a'), artifact('b'), artifact('c')), + guest_build: ArtifactIdentity::new("guest-build-1").unwrap(), + hypervisor: Hypervisor::Kvm, + cpu_profile: "auto".to_owned(), + processors: 2, + boot_processors: 2, + memory: 512 * MIB, + memory_capacity: 2048 * MIB, + distro: BlockGeometry { + length: 1024 * MIB, + logical_block_size: 512, + physical_block_size: 4096, + }, + scratch: BlockGeometry { + length: 64 * MIB, + logical_block_size: 512, + physical_block_size: 512, + }, + network: None, + invariants: Invariants::new(), + } +} + +/// Returns the class of the capture of [`ManifestSpec::with_network`]. +pub(super) fn network_class() -> CaptureClass { + CaptureClass { + network: Some( + SandboxNetwork::new("10.0.0.2/24", NetworkPolicy::new(Access::Allow)).unwrap(), + ), + ..class() + } +} diff --git a/nvx-rs/src/host/template/wire.rs b/nvx-rs/src/host/template/wire.rs new file mode 100644 index 000000000..380c5f42a --- /dev/null +++ b/nvx-rs/src/host/template/wire.rs @@ -0,0 +1,464 @@ +//! A strict reader of the protobuf wire format, in which OpenVMM's `mesh` encodes the manifest of +//! a capture. +//! +//! A message is a sequence of fields, each a key, which holds the field's number and wire type, +//! and a value of that type: a variable-length integer (varint), eight or four little-endian bytes, +//! or a length and that many bytes, which hold a string, bytes, a nested message, or packed +//! integers. `mesh` omits a field whose value is zero or empty, except an `Option` that is +//! `Some`, writes a repeated field once per element, and packs a list of integers into one field. +//! +//! The reader takes only what such a writer produces, within the bounds of its input: a group or +//! an unknown wire type, a varint longer than ten bytes or beyond 64 bits, a field number outside +//! 1 to 2^29 - 1, and a value that runs past its message are refused. [`Message`] refuses the +//! fields that a message type does not know, and a field that is not repeated but appears twice, +//! which a writer never produces. The reader never recurses on its own: each message type decodes +//! its nested messages, whose depth the manifest's types bound. + +use std::fmt; + +/// The largest field number of the wire format: 2^29 - 1. +const MAX_FIELD_NUMBER: u64 = (1 << 29) - 1; + +/// The longest varint, in bytes: ten, for 64 bits. +const MAX_VARINT_LEN: usize = 10; + +/// Why bytes are not a message, or a field is not of its type. +/// +/// Its `Display` text is a predicate, whose subject, such as "the manifest", the caller supplies: +/// "is truncated", or "holds the unknown field 9". +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum WireError { + /// A key, a varint, or a value runs past the end of its message. + Truncated, + /// A varint is longer than ten bytes, or has bits beyond 64. + Varint, + /// A key names a group, or a wire type that does not exist. + WireType(u8), + /// A key names field 0, or a field number beyond 2^29 - 1. + FieldNumber(u64), + /// The message type does not know the field. + Unknown(u32), + /// A field that is not repeated appears twice. + Repeated(u32), + /// A field has another wire type than its type. + Type { + /// The field. + number: u32, + /// The type that the message type gives it. + expected: &'static str, + }, + /// A field's value is not of its type, for the reason that this gives. + Value { + /// The field. + number: u32, + /// Why the value is invalid, as a predicate of the value. + problem: String, + }, +} + +impl fmt::Display for WireError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Truncated => formatter.write_str("is truncated"), + Self::Varint => formatter.write_str("holds an integer longer than 64 bits"), + Self::WireType(wire_type) => { + write!(formatter, "holds a field of wire type {wire_type}") + } + Self::FieldNumber(number) => write!(formatter, "holds a field numbered {number}"), + Self::Unknown(number) => write!(formatter, "holds the unknown field {number}"), + Self::Repeated(number) => write!(formatter, "holds field {number} more than once"), + Self::Type { number, expected } => { + write!(formatter, "holds field {number}, which is not {expected}") + } + Self::Value { number, problem } => { + write!(formatter, "holds field {number}, which {problem}") + } + } + } +} + +/// The value of a field, as its wire type holds it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum Value<'a> { + /// Wire type 0: a varint. + Varint(u64), + /// Wire type 1: eight little-endian bytes. + Fixed64(u64), + /// Wire type 2: a length and that many bytes. + Bytes(&'a [u8]), + /// Wire type 5: four little-endian bytes. + Fixed32(u32), +} + +/// A field of a message: its number and its value. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct Field<'a> { + /// The field's number, from 1 to 2^29 - 1. + pub(super) number: u32, + /// The field's value. + pub(super) value: Value<'a>, +} + +impl<'a> Field<'a> { + /// Returns the error of a field that is not of the wire type of `expected`, as messages name + /// the type. + fn mistyped(&self, expected: &'static str) -> WireError { + WireError::Type { + number: self.number, + expected, + } + } + + /// Returns the error of a field whose value is invalid, for the reason `problem`, a predicate + /// of the value. + pub(super) fn invalid(&self, problem: impl Into) -> WireError { + WireError::Value { + number: self.number, + problem: problem.into(), + } + } + + /// Returns the field's varint. + pub(super) fn uint64(&self) -> Result { + match self.value { + Value::Varint(value) => Ok(value), + _ => Err(self.mistyped("an integer")), + } + } + + /// Returns the field's varint, which must fit 32 bits. + pub(super) fn uint32(&self) -> Result { + u32::try_from(self.uint64()?).map_err(|_| self.invalid("does not fit 32 bits")) + } + + /// Returns the field's varint as a signed 64-bit integer in two's complement, as `mesh` + /// encodes a timestamp's seconds. + pub(super) fn int64(&self) -> Result { + Ok(self.uint64()? as i64) + } + + /// Returns the field's varint as a signed 32-bit integer in two's complement, which a + /// negative one extends to 64 bits, as `mesh` encodes a timestamp's nanoseconds. + pub(super) fn int32(&self) -> Result { + i32::try_from(self.int64()?).map_err(|_| self.invalid("does not fit 32 bits")) + } + + /// Returns the field's varint as a boolean: 0 or 1. + pub(super) fn boolean(&self) -> Result { + match self.uint64()? { + 0 => Ok(false), + 1 => Ok(true), + value => Err(self.invalid(format!("is {value}, not a boolean"))), + } + } + + /// Returns the field's bytes. + pub(super) fn bytes(&self) -> Result<&'a [u8], WireError> { + match self.value { + Value::Bytes(bytes) => Ok(bytes), + _ => Err(self.mistyped("bytes")), + } + } + + /// Returns the field's bytes as UTF-8 text. + pub(super) fn text(&self) -> Result<&'a str, WireError> { + std::str::from_utf8(self.bytes()?).map_err(|_| self.invalid("is not UTF-8")) + } + + /// Returns the field's packed varints, each of which must fit 32 bits. + pub(super) fn packed_uint32(&self) -> Result, WireError> { + let mut bytes = self.bytes()?; + let mut values = Vec::new(); + while !bytes.is_empty() { + let value = read_varint(&mut bytes)?; + values.push(u32::try_from(value).map_err(|_| self.invalid("does not fit 32 bits"))?); + } + Ok(values) + } +} + +/// Reads a varint from the front of `data`, and advances `data` past it. +fn read_varint(data: &mut &[u8]) -> Result { + let mut value = 0u64; + for (index, &byte) in data.iter().enumerate().take(MAX_VARINT_LEN) { + let bits = u64::from(byte & 0x7f); + // The tenth byte holds the 64th bit alone. + if index == MAX_VARINT_LEN - 1 && bits > 1 { + return Err(WireError::Varint); + } + value |= bits << (7 * index); + if byte & 0x80 == 0 { + *data = &data[index + 1..]; + return Ok(value); + } + } + if data.len() < MAX_VARINT_LEN { + Err(WireError::Truncated) + } else { + Err(WireError::Varint) + } +} + +/// Splits `count` bytes from the front of `data`, and advances `data` past them. +fn take<'a>(data: &mut &'a [u8], count: u64) -> Result<&'a [u8], WireError> { + let count = usize::try_from(count).map_err(|_| WireError::Truncated)?; + if count > data.len() { + return Err(WireError::Truncated); + } + let (taken, rest) = data.split_at(count); + *data = rest; + Ok(taken) +} + +/// Reads one field from the front of `data`, and advances `data` past it. +fn read_field<'a>(data: &mut &'a [u8]) -> Result, WireError> { + let key = read_varint(data)?; + let number = key >> 3; + if number == 0 || number > MAX_FIELD_NUMBER { + return Err(WireError::FieldNumber(number)); + } + let number = number as u32; + let value = match key & 7 { + 0 => Value::Varint(read_varint(data)?), + 1 => Value::Fixed64(u64::from_le_bytes( + take(data, 8)?.try_into().expect("eight bytes were taken"), + )), + 2 => { + let len = read_varint(data)?; + Value::Bytes(take(data, len)?) + } + 5 => Value::Fixed32(u32::from_le_bytes( + take(data, 4)?.try_into().expect("four bytes were taken"), + )), + wire_type => return Err(WireError::WireType(wire_type as u8)), + }; + Ok(Field { number, value }) +} + +/// The fields of a message, in their order, which refuses an unknown field and a field that is +/// not repeated but appears twice, as its caller declares them. +pub(super) struct Message<'a> { + /// The fields that remain to be read. + data: &'a [u8], + /// The numbers of the fields that the message type knows, each below 64. + known: u64, + /// The numbers of the known fields that are repeated. + repeated: u64, + /// The numbers of the fields read so far. + seen: u64, +} + +impl<'a> Message<'a> { + /// Returns the fields of the message `data`, whose type knows the fields `known` and the + /// fields `repeated`, which may appear more than once. Every number must be below 64. + pub(super) fn new(data: &'a [u8], known: &[u32], repeated: &[u32]) -> Self { + let mask = |numbers: &[u32]| { + numbers.iter().fold(0u64, |mask, &number| { + assert!(number < 64, "field {number} is beyond the reader's mask"); + mask | 1 << number + }) + }; + let repeated = mask(repeated); + Self { + data, + known: mask(known) | repeated, + repeated, + seen: 0, + } + } + + /// Returns the next field, `None` at the end of the message, or why the message is invalid. + pub(super) fn next_field(&mut self) -> Result>, WireError> { + if self.data.is_empty() { + return Ok(None); + } + let field = read_field(&mut self.data)?; + let bit = 1u64.checked_shl(field.number).unwrap_or(0); + if bit == 0 || self.known & bit == 0 { + return Err(WireError::Unknown(field.number)); + } + if self.repeated & bit == 0 && self.seen & bit != 0 { + return Err(WireError::Repeated(field.number)); + } + self.seen |= bit; + Ok(Some(field)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Returns every field of `data`, or the first error. + fn fields<'a>( + data: &'a [u8], + known: &[u32], + repeated: &[u32], + ) -> Result>, WireError> { + let mut message = Message::new(data, known, repeated); + let mut fields = Vec::new(); + while let Some(field) = message.next_field()? { + fields.push(field); + } + Ok(fields) + } + + #[test] + fn each_wire_type_reads_its_value() { + let data = [ + 0x08, 0x96, 0x01, // 1: varint 150 + 0x11, 1, 2, 3, 4, 5, 6, 7, 8, // 2: fixed64 + 0x1a, 0x03, b'a', b'b', b'c', // 3: bytes "abc" + 0x25, 1, 0, 0, 0x80, // 4: fixed32 + ]; + let read = fields(&data, &[1, 2, 3, 4], &[]).unwrap(); + assert_eq!( + read, + [ + Field { + number: 1, + value: Value::Varint(150) + }, + Field { + number: 2, + value: Value::Fixed64(0x0807_0605_0403_0201) + }, + Field { + number: 3, + value: Value::Bytes(b"abc") + }, + Field { + number: 4, + value: Value::Fixed32(0x8000_0001) + }, + ] + ); + assert_eq!(read[0].uint32().unwrap(), 150); + assert_eq!(read[2].text().unwrap(), "abc"); + assert_eq!( + read[0].bytes().unwrap_err(), + WireError::Type { + number: 1, + expected: "bytes" + } + ); + assert_eq!( + read[2].uint64().unwrap_err(), + WireError::Type { + number: 3, + expected: "an integer" + } + ); + } + + #[test] + fn varints_hold_64_bits_in_at_most_ten_bytes() { + let max = [0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01]; + assert_eq!(read_varint(&mut &max[..]).unwrap(), u64::MAX); + // A tenth byte with more than the 64th bit, and an eleventh byte, overflow. + let mut over = max; + over[9] = 0x02; + assert_eq!(read_varint(&mut &over[..]), Err(WireError::Varint)); + let long = [0x80; 11]; + assert_eq!(read_varint(&mut &long[..]), Err(WireError::Varint)); + // A varint whose last byte is missing is truncated. + assert_eq!( + read_varint(&mut &[0x80, 0x80][..]), + Err(WireError::Truncated) + ); + assert_eq!(read_varint(&mut &[][..]), Err(WireError::Truncated)); + // A longer encoding than needed still reads. + assert_eq!(read_varint(&mut &[0x81, 0x00][..]).unwrap(), 1); + } + + #[test] + fn signed_integers_are_twos_complement() { + let minus_one = [ + 0x08, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01, + ]; + let read = fields(&minus_one, &[1], &[]).unwrap(); + assert_eq!(read[0].int64().unwrap(), -1); + assert_eq!(read[0].int32().unwrap(), -1); + assert!(read[0].uint32().is_err()); + let large = [0x08, 0x80, 0x80, 0x80, 0x80, 0x10]; + assert!(fields(&large, &[1], &[]).unwrap()[0].int32().is_err()); + } + + #[test] + fn booleans_and_packed_lists_are_strict() { + let read = fields(&[0x08, 0x01, 0x10, 0x02], &[1, 2], &[]).unwrap(); + assert!(read[0].boolean().unwrap()); + assert_eq!( + read[1].boolean().unwrap_err().to_string(), + "holds field 2, which is 2, not a boolean" + ); + let packed = [0x0a, 0x04, 0x01, 0x80, 0x01, 0x00]; + assert_eq!( + fields(&packed, &[1], &[]).unwrap()[0] + .packed_uint32() + .unwrap(), + [1, 128, 0] + ); + let truncated = [0x0a, 0x02, 0x01, 0x80]; + assert_eq!( + fields(&truncated, &[1], &[]).unwrap()[0].packed_uint32(), + Err(WireError::Truncated) + ); + let wide = [0x0a, 0x05, 0x80, 0x80, 0x80, 0x80, 0x10]; + assert!( + fields(&wide, &[1], &[]).unwrap()[0] + .packed_uint32() + .is_err() + ); + } + + #[test] + fn malformed_messages_are_refused() { + // A length past the end, a fixed value cut short, and a key without its value. + assert_eq!( + fields(&[0x0a, 0x05, b'a'], &[1], &[]), + Err(WireError::Truncated) + ); + assert_eq!( + fields(&[0x09, 1, 2, 3], &[1], &[]), + Err(WireError::Truncated) + ); + assert_eq!(fields(&[0x08], &[1], &[]), Err(WireError::Truncated)); + // Groups and the wire types that do not exist. + for (key, wire_type) in [(0x0b, 3), (0x0c, 4), (0x0e, 6), (0x0f, 7)] { + assert_eq!( + fields(&[key], &[1], &[]), + Err(WireError::WireType(wire_type)) + ); + } + // Field 0 and a number beyond 2^29 - 1. + assert_eq!( + fields(&[0x00, 0x00], &[1], &[]), + Err(WireError::FieldNumber(0)) + ); + let beyond = [0x80, 0x80, 0x80, 0x80, 0x10, 0x00]; + assert_eq!( + fields(&beyond, &[1], &[]), + Err(WireError::FieldNumber(1 << 29)) + ); + } + + #[test] + fn messages_refuse_unknown_and_repeated_fields() { + assert_eq!(fields(&[0x10, 0x01], &[1], &[]), Err(WireError::Unknown(2))); + // A field number beyond any that a message type knows is unknown too. + assert_eq!( + fields(&[0xc0, 0x0c, 0x01], &[1], &[]), + Err(WireError::Unknown(200)) + ); + assert_eq!( + fields(&[0x08, 0x01, 0x08, 0x02], &[1], &[]), + Err(WireError::Repeated(1)) + ); + assert_eq!( + fields(&[0x0a, 0x00, 0x0a, 0x00], &[], &[1]).unwrap().len(), + 2 + ); + assert_eq!(fields(&[], &[1], &[]).unwrap(), []); + } +} diff --git a/nvx-rs/src/lib.rs b/nvx-rs/src/lib.rs index 5f1bebeda..296018eef 100644 --- a/nvx-rs/src/lib.rs +++ b/nvx-rs/src/lib.rs @@ -12,8 +12,9 @@ //! record of the settings that its activation applies, which a clone restored from a template //! reads in place of its captured configuration, and what Ready reports about the activation. //! It needs no features, so the host, the guest, and every adapter's framing share it. -//! - [`contract`]: the control-contract versions that this crate speaks, and the compatibility of -//! NVX releases with them. +//! - [`contract`]: the control-contract versions that this crate speaks and the compatibility of +//! NVX releases with them, and the versions of the contracts through which OpenVMM restores a +//! snapshot. //! - [`exec`]: the credit and stdio engine of an execution, which the side that runs a workload //! and the side that requested it share with any adapter, whatever its framing: the state of //! each stream, the credit that bounds the bytes in flight, the order of each stream's data and @@ -32,7 +33,8 @@ //! - [`host`] (feature `host`): artifact discovery, the OpenVMM command line, the lowering of //! network and filesystem policies, the host's NVXS connection, control sessions, process and //! platform primitives, the supervision of VM instances: starting, reattaching, executing -//! workloads, and stopping, and the store of a sandbox's runtime records and launch journal. +//! workloads, and stopping, the store of a sandbox's runtime records and launch journal, and the +//! manifests and compatibility keys of templates. //! - `testing` (feature `testing`): a fake OpenVMM that serves the control console without //! running a VM. //! - `guest` (feature `guest`, Linux): the programs that run in NVX guests, such as diff --git a/nvx-rs/tests/fixtures/template/README.md b/nvx-rs/tests/fixtures/template/README.md new file mode 100644 index 000000000..9d407578b --- /dev/null +++ b/nvx-rs/tests/fixtures/template/README.md @@ -0,0 +1,37 @@ +# Template manifests + +Each directory holds the `manifest.bin` of a template's capture, as OpenVMM +writes it: one `openvmm.snapshot.SnapshotManifest` of version 6 of OpenVMM's +snapshot format, in the protobuf wire format of its `mesh`. The tests of +`nvx::host::template` read them with `TemplateManifest::read`, so that the +reader is checked against bytes that OpenVMM's own code wrote, rather than +against its own idea of them. + +| Directory | Capture | +| --- | --- | +| [`platform-kvm`](platform-kvm) | A KVM microVM of 2 processors and 512 MiB of RAM, of a capacity of 2 GiB, with a `distro` of 1 GiB, a fresh scratch of 64 MiB, the dormant filesystem slot, a console, a control console, and no NIC | +| [`platform-mshv-network`](platform-mshv-network) | The same on MSHV, with a fixed NIC at `10.0.0.2/24`, its IPv6 identity, and an allow-list policy | + +Both are platform-tier captures for clones, after the invariant configuration +alone, with OpenVMM's test records of the time contract and of the CPU profile +`intel.icelake-sp.v1`. + +## How they were made + +[`generate.rs`](generate.rs) builds each manifest as a capture does, with +`microvm_machine_contract` of OpenVMM's `openvmm_helpers`, checks it with +OpenVMM's own validation, and encodes it with `mesh`. It wrote these files at +OpenVMM commit `79a501b63fcd2f150e94928d0ceba79cd2fe96c4`. + +To write them again, as when a new version of the snapshot format retires these, +append `generate.rs` to `openvmm/openvmm_helpers/src/snapshot/microvm.rs` in a +scratch checkout of the pinned OpenVMM, and run, from that checkout's root: + +```bash +cargo xflowey restore-packages --no-compat-igvm +NVX_FIXTURE_DIR=/tmp/fixtures cargo test --locked -p openvmm_helpers --lib \ + nvx_fixture -- --ignored +``` + +Then copy each `NAME.bin` to `NAME/manifest.bin` here, and record the commit +above. diff --git a/nvx-rs/tests/fixtures/template/generate.rs b/nvx-rs/tests/fixtures/template/generate.rs new file mode 100644 index 000000000..00d4597fb --- /dev/null +++ b/nvx-rs/tests/fixtures/template/generate.rs @@ -0,0 +1,221 @@ + +// Writes the template manifests of nvx-rs/tests/fixtures/template/ with OpenVMM's own code, as +// README.md there describes: append this file to openvmm/openvmm_helpers/src/snapshot/microvm.rs +// in a scratch checkout of the pinned OpenVMM, and run its ignored test. It is not part of either +// build. +#[cfg(test)] +mod nvx_fixture { + use super::*; + use crate::snapshot::SnapshotManifest; + use crate::snapshot::format::SAVED_STATE_ROOT_TYPE; + use crate::snapshot::format::SAVED_STATE_SCHEMA_VERSION; + use crate::snapshot::format::SNAPSHOT_CONFIG_INVARIANTS; + use crate::snapshot::format::SNAPSHOT_FORMAT_MAGIC; + use crate::snapshot::format::SNAPSHOT_RESTORE_POLICY_CLONE; + use crate::snapshot::format::SNAPSHOT_TIER_PLATFORM; + use mesh::payload::Timestamp; + use openvmm_defs::microvm as defs; + + fn attachment( + stable_id: &str, + kind: &str, + policy: &str, + identity_kind: &str, + identity: &[u8], + ) -> SnapshotAttachment { + SnapshotAttachment { + stable_id: stable_id.to_owned(), + kind: kind.to_owned(), + required: false, + reconnect_policy: policy.to_owned(), + identity_kind: identity_kind.to_owned(), + identity: identity.to_vec(), + length: 0, + reconnect_timeout_ms: 0, + } + } + + #[test] + #[ignore = "writes NVX's template manifest fixtures"] + fn write_nvx_template_fixtures() { + const MB: u64 = 1024 * 1024; + let directory = std::path::PathBuf::from(std::env::var("NVX_FIXTURE_DIR").unwrap()); + for (name, hypervisor, with_network) in [ + ("platform-kvm", "kvm", false), + ("platform-mshv-network", "mshv", true), + ] { + let mut command_line = defs::build_microvm_control_command_line( + &[ + "nvx_sandbox=1".to_owned(), + "nvx_config=0xd0010000,65536".to_owned(), + ], + true, + ) + .unwrap(); + defs::append_microvm_processor_limit(&mut command_line, 2).unwrap(); + command_line.push_str(" nvx_snapshot_tier=platform"); + let network: defs::MicrovmNetworkConfig = "10.0.0.2/24".parse().unwrap(); + let egress_policy = network + .bind_egress_policy( + net_backend_resources::egress::EgressPolicyMode::AllowList(vec![ + "192.0.2.0/24".parse().unwrap(), + ]), + ) + .unwrap(); + let irq = defs::microvm_virtio_net_irq(Some(hypervisor)).unwrap(); + let blocks = [ + defs::MicrovmSandboxBlockConfig { + role: defs::MicrovmSandboxBlockRole::Distro, + read_only: true, + }, + defs::MicrovmSandboxBlockConfig { + role: defs::MicrovmSandboxBlockRole::Scratch, + read_only: false, + }, + ]; + defs::append_microvm_virtio_discovery( + &mut command_line, + with_network.then(|| (&network, irq, egress_policy.gateway_dns_server(), None)), + true, + &[], + true, + true, + &blocks, + ) + .unwrap(); + let sandbox_blocks = vec![ + SnapshotMicrovmSandboxBlock { + role: "distro".to_owned(), + read_only: true, + length: 1024 * MB, + identity_kind: "unbound".to_owned(), + identity: Vec::new(), + artifact: String::new(), + logical_block_size: 512, + physical_block_size: 4096, + restore_mode: String::new(), + }, + SnapshotMicrovmSandboxBlock { + role: "scratch".to_owned(), + read_only: false, + length: 64 * MB, + identity_kind: "fresh".to_owned(), + identity: Vec::new(), + artifact: String::new(), + logical_block_size: 512, + physical_block_size: 512, + restore_mode: String::new(), + }, + ]; + let mut state_unit_names: Vec = [ + "partition", + "vmtime", + "pic", + "ioapic", + "pit", + "rtc", + "microvm-portb", + "microvm-shutdown", + "microvm-snapshot-request", + ] + .iter() + .map(|name| (*name).to_owned()) + .collect(); + if with_network { + state_unit_names.push(format!( + "virtio-net-{}", + defs::MICROVM_VIRTIO_NET_MMIO_BASE + )); + } + state_unit_names.push(format!( + "virtiofs-{}", + defs::MICROVM_FILESYSTEM_SLOTS[0].mmio_base + )); + state_unit_names.push(format!( + "virtio-console-{}", + defs::MICROVM_VIRTIO_CONSOLE_MMIO_BASE + )); + for block in &blocks { + state_unit_names.push(format!("virtio-blk-{}", block.role.mmio_base())); + } + state_unit_names.push(format!( + "virtio-control-console-{}", + defs::MICROVM_VIRTIO_CONTROL_CONSOLE_MMIO_BASE + )); + let contract = microvm_machine_contract( + hypervisor, + MICROVM_BOOT_LAYOUT_VERSION, + command_line, + with_network.then(|| { + ( + &network, + &egress_policy, + attachment( + "net:microvm0", + "virtio-net", + "recreate-endpoint", + "user-mode-nat", + b"consomme", + ), + ) + }), + true, + None, + Some(attachment( + "console:microvm-virtio0", + "virtio-console", + "discard-while-disconnected", + "disconnected", + b"discard", + )), + Some(attachment( + "console:microvm-control0", + "virtio-control-console", + "broker-authenticated-listener", + "unix-socket", + b"/run/nvx/control.sock", + )), + sandbox_blocks, + 2, + 512 * MB, + Some(2048 * MB), + state_unit_names, + crate::snapshot::time::tests::test_time_contract(), + crate::snapshot::time::tests::test_cpu_profile(), + ) + .unwrap(); + validate_supported_microvm_contract(&contract).unwrap(); + let manifest = SnapshotManifest { + version: crate::snapshot::MANIFEST_VERSION, + created_at: Timestamp { + seconds: 1_761_300_000, + nanos: 123_456_789, + }, + openvmm_version: "0.0.0".to_owned(), + memory_size_bytes: 512 * MB, + vp_count: 2, + page_size: 4096, + architecture: "x86_64".to_owned(), + state_size_bytes: 65_536, + machine_contract: Some(contract), + format_magic: SNAPSHOT_FORMAT_MAGIC.to_vec(), + saved_state_schema_version: SAVED_STATE_SCHEMA_VERSION, + saved_state_root_type: SAVED_STATE_ROOT_TYPE.to_owned(), + snapshot_tier: SNAPSHOT_TIER_PLATFORM.to_owned(), + restore_policy: SNAPSHOT_RESTORE_POLICY_CLONE.to_owned(), + consumed_config_sections: SNAPSHOT_CONFIG_INVARIANTS, + }; + crate::snapshot::format::validate_manifest_header(&manifest).unwrap(); + crate::snapshot::format::validate_manifest_contents(&manifest).unwrap(); + let bytes = mesh::payload::encode(manifest); + let decoded: SnapshotManifest = mesh::payload::decode(&bytes).unwrap(); + crate::snapshot::format::validate_manifest_header(&decoded).unwrap(); + std::fs::write(directory.join(format!("{name}.bin")), bytes).unwrap(); + std::fs::write( + directory.join(format!("{name}.txt")), + format!("{:#?}\n", decoded.machine_contract), + ) + .unwrap(); + } + } +} diff --git a/nvx-rs/tests/fixtures/template/platform-kvm/manifest.bin b/nvx-rs/tests/fixtures/template/platform-kvm/manifest.bin new file mode 100644 index 000000000..a93c1c45a Binary files /dev/null and b/nvx-rs/tests/fixtures/template/platform-kvm/manifest.bin differ diff --git a/nvx-rs/tests/fixtures/template/platform-mshv-network/manifest.bin b/nvx-rs/tests/fixtures/template/platform-mshv-network/manifest.bin new file mode 100644 index 000000000..fe1b9f266 Binary files /dev/null and b/nvx-rs/tests/fixtures/template/platform-mshv-network/manifest.bin differ diff --git a/nvx-rs/tests/openvmm_vectors.rs b/nvx-rs/tests/openvmm_vectors.rs index 2112e101c..0edb51059 100644 --- a/nvx-rs/tests/openvmm_vectors.rs +++ b/nvx-rs/tests/openvmm_vectors.rs @@ -1,5 +1,6 @@ -//! Compares the copies of OpenVMM's control-session protocol vectors in `tests/vectors/openvmm/` -//! with the files of the OpenVMM submodule that they come from. +//! Compares what `nvx-rs` copies from OpenVMM with the OpenVMM submodule: the control-session +//! protocol vectors in `tests/vectors/openvmm/`, and the versions of OpenVMM's contracts in +//! `nvx::contract`. //! //! The protocol's tests read the copies, because the `nvx-rs` job cannot check out the submodule, //! whose deploy key fork pull requests lack. This test is ignored by default: the NVX CLI jobs, @@ -8,6 +9,8 @@ use std::fs; use std::path::Path; +use nvx::contract; + /// The files of OpenVMM's `vm/devices/virtio/virtio_console/test_data/` that /// `tests/vectors/openvmm/` copies. const FILES: [&str; 2] = [ @@ -15,6 +18,32 @@ const FILES: [&str; 2] = [ "control_session_protocol_vectors.txt", ]; +/// The versions of `nvx::contract` that are OpenVMM's: for each, the file of the OpenVMM +/// submodule that defines OpenVMM's, the start of the line that defines it, up to its value, and +/// the version of `nvx::contract`. +const VERSIONS: [(&str, &str, u64); 4] = [ + ( + "openvmm/openvmm_helpers/src/snapshot.rs", + "pub const MANIFEST_VERSION: u32 = ", + contract::SNAPSHOT_FORMAT_VERSION, + ), + ( + "openvmm/openvmm_helpers/src/snapshot/format.rs", + "pub const SAVED_STATE_SCHEMA_VERSION: u32 = ", + contract::DEVICE_STATE_SCHEMA_VERSION, + ), + ( + "vmm_core/virt/src/time_abi/mod.rs", + "pub const TIME_ABI_VERSION: u32 = ", + contract::TIME_ABI_VERSION, + ), + ( + "vm/devices/chipset_resources/src/microvm_time.rs", + "pub const RESTORE_PACKET_VERSION: u8 = ", + contract::RESTORE_PACKET_VERSION, + ), +]; + #[test] #[ignore = "needs the OpenVMM submodule, which the NVX CLI jobs check out"] fn the_copies_match_the_submodules_files() { @@ -42,3 +71,28 @@ fn the_copies_match_the_submodules_files() { ); } } + +#[test] +#[ignore = "needs the OpenVMM submodule, which the NVX CLI jobs check out"] +fn the_contract_versions_are_the_submodules() { + let openvmm = Path::new(env!("CARGO_MANIFEST_DIR")).join("../openvmm"); + for (file, definition, version) in VERSIONS { + let source = fs::read_to_string(openvmm.join(file)).unwrap_or_else(|error| { + panic!( + "cannot read {file} of the OpenVMM submodule, which `scripts/nvx.py init` checks \ + out: {error}" + ) + }); + let values: Vec<&str> = source + .lines() + .filter_map(|line| line.trim().strip_prefix(definition)?.strip_suffix(';')) + .collect(); + assert_eq!( + values, + [version.to_string()], + "{file} of the OpenVMM submodule does not define `{definition}{version};` once: \ + change the version of nvx::contract that names {file} with OpenVMM's, and the code \ + that reads what it versions" + ); + } +} diff --git a/nvx-rs/tests/vectors/README.md b/nvx-rs/tests/vectors/README.md index 6f6099ab9..1b8cbb841 100644 --- a/nvx-rs/tests/vectors/README.md +++ b/nvx-rs/tests/vectors/README.md @@ -12,6 +12,7 @@ yet breaks those peers. Only bytes that no codec produced catch such a change. | [`nvxc-v1.txt`](nvxc-v1.txt) | Version 1 of the control protocol: `NVXC` frames, their `EXEC`, `MAPS`, and features payloads, and `NVXS` records of NVX's host and guest | | [`nvxl-v1.txt`](nvxl-v1.txt) | Version 1 of the `NVXL` listener announcement | | [`activation-v1.txt`](activation-v1.txt) | Version 1 of the activation record of a layered root, and what Ready reports about an activation | +| [`template-key-v1.txt`](template-key-v1.txt) | Version 1 of the canonical encodings of a template's compatibility key and of a capture class, with their identities | | [`openvmm/`](openvmm) | OpenVMM's vectors and cases of the `NVXS` session, unchanged | [4.4 Host-Guest Control Interface](../../../doc/design/4.4-host-guest-control-interface.md) @@ -22,9 +23,9 @@ describe the protocols, and the documentation of `nvx::activation`, in ## Line format -`nvxc-v1.txt`, `nvxl-v1.txt`, and `activation-v1.txt` share one line format. A -line that starts with `#` is a comment, and an empty line is ignored. Every -other line is one vector, whose fields `|` separates: +`nvxc-v1.txt`, `nvxl-v1.txt`, `activation-v1.txt`, and `template-key-v1.txt` +share one line format. A line that starts with `#` is a comment, and an empty +line is ignored. Every other line is one vector, whose fields `|` separates: ```text KIND|NAME|BYTES|VALUE|... @@ -48,6 +49,12 @@ A decoder must decode the bytes of each vector to its values, and an encoder must encode the values to exactly its bytes. A kind whose name starts with `INVALID_` has no values: every decoder must refuse the bytes of its vectors. +A key or a class holds too many values for one line, so the value of each +vector of `template-key-v1.txt` is its identity, and the file's header names +the captures whose keys and classes the vectors encode. The tests of +`nvx::host::template` derive each key from its capture, and check that it is +the key that the vector's bytes decode to. + ## Rules - A vector changes only with a protocol version. A new version gets files of @@ -73,8 +80,11 @@ The tests cannot call OpenVMM's broker: OpenVMM's workspace needs a later Rust release than `nvx`'s minimum, and the `nvx-rs` job cannot check out the OpenVMM submodule, whose deploy key fork pull requests lack. They read the copies instead, and the ignored test `tests/openvmm_vectors.rs` compares the copies -with the submodule's files. The NVX CLI jobs, which check out the submodule, -run it, and so does an OpenVMM pin promotion, from the repository's root: +with the submodule's files. It also checks that the versions of OpenVMM's +snapshot format, saved-state schema, time ABI, and restore packet in +`nvx::contract` are the submodule's. The NVX CLI jobs, which check out the +submodule, run it, and so does an OpenVMM pin promotion, from the repository's +root: ```bash cargo test --locked --manifest-path nvx-rs/Cargo.toml --no-default-features \ diff --git a/nvx-rs/tests/vectors/template-key-v1.txt b/nvx-rs/tests/vectors/template-key-v1.txt new file mode 100644 index 000000000..bff4a0c2f --- /dev/null +++ b/nvx-rs/tests/vectors/template-key-v1.txt @@ -0,0 +1,95 @@ +# Conformance vectors of version 1 of the canonical encodings of a template's compatibility key, +# nvx::host::template::TemplateKey, and of a capture class, nvx::host::template::CaptureClass, +# which the module nvx::host::template describes. README.md describes the line format and the +# rules that the vectors follow. +# +# KEY|NAME|BYTES|IDENTITY +# The canonical encoding of a key, and its identity: the SHA-256 digest of "nvx template 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. +# 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. +# INVALID_KEY_VERSION|NAME|BYTES +# A key of another version of the encoding, which every decoder refuses as unsupported. +# INVALID_KEY|NAME|BYTES +# Bytes that every decoder of a key must refuse. +# +# The keys and classes are those of the captures that tests/fixtures/template/README.md +# describes, with the OpenVMM, kernel, and initramfs identities sha256:a..., sha256:b..., and +# sha256:c..., the guest build guest-build-1, boot contract 3, control contract +# 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. +# 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|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d05000000696e74656c0a0000006963656c616b652d7370010000005a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a58000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253545556570100000001000000020000000100000002000000000000000100000000752b7d0000000000c2eb0b00000000fa000000020000000000000000000020000000000000008000000000000000080000000001000000000000000000000000000020000000000100000000000020000000000000006000000000530100006561726c79636f6e3d78653920636f6e736f6c653d68766331207265626f6f743d742070616e69633d2d31206e76785f73616e64626f783d31206e76785f636f6e6669673d307864303031303030302c3635353336206e725f637075733d32206e76785f736e617073686f745f746965723d706c6174666f726d2076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030313030303a362076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030323030303a372076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030333030303a342076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030363030303a31312076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030373030303a33206e76785f636f6e74726f6c5f7474793d6876633217f80f8858431ca75fcc7ca694ea68637e017ede02db15e18d0ed4581067858d000000400000000000020000001000000000000400000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000001000000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000003000000767030040000007663707509000000706172746974696f6e00000000000000000000000000000000000000000006000000766d74696d6505000000636c6f636b06000000766d74696d65000000000000000000000000000000000000000000030000007069630300000070696303000000706963000000000200000004000000706d696f2000000000000000020000000000000004000000706d696fa00000000000000002000000000000000000000000000000000000000006000000696f6170696306000000696f6170696306000000696f617069630000000001000000040000006d6d696f0000c0fe00000000001000000000000000000000000000000000000000050000006c61706963050000006c6170696309000000706172746974696f6e0000000001000000040000006d6d696f0000e0fe00000000001000000000000000000000000000000000000000030000007069740300000070697403000000706974000000000100000004000000706d696f400000000000000004000000000000000100000000000000000000000000000000030000007274630300000072746303000000727463000000000100000004000000706d696f7000000000000000020000000000000001080000000000000000000000000000000d0000006d6963726f766d2d706f72746205000000706f7274620d0000006d6963726f766d2d706f727462000000000100000004000000706d696fe900000000000000020000000000000000000000000000000000000000100000006d6963726f766d2d73687574646f776e0800000073687574646f776e100000006d6963726f766d2d73687574646f776e000000000100000004000000706d696f0406000000000000010000000000000000000000000000000000000000180000006d6963726f766d2d736e617073686f742d7265717565737410000000736e617073686f742d72657175657374180000006d6963726f766d2d736e617073686f742d72657175657374000000000100000004000000706d696f05060000000000000100000000000000000000000000000000000000000b00000066733a6d6963726f766d300900000076697274696f2d66731300000076697274696f66732d333438393636353032340b00000076697274696f2d6d6d696f01000000040000006d6d696f001000d000000000001000000000000001060000000200000000000030030000000200000002000000000100000001000017000000636f6e736f6c653a6d6963726f766d2d76697274696f300e00000076697274696f2d636f6e736f6c651900000076697274696f2d636f6e736f6c652d333438393636393132300b00000076697274696f2d6d6d696f01000000040000006d6d696f002000d000000000001000000000000001070000000200000001000030030000000200000002000000000100000001000012000000626c6b3a73616e64626f783a64697374726f0a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393637333231360b00000076697274696f2d6d6d696f01000000040000006d6d696f003000d0000000000010000000000000010400000002000000640600300300000001000000010000000001000013000000626c6b3a73616e64626f783a736372617463680a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393638353530340b00000076697274696f2d6d6d696f01000000040000006d6d696f006000d0000000000010000000000000010b00000002000000440600300300000001000000010000000001000018000000636f6e736f6c653a6d6963726f766d2d636f6e74726f6c301600000076697274696f2d636f6e74726f6c2d636f6e736f6c652100000076697274696f2d636f6e74726f6c2d636f6e736f6c652d333438393638393630300b00000076697274696f2d6d6d696f01000000040000006d6d696f007000d000000000001000000000000001030000000200000001000030030000000200000002000000000100000001000001000000070000006d6963726f766d0000000000|81901eda9602b4f2099c44415b7a38b035ff215b9bc50f860c8c70ca2c6f593f +# 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|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d05000000696e74656c0a0000006963656c616b652d7370010000005a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a58000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253545556570100000001000000020000000100000002000000000000000100000000752b7d0000000000c2eb0b00000000fa000000020000000000000000000020000000000000008000000000000000080000000001000000000000000000000000000020000000000100000000000020000000000000006000000000f20100006561726c79636f6e3d78653920636f6e736f6c653d68766331207265626f6f743d742070616e69633d2d31206e76785f73616e64626f783d31206e76785f636f6e6669673d307864303031303030302c3635353336206e725f637075733d32206e76785f736e617073686f745f746965723d706c6174666f726d2076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030303030303a31302076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030313030303a362076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030323030303a372076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030333030303a342076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030363030303a31312076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030373030303a33206e76785f636f6e74726f6c5f7474793d6876633220766972746e65745f69703d31302e302e302e3220766972746e65745f6d61736b3d3235352e3235352e3235352e3020766972746e65745f67773d31302e302e302e3120766972746e65745f6970363d666430303a3a6130303a322f31323020766972746e65745f6777363d666430303a3a6130303a315e7d49c200c9d60e74547b549917b665501635035894c15c2d7780210f95caf0000000400000000000020000001000000000000400000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000001100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000003000000767030040000007663707509000000706172746974696f6e00000000000000000000000000000000000000000006000000766d74696d6505000000636c6f636b06000000766d74696d65000000000000000000000000000000000000000000030000007069630300000070696303000000706963000000000200000004000000706d696f2000000000000000020000000000000004000000706d696fa00000000000000002000000000000000000000000000000000000000006000000696f6170696306000000696f6170696306000000696f617069630000000001000000040000006d6d696f0000c0fe00000000001000000000000000000000000000000000000000050000006c61706963050000006c6170696309000000706172746974696f6e0000000001000000040000006d6d696f0000e0fe00000000001000000000000000000000000000000000000000030000007069740300000070697403000000706974000000000100000004000000706d696f400000000000000004000000000000000100000000000000000000000000000000030000007274630300000072746303000000727463000000000100000004000000706d696f7000000000000000020000000000000001080000000000000000000000000000000d0000006d6963726f766d2d706f72746205000000706f7274620d0000006d6963726f766d2d706f727462000000000100000004000000706d696fe900000000000000020000000000000000000000000000000000000000100000006d6963726f766d2d73687574646f776e0800000073687574646f776e100000006d6963726f766d2d73687574646f776e000000000100000004000000706d696f0406000000000000010000000000000000000000000000000000000000180000006d6963726f766d2d736e617073686f742d7265717565737410000000736e617073686f742d72657175657374180000006d6963726f766d2d736e617073686f742d72657175657374000000000100000004000000706d696f05060000000000000100000000000000000000000000000000000000000c0000006e65743a6d6963726f766d300a00000076697274696f2d6e65741500000076697274696f2d6e65742d333438393636303932380b00000076697274696f2d6d6d696f01000000040000006d6d696f000000d0000000000010000000000000010a000000020000002000000001000000020000000200000000010000000100000b00000066733a6d6963726f766d300900000076697274696f2d66731300000076697274696f66732d333438393636353032340b00000076697274696f2d6d6d696f01000000040000006d6d696f001000d000000000001000000000000001060000000200000000000030030000000200000002000000000100000001000017000000636f6e736f6c653a6d6963726f766d2d76697274696f300e00000076697274696f2d636f6e736f6c651900000076697274696f2d636f6e736f6c652d333438393636393132300b00000076697274696f2d6d6d696f01000000040000006d6d696f002000d000000000001000000000000001070000000200000001000030030000000200000002000000000100000001000012000000626c6b3a73616e64626f783a64697374726f0a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393637333231360b00000076697274696f2d6d6d696f01000000040000006d6d696f003000d0000000000010000000000000010400000002000000640600300300000001000000010000000001000013000000626c6b3a73616e64626f783a736372617463680a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393638353530340b00000076697274696f2d6d6d696f01000000040000006d6d696f006000d0000000000010000000000000010b00000002000000440600300300000001000000010000000001000018000000636f6e736f6c653a6d6963726f766d2d636f6e74726f6c301600000076697274696f2d636f6e74726f6c2d636f6e736f6c652100000076697274696f2d636f6e74726f6c2d636f6e736f6c652d333438393638393630300b00000076697274696f2d6d6d696f01000000040000006d6d696f007000d000000000001000000000000001030000000200000001000030030000000200000002000000000100000001000001000000070000006d6963726f766d0108000000706f727461626c655254000000025254000000010a000002180a00000101fd00000000000000000000000a00000278fd00000000000000000000000a0000010a000000616c6c6f772d6c69737407070707070707070707070707070707070707070707070707070707070707070104000000020000001100000069736f6c6174696f6e2e736563636f6d70060000007374726963740b0000006c696d6974732e7069647303000000353132|f7418d9454a78af64bc5bc46e82206ccd085baf64cf7140e5ae4faf04b7c1ebe +# TEMPLATE on the host profile intel.host.v1. +KEY|HOST_PROFILE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d05000000696e74656c04000000686f7374010000005a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a58000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f50515253545556570100000001000000020000000100000002000000000000000100000000752b7d0000000000c2eb0b00000000fa000000020000000000000000000020000000000000008000000000000000080000000001000000000000000000000000000020000000000100000000000020000000000000006000000000530100006561726c79636f6e3d78653920636f6e736f6c653d68766331207265626f6f743d742070616e69633d2d31206e76785f73616e64626f783d31206e76785f636f6e6669673d307864303031303030302c3635353336206e725f637075733d32206e76785f736e617073686f745f746965723d706c6174666f726d2076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030313030303a362076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030323030303a372076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030333030303a342076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030363030303a31312076697274696f5f6d6d696f2e6465766963653d30783130303040307864303030373030303a33206e76785f636f6e74726f6c5f7474793d6876633217f80f8858431ca75fcc7ca694ea68637e017ede02db15e18d0ed4581067858d000000400000000000020000001000000000000400000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000001000000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000003000000767030040000007663707509000000706172746974696f6e00000000000000000000000000000000000000000006000000766d74696d6505000000636c6f636b06000000766d74696d65000000000000000000000000000000000000000000030000007069630300000070696303000000706963000000000200000004000000706d696f2000000000000000020000000000000004000000706d696fa00000000000000002000000000000000000000000000000000000000006000000696f6170696306000000696f6170696306000000696f617069630000000001000000040000006d6d696f0000c0fe00000000001000000000000000000000000000000000000000050000006c61706963050000006c6170696309000000706172746974696f6e0000000001000000040000006d6d696f0000e0fe00000000001000000000000000000000000000000000000000030000007069740300000070697403000000706974000000000100000004000000706d696f400000000000000004000000000000000100000000000000000000000000000000030000007274630300000072746303000000727463000000000100000004000000706d696f7000000000000000020000000000000001080000000000000000000000000000000d0000006d6963726f766d2d706f72746205000000706f7274620d0000006d6963726f766d2d706f727462000000000100000004000000706d696fe900000000000000020000000000000000000000000000000000000000100000006d6963726f766d2d73687574646f776e0800000073687574646f776e100000006d6963726f766d2d73687574646f776e000000000100000004000000706d696f0406000000000000010000000000000000000000000000000000000000180000006d6963726f766d2d736e617073686f742d7265717565737410000000736e617073686f742d72657175657374180000006d6963726f766d2d736e617073686f742d72657175657374000000000100000004000000706d696f05060000000000000100000000000000000000000000000000000000000b00000066733a6d6963726f766d300900000076697274696f2d66731300000076697274696f66732d333438393636353032340b00000076697274696f2d6d6d696f01000000040000006d6d696f001000d000000000001000000000000001060000000200000000000030030000000200000002000000000100000001000017000000636f6e736f6c653a6d6963726f766d2d76697274696f300e00000076697274696f2d636f6e736f6c651900000076697274696f2d636f6e736f6c652d333438393636393132300b00000076697274696f2d6d6d696f01000000040000006d6d696f002000d000000000001000000000000001070000000200000001000030030000000200000002000000000100000001000012000000626c6b3a73616e64626f783a64697374726f0a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393637333231360b00000076697274696f2d6d6d696f01000000040000006d6d696f003000d0000000000010000000000000010400000002000000640600300300000001000000010000000001000013000000626c6b3a73616e64626f783a736372617463680a00000076697274696f2d626c6b1500000076697274696f2d626c6b2d333438393638353530340b00000076697274696f2d6d6d696f01000000040000006d6d696f006000d0000000000010000000000000010b00000002000000440600300300000001000000010000000001000018000000636f6e736f6c653a6d6963726f766d2d636f6e74726f6c301600000076697274696f2d636f6e74726f6c2d636f6e736f6c652100000076697274696f2d636f6e74726f6c2d636f6e736f6c652d333438393638393630300b00000076697274696f2d6d6d696f01000000040000006d6d696f007000d000000000001000000000000001030000000200000001000030030000000200000002000000000100000001000001000000070000006d6963726f766d0000000000|93afa8bdde64df606707f5534c58420b92888acbbb6c8245e83006768c3ac940 +# A machine of one processor and one device, which the invalid vectors change. +KEY|MINIMAL|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000|d79a8fefc401da8568586cdc595a93a7e6c988aefbc62b03038901940211c1f1 + +# The class of the capture of TEMPLATE: the CPU profile auto, and 2 processors, all online at +# boot. +CLASS|TEMPLATE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d040000006175746f02000000020000000000002000000000000000800000000000000040000000000002000000100000000000040000000000020000000200000000000000000000|b53b0169db5d983b31f24bec900cd0b962d26f12480d90803f21ae1d5c3ff334 +# The class of the capture of NETWORK, whose network allows egress, and denies ingress and +# host-loopback access. +CLASS|NETWORK|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d040000006175746f02000000020000000000002000000000000000800000000000000040000000000002000000100000000000040000000000020000000200000a000000050000002d2d6e65740b00000031302e302e302e322f3234110000002d2d6e6574776f726b2d70726f66696c6508000000706f727461626c65100000002d2d6e6574776f726b2d65677265737305000000616c6c6f77110000002d2d6e6574776f726b2d696e67726573730400000064656e790f0000002d2d686f73742d6c6f6f706261636b0400000064656e79020000001100000069736f6c6174696f6e2e736563636f6d70060000007374726963740b0000006c696d6974732e7069647303000000353132|3ed4ab3d6536faa5872354282d702ae63ed89d14bb3c12dc9102d79ea5b38c6d + +# MINIMAL in other versions of the encoding. +INVALID_KEY_VERSION|VERSION_0|0000470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY_VERSION|VERSION_2|0200470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +INVALID_KEY_VERSION|VERSION_65535|ffff470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 + +# No bytes. +INVALID_KEY|EMPTY| +# MINIMAL without its last byte. +INVALID_KEY|TRUNCATED|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00000000 +# MINIMAL and a byte after it. +INVALID_KEY|TRAILING_BYTE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d000000000000 +# A command line whose digest does not match it. +INVALID_KEY|COMMAND_LINE_DIGEST|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663300000000000000000000000000000000000000000000000000000000000000000001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +# A backend that does not exist. +INVALID_KEY|UNKNOWN_BACKEND|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d31060000000000000001000000000000000100000000000000040000000000000001000000000000000300000078656e03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +# A device range of an address space that does not exist. +INVALID_KEY|UNKNOWN_ADDRESS_SPACE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e000000000100000004000000706f7274200000000000000002000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +# An interrupt delivery mode that is not UTF-8. +INVALID_KEY|INTERRUPT_MODE_NOT_UTF8|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000013000000656467652d7368617265642dff737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +# A NIC whose presence tag is 2. +INVALID_KEY|PRESENCE_TAG_2|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0200000000 +# A NIC whose policy's enforcement is 2. +INVALID_KEY|BOOLEAN_2|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0108000000706f727461626c655254000000025254000000010a000002180a00000101fd00000000000000000000000a00000278fd00000000000000000000000a0000010a000000616c6c6f772d6c6973740707070707070707070707070707070707070707070707070707070707070707020400000000000000 +# Invariant settings out of the order of their names. +INVALID_KEY|UNSORTED_SETTINGS|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00020000000b0000006c696d6974732e70696473030000003531321100000069736f6c6174696f6e2e736563636f6d7006000000737472696374 +# An invariant setting named twice. +INVALID_KEY|DUPLICATE_SETTINGS|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00020000000b0000006c696d6974732e70696473030000003531320b0000006c696d6974732e7069647303000000353132 +# An invariant setting whose name holds an uppercase letter. +INVALID_KEY|UPPERCASE_SETTING|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d00010000000b0000004c696d6974732e7069647303000000353132 +# More APIC IDs than a microVM has processors. +INVALID_KEY|TOO_MANY_APIC_IDS|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b010000000100000001000000010000000900000000000000010000000200000003000000040000000500000006000000070000000800000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +# 3 processors, which a microVM cannot have. +INVALID_KEY|THREE_PROCESSORS|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b010000000100000003000000010000000300000000000000010000000200000000ca9a3b0000000000e1f505000000000000000003000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +# RAM ranges that do not hold the RAM. +INVALID_KEY|MEMORY_RANGES|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000400000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +# A machine without devices. +INVALID_KEY|NO_DEVICES|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000000000001000000070000006d6963726f766d0000000000 +# A device ID twice. +INVALID_KEY|DUPLICATE_DEVICE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000200000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000009000000706172746974696f6e05000000636c6f636b06000000766d74696d6500000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +# A filesystem slot that is not the dormant slot of version 1. +INVALID_KEY|SLOT_VERSION_0|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000000000000070000006d6963726f766d0000000000 +# A filesystem slot without a tag. +INVALID_KEY|EMPTY_TAG|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000000000000000000000 +# A distro whose logical block size is 256. +INVALID_KEY|DISTRO_BLOCK_SIZE|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000010000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +# An OpenVMM identity that holds a space. +INVALID_KEY|ARTIFACT_WITH_SPACE|0100470000007368613235363a20616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +# A CPU profile of revision 0. +INVALID_KEY|CPU_PROFILE_REVISION_0|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610000000011111111111111111111111111111111111111111111111111111111111111112c000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000 +# An effective CPUID that is not whole entries of 44 bytes. +INVALID_KEY|CPUID_LENGTH|0100470000007368613235363a61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161470000007368613235363a62626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262626262470000007368613235363a636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363636363630300000000000000190000006e76782d6d6963726f766d2d76322d636f6e74726f6c2d763201000000000000000d00000067756573742d6275696c642d3106000000000000000100000000000000010000000000000004000000000000000100000000000000030000006b766d03000000616d640500000067656e6f610100000011111111111111111111111111111111111111111111111111111111111111112b000000000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a01000000010000000100000001000000010000000000000000ca9a3b0000000000e1f505000000000000000001000000000000000000000800000000000000080000000000000008000000000100000000000000000000000000000800000000000000000c000000636f6e736f6c653d687663308c7f813b328f96bdb26c4e97b6f5c874cbd968e8664778addc56a7c81ea55113001000000000000000020000000200000010000000000000000200000002000012000000656467652d7368617265642d737461747573000003000000000000100000000000000100000009000000706172746974696f6e09000000706172746974696f6e09000000706172746974696f6e00000000000000000000000000000000000000000001000000070000006d6963726f766d0000000000