diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 1bbaa3391..430677c3d 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -77,7 +77,7 @@ nvx-rs/ | `nvx::protocol::nvxs` | The NVXS session, independent of `NVXC` and of any other application protocol: `RecordType`, `Record`, `encode_record`, and `RecordParser`, which parses records incrementally across partial reads with buffers bounded by one record; `HostSession` and `GuestSession`, the state machines of the host's and the guest's roles, which validate every record's type, identity, and sequence number, and the guest's receive window and credits, without I/O. An application message may span DATA records, and the application's own framing reassembles it | | `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: the `ConfigurationIdentity`, and a clone's transaction ID. It needs no features | +| `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::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 | @@ -1115,12 +1115,14 @@ sides of every record, frame, and payload: the codec decodes each vector of `tests/vectors/nvxc-v1.txt` to its values and encodes them to its bytes, it accepts or refuses OpenVMM's vectors and cases of the `NVXS` session as they say, and, on Linux, the guest's listener announcements match -`tests/vectors/nvxl-v1.txt`. Property tests check that records and activation -records round-trip, that the activation record decoder accepts only canonical -encodings, and that the `NVXS` record parser, the activation record decoder, -and the host's and the guest's `EXEC` and `MAPS` decoders return, never panic, -on arbitrary bytes; those of the egress policy -compiler compare it with a reference model of the format +`tests/vectors/nvxl-v1.txt`. The activation codecs decode each record and +Ready activation of `tests/vectors/activation-v1.txt` to its values, encode +them to its bytes, and refuse its invalid bytes. Property tests check that +records and activation records round-trip, that the activation record and +Ready activation decoders accept only canonical encodings, and that the +`NVXS` record parser, those decoders, and the host's and the guest's `EXEC` +and `MAPS` decoders return, never panic, on arbitrary bytes; those of the +egress policy compiler compare it with a reference model of the format ([Egress policy files](#egress-policy-files)). The NVXS test drives the host's and the guest's roles of the session and their connections through a broker that relays records as OpenVMM's does, with messages of an application diff --git a/nvx-rs/src/activation/codec/tests.rs b/nvx-rs/src/activation/codec/tests.rs index 7c5700208..6ed85c9a7 100644 --- a/nvx-rs/src/activation/codec/tests.rs +++ b/nvx-rs/src/activation/codec/tests.rs @@ -10,6 +10,8 @@ use super::*; use crate::activation::tests::{configuration, maximal, restore}; use crate::activation::{MAX_HOSTNAME_BYTES, MAX_MAPPINGS, MAX_PROXY_ENDPOINT_BYTES}; +mod vectors; + /// A field of an encoding, as the tests build it: its tag and its value. type RawField = (u8, Vec); diff --git a/nvx-rs/src/activation/codec/tests/vectors.rs b/nvx-rs/src/activation/codec/tests/vectors.rs new file mode 100644 index 000000000..b837f14c9 --- /dev/null +++ b/nvx-rs/src/activation/codec/tests/vectors.rs @@ -0,0 +1,234 @@ +//! Tests of the canonical encoding against the conformance vectors, which give records from the +//! encoding's definition, never from the code that they check. + +use std::net::IpAddr; +use std::num::NonZeroU64; + +use crate::activation::{ + ActivationError, ActivationRecord, Configuration, ExpectedResources, Export, ExportKind, + Identity, Image, Journal, MachineIdMode, Network, OverlayMode, Publication, RestoreSection, + StoreGeneration, TemplateReference, TransactionId, Upper, WorkloadIdentity, WorkloadView, +}; +use crate::tests::{ACTIVATION_V1, Values, Vector, of_kind}; +use crate::uuid::Uuid; + +/// The names of the vectors of records, in order. +const RECORDS: [&str; 5] = [ + "COLD", + "CLONE", + "SCRATCH_AGGREGATE", + "GPT_DIRECTORY", + "LARGEST", +]; + +/// Reads a byte string of exactly `N` bytes. +fn array(values: &mut Values<'_>) -> [u8; N] { + values + .bytes() + .try_into() + .unwrap_or_else(|bytes: Vec| panic!("{} bytes are not {N}", bytes.len())) +} + +/// Reads an identity. +fn identity(values: &mut Values<'_>) -> Identity { + Identity::from_bytes(array(values)) +} + +/// Reads a UUID or a GUID, in the order of its canonical text. +fn uuid(values: &mut Values<'_>) -> Uuid { + Uuid::from_bytes(array(values)) +} + +/// Reads a word, which must be one of those of `choices`, and returns the choice that it names. +fn choice(values: &mut Values<'_>, choices: &[(&str, T)]) -> T { + let word = values.word(); + choices + .iter() + .find(|(name, _)| *name == word) + .map(|(_, choice)| *choice) + .unwrap_or_else(|| panic!("{word} names no choice of its value")) +} + +/// Returns the record that a `RECORD` vector's values give. +fn record_of(vector: &Vector) -> ActivationRecord { + let mut values = vector.values(); + let restore = values.optional(|values| RestoreSection { + transaction: TransactionId::from_bytes(array(values)), + template: TemplateReference { + generation: StoreGeneration::new( + NonZeroU64::new(values.number()).expect("a store generation of 0"), + ), + key: identity(values), + }, + }); + let image = match values.word() { + "EROFS" => Image::Erofs { + binding: identity(&mut values), + uuid: values.optional(uuid), + }, + "GPT" => Image::Gpt { + binding: identity(&mut values), + first_partition_bound: values.number(), + disk_guid: values.optional(uuid), + }, + format => panic!("{format} is no image format"), + }; + let upper = match values.word() { + "TMPFS" => Upper::Tmpfs, + "SCRATCH" => Upper::Scratch { + binding: identity(&mut values), + journal: choice( + &mut values, + &[ + ("ALLOWED", Journal::Allowed), + ("FORBIDDEN", Journal::Forbidden), + ], + ), + }, + upper => panic!("{upper} is no upper"), + }; + let overlay = choice( + &mut values, + &[ + ("TRUSTED_LOWERS", OverlayMode::TrustedLowers), + ("UNTRUSTED_LOWERS", OverlayMode::UntrustedLowers), + ], + ); + let export = values.optional(|values| { + let kind = choice( + values, + &[ + ("DIRECTORY", ExportKind::Directory), + ("AGGREGATE", ExportKind::Aggregate), + ], + ); + let plan = identity(values); + (Export { kind, plan }, values.number()) + }); + let (export, mappings) = match export { + Some((export, mappings)) => (Some(export), mappings), + None => (None, 0), + }; + let network = values.optional(|values| Network { + policy: identity(values), + publication: choice( + values, + &[ + ("PUBLISHED", Publication::Published), + ("UNPUBLISHED", Publication::Unpublished), + ], + ), + dns: values.optional(|values| { + let bytes = values.bytes(); + match <[u8; 4]>::try_from(&bytes[..]) { + Ok(address) => IpAddr::from(address), + Err(_) => IpAddr::from(<[u8; 16]>::try_from(&bytes[..]).unwrap()), + } + }), + proxy: values.optional(|values| String::from_utf8(values.text()).unwrap()), + }); + let hostname = values.text(); + let machine_id = choice( + &mut values, + &[ + ("IMAGE", MachineIdMode::Image), + ("GENERATED", MachineIdMode::Generated), + ], + ); + let workload_identity = values.optional(|values| WorkloadIdentity { + uid: values.number(), + gid: values.number(), + }); + let resources = ExpectedResources { + cpus: values.number(), + memory: values.number(), + }; + drop(values); + let configuration = Configuration { + image, + upper, + overlay, + export, + mappings, + network, + workload: WorkloadView { + hostname, + machine_id, + identity: workload_identity, + }, + resources, + }; + ActivationRecord::new(restore, configuration) + .unwrap_or_else(|error| panic!("RECORD {}: {error}", vector.name)) +} + +#[test] +fn records_match_their_vectors() { + for vector in of_kind(ACTIVATION_V1, "RECORD", &RECORDS) { + let name = vector.name; + let record = record_of(&vector); + assert_eq!(record.encode(), vector.bytes, "{name}"); + assert_eq!( + ActivationRecord::decode(&vector.bytes).as_ref(), + Ok(&record), + "{name}" + ); + } +} + +#[test] +fn invalid_records_are_refused() { + let names = [ + "EMPTY", + "TRUNCATED_VERSION", + "NO_FIELDS", + "VERSION_0", + "VERSION_2", + "TOO_LONG", + "TRUNCATED_FIELD_HEADER", + "TRUNCATED_FIELD_VALUE", + "UNKNOWN_FIELD", + "REPEATED_FIELD", + "UPPER_BEFORE_IMAGE", + "RESTORE_LAST", + "EMPTY_OVERLAY", + "LONG_OVERLAY", + "OVERLAY_CODE_0", + "OVERLAY_CODE_3", + "IMAGE_FORMAT_3", + "GPT_WITHOUT_BOUND", + "SHORT_RESTORE", + "SHORT_IMAGE_IDENTITY", + "SCRATCH_WITHOUT_JOURNAL", + "MACHINE_ID_CODE_0", + "DNS_OF_5_BYTES", + "SHORT_RESOURCES", + "ZERO_GENERATION", + "MISSING_IMAGE", + "MISSING_RESOURCES", + "EXPORT_WITHOUT_MAPPINGS", + "MAPPINGS_WITHOUT_EXPORT", + "ZERO_MAPPINGS", + "DNS_WITHOUT_NETWORK", + "PROXY_WITHOUT_NETWORK", + "TOO_MANY_MAPPINGS", + "EMPTY_PROXY", + "PROXY_WITH_CREDENTIALS", + "PROXY_WITH_SPACE", + "PROXY_NOT_ASCII", + "EMPTY_HOSTNAME", + "HOSTNAME_WITH_NUL", + "LONG_HOSTNAME", + "ZERO_FIRST_PARTITION_BOUND", + "ZERO_UID", + "ZERO_GID", + "UNCHANGED_UID", + "UNCHANGED_GID", + "ZERO_CPUS", + "ZERO_MEMORY", + ]; + for vector in of_kind(ACTIVATION_V1, "INVALID_RECORD", &names) { + let refusal: Result<_, ActivationError> = ActivationRecord::decode(&vector.bytes); + assert!(refusal.is_err(), "{} is admitted", vector.name); + } +} diff --git a/nvx-rs/src/activation/mod.rs b/nvx-rs/src/activation/mod.rs index dd25eaaa2..c47ee54b3 100644 --- a/nvx-rs/src/activation/mod.rs +++ b/nvx-rs/src/activation/mod.rs @@ -5,8 +5,8 @@ //! A layered root's guest assembles its root and enters its workload with settings that the host //! chooses for each launch. A clone that the host restores from a template keeps the template's //! captured configuration, so its record gives it the settings of its own launch: everything that -//! it needs to activate. A cold boot's record holds the settings that its guest applied, so that -//! every layered root reports the identity of its configuration in the same way: +//! it needs to activate. A cold boot's record holds the settings that its guest applied, in the +//! same form, and every layered root reports its activation in Ready in the same way: //! //! - [`ActivationRecord`]: the [`VERSION`] of its schema, the [`RestoreSection`] of a clone, and //! the [`Configuration`]. A clone's restore section holds the restore's [`TransactionId`], 16 @@ -16,15 +16,16 @@ //! [`Identity`] of its key. A cold boot's record has no restore section. //! - [`Configuration`]: the non-secret settings that the activation applies, which the //! [configuration](#configuration) lists. -//! - [`ReadyActivation`]: what Ready reports about the activation: the [`ConfigurationIdentity`], -//! which every layered root reports, and the transaction ID, which only a clone reports. +//! - [`ReadyActivation`]: what Ready reports about the activation: that it applied, which every +//! layered root reports, and the transaction ID, which only a clone reports. //! //! [`ActivationRecord::new`] admits a configuration only once [`Configuration::check`] finds that //! it keeps every [limit](#limits), so that every record holds settings that a guest can apply. //! [`ActivationRecord::encode`] writes a record's canonical [encoding](#encoding), and -//! [`ActivationRecord::decode`] reads it back, by the same rules. The module depends on nothing -//! outside the standard library and the crate's `protocol` and `uuid` modules, so it builds -//! without features, and the host, the guest, and every adapter share it. +//! [`ActivationRecord::decode`] reads it back, by the same rules, and [`ReadyActivation`] has a +//! canonical [encoding](#ready) of its own. The module depends on nothing outside the standard +//! library and the crate's `protocol` and `uuid` modules, so it builds without features, and the +//! host, the guest, and every adapter's framing share it. //! //! # Configuration //! @@ -111,6 +112,23 @@ //! exactly one encoding, which [`ActivationRecord::encode`] writes, and which the decoder reads //! back to the same record. //! +//! # Ready +//! +//! A guest reports Ready once its activation has applied its record, and +//! [`ReadyActivation::of`] returns what that Ready reports about the activation: a clone's +//! transaction ID, and nothing more for a cold boot. The transaction ID ties a clone's Ready to +//! the record of its restore, since the host picks a new transaction ID for each restore and +//! sends no other record under it. What Ready reports has a canonical encoding, which +//! [`ReadyActivation::encode`] writes and [`ReadyActivation::decode`] reads back: `0` for a cold +//! boot, or `1` and the clone's 16-byte transaction ID, [`ReadyActivation::COLD_LEN`] or +//! [`ReadyActivation::CLONE_LEN`] bytes. A Ready that embeds these bytes therefore has the same +//! encoding for a clone as for a cold boot. +//! +//! The conformance vectors of `tests/vectors/activation-v1.txt` hold records, and what Ready +//! reports about their activations, in both encodings, and bytes that every decoder refuses. +//! +//! # Example +//! //! ``` //! use std::num::NonZeroU64; //! @@ -167,6 +185,7 @@ use crate::protocol::root::ImageFormat; use crate::uuid::Uuid; mod codec; +mod ready; #[cfg(test)] mod tests; @@ -286,40 +305,6 @@ impl fmt::Display for StoreGeneration { } } -/// The identity of the configuration that a layered root's activation applied, which its Ready -/// reports: 32 bytes. -/// -/// It displays as 64 lowercase hexadecimal digits. -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct ConfigurationIdentity([u8; 32]); - -impl ConfigurationIdentity { - /// The length of a configuration identity, in bytes. - pub const LEN: usize = 32; - - /// Returns the configuration identity whose bytes are `bytes`. - pub const fn from_bytes(bytes: [u8; Self::LEN]) -> Self { - Self(bytes) - } - - /// Returns the configuration identity's bytes. - pub const fn as_bytes(&self) -> &[u8; Self::LEN] { - &self.0 - } -} - -impl fmt::Display for ConfigurationIdentity { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - write_hex(formatter, &self.0) - } -} - -impl fmt::Debug for ConfigurationIdentity { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(formatter, "ConfigurationIdentity({self})") - } -} - /// The template that a clone restores: the generation of the template in its store, and the /// identity of the template's key. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -629,17 +614,17 @@ impl ActivationRecord { } } -/// What Ready reports about a layered root's activation: the identity of the configuration that -/// the activation applied, and, for a clone, the transaction ID of the restore that it activated. +/// What Ready reports about a layered root's activation: that it applied, and, for a clone, the +/// transaction ID of the restore that it activated. Its canonical [encoding](self#ready) holds +/// [`COLD_LEN`](Self::COLD_LEN) or [`CLONE_LEN`](Self::CLONE_LEN) bytes. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ReadyActivation { - /// The identity of the configuration that the activation applied. - pub identity: ConfigurationIdentity, /// The transaction ID of a clone's restore, or `None` for a cold boot. pub transaction: Option, } -/// Why an activation record was refused: the limit or the rule that it breaks. +/// Why an activation record, or what Ready reports about an activation, was refused: the limit or +/// the rule that it breaks. #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum ActivationError { @@ -685,6 +670,8 @@ pub enum ActivationError { /// A field that only a record with a network holds, its DNS server's or its proxy's, is /// present without one. WithoutNetwork(Field), + /// The bytes of what Ready reports are neither `0`, nor `1` followed by a transaction ID. + Ready, } impl fmt::Display for ActivationError { @@ -751,6 +738,8 @@ impl fmt::Display for ActivationError { Self::WithoutNetwork(field) => { write!(formatter, "the record has a {field} field but no network") } + Self::Ready => formatter + .write_str("what Ready reports is neither 0, nor 1 followed by a transaction ID"), } } } diff --git a/nvx-rs/src/activation/ready.rs b/nvx-rs/src/activation/ready.rs new file mode 100644 index 000000000..e6fa98ed6 --- /dev/null +++ b/nvx-rs/src/activation/ready.rs @@ -0,0 +1,58 @@ +//! What Ready reports about an activation, which the [module](super#ready) describes: +//! [`ReadyActivation::of`] returns it for a record, [`ReadyActivation::encode`] writes its +//! canonical encoding, and [`ReadyActivation::decode`] reads it back. + +use super::{ActivationError, ActivationRecord, ReadyActivation, TransactionId}; + +#[cfg(test)] +mod tests; + +/// The byte of a cold boot's activation, which has no transaction. +const COLD: u8 = 0; +/// The byte of a clone's activation, before its transaction ID. +const CLONE: u8 = 1; + +impl ReadyActivation { + /// The bytes of a cold boot's activation: a `0`. + pub const COLD_LEN: usize = 1; + + /// The bytes of a clone's activation: a `1`, and its transaction ID. + pub const CLONE_LEN: usize = Self::COLD_LEN + TransactionId::LEN; + + /// Returns what Ready reports about the activation of `record`: the transaction ID of a + /// clone's restore, and nothing more for a cold boot. + pub fn of(record: &ActivationRecord) -> Self { + Self { + transaction: record.restore().map(|restore| restore.transaction), + } + } + + /// Returns the activation's canonical encoding, as the [module](super#ready) describes it: + /// `0` for a cold boot, or `1` and a clone's transaction ID. + pub fn encode(&self) -> Vec { + let mut bytes = Vec::with_capacity(Self::CLONE_LEN); + match &self.transaction { + None => bytes.push(COLD), + Some(transaction) => { + bytes.push(CLONE); + bytes.extend_from_slice(transaction.as_bytes()); + } + } + bytes + } + + /// Reads an activation from its canonical encoding, as the [module](super#ready) describes it. + /// + /// Fails with [`ActivationError::Ready`] unless the bytes are `0`, or `1` and a transaction + /// ID, and nothing more. + pub fn decode(bytes: &[u8]) -> Result { + let transaction = match bytes { + [COLD] => None, + [CLONE, transaction @ ..] => Some(TransactionId::from_bytes( + transaction.try_into().map_err(|_| ActivationError::Ready)?, + )), + _ => return Err(ActivationError::Ready), + }; + Ok(Self { transaction }) + } +} diff --git a/nvx-rs/src/activation/ready/tests.rs b/nvx-rs/src/activation/ready/tests.rs new file mode 100644 index 000000000..728673f35 --- /dev/null +++ b/nvx-rs/src/activation/ready/tests.rs @@ -0,0 +1,173 @@ +//! Tests of what Ready reports about an activation, and of its canonical encoding. The tests build +//! its bytes from the encoding's definition, never with the encoder that they check. + +use proptest::prelude::*; + +use super::*; +use crate::tests::{ACTIVATION_V1, of_kind, vector}; + +/// The names of the vectors of what Ready reports, in order: those of the records whose +/// activations they report. +const READY: [&str; 5] = [ + "COLD", + "CLONE", + "SCRATCH_AGGREGATE", + "GPT_DIRECTORY", + "LARGEST", +]; + +/// Returns the transaction ID of the tests: the bytes a0 through af. +fn transaction() -> TransactionId { + TransactionId::from_bytes(std::array::from_fn(|index| { + 0xa0 + u8::try_from(index).unwrap() + })) +} + +#[test] +fn the_lengths_keep_their_values() { + assert_eq!( + (ReadyActivation::COLD_LEN, ReadyActivation::CLONE_LEN), + (1, 17) + ); +} + +#[test] +fn a_cold_boot_reports_a_zero() { + let ready = ReadyActivation { transaction: None }; + assert_eq!(ready.encode(), [0]); + assert_eq!(ready.encode().len(), ReadyActivation::COLD_LEN); + assert_eq!(ReadyActivation::decode(&[0]), Ok(ready)); +} + +#[test] +fn a_clone_reports_a_one_then_its_transaction() { + let ready = ReadyActivation { + transaction: Some(transaction()), + }; + let bytes = [&[1][..], transaction().as_bytes()].concat(); + assert_eq!(ready.encode(), bytes); + assert_eq!(bytes.len(), ReadyActivation::CLONE_LEN); + assert_eq!(ReadyActivation::decode(&bytes), Ok(ready)); +} + +#[test] +fn every_other_form_is_refused() { + assert_eq!(ReadyActivation::decode(&[]), Err(ActivationError::Ready)); + for flag in u8::MIN..=u8::MAX { + for tail in 0..=TransactionId::LEN + 1 { + let bytes = [&[flag][..], &transaction().as_bytes().repeat(2)[..tail]].concat(); + let decoded = ReadyActivation::decode(&bytes); + match (flag, tail) { + (0, 0) | (1, TransactionId::LEN) => assert!(decoded.is_ok()), + _ => assert_eq!( + decoded, + Err(ActivationError::Ready), + "flag {flag}, then {tail} bytes" + ), + } + } + } +} + +#[test] +fn ready_reports_a_clone_s_transaction_and_nothing_more_for_a_cold_boot() { + for name in READY { + let record = ActivationRecord::decode(&vector(ACTIVATION_V1, "RECORD", name).bytes) + .unwrap_or_else(|error| panic!("RECORD {name}: {error}")); + let ready = ReadyActivation::of(&record); + assert_eq!( + ready.transaction, + record.restore().map(|restore| restore.transaction), + "{name}" + ); + assert_eq!( + ready.encode(), + vector(ACTIVATION_V1, "READY", name).bytes, + "{name}" + ); + } +} + +#[test] +fn ready_activations_match_their_vectors() { + for vector in of_kind(ACTIVATION_V1, "READY", &READY) { + let mut values = vector.values(); + let transaction = + values.optional(|values| TransactionId::from_bytes(values.bytes().try_into().unwrap())); + drop(values); + let ready = ReadyActivation { transaction }; + assert_eq!(ready.encode(), vector.bytes, "{}", vector.name); + assert_eq!( + ReadyActivation::decode(&vector.bytes), + Ok(ready), + "{}", + vector.name + ); + } +} + +#[test] +fn invalid_ready_activations_are_refused() { + let names = [ + "EMPTY", + "FLAG_2", + "COLD_WITH_TRANSACTION", + "COLD_WITH_TRAILING_BYTE", + "CLONE_WITHOUT_TRANSACTION", + "SHORT_TRANSACTION", + "LONG_TRANSACTION", + ]; + for vector in of_kind(ACTIVATION_V1, "INVALID_READY", &names) { + assert_eq!( + ReadyActivation::decode(&vector.bytes), + Err(ActivationError::Ready), + "{}", + vector.name + ); + } +} + +#[test] +fn the_refusal_names_the_rule() { + assert_eq!( + ActivationError::Ready.to_string(), + "what Ready reports is neither 0, nor 1 followed by a transaction ID" + ); +} + +/// Returns bytes that a decoder may meet: arbitrary bytes, and a byte from 0 to 2 followed by up +/// to 17 more bytes. +fn inputs() -> impl Strategy> { + let forms = (0_u8..=2, prop::collection::vec(any::(), 0..=17)) + .prop_map(|(flag, tail)| [&[flag][..], &tail].concat()); + prop_oneof![prop::collection::vec(any::(), 0..32), forms] +} + +proptest! { + /// Every activation encodes to bytes that decode back to it, while every prefix of them, and + /// every longer form, is refused. + #[test] + fn ready_activations_round_trip( + transaction in any::>(), + cut in any::(), + extra in any::(), + ) { + let ready = ReadyActivation { + transaction: transaction.map(TransactionId::from_bytes), + }; + let mut bytes = ready.encode(); + prop_assert_eq!(ReadyActivation::decode(&bytes), Ok(ready)); + prop_assert!(ReadyActivation::decode(&bytes[..cut.index(bytes.len())]).is_err()); + bytes.push(extra); + prop_assert!(ReadyActivation::decode(&bytes).is_err()); + } + + /// The decoder returns, never panics, on any bytes, and accepts only the one encoding of the + /// activation that it returns. + #[test] + fn the_decoder_accepts_only_canonical_encodings(bytes in inputs()) { + if let Ok(ready) = ReadyActivation::decode(&bytes) { + prop_assert_eq!(ready.encode(), bytes); + } + } +} diff --git a/nvx-rs/src/activation/tests.rs b/nvx-rs/src/activation/tests.rs index aed8595b6..353c649c6 100644 --- a/nvx-rs/src/activation/tests.rs +++ b/nvx-rs/src/activation/tests.rs @@ -121,7 +121,6 @@ fn the_limits_keep_their_values() { assert_eq!(MAX_HOSTNAME_BYTES, 64); assert_eq!(MAX_PROXY_ENDPOINT_BYTES, 255); assert_eq!(Identity::LEN, 32); - assert_eq!(ConfigurationIdentity::LEN, 32); assert_eq!(TransactionId::LEN, 16); } @@ -385,13 +384,6 @@ fn opaque_values_keep_their_bytes_and_display_them_in_hexadecimal() { assert_eq!(identity.as_bytes(), &bytes); assert_eq!(identity.to_string(), digits); assert_eq!(format!("{identity:?}"), format!("Identity({digits})")); - let configuration = ConfigurationIdentity::from_bytes(bytes); - assert_eq!(configuration.as_bytes(), &bytes); - assert_eq!(configuration.to_string(), digits); - assert_eq!( - format!("{configuration:?}"), - format!("ConfigurationIdentity({digits})") - ); let bytes: [u8; 16] = std::array::from_fn(|index| 0xf0 - index as u8); let digits = "f0efeeedecebeae9e8e7e6e5e4e3e2e1"; let transaction = TransactionId::from_bytes(bytes); @@ -443,16 +435,10 @@ fn an_image_reports_its_format_binding_and_expected_identity() { #[test] fn ready_reports_a_transaction_only_for_a_clone() { - let identity = ConfigurationIdentity::from_bytes([0x99; 32]); - let cold = ReadyActivation { - identity, - transaction: None, - }; + let cold = ReadyActivation { transaction: None }; let clone = ReadyActivation { - identity, transaction: Some(restore().transaction), }; - assert_eq!(cold.identity, clone.identity); assert_eq!( clone.transaction, Some(TransactionId::from_bytes([0x22; 16])) diff --git a/nvx-rs/src/tests.rs b/nvx-rs/src/tests.rs index 85ad83a77..13603041a 100644 --- a/nvx-rs/src/tests.rs +++ b/nvx-rs/src/tests.rs @@ -5,6 +5,9 @@ use std::fmt::Debug; /// The conformance vectors of version 1 of the control protocol. pub(crate) const NVXC_V1: &str = include_str!("../tests/vectors/nvxc-v1.txt"); +/// The conformance vectors of version 1 of the activation record, and of what Ready reports about +/// an activation. +pub(crate) const ACTIVATION_V1: &str = include_str!("../tests/vectors/activation-v1.txt"); /// The conformance vectors of version 1 of the listener announcement. #[cfg(all(feature = "guest", target_os = "linux"))] pub(crate) const NVXL_V1: &str = include_str!("../tests/vectors/nvxl-v1.txt"); diff --git a/nvx-rs/tests/vectors/README.md b/nvx-rs/tests/vectors/README.md index 2c67dea19..6f6099ab9 100644 --- a/nvx-rs/tests/vectors/README.md +++ b/nvx-rs/tests/vectors/README.md @@ -11,18 +11,20 @@ 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 | | [`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) and [3.5.4 Host-Local Access and Ingress](../../../doc/design/3.5-network.md#354-host-local-access-and-ingress) -describe the protocols. +describe the protocols, and the documentation of `nvx::activation`, in +[`src/activation/mod.rs`](../../src/activation/mod.rs), the activation record. ## Line format -`nvxc-v1.txt` and `nvxl-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`, 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: ```text KIND|NAME|BYTES|VALUE|... diff --git a/nvx-rs/tests/vectors/activation-v1.txt b/nvx-rs/tests/vectors/activation-v1.txt new file mode 100644 index 000000000..1b9a83402 --- /dev/null +++ b/nvx-rs/tests/vectors/activation-v1.txt @@ -0,0 +1,115 @@ +# Conformance vectors of version 1 of the activation record of a layered root, and of what Ready +# reports about an activation. The documentation of nvx::activation defines the encodings. +# README.md describes the line format and the rules that the vectors follow. +# +# RECORD|NAME|BYTES|RESTORE|IMAGE|UPPER|OVERLAY|EXPORT|NETWORK|HOSTNAME|MACHINE_ID|WORKLOAD|CPUS|MEMORY +# An activation record. RESTORE is absent, for a cold boot, or a clone's +# TRANSACTION|GENERATION|KEY, with GENERATION a number. IMAGE is EROFS|BINDING|UUID or +# GPT|BINDING|BOUND|GUID, with UUID and GUID absent where the guest expects none. UPPER is TMPFS, +# or SCRATCH|BINDING|JOURNAL with JOURNAL ALLOWED or FORBIDDEN. OVERLAY is TRUSTED_LOWERS or +# UNTRUSTED_LOWERS. EXPORT is absent, or DIRECTORY or AGGREGATE followed by PLAN|MAPPINGS. NETWORK +# is absent, or POLICY|PUBLICATION|DNS|PROXY, with PUBLICATION PUBLISHED or UNPUBLISHED, DNS the +# bytes of an IPv4 or IPv6 address, or absent, and PROXY a text, or absent. HOSTNAME is a text, +# MACHINE_ID IMAGE or GENERATED, and WORKLOAD absent, or UID|GID. Identities, IDs, UUIDs, and +# GUIDs are byte strings. +# READY|NAME|BYTES|TRANSACTION +# What Ready reports about the activation of the record of the same name: a clone's transaction +# ID, or absent for a cold boot. +# INVALID_RECORD|NAME|BYTES +# Bytes that every decoder refuses as an activation record. +# INVALID_READY|NAME|BYTES +# Bytes that every decoder refuses as what Ready reports about an activation. + +# The smallest record: a cold boot's EROFS image over a tmpfs, without an export, a +# network, or a workload identity. +RECORD|COLD|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000|-|EROFS|000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f|-|TMPFS|TRUSTED_LOWERS|-|-|nvx-sandbox|IMAGE|-|1|268435456 + +# The same configuration in a clone's record, with its restore section. +RECORD|CLONE|01000138a0a1a2a3a4a5a6a7a8a9aaabacadaeaf0100000000000000c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000|a0a1a2a3a4a5a6a7a8a9aaabacadaeaf|1|c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf|EROFS|000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f|-|TMPFS|TRUSTED_LOWERS|-|-|nvx-sandbox|IMAGE|-|1|268435456 + +# A cold boot's EROFS image with a filesystem UUID over the scratch, an aggregate export with +# the most mappings, and a network with its policy alone. +RECORD|SCRATCH_AGGREGATE|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f11100123456789abcdef0123456789abcdef202202202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f01300101402102404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f410200105021606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f026006776f726b6572610101700c040000000000008000000000|-|EROFS|000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f|0123456789abcdef0123456789abcdef|SCRATCH|202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f|ALLOWED|TRUSTED_LOWERS|AGGREGATE|404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f|4096|606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f|UNPUBLISHED|-|-|worker|IMAGE|-|4|2147483648 + +# A clone's GPT disk with a disk GUID over a tmpfs, a directory export with one mapping, a +# network with an IPv4 DNS server and a proxy, a generated machine ID, and a workload identity. +RECORD|GPT_DIRECTORY|010001380102030405060708090a0b0c0d0e0f1008070605040302012122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40102902808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f00001000000000001110fedcba9876543210fedcba9876543210200101300102402101e0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff4102010050214142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f600151040a0000015214687474703a2f2f31302e302e302e313a333132386007636c6f6e652d316101026208e8030000e9030000700c020000000000004000000000|0102030405060708090a0b0c0d0e0f10|0x102030405060708|2122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40|GPT|808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f|1048576|fedcba9876543210fedcba9876543210|TMPFS|UNTRUSTED_LOWERS|DIRECTORY|e0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff|1|4142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f60|PUBLISHED|0a000001|http://10.0.0.1:3128|clone-1|GENERATED|1000|1001|2|1073741824 + +# Every field at its largest: 602 bytes. The hostname holds the bytes 01 through 40. +RECORD|LARGEST|01000138f0f1f2f3f4f5f6f7f8f9fafbfcfdfeffffffffffffffffffc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf102902a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfffffffffffffffff1110ffffffffffffffffffffffffffffffff202202808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f02300102402102606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f410200105021404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f015110fd00000000000000000000000a00000152ff687474703a2f2f616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161612e746573743a3331323860400102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f406101026208fefffffffeffffff700cffffffffffffffffffffffff|f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff|0xffffffffffffffff|c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf|GPT|a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf|0xffffffffffffffff|ffffffffffffffffffffffffffffffff|SCRATCH|808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f|FORBIDDEN|UNTRUSTED_LOWERS|AGGREGATE|606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f|4096|404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f|PUBLISHED|fd00000000000000000000000a000001|http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.test:3128|%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f !"#$%25&'()*+,-./0123456789:;<=>?@|GENERATED|4294967294|4294967294|0xffffffff|0xffffffffffffffff + +# What Ready reports about the activation of each record. +READY|COLD|00|- +READY|CLONE|01a0a1a2a3a4a5a6a7a8a9aaabacadaeaf|a0a1a2a3a4a5a6a7a8a9aaabacadaeaf +READY|SCRATCH_AGGREGATE|00|- +READY|GPT_DIRECTORY|010102030405060708090a0b0c0d0e0f10|0102030405060708090a0b0c0d0e0f10 +READY|LARGEST|01f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff|f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff + +# Bytes that end inside the version, or hold no field. +INVALID_RECORD|EMPTY| +INVALID_RECORD|TRUNCATED_VERSION|01 +INVALID_RECORD|NO_FIELDS|0100 + +# Another version, and bytes past the limit. +INVALID_RECORD|VERSION_0|0000102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|VERSION_2|0200102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|TOO_LONG|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c01000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + +# Fields that end past the bytes, are unknown, repeat, or come out of order. +INVALID_RECORD|TRUNCATED_FIELD_HEADER|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c01000000000000100000000071 +INVALID_RECORD|TRUNCATED_FIELD_VALUE|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c0100000000000010000000 +INVALID_RECORD|UNKNOWN_FIELD|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f120101200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|REPEATED_FIELD|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|UPPER_BEFORE_IMAGE|0100200101102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|RESTORE_LAST|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c0100000000000010000000000138a0a1a2a3a4a5a6a7a8a9aaabacadaeaf0100000000000000c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf + +# Values of another length, with a code that version 1 does not define, or a store generation of 0. +INVALID_RECORD|EMPTY_OVERLAY|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013000600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|LONG_OVERLAY|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20010130020101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|OVERLAY_CODE_0|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300100600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|OVERLAY_CODE_3|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300103600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|IMAGE_FORMAT_3|0100102103000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|GPT_WITHOUT_BOUND|0100102102000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|SHORT_RESTORE|01000137a0a1a2a3a4a5a6a7a8a9aaabacadaeaf0100000000000000c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcddde102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|SHORT_IMAGE_IDENTITY|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f110f000102030405060708090a0b0c0d0e200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|SCRATCH_WITHOUT_JOURNAL|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202102202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|MACHINE_ID_CODE_0|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610100700c010000000000001000000000 +INVALID_RECORD|DNS_OF_5_BYTES|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013001015021606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f0251050a00000100600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|SHORT_RESOURCES|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700b0100000000000010000000 +INVALID_RECORD|ZERO_GENERATION|01000138a0a1a2a3a4a5a6a7a8a9aaabacadaeaf0000000000000000c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 + +# Missing fields, and fields that need another. +INVALID_RECORD|MISSING_IMAGE|0100200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|MISSING_RESOURCES|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101 +INVALID_RECORD|EXPORT_WITHOUT_MAPPINGS|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101402102404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|MAPPINGS_WITHOUT_EXPORT|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20010130010141020100600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|ZERO_MAPPINGS|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101402102404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f41020000600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|DNS_WITHOUT_NETWORK|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20010130010151040a000001600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|PROXY_WITHOUT_NETWORK|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013001015214687474703a2f2f31302e302e302e313a33313238600b6e76782d73616e64626f78610101700c010000000000001000000000 + +# Values past a limit, or that no guest can apply. +INVALID_RECORD|TOO_MANY_MAPPINGS|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101402102404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f41020110600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|EMPTY_PROXY|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013001015021606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f025200600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|PROXY_WITH_CREDENTIALS|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013001015021606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f025220687474703a2f2f757365723a7365637265744031302e302e302e313a33313238600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|PROXY_WITH_SPACE|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013001015021606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f025215687474703a2f2f31302e302e302e313a3331323820600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|PROXY_NOT_ASCII|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013001015021606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f02520e687474703a2f2fc3a13a33313238600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|EMPTY_HOSTNAME|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013001016000610101700c010000000000001000000000 +INVALID_RECORD|HOSTNAME_WITH_NUL|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76780073616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|LONG_HOSTNAME|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20010130010160416161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610101700c010000000000001000000000 +INVALID_RECORD|ZERO_FIRST_PARTITION_BOUND|0100102902000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0000000000000000200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|ZERO_UID|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101620800000000e8030000700c010000000000001000000000 +INVALID_RECORD|ZERO_GID|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f786101016208e803000000000000700c010000000000001000000000 +INVALID_RECORD|UNCHANGED_UID|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f786101016208ffffffffe8030000700c010000000000001000000000 +INVALID_RECORD|UNCHANGED_GID|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f786101016208e8030000ffffffff700c010000000000001000000000 +INVALID_RECORD|ZERO_CPUS|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c000000000000001000000000 +INVALID_RECORD|ZERO_MEMORY|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000000000000000 + +# What Ready reports: no bytes, a byte other than 0 and 1, a cold boot's with bytes after +# it, and a clone's whose transaction ID isn't 16 bytes. +INVALID_READY|EMPTY| +INVALID_READY|FLAG_2|02 +INVALID_READY|COLD_WITH_TRANSACTION|00a0a1a2a3a4a5a6a7a8a9aaabacadaeaf +INVALID_READY|COLD_WITH_TRAILING_BYTE|0000 +INVALID_READY|CLONE_WITHOUT_TRANSACTION|01 +INVALID_READY|SHORT_TRANSACTION|01a0a1a2a3a4a5a6a7a8a9aaabacadae +INVALID_READY|LONG_TRANSACTION|01a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0