From 67f68712419a7b2401feb8d0d071f68cc94808ed Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Sat, 24 Oct 2026 11:12:55 -0700 Subject: [PATCH 1/2] nvx-rs: identify activation configurations and encode Ready's activation A host that gives a guest an activation record must learn from Ready that the guest applied it, and a clone's Ready must also name the restore that it activated, in the same encoding as a cold boot's Ready (#838). ActivationRecord::identity computes the record's ConfigurationIdentity: the SHA-256 digest of the label "nvx activation configuration identity v1", a NUL, and the record's canonical encoding. The label sets the identity apart from any other digest of the same bytes and names its version. The identity covers the whole record, a clone's restore section included, and hashes at most 1,024 bytes of settings, never an object that the record names. The guest feature now enables sha2, as the host feature does, and the identity needs one of them; the records and their encodings still need no features. ReadyActivation::of reports a record's identity and a clone's transaction ID. ReadyActivation's canonical encoding is the identity's 32 bytes, then 0 for a cold boot, or 1 and the clone's 16-byte transaction ID: COLD_LEN, 33, or CLONE_LEN, 49, bytes, which ReadyActivation::encode writes. ReadyActivation::decode refuses every other form. tests/vectors/activation-v1.txt holds conformance vectors in the line format of the other vector files: five records, from the smallest to the largest, with their identities, what Ready reports about each, 47 records and 8 Ready activations that every decoder refuses. A separate implementation of the definitions computed their bytes and identities. The tests decode and encode every vector, refuse every invalid one, check that each field changes the identity, and round-trip generated Ready activations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f7bd07b-b09f-48d5-bc49-ad06f47ef3f9 --- doc/project-structure.md | 2 +- nvx-rs/Cargo.toml | 10 +- nvx-rs/README.md | 25 +- nvx-rs/src/activation/identity.rs | 37 +++ nvx-rs/src/activation/identity/tests.rs | 397 ++++++++++++++++++++++++ nvx-rs/src/activation/mod.rs | 53 +++- nvx-rs/src/activation/ready.rs | 57 ++++ nvx-rs/src/activation/ready/tests.rs | 188 +++++++++++ nvx-rs/src/lib.rs | 3 +- nvx-rs/src/tests.rs | 3 + nvx-rs/tests/vectors/README.md | 10 +- nvx-rs/tests/vectors/activation-v1.txt | 118 +++++++ 12 files changed, 878 insertions(+), 25 deletions(-) create mode 100644 nvx-rs/src/activation/identity.rs create mode 100644 nvx-rs/src/activation/identity/tests.rs create mode 100644 nvx-rs/src/activation/ready.rs create mode 100644 nvx-rs/src/activation/ready/tests.rs create mode 100644 nvx-rs/tests/vectors/activation-v1.txt diff --git a/doc/project-structure.md b/doc/project-structure.md index 82e2c7e44..ce5ea9d22 100644 --- a/doc/project-structure.md +++ b/doc/project-structure.md @@ -102,7 +102,7 @@ nvx/ |-- nvx-rs/ Rust crate `nvx` for the NVX mechanics | |-- src/ | | |-- protocol/ Control-console codec, NVXS session, and the guest targets that live shares may not take, shared by the host and the guest -| | |-- activation/ The activation record of a layered root, its canonical encoding, and what Ready reports about it, shared by the host, the guest, and every adapter's framing +| | |-- activation/ The activation record of a layered root, its canonical encoding and configuration identity, and what Ready reports about it, shared by the host, the guest, and every adapter's framing | | |-- contract.rs Control-contract versions and release compatibility | | |-- 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, shared by the host and the guest diff --git a/nvx-rs/Cargo.toml b/nvx-rs/Cargo.toml index 94a9e8730..6307eb5ff 100644 --- a/nvx-rs/Cargo.toml +++ b/nvx-rs/Cargo.toml @@ -26,7 +26,8 @@ name = "nvx" default = ["host"] # The host side: artifact discovery, launch, network and filesystem lowering, control sessions, # process and platform primitives, the supervision of VM instances, and the sandbox launch -# profile, whose aggregate child names are SHA-256 digests. +# profile, whose aggregate child names are SHA-256 digests, as are the configuration identities of +# activation records. host = [ "serde", "dep:getrandom", @@ -47,14 +48,15 @@ cli = ["host"] # The serde form of `Hypervisor`. The `host` feature enables it. serde = ["dep:serde"] # The programs that run in Linux guests: the `guest` module and the `nvx-*` guest binaries. Build -# them with `--no-default-features --features guest` and the `guest` profile; see README.md. -guest = ["dep:nix", "dep:getrandom"] +# them with `--no-default-features --features guest` and the `guest` profile; see README.md. It +# enables `sha2` for the configuration identities of activation records. +guest = ["dep:nix", "dep:getrandom", "dep:sha2"] [dependencies] getrandom = { version = "0.4", optional = true } serde = { version = "1.0.200", features = ["derive"], optional = true } serde_json = { version = "1.0.120", optional = true } -# The same release as the build script's: `host` enables both. +# The same release as the build script's: `host` enables both, and `guest` this one. sha2 = { version = "0.10", optional = true } [target.'cfg(windows)'.dependencies] diff --git a/nvx-rs/README.md b/nvx-rs/README.md index e3c0bd4ac..89580aa6f 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -73,7 +73,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. `ActivationRecord::identity`, with `host` or `guest`, computes the record's `ConfigurationIdentity`, the SHA-256 digest of a label that names its version and of the record's encoding. `ReadyActivation` is what Ready reports: the `ConfigurationIdentity`, and a clone's transaction ID, which `ReadyActivation::of` takes from a record, and whose canonical encoding, of `COLD_LEN` or `CLONE_LEN` bytes, `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 | @@ -864,17 +864,18 @@ caught. | Feature | Contents | | --- | --- | -| `host` (default) | The `host` module. It enables `serde`, and `sha2`, which names the children of a sandbox's aggregate share and hashes distro layers in their explicit verification | +| `host` (default) | The `host` module. It enables `serde`, and `sha2`, which names the children of a sandbox's aggregate share, hashes distro layers in their explicit verification, and computes the configuration identities of activation records | | `bundled` | Stages a release at build time; see [Bundled artifacts](#bundled-artifacts) | | `testing` | The fake OpenVMM and the `nvx-fake-openvmm` binary | | `cli` | The `nvx-rs` CLI of `host::cli` and its `nvx-rs` binary; it enables `host`. See [The nvx-rs CLI](#the-nvx-rs-cli) | | `serde` | The serde form of `Hypervisor` | -| `guest` | The `guest` module and the guest binaries; see [Guest binaries](#guest-binaries) | +| `guest` | The `guest` module and the guest binaries; see [Guest binaries](#guest-binaries). It enables `sha2`, which computes the configuration identities of activation records | Without default features, the crate has only `protocol`, `activation`, `exec`, `contract`, `net`, `gpt`, `uuid`, `Hypervisor`, and `Error`, and depends on -nothing but `serde` when `serde` is enabled and `nix` and `getrandom` when -`guest` is; `aci_edge_sandboxes_model` uses it that way. +nothing but `serde` when `serde` is enabled and `nix`, `getrandom`, and `sha2` +when `guest` is; `aci_edge_sandboxes_model` uses it that way. Without `host` +or `guest`, `activation` lacks only the configuration identity. ## Example @@ -1107,11 +1108,15 @@ 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 +`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, and each record's +configuration identity matches its vector's. 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 diff --git a/nvx-rs/src/activation/identity.rs b/nvx-rs/src/activation/identity.rs new file mode 100644 index 000000000..126361df3 --- /dev/null +++ b/nvx-rs/src/activation/identity.rs @@ -0,0 +1,37 @@ +//! The configuration identity of an activation record, which the [module](super#identity) +//! describes: [`ActivationRecord::identity`] computes it, and [`ReadyActivation::of`] reports it. +//! It needs `sha2`, which the `host` and the `guest` features enable. + +use sha2::{Digest, Sha256}; + +use super::{ActivationRecord, ConfigurationIdentity, ReadyActivation}; + +#[cfg(test)] +mod tests; + +/// What a configuration identity's digest hashes before the record's encoding: the label that +/// names version 1 of the identity, and the NUL that ends it. +const LABEL: &[u8] = b"nvx activation configuration identity v1\0"; + +impl ActivationRecord { + /// Returns the record's configuration identity, as the [module](super#identity) describes it: + /// the SHA-256 digest of a label and of the record's canonical [encoding](Self::encode). + pub fn identity(&self) -> ConfigurationIdentity { + let digest = Sha256::new() + .chain_update(LABEL) + .chain_update(self.encode()) + .finalize(); + ConfigurationIdentity::from_bytes(digest.into()) + } +} + +impl ReadyActivation { + /// Returns what Ready reports about the activation of `record`: the record's + /// [identity](ActivationRecord::identity), and the transaction ID of a clone's restore. + pub fn of(record: &ActivationRecord) -> Self { + Self { + identity: record.identity(), + transaction: record.restore().map(|restore| restore.transaction), + } + } +} diff --git a/nvx-rs/src/activation/identity/tests.rs b/nvx-rs/src/activation/identity/tests.rs new file mode 100644 index 000000000..6e065dcf8 --- /dev/null +++ b/nvx-rs/src/activation/identity/tests.rs @@ -0,0 +1,397 @@ +//! Tests of the configuration identity. The conformance vectors give records and their +//! identities from the definitions of the encoding and of the identity, never from the code that +//! they check. + +use std::collections::HashSet; +use std::net::{IpAddr, Ipv4Addr}; +use std::num::NonZeroU64; + +use super::*; +use crate::activation::tests::{configuration, maximal, restore}; +use crate::activation::{ + ActivationError, 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, vector}; +use crate::uuid::Uuid; + +/// The names of the vectors of records, in order, which are also those of the vectors of what +/// Ready reports about their activations. +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, and the identity that they give it. +fn record_of(vector: &Vector) -> (ActivationRecord, ConfigurationIdentity) { + 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(), + }; + let configuration_identity = ConfigurationIdentity::from_bytes(array(&mut values)); + drop(values); + let configuration = Configuration { + image, + upper, + overlay, + export, + mappings, + network, + workload: WorkloadView { + hostname, + machine_id, + identity: workload_identity, + }, + resources, + }; + let record = ActivationRecord::new(restore, configuration) + .unwrap_or_else(|error| panic!("RECORD {}: {error}", vector.name)); + (record, configuration_identity) +} + +#[test] +fn records_match_their_vectors() { + for vector in of_kind(ACTIVATION_V1, "RECORD", &RECORDS) { + let name = vector.name; + let (record, identity) = record_of(&vector); + assert_eq!(record.encode(), vector.bytes, "{name}"); + assert_eq!( + ActivationRecord::decode(&vector.bytes).as_ref(), + Ok(&record), + "{name}" + ); + assert_eq!(record.identity(), identity, "{name}"); + } +} + +#[test] +fn ready_reports_the_identity_of_its_record_and_a_clone_s_transaction() { + for name in RECORDS { + let (record, identity) = record_of(&vector(ACTIVATION_V1, "RECORD", name)); + let ready = ReadyActivation::of(&record); + assert_eq!(ready.identity, identity, "{name}"); + assert_eq!( + ready.transaction, + record.restore().map(|restore| restore.transaction), + "{name}" + ); + assert_eq!( + ready.encode(), + vector(ACTIVATION_V1, "READY", name).bytes, + "{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); + } +} + +#[test] +fn identities_change_with_every_field() { + /// Returns the record of the largest clone once `change` has changed it. + fn changed( + change: impl FnOnce(&mut Option, &mut Configuration), + ) -> ActivationRecord { + let (mut section, mut configuration) = (Some(restore()), maximal()); + change(&mut section, &mut configuration); + ActivationRecord::new(section, configuration).unwrap() + } + /// Returns the restore section of a record that has one. + fn restore_section(restore: &mut Option) -> &mut RestoreSection { + restore.as_mut().unwrap() + } + /// Returns the network of a configuration that has one. + fn network(configuration: &mut Configuration) -> &mut Network { + configuration.network.as_mut().unwrap() + } + /// Returns the workload identity of a configuration that has one. + fn ids(configuration: &mut Configuration) -> &mut WorkloadIdentity { + configuration.workload.identity.as_mut().unwrap() + } + let other = Identity::from_bytes([0x5a; 32]); + let records = [ + changed(|_, _| {}), + changed(|restore, _| *restore = None), + changed(|restore, _| { + restore_section(restore).transaction = TransactionId::from_bytes([1; 16]); + }), + changed(|restore, _| { + restore_section(restore).template.generation = StoreGeneration::new(NonZeroU64::MIN); + }), + changed(|restore, _| restore_section(restore).template.key = other), + changed(|_, configuration| { + configuration.image = Image::Erofs { + binding: *configuration.image.binding(), + uuid: configuration.image.expected(), + }; + }), + changed(|_, configuration| { + if let Image::Gpt { binding, .. } = &mut configuration.image { + *binding = other; + } + }), + changed(|_, configuration| { + if let Image::Gpt { disk_guid, .. } = &mut configuration.image { + *disk_guid = None; + } + }), + changed(|_, configuration| { + if let Image::Gpt { disk_guid, .. } = &mut configuration.image { + *disk_guid = Some(Uuid::from_bytes([1; 16])); + } + }), + changed(|_, configuration| { + if let Image::Gpt { + first_partition_bound, + .. + } = &mut configuration.image + { + *first_partition_bound = 1; + } + }), + changed(|_, configuration| configuration.upper = Upper::Tmpfs), + changed(|_, configuration| { + if let Upper::Scratch { binding, .. } = &mut configuration.upper { + *binding = other; + } + }), + changed(|_, configuration| { + if let Upper::Scratch { journal, .. } = &mut configuration.upper { + *journal = Journal::Allowed; + } + }), + changed(|_, configuration| configuration.overlay = OverlayMode::TrustedLowers), + changed(|_, configuration| { + configuration.export = None; + configuration.mappings = 0; + }), + changed(|_, configuration| { + configuration.export.as_mut().unwrap().kind = ExportKind::Directory + }), + changed(|_, configuration| configuration.export.as_mut().unwrap().plan = other), + changed(|_, configuration| configuration.mappings = 1), + changed(|_, configuration| configuration.network = None), + changed(|_, configuration| network(configuration).policy = other), + changed(|_, configuration| network(configuration).dns = None), + changed(|_, configuration| { + network(configuration).dns = Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))); + }), + changed(|_, configuration| network(configuration).publication = Publication::Unpublished), + changed(|_, configuration| network(configuration).proxy = None), + changed(|_, configuration| network(configuration).proxy = Some("10.0.0.1:3128".to_owned())), + changed(|_, configuration| configuration.workload.hostname = b"other".to_vec()), + changed(|_, configuration| configuration.workload.machine_id = MachineIdMode::Image), + changed(|_, configuration| configuration.workload.identity = None), + changed(|_, configuration| ids(configuration).uid = 1000), + changed(|_, configuration| ids(configuration).gid = 1000), + changed(|_, configuration| configuration.resources.cpus = 1), + changed(|_, configuration| configuration.resources.memory = 1), + ]; + let identities: HashSet<_> = records.iter().map(ActivationRecord::identity).collect(); + assert_eq!(identities.len(), records.len()); +} + +#[test] +fn a_clone_s_restore_section_is_part_of_its_identity() { + let cold = ActivationRecord::new(None, configuration()).unwrap(); + let clone = ActivationRecord::new(Some(restore()), configuration()).unwrap(); + assert_ne!(cold.identity(), clone.identity()); + assert_eq!( + ReadyActivation::of(&cold), + ReadyActivation { + identity: cold.identity(), + transaction: None, + } + ); + assert_eq!( + ReadyActivation::of(&clone), + ReadyActivation { + identity: clone.identity(), + transaction: Some(restore().transaction), + } + ); +} + +#[test] +fn the_label_sets_the_identity_apart_from_the_digest_of_the_record() { + let record = ActivationRecord::new(Some(restore()), maximal()).unwrap(); + assert_ne!( + record.identity().as_bytes()[..], + Sha256::digest(record.encode())[..] + ); + assert_eq!(LABEL, b"nvx activation configuration identity v1\0"); +} diff --git a/nvx-rs/src/activation/mod.rs b/nvx-rs/src/activation/mod.rs index dd25eaaa2..cc4445ef5 100644 --- a/nvx-rs/src/activation/mod.rs +++ b/nvx-rs/src/activation/mod.rs @@ -22,9 +22,12 @@ //! [`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. [`ActivationRecord::identity`] +//! computes the record's [identity](#identity), 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, except for the identity, a SHA-256 digest, which +//! needs the `host` or the `guest` feature. So the records and their encodings build without +//! features, and the host, the guest, and every adapter's framing share them. //! //! # Configuration //! @@ -111,6 +114,33 @@ //! exactly one encoding, which [`ActivationRecord::encode`] writes, and which the decoder reads //! back to the same record. //! +//! # Identity +//! +//! A record's [`ConfigurationIdentity`] is the SHA-256 digest of the label +//! `nvx activation configuration identity v1`, a NUL byte, and the record's canonical +//! [encoding](#encoding). [`ActivationRecord::identity`] computes it, with the `host` or the +//! `guest` feature. The label sets the identity apart from any other digest of the same bytes, +//! and names version 1 of the identity, which hashes records of version 1. The identity covers +//! the whole record, a clone's restore section included, so the host learns, from the identity +//! that Ready reports, that the guest applied exactly the record that the host gave it. It +//! hashes at most [`MAX_RECORD_BYTES`] bytes of settings, never an object that the record names. +//! +//! # Ready +//! +//! [`ReadyActivation::of`] returns what Ready reports about the activation of a record: the +//! record's identity, and a clone's transaction ID. Its canonical encoding, which +//! [`ReadyActivation::encode`] writes and [`ReadyActivation::decode`] reads back, is the +//! identity's 32 bytes, then `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 encoding needs +//! no features. +//! +//! The conformance vectors of `tests/vectors/activation-v1.txt` hold records, with their +//! identities, and what Ready reports about their activations, in both encodings, and bytes that +//! every decoder refuses. +//! +//! # Example +//! //! ``` //! use std::num::NonZeroU64; //! @@ -167,6 +197,9 @@ use crate::protocol::root::ImageFormat; use crate::uuid::Uuid; mod codec; +#[cfg(any(feature = "host", feature = "guest"))] +mod identity; +mod ready; #[cfg(test)] mod tests; @@ -287,7 +320,8 @@ impl fmt::Display for StoreGeneration { } /// The identity of the configuration that a layered root's activation applied, which its Ready -/// reports: 32 bytes. +/// reports: the SHA-256 digest of its record that the [module](self#identity) describes, 32 +/// bytes. /// /// It displays as 64 lowercase hexadecimal digits. #[derive(Clone, Copy, PartialEq, Eq, Hash)] @@ -631,6 +665,8 @@ 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. +/// 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. @@ -639,7 +675,8 @@ pub struct ReadyActivation { 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 +722,9 @@ 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 not a configuration identity followed by `0`, or by + /// `1` and a transaction ID. + Ready, } impl fmt::Display for ActivationError { @@ -751,6 +791,9 @@ 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 not an identity followed by 0, or by 1 and 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..8eb7b3f08 --- /dev/null +++ b/nvx-rs/src/activation/ready.rs @@ -0,0 +1,57 @@ +//! The canonical encoding of what Ready reports about an activation, which the +//! [module](super#ready) describes: [`ReadyActivation::encode`] writes it, and +//! [`ReadyActivation::decode`] reads it back. + +use super::{ActivationError, ConfigurationIdentity, ReadyActivation, TransactionId}; + +#[cfg(test)] +mod tests; + +/// The byte that follows the identity of a cold boot's activation, which has no transaction. +const COLD: u8 = 0; +/// The byte that follows the identity of a clone's activation, before its transaction ID. +const CLONE: u8 = 1; + +impl ReadyActivation { + /// The bytes of a cold boot's activation: its identity, and a `0`. + pub const COLD_LEN: usize = ConfigurationIdentity::LEN + 1; + + /// The bytes of a clone's activation: its identity, a `1`, and its transaction ID. + pub const CLONE_LEN: usize = Self::COLD_LEN + TransactionId::LEN; + + /// Returns the activation's canonical encoding, as the [module](super#ready) describes it: the + /// identity, then `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); + bytes.extend_from_slice(self.identity.as_bytes()); + 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 an identity followed by `0`, or + /// by `1` and a transaction ID, and nothing more. + pub fn decode(bytes: &[u8]) -> Result { + let (identity, rest) = bytes + .split_first_chunk::<{ ConfigurationIdentity::LEN }>() + .ok_or(ActivationError::Ready)?; + let transaction = match rest { + [COLD] => None, + [CLONE, transaction @ ..] => Some(TransactionId::from_bytes( + transaction.try_into().map_err(|_| ActivationError::Ready)?, + )), + _ => return Err(ActivationError::Ready), + }; + Ok(Self { + identity: ConfigurationIdentity::from_bytes(*identity), + 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..35a4bffea --- /dev/null +++ b/nvx-rs/src/activation/ready/tests.rs @@ -0,0 +1,188 @@ +//! Tests of the canonical encoding of what Ready reports about an activation. 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}; + +/// 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 identity of the tests: the bytes 00 through 1f. +fn identity() -> ConfigurationIdentity { + ConfigurationIdentity::from_bytes(std::array::from_fn(|index| u8::try_from(index).unwrap())) +} + +/// 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), + (33, 49) + ); +} + +#[test] +fn a_cold_boot_reports_its_identity_then_a_zero() { + let ready = ReadyActivation { + identity: identity(), + transaction: None, + }; + let bytes = [&identity().as_bytes()[..], &[0]].concat(); + assert_eq!(ready.encode(), bytes); + assert_eq!(bytes.len(), ReadyActivation::COLD_LEN); + assert_eq!(ReadyActivation::decode(&bytes), Ok(ready)); +} + +#[test] +fn a_clone_reports_its_identity_then_a_one_and_its_transaction() { + let ready = ReadyActivation { + identity: identity(), + transaction: Some(transaction()), + }; + let bytes = [&identity().as_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() { + for length in 0..=ConfigurationIdentity::LEN { + assert_eq!( + ReadyActivation::decode(&identity().as_bytes()[..length]), + Err(ActivationError::Ready), + "{length} bytes" + ); + } + for flag in u8::MIN..=u8::MAX { + for tail in 0..=TransactionId::LEN + 1 { + let bytes = [ + &identity().as_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_activations_match_their_vectors() { + for vector in of_kind(ACTIVATION_V1, "READY", &READY) { + let mut values = vector.values(); + let identity = ConfigurationIdentity::from_bytes(values.bytes().try_into().unwrap()); + let transaction = + values.optional(|values| TransactionId::from_bytes(values.bytes().try_into().unwrap())); + drop(values); + let ready = ReadyActivation { + identity, + 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", + "IDENTITY_ONLY", + "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 not an identity followed by 0, or by 1 and a transaction ID" + ); +} + +/// Returns bytes that a decoder may meet: arbitrary bytes, and an identity followed by a byte +/// from 0 to 2 and up to 17 more bytes. +fn inputs() -> impl Strategy> { + prop_oneof![ + prop::collection::vec(any::(), 0..64), + ( + any::<[u8; 32]>(), + 0_u8..=2, + prop::collection::vec(any::(), 0..=17) + ) + .prop_map(|(identity, flag, tail)| [&identity[..], &[flag], &tail].concat()), + ] +} + +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( + identity in any::<[u8; 32]>(), + transaction in any::>(), + cut in any::(), + extra in any::(), + ) { + let ready = ReadyActivation { + identity: ConfigurationIdentity::from_bytes(identity), + 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/lib.rs b/nvx-rs/src/lib.rs index 5f1bebeda..66ea00713 100644 --- a/nvx-rs/src/lib.rs +++ b/nvx-rs/src/lib.rs @@ -10,7 +10,8 @@ //! not take. It has no dependencies, so the host, the guest, and test doubles share it. //! - [`activation`]: the activation record of a layered root, the 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, and what Ready reports about the activation. +//! reads in place of its captured configuration, its canonical encoding and configuration +//! identity, 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. diff --git a/nvx-rs/src/tests.rs b/nvx-rs/src/tests.rs index 85ad83a77..4b1c0fcdc 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, with its configuration +/// identities, 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..e11cda97f 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, with the configuration identity of each record, 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..a29eb61d6 --- /dev/null +++ b/nvx-rs/tests/vectors/activation-v1.txt @@ -0,0 +1,118 @@ +# Conformance vectors of version 1 of the activation record of a layered root, with the +# configuration identity of each record, and of what Ready reports about an activation. The +# documentation of nvx::activation defines the encodings and the identity. 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|IDENTITY +# 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. IDENTITY is the record's configuration identity: the SHA-256 digest of +# the label "nvx activation configuration identity v1", a NUL, and BYTES. +# READY|NAME|BYTES|IDENTITY|TRANSACTION +# What Ready reports about the activation of the record of the same name: its configuration +# identity, and 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|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b86982540 + +# The same configuration in a clone's record, whose restore section changes its identity. +RECORD|CLONE|01000138a0a1a2a3a4a5a6a7a8a9aaabacadaeaf0100000000000000c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000|a0a1a2a3a4a5a6a7a8a9aaabacadaeaf|1|c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf|EROFS|000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f|-|TMPFS|TRUSTED_LOWERS|-|-|nvx-sandbox|IMAGE|-|1|268435456|420e7916326f0b92fbe02ec9a4dd87d28829695745c9d8e58813ed2371ea66ed + +# 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|1a9ebc6ed5bb83544e1be56f2ff4619d3b3cb6a6de49c03f5bd4a7962dcc7f61 + +# 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|1e074b18ba5ac3db01ebc2de8a0382fb65f3556f66c26d97c2adb4795c2411bc + +# 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|7d213fe715fb6ff77d20e1348d52e0c3b72e3ca227562be415a268c69f8f1f67 + +# What Ready reports about the activation of each record. +READY|COLD|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b8698254000|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b86982540|- +READY|CLONE|420e7916326f0b92fbe02ec9a4dd87d28829695745c9d8e58813ed2371ea66ed01a0a1a2a3a4a5a6a7a8a9aaabacadaeaf|420e7916326f0b92fbe02ec9a4dd87d28829695745c9d8e58813ed2371ea66ed|a0a1a2a3a4a5a6a7a8a9aaabacadaeaf +READY|SCRATCH_AGGREGATE|1a9ebc6ed5bb83544e1be56f2ff4619d3b3cb6a6de49c03f5bd4a7962dcc7f6100|1a9ebc6ed5bb83544e1be56f2ff4619d3b3cb6a6de49c03f5bd4a7962dcc7f61|- +READY|GPT_DIRECTORY|1e074b18ba5ac3db01ebc2de8a0382fb65f3556f66c26d97c2adb4795c2411bc010102030405060708090a0b0c0d0e0f10|1e074b18ba5ac3db01ebc2de8a0382fb65f3556f66c26d97c2adb4795c2411bc|0102030405060708090a0b0c0d0e0f10 +READY|LARGEST|7d213fe715fb6ff77d20e1348d52e0c3b72e3ca227562be415a268c69f8f1f6701f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff|7d213fe715fb6ff77d20e1348d52e0c3b72e3ca227562be415a268c69f8f1f67|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: an identity alone, or followed by 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|IDENTITY_ONLY|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b86982540 +INVALID_READY|FLAG_2|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b8698254002 +INVALID_READY|COLD_WITH_TRANSACTION|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b8698254000a0a1a2a3a4a5a6a7a8a9aaabacadaeaf +INVALID_READY|COLD_WITH_TRAILING_BYTE|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b869825400000 +INVALID_READY|CLONE_WITHOUT_TRANSACTION|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b8698254001 +INVALID_READY|SHORT_TRANSACTION|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b8698254001a0a1a2a3a4a5a6a7a8a9aaabacadae +INVALID_READY|LONG_TRANSACTION|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b8698254001a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0 From fa45e48f207a4cbcc0c476a63a65b90adbf0710b Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Mon, 26 Oct 2026 20:05:10 -0700 Subject: [PATCH 2/2] nvx-rs: report an activation in Ready without a configuration identity Ready reported a SHA-256 digest of the activation record, so that the host could learn that the guest applied exactly the record that it sent (#838). A clone's transaction ID already ties its Ready to that record: the host picks a new one for each restore and sends no other record under it. And a guest computes the digest from the bytes that it received, not from what it applied, so the digest told the host no more, at the cost of a hash on every activation, on the host and in the guest, and of sha2 in the guest. Remove ConfigurationIdentity and ActivationRecord::identity. Ready now reports that the activation applied: 0 for a cold boot, or 1 and the clone's 16-byte transaction ID, so COLD_LEN is 1 and CLONE_LEN 17. ReadyActivation::of moves beside the encoding and needs no features, and the guest feature no longer enables sha2. The record vectors lose their identities, and their tests move to the codec's tests, so they run without features. The Ready vectors hold the new encoding, and 7 Ready activations that every decoder refuses. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f7bd07b-b09f-48d5-bc49-ad06f47ef3f9 --- doc/project-structure.md | 2 +- nvx-rs/Cargo.toml | 10 +- nvx-rs/README.md | 25 +- nvx-rs/src/activation/codec/tests.rs | 2 + nvx-rs/src/activation/codec/tests/vectors.rs | 234 +++++++++++ nvx-rs/src/activation/identity.rs | 37 -- nvx-rs/src/activation/identity/tests.rs | 397 ------------------- nvx-rs/src/activation/mod.rs | 104 ++--- nvx-rs/src/activation/ready.rs | 45 ++- nvx-rs/src/activation/ready/tests.rs | 93 ++--- nvx-rs/src/activation/tests.rs | 16 +- nvx-rs/src/lib.rs | 3 +- nvx-rs/src/tests.rs | 4 +- nvx-rs/tests/vectors/README.md | 2 +- nvx-rs/tests/vectors/activation-v1.txt | 57 ++- 15 files changed, 371 insertions(+), 660 deletions(-) create mode 100644 nvx-rs/src/activation/codec/tests/vectors.rs delete mode 100644 nvx-rs/src/activation/identity.rs delete mode 100644 nvx-rs/src/activation/identity/tests.rs diff --git a/doc/project-structure.md b/doc/project-structure.md index ce5ea9d22..82e2c7e44 100644 --- a/doc/project-structure.md +++ b/doc/project-structure.md @@ -102,7 +102,7 @@ nvx/ |-- nvx-rs/ Rust crate `nvx` for the NVX mechanics | |-- src/ | | |-- protocol/ Control-console codec, NVXS session, and the guest targets that live shares may not take, shared by the host and the guest -| | |-- activation/ The activation record of a layered root, its canonical encoding and configuration identity, and what Ready reports about it, shared by the host, the guest, and every adapter's framing +| | |-- activation/ The activation record of a layered root, its canonical encoding, and what Ready reports about it, shared by the host, the guest, and every adapter's framing | | |-- contract.rs Control-contract versions and release compatibility | | |-- 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, shared by the host and the guest diff --git a/nvx-rs/Cargo.toml b/nvx-rs/Cargo.toml index 6307eb5ff..94a9e8730 100644 --- a/nvx-rs/Cargo.toml +++ b/nvx-rs/Cargo.toml @@ -26,8 +26,7 @@ name = "nvx" default = ["host"] # The host side: artifact discovery, launch, network and filesystem lowering, control sessions, # process and platform primitives, the supervision of VM instances, and the sandbox launch -# profile, whose aggregate child names are SHA-256 digests, as are the configuration identities of -# activation records. +# profile, whose aggregate child names are SHA-256 digests. host = [ "serde", "dep:getrandom", @@ -48,15 +47,14 @@ cli = ["host"] # The serde form of `Hypervisor`. The `host` feature enables it. serde = ["dep:serde"] # The programs that run in Linux guests: the `guest` module and the `nvx-*` guest binaries. Build -# them with `--no-default-features --features guest` and the `guest` profile; see README.md. It -# enables `sha2` for the configuration identities of activation records. -guest = ["dep:nix", "dep:getrandom", "dep:sha2"] +# them with `--no-default-features --features guest` and the `guest` profile; see README.md. +guest = ["dep:nix", "dep:getrandom"] [dependencies] getrandom = { version = "0.4", optional = true } serde = { version = "1.0.200", features = ["derive"], optional = true } serde_json = { version = "1.0.120", optional = true } -# The same release as the build script's: `host` enables both, and `guest` this one. +# The same release as the build script's: `host` enables both. sha2 = { version = "0.10", optional = true } [target.'cfg(windows)'.dependencies] diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 89580aa6f..87fc2ddf7 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -73,7 +73,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. `ActivationRecord::identity`, with `host` or `guest`, computes the record's `ConfigurationIdentity`, the SHA-256 digest of a label that names its version and of the record's encoding. `ReadyActivation` is what Ready reports: the `ConfigurationIdentity`, and a clone's transaction ID, which `ReadyActivation::of` takes from a record, and whose canonical encoding, of `COLD_LEN` or `CLONE_LEN` bytes, `ReadyActivation::encode` writes and `ReadyActivation::decode` reads. The records and both encodings need 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 | @@ -864,18 +864,17 @@ caught. | Feature | Contents | | --- | --- | -| `host` (default) | The `host` module. It enables `serde`, and `sha2`, which names the children of a sandbox's aggregate share, hashes distro layers in their explicit verification, and computes the configuration identities of activation records | +| `host` (default) | The `host` module. It enables `serde`, and `sha2`, which names the children of a sandbox's aggregate share and hashes distro layers in their explicit verification | | `bundled` | Stages a release at build time; see [Bundled artifacts](#bundled-artifacts) | | `testing` | The fake OpenVMM and the `nvx-fake-openvmm` binary | | `cli` | The `nvx-rs` CLI of `host::cli` and its `nvx-rs` binary; it enables `host`. See [The nvx-rs CLI](#the-nvx-rs-cli) | | `serde` | The serde form of `Hypervisor` | -| `guest` | The `guest` module and the guest binaries; see [Guest binaries](#guest-binaries). It enables `sha2`, which computes the configuration identities of activation records | +| `guest` | The `guest` module and the guest binaries; see [Guest binaries](#guest-binaries) | Without default features, the crate has only `protocol`, `activation`, `exec`, `contract`, `net`, `gpt`, `uuid`, `Hypervisor`, and `Error`, and depends on -nothing but `serde` when `serde` is enabled and `nix`, `getrandom`, and `sha2` -when `guest` is; `aci_edge_sandboxes_model` uses it that way. Without `host` -or `guest`, `activation` lacks only the configuration identity. +nothing but `serde` when `serde` is enabled and `nix` and `getrandom` when +`guest` is; `aci_edge_sandboxes_model` uses it that way. ## Example @@ -1110,14 +1109,12 @@ 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`. 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, and each record's -configuration identity matches its vector's. 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 +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/identity.rs b/nvx-rs/src/activation/identity.rs deleted file mode 100644 index 126361df3..000000000 --- a/nvx-rs/src/activation/identity.rs +++ /dev/null @@ -1,37 +0,0 @@ -//! The configuration identity of an activation record, which the [module](super#identity) -//! describes: [`ActivationRecord::identity`] computes it, and [`ReadyActivation::of`] reports it. -//! It needs `sha2`, which the `host` and the `guest` features enable. - -use sha2::{Digest, Sha256}; - -use super::{ActivationRecord, ConfigurationIdentity, ReadyActivation}; - -#[cfg(test)] -mod tests; - -/// What a configuration identity's digest hashes before the record's encoding: the label that -/// names version 1 of the identity, and the NUL that ends it. -const LABEL: &[u8] = b"nvx activation configuration identity v1\0"; - -impl ActivationRecord { - /// Returns the record's configuration identity, as the [module](super#identity) describes it: - /// the SHA-256 digest of a label and of the record's canonical [encoding](Self::encode). - pub fn identity(&self) -> ConfigurationIdentity { - let digest = Sha256::new() - .chain_update(LABEL) - .chain_update(self.encode()) - .finalize(); - ConfigurationIdentity::from_bytes(digest.into()) - } -} - -impl ReadyActivation { - /// Returns what Ready reports about the activation of `record`: the record's - /// [identity](ActivationRecord::identity), and the transaction ID of a clone's restore. - pub fn of(record: &ActivationRecord) -> Self { - Self { - identity: record.identity(), - transaction: record.restore().map(|restore| restore.transaction), - } - } -} diff --git a/nvx-rs/src/activation/identity/tests.rs b/nvx-rs/src/activation/identity/tests.rs deleted file mode 100644 index 6e065dcf8..000000000 --- a/nvx-rs/src/activation/identity/tests.rs +++ /dev/null @@ -1,397 +0,0 @@ -//! Tests of the configuration identity. The conformance vectors give records and their -//! identities from the definitions of the encoding and of the identity, never from the code that -//! they check. - -use std::collections::HashSet; -use std::net::{IpAddr, Ipv4Addr}; -use std::num::NonZeroU64; - -use super::*; -use crate::activation::tests::{configuration, maximal, restore}; -use crate::activation::{ - ActivationError, 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, vector}; -use crate::uuid::Uuid; - -/// The names of the vectors of records, in order, which are also those of the vectors of what -/// Ready reports about their activations. -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, and the identity that they give it. -fn record_of(vector: &Vector) -> (ActivationRecord, ConfigurationIdentity) { - 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(), - }; - let configuration_identity = ConfigurationIdentity::from_bytes(array(&mut values)); - drop(values); - let configuration = Configuration { - image, - upper, - overlay, - export, - mappings, - network, - workload: WorkloadView { - hostname, - machine_id, - identity: workload_identity, - }, - resources, - }; - let record = ActivationRecord::new(restore, configuration) - .unwrap_or_else(|error| panic!("RECORD {}: {error}", vector.name)); - (record, configuration_identity) -} - -#[test] -fn records_match_their_vectors() { - for vector in of_kind(ACTIVATION_V1, "RECORD", &RECORDS) { - let name = vector.name; - let (record, identity) = record_of(&vector); - assert_eq!(record.encode(), vector.bytes, "{name}"); - assert_eq!( - ActivationRecord::decode(&vector.bytes).as_ref(), - Ok(&record), - "{name}" - ); - assert_eq!(record.identity(), identity, "{name}"); - } -} - -#[test] -fn ready_reports_the_identity_of_its_record_and_a_clone_s_transaction() { - for name in RECORDS { - let (record, identity) = record_of(&vector(ACTIVATION_V1, "RECORD", name)); - let ready = ReadyActivation::of(&record); - assert_eq!(ready.identity, identity, "{name}"); - assert_eq!( - ready.transaction, - record.restore().map(|restore| restore.transaction), - "{name}" - ); - assert_eq!( - ready.encode(), - vector(ACTIVATION_V1, "READY", name).bytes, - "{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); - } -} - -#[test] -fn identities_change_with_every_field() { - /// Returns the record of the largest clone once `change` has changed it. - fn changed( - change: impl FnOnce(&mut Option, &mut Configuration), - ) -> ActivationRecord { - let (mut section, mut configuration) = (Some(restore()), maximal()); - change(&mut section, &mut configuration); - ActivationRecord::new(section, configuration).unwrap() - } - /// Returns the restore section of a record that has one. - fn restore_section(restore: &mut Option) -> &mut RestoreSection { - restore.as_mut().unwrap() - } - /// Returns the network of a configuration that has one. - fn network(configuration: &mut Configuration) -> &mut Network { - configuration.network.as_mut().unwrap() - } - /// Returns the workload identity of a configuration that has one. - fn ids(configuration: &mut Configuration) -> &mut WorkloadIdentity { - configuration.workload.identity.as_mut().unwrap() - } - let other = Identity::from_bytes([0x5a; 32]); - let records = [ - changed(|_, _| {}), - changed(|restore, _| *restore = None), - changed(|restore, _| { - restore_section(restore).transaction = TransactionId::from_bytes([1; 16]); - }), - changed(|restore, _| { - restore_section(restore).template.generation = StoreGeneration::new(NonZeroU64::MIN); - }), - changed(|restore, _| restore_section(restore).template.key = other), - changed(|_, configuration| { - configuration.image = Image::Erofs { - binding: *configuration.image.binding(), - uuid: configuration.image.expected(), - }; - }), - changed(|_, configuration| { - if let Image::Gpt { binding, .. } = &mut configuration.image { - *binding = other; - } - }), - changed(|_, configuration| { - if let Image::Gpt { disk_guid, .. } = &mut configuration.image { - *disk_guid = None; - } - }), - changed(|_, configuration| { - if let Image::Gpt { disk_guid, .. } = &mut configuration.image { - *disk_guid = Some(Uuid::from_bytes([1; 16])); - } - }), - changed(|_, configuration| { - if let Image::Gpt { - first_partition_bound, - .. - } = &mut configuration.image - { - *first_partition_bound = 1; - } - }), - changed(|_, configuration| configuration.upper = Upper::Tmpfs), - changed(|_, configuration| { - if let Upper::Scratch { binding, .. } = &mut configuration.upper { - *binding = other; - } - }), - changed(|_, configuration| { - if let Upper::Scratch { journal, .. } = &mut configuration.upper { - *journal = Journal::Allowed; - } - }), - changed(|_, configuration| configuration.overlay = OverlayMode::TrustedLowers), - changed(|_, configuration| { - configuration.export = None; - configuration.mappings = 0; - }), - changed(|_, configuration| { - configuration.export.as_mut().unwrap().kind = ExportKind::Directory - }), - changed(|_, configuration| configuration.export.as_mut().unwrap().plan = other), - changed(|_, configuration| configuration.mappings = 1), - changed(|_, configuration| configuration.network = None), - changed(|_, configuration| network(configuration).policy = other), - changed(|_, configuration| network(configuration).dns = None), - changed(|_, configuration| { - network(configuration).dns = Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))); - }), - changed(|_, configuration| network(configuration).publication = Publication::Unpublished), - changed(|_, configuration| network(configuration).proxy = None), - changed(|_, configuration| network(configuration).proxy = Some("10.0.0.1:3128".to_owned())), - changed(|_, configuration| configuration.workload.hostname = b"other".to_vec()), - changed(|_, configuration| configuration.workload.machine_id = MachineIdMode::Image), - changed(|_, configuration| configuration.workload.identity = None), - changed(|_, configuration| ids(configuration).uid = 1000), - changed(|_, configuration| ids(configuration).gid = 1000), - changed(|_, configuration| configuration.resources.cpus = 1), - changed(|_, configuration| configuration.resources.memory = 1), - ]; - let identities: HashSet<_> = records.iter().map(ActivationRecord::identity).collect(); - assert_eq!(identities.len(), records.len()); -} - -#[test] -fn a_clone_s_restore_section_is_part_of_its_identity() { - let cold = ActivationRecord::new(None, configuration()).unwrap(); - let clone = ActivationRecord::new(Some(restore()), configuration()).unwrap(); - assert_ne!(cold.identity(), clone.identity()); - assert_eq!( - ReadyActivation::of(&cold), - ReadyActivation { - identity: cold.identity(), - transaction: None, - } - ); - assert_eq!( - ReadyActivation::of(&clone), - ReadyActivation { - identity: clone.identity(), - transaction: Some(restore().transaction), - } - ); -} - -#[test] -fn the_label_sets_the_identity_apart_from_the_digest_of_the_record() { - let record = ActivationRecord::new(Some(restore()), maximal()).unwrap(); - assert_ne!( - record.identity().as_bytes()[..], - Sha256::digest(record.encode())[..] - ); - assert_eq!(LABEL, b"nvx activation configuration identity v1\0"); -} diff --git a/nvx-rs/src/activation/mod.rs b/nvx-rs/src/activation/mod.rs index cc4445ef5..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,18 +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. [`ActivationRecord::identity`] -//! computes the record's [identity](#identity), 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, except for the identity, a SHA-256 digest, which -//! needs the `host` or the `guest` feature. So the records and their encodings build without -//! features, and the host, the guest, and every adapter's framing share them. +//! [`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 //! @@ -114,30 +112,20 @@ //! exactly one encoding, which [`ActivationRecord::encode`] writes, and which the decoder reads //! back to the same record. //! -//! # Identity -//! -//! A record's [`ConfigurationIdentity`] is the SHA-256 digest of the label -//! `nvx activation configuration identity v1`, a NUL byte, and the record's canonical -//! [encoding](#encoding). [`ActivationRecord::identity`] computes it, with the `host` or the -//! `guest` feature. The label sets the identity apart from any other digest of the same bytes, -//! and names version 1 of the identity, which hashes records of version 1. The identity covers -//! the whole record, a clone's restore section included, so the host learns, from the identity -//! that Ready reports, that the guest applied exactly the record that the host gave it. It -//! hashes at most [`MAX_RECORD_BYTES`] bytes of settings, never an object that the record names. -//! //! # Ready //! -//! [`ReadyActivation::of`] returns what Ready reports about the activation of a record: the -//! record's identity, and a clone's transaction ID. Its canonical encoding, which -//! [`ReadyActivation::encode`] writes and [`ReadyActivation::decode`] reads back, is the -//! identity's 32 bytes, then `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 encoding needs -//! no features. +//! 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, with their -//! identities, and what Ready reports about their activations, in both encodings, and bytes that -//! every decoder refuses. +//! 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 //! @@ -197,8 +185,6 @@ use crate::protocol::root::ImageFormat; use crate::uuid::Uuid; mod codec; -#[cfg(any(feature = "host", feature = "guest"))] -mod identity; mod ready; #[cfg(test)] mod tests; @@ -319,41 +305,6 @@ impl fmt::Display for StoreGeneration { } } -/// The identity of the configuration that a layered root's activation applied, which its Ready -/// reports: the SHA-256 digest of its record that the [module](self#identity) describes, 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)] @@ -663,14 +614,11 @@ 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. -/// Its canonical [encoding](self#ready) holds [`COLD_LEN`](Self::COLD_LEN) or -/// [`CLONE_LEN`](Self::CLONE_LEN) bytes. +/// 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, } @@ -722,8 +670,7 @@ 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 not a configuration identity followed by `0`, or by - /// `1` and a transaction ID. + /// The bytes of what Ready reports are neither `0`, nor `1` followed by a transaction ID. Ready, } @@ -791,9 +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 not an identity followed by 0, or by 1 and a transaction ID", - ), + 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 index 8eb7b3f08..e6fa98ed6 100644 --- a/nvx-rs/src/activation/ready.rs +++ b/nvx-rs/src/activation/ready.rs @@ -1,29 +1,36 @@ -//! The canonical encoding of what Ready reports about an activation, which the -//! [module](super#ready) describes: [`ReadyActivation::encode`] writes it, and -//! [`ReadyActivation::decode`] reads it back. +//! 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, ConfigurationIdentity, ReadyActivation, TransactionId}; +use super::{ActivationError, ActivationRecord, ReadyActivation, TransactionId}; #[cfg(test)] mod tests; -/// The byte that follows the identity of a cold boot's activation, which has no transaction. +/// The byte of a cold boot's activation, which has no transaction. const COLD: u8 = 0; -/// The byte that follows the identity of a clone's activation, before its transaction ID. +/// 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: its identity, and a `0`. - pub const COLD_LEN: usize = ConfigurationIdentity::LEN + 1; + /// The bytes of a cold boot's activation: a `0`. + pub const COLD_LEN: usize = 1; - /// The bytes of a clone's activation: its identity, a `1`, and its transaction ID. + /// The bytes of a clone's activation: a `1`, and its transaction ID. pub const CLONE_LEN: usize = Self::COLD_LEN + TransactionId::LEN; - /// Returns the activation's canonical encoding, as the [module](super#ready) describes it: the - /// identity, then `0` for a cold boot, or `1` and a clone's transaction ID. + /// 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); - bytes.extend_from_slice(self.identity.as_bytes()); match &self.transaction { None => bytes.push(COLD), Some(transaction) => { @@ -36,22 +43,16 @@ impl ReadyActivation { /// Reads an activation from its canonical encoding, as the [module](super#ready) describes it. /// - /// Fails with [`ActivationError::Ready`] unless the bytes are an identity followed by `0`, or - /// by `1` and a transaction ID, and nothing more. + /// 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 (identity, rest) = bytes - .split_first_chunk::<{ ConfigurationIdentity::LEN }>() - .ok_or(ActivationError::Ready)?; - let transaction = match rest { + 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 { - identity: ConfigurationIdentity::from_bytes(*identity), - transaction, - }) + Ok(Self { transaction }) } } diff --git a/nvx-rs/src/activation/ready/tests.rs b/nvx-rs/src/activation/ready/tests.rs index 35a4bffea..728673f35 100644 --- a/nvx-rs/src/activation/ready/tests.rs +++ b/nvx-rs/src/activation/ready/tests.rs @@ -1,10 +1,10 @@ -//! Tests of the canonical encoding of what Ready reports about an activation. The tests build its -//! bytes from the encoding's definition, never with the encoder that they check. +//! 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}; +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. @@ -16,11 +16,6 @@ const READY: [&str; 5] = [ "LARGEST", ]; -/// Returns the identity of the tests: the bytes 00 through 1f. -fn identity() -> ConfigurationIdentity { - ConfigurationIdentity::from_bytes(std::array::from_fn(|index| u8::try_from(index).unwrap())) -} - /// Returns the transaction ID of the tests: the bytes a0 through af. fn transaction() -> TransactionId { TransactionId::from_bytes(std::array::from_fn(|index| { @@ -32,29 +27,24 @@ fn transaction() -> TransactionId { fn the_lengths_keep_their_values() { assert_eq!( (ReadyActivation::COLD_LEN, ReadyActivation::CLONE_LEN), - (33, 49) + (1, 17) ); } #[test] -fn a_cold_boot_reports_its_identity_then_a_zero() { - let ready = ReadyActivation { - identity: identity(), - transaction: None, - }; - let bytes = [&identity().as_bytes()[..], &[0]].concat(); - assert_eq!(ready.encode(), bytes); - assert_eq!(bytes.len(), ReadyActivation::COLD_LEN); - assert_eq!(ReadyActivation::decode(&bytes), Ok(ready)); +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_its_identity_then_a_one_and_its_transaction() { +fn a_clone_reports_a_one_then_its_transaction() { let ready = ReadyActivation { - identity: identity(), transaction: Some(transaction()), }; - let bytes = [&identity().as_bytes()[..], &[1], transaction().as_bytes()].concat(); + 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)); @@ -62,21 +52,10 @@ fn a_clone_reports_its_identity_then_a_one_and_its_transaction() { #[test] fn every_other_form_is_refused() { - for length in 0..=ConfigurationIdentity::LEN { - assert_eq!( - ReadyActivation::decode(&identity().as_bytes()[..length]), - Err(ActivationError::Ready), - "{length} bytes" - ); - } + assert_eq!(ReadyActivation::decode(&[]), Err(ActivationError::Ready)); for flag in u8::MIN..=u8::MAX { for tail in 0..=TransactionId::LEN + 1 { - let bytes = [ - &identity().as_bytes()[..], - &[flag], - &transaction().as_bytes().repeat(2)[..tail], - ] - .concat(); + 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()), @@ -90,18 +69,33 @@ fn every_other_form_is_refused() { } } +#[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 identity = ConfigurationIdentity::from_bytes(values.bytes().try_into().unwrap()); let transaction = values.optional(|values| TransactionId::from_bytes(values.bytes().try_into().unwrap())); drop(values); - let ready = ReadyActivation { - identity, - transaction, - }; + let ready = ReadyActivation { transaction }; assert_eq!(ready.encode(), vector.bytes, "{}", vector.name); assert_eq!( ReadyActivation::decode(&vector.bytes), @@ -116,7 +110,6 @@ fn ready_activations_match_their_vectors() { fn invalid_ready_activations_are_refused() { let names = [ "EMPTY", - "IDENTITY_ONLY", "FLAG_2", "COLD_WITH_TRANSACTION", "COLD_WITH_TRAILING_BYTE", @@ -138,22 +131,16 @@ fn invalid_ready_activations_are_refused() { fn the_refusal_names_the_rule() { assert_eq!( ActivationError::Ready.to_string(), - "what Ready reports is not an identity followed by 0, or by 1 and a transaction ID" + "what Ready reports is neither 0, nor 1 followed by a transaction ID" ); } -/// Returns bytes that a decoder may meet: arbitrary bytes, and an identity followed by a byte -/// from 0 to 2 and up to 17 more bytes. +/// 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> { - prop_oneof![ - prop::collection::vec(any::(), 0..64), - ( - any::<[u8; 32]>(), - 0_u8..=2, - prop::collection::vec(any::(), 0..=17) - ) - .prop_map(|(identity, flag, tail)| [&identity[..], &[flag], &tail].concat()), - ] + 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! { @@ -161,13 +148,11 @@ proptest! { /// every longer form, is refused. #[test] fn ready_activations_round_trip( - identity in any::<[u8; 32]>(), transaction in any::>(), cut in any::(), extra in any::(), ) { let ready = ReadyActivation { - identity: ConfigurationIdentity::from_bytes(identity), transaction: transaction.map(TransactionId::from_bytes), }; let mut bytes = ready.encode(); 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/lib.rs b/nvx-rs/src/lib.rs index 66ea00713..5f1bebeda 100644 --- a/nvx-rs/src/lib.rs +++ b/nvx-rs/src/lib.rs @@ -10,8 +10,7 @@ //! not take. It has no dependencies, so the host, the guest, and test doubles share it. //! - [`activation`]: the activation record of a layered root, the 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, its canonical encoding and configuration -//! identity, and what Ready reports about the activation. +//! 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. diff --git a/nvx-rs/src/tests.rs b/nvx-rs/src/tests.rs index 4b1c0fcdc..13603041a 100644 --- a/nvx-rs/src/tests.rs +++ b/nvx-rs/src/tests.rs @@ -5,8 +5,8 @@ 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, with its configuration -/// identities, and of what Ready reports about an activation. +/// 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"))] diff --git a/nvx-rs/tests/vectors/README.md b/nvx-rs/tests/vectors/README.md index e11cda97f..6f6099ab9 100644 --- a/nvx-rs/tests/vectors/README.md +++ b/nvx-rs/tests/vectors/README.md @@ -11,7 +11,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, with the configuration identity of each record, and what Ready reports about an activation | +| [`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) diff --git a/nvx-rs/tests/vectors/activation-v1.txt b/nvx-rs/tests/vectors/activation-v1.txt index a29eb61d6..1b9a83402 100644 --- a/nvx-rs/tests/vectors/activation-v1.txt +++ b/nvx-rs/tests/vectors/activation-v1.txt @@ -1,9 +1,8 @@ -# Conformance vectors of version 1 of the activation record of a layered root, with the -# configuration identity of each record, and of what Ready reports about an activation. The -# documentation of nvx::activation defines the encodings and the identity. README.md describes the -# line format and the rules that the vectors follow. +# 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|IDENTITY +# 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, @@ -12,11 +11,10 @@ # 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. IDENTITY is the record's configuration identity: the SHA-256 digest of -# the label "nvx activation configuration identity v1", a NUL, and BYTES. -# READY|NAME|BYTES|IDENTITY|TRANSACTION -# What Ready reports about the activation of the record of the same name: its configuration -# identity, and a clone's transaction ID, or absent for a cold boot. +# 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 @@ -24,28 +22,28 @@ # 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|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b86982540 +RECORD|COLD|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000|-|EROFS|000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f|-|TMPFS|TRUSTED_LOWERS|-|-|nvx-sandbox|IMAGE|-|1|268435456 -# The same configuration in a clone's record, whose restore section changes its identity. -RECORD|CLONE|01000138a0a1a2a3a4a5a6a7a8a9aaabacadaeaf0100000000000000c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000|a0a1a2a3a4a5a6a7a8a9aaabacadaeaf|1|c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf|EROFS|000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f|-|TMPFS|TRUSTED_LOWERS|-|-|nvx-sandbox|IMAGE|-|1|268435456|420e7916326f0b92fbe02ec9a4dd87d28829695745c9d8e58813ed2371ea66ed +# 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|1a9ebc6ed5bb83544e1be56f2ff4619d3b3cb6a6de49c03f5bd4a7962dcc7f61 +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|1e074b18ba5ac3db01ebc2de8a0382fb65f3556f66c26d97c2adb4795c2411bc +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|7d213fe715fb6ff77d20e1348d52e0c3b72e3ca227562be415a268c69f8f1f67 +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|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b8698254000|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b86982540|- -READY|CLONE|420e7916326f0b92fbe02ec9a4dd87d28829695745c9d8e58813ed2371ea66ed01a0a1a2a3a4a5a6a7a8a9aaabacadaeaf|420e7916326f0b92fbe02ec9a4dd87d28829695745c9d8e58813ed2371ea66ed|a0a1a2a3a4a5a6a7a8a9aaabacadaeaf -READY|SCRATCH_AGGREGATE|1a9ebc6ed5bb83544e1be56f2ff4619d3b3cb6a6de49c03f5bd4a7962dcc7f6100|1a9ebc6ed5bb83544e1be56f2ff4619d3b3cb6a6de49c03f5bd4a7962dcc7f61|- -READY|GPT_DIRECTORY|1e074b18ba5ac3db01ebc2de8a0382fb65f3556f66c26d97c2adb4795c2411bc010102030405060708090a0b0c0d0e0f10|1e074b18ba5ac3db01ebc2de8a0382fb65f3556f66c26d97c2adb4795c2411bc|0102030405060708090a0b0c0d0e0f10 -READY|LARGEST|7d213fe715fb6ff77d20e1348d52e0c3b72e3ca227562be415a268c69f8f1f6701f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff|7d213fe715fb6ff77d20e1348d52e0c3b72e3ca227562be415a268c69f8f1f67|f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff +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| @@ -106,13 +104,12 @@ INVALID_RECORD|UNCHANGED_GID|0100102101000102030405060708090a0b0c0d0e0f101112131 INVALID_RECORD|ZERO_CPUS|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c000000000000001000000000 INVALID_RECORD|ZERO_MEMORY|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000000000000000 -# What Ready reports: an identity alone, or followed by 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. +# 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|IDENTITY_ONLY|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b86982540 -INVALID_READY|FLAG_2|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b8698254002 -INVALID_READY|COLD_WITH_TRANSACTION|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b8698254000a0a1a2a3a4a5a6a7a8a9aaabacadaeaf -INVALID_READY|COLD_WITH_TRAILING_BYTE|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b869825400000 -INVALID_READY|CLONE_WITHOUT_TRANSACTION|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b8698254001 -INVALID_READY|SHORT_TRANSACTION|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b8698254001a0a1a2a3a4a5a6a7a8a9aaabacadae -INVALID_READY|LONG_TRANSACTION|3d5552c256a7e34a8cc3b29df9b9aaf2276528c3df82e4c24b15149b8698254001a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0 +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