From 26267bb4f44e1b4b0790580a18802451aad0b19e Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Thu, 5 Nov 2026 20:17:23 -0800 Subject: [PATCH 1/3] activation: keep only guest-applied settings in record v2 Remove store references and host planning identities from the shared record while retaining expected image UUIDs and disk GUIDs. Use View without execution credentials and carry the tmpfs memory cap as a bounded percentage. Keep store identity types for their existing callers. Update producers, application and canonical fixtures together, and reject record version 1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3532e012-2beb-4ff7-81ab-3795de501985 --- nvx-rs/README.md | 61 ++-- nvx-rs/src/activation/codec.rs | 136 +++------ nvx-rs/src/activation/codec/tests.rs | 285 +++++------------- nvx-rs/src/activation/codec/tests/vectors.rs | 57 ++-- nvx-rs/src/activation/mod.rs | 170 ++++------- nvx-rs/src/activation/ready/tests.rs | 10 +- nvx-rs/src/activation/tests.rs | 114 +++---- nvx-rs/src/guest/clone.rs | 5 +- nvx-rs/src/guest/clone/activation/apply.rs | 21 +- .../src/guest/clone/activation/apply/tests.rs | 131 ++++---- nvx-rs/src/guest/clone/activation/tests.rs | 26 +- nvx-rs/src/host/store/lease.rs | 4 +- nvx-rs/src/host/template/clone.rs | 36 +-- nvx-rs/src/host/template/clone/record.rs | 133 ++------ nvx-rs/src/host/template/clone/tests.rs | 104 +++---- .../src/host/template/clone/tests/launch.rs | 6 +- .../src/host/template/clone/tests/restore.rs | 1 - nvx-rs/src/host/template/key.rs | 4 +- nvx-rs/src/host/template/key/tests.rs | 2 +- nvx-rs/src/host/template/store.rs | 4 +- nvx-rs/src/tests.rs | 4 +- nvx-rs/tests/vectors/README.md | 4 +- nvx-rs/tests/vectors/activation-v1.txt | 115 ------- nvx-rs/tests/vectors/activation-v2.txt | 93 ++++++ tests/runtime-adapter/README.md | 39 ++- .../src/bin/runtime-adapter-host.rs | 11 +- tests/runtime-adapter/src/clone.rs | 20 +- tests/runtime-adapter/src/worker.rs | 8 +- .../runtime-adapter/src/worker/activation.rs | 2 +- .../src/worker/activation/tests.rs | 36 +-- tests/runtime-adapter/tests/clone_start.rs | 16 +- tests/runtime-adapter/tests/guest_session.rs | 88 +++--- tests/runtime-adapter/tests/vm_clone.rs | 51 ++-- 33 files changed, 628 insertions(+), 1169 deletions(-) delete mode 100644 nvx-rs/tests/vectors/activation-v1.txt create mode 100644 nvx-rs/tests/vectors/activation-v2.txt diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 7708a7666..0bbf4be63 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -295,19 +295,16 @@ 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. +An `ActivationRecord` holds the schema's `VERSION`, 2, a clone's +`RestoreSection`, only the restore's `TransactionId`, and the `Configuration`: +the `Image`, EROFS or GPT, with the UUID or disk GUID that the guest expects; +the `Upper`, a tmpfs with its cap in percent of the VM's memory or the scratch +with its `Journal` policy; the `OverlayMode`; an `Export`, a directory or an +aggregate, and its 1 to 4,096 mappings; the `Network`, a DNS address, the +listeners' `Publication`, and a proxy endpoint without credentials; the `View`, +its hostname and `MachineIdMode`; and the `ExpectedResources`. Store, binding, +plan, policy and execution identities never enter a record. The other inputs +of the root's assembly are invariants of the template. `View` is the view that a VM's configuration, a [`VmConfig`](#nvxhostconfig), gives its workload: its hostname and `MachineIdMode`, without an identity, which @@ -316,13 +313,13 @@ each execution names. `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. +or with an export without mappings, mappings without an export, a tmpfs cap +outside 1 to 90 percent, 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. +424 bytes at most, and `ActivationRecord::decode` reads exactly those bytes back +and refuses any others, naming the `Field` at fault. Version 1 is refused. `ReadyActivation` is what Ready reports once the activation applied: a clone's transaction ID, which `ReadyActivation::of` takes from a record, and nothing @@ -902,8 +899,8 @@ capture's own metadata, such as its time anchor and host, stays out of the key. A key and a class each have one canonical, versioned encoding, whose domain-separated SHA-256 digest is its identity, a `KeyIdentity`, which converts -to and from the `nvx::activation::Identity` by which a clone's activation record -names its template's key, or a `ClassIdentity`; the +to and from the `nvx::activation::Identity` by which a clone's lease and durable records +name its template's key, or a `ClassIdentity`; the [conformance vectors](tests/vectors/README.md) of version 1, `tests/vectors/template-key-v1.txt`, pin both encodings and identities. The key's encoding, version 1, leaves the network policy out, since templates @@ -930,7 +927,7 @@ its directories and files. Four namespaces stay apart: the `nvx::activation::StoreGeneration`, the host's publication ID, which the index assigns in the order of publication and never -twice, and by which a clone's activation record names its template; the +twice, and by which a clone's lease and durable records name its template; the bound-block `StorageGeneration` of a capture, which belongs to the storage owner; the time lineage counter of the time contract; and the VM generation ID, which the store never records. @@ -990,22 +987,20 @@ as those above a clone's block devices must; and a fresh capability. It builds the clone's activation record, `ClonePlan::activation`, which the clone's guest applies in place of the configuration that the template captured, -from the request's `WorkloadView`, overlay mode, journal policy, and bound of a +from the request's `View`, overlay mode, journal policy, and bound of a GPT disk's first partition, and from the checked inputs: a restore section with -a fresh transaction ID of 16 random bytes and the lease's `TemplateReference`; -the image, an EROFS layer or a GPT disk, as its binding names it, with its -binding's identity and the filesystem UUID or disk GUID that the adapter's plan +a fresh transaction ID of 16 random bytes alone; +the image, an EROFS layer or a GPT disk, as its binding names it, with the +filesystem UUID or disk GUID that the adapter's plan expects of it, and for a GPT disk the bound of its first partition, whose -partition table the guest checks with `nvx::gpt` as at a cold launch; a tmpfs, -or the scratch image's binding identity with the journal policy; the export, an -aggregate, with its plan's identity, and its mappings, the plan's binds, from 1 +partition table the guest checks with `nvx::gpt` as at a cold launch; a tmpfs +capped at half of the VM's memory, or the scratch image with the journal policy; +the export, an aggregate, and its mappings, the plan's binds, from 1 to 4,096, or no export and no mappings, which leaves the slot dormant; the -clone's network, with its identity, the DNS server that OpenVMM serves the guest +clone's network, the DNS server that OpenVMM serves the guest at a gateway under its policy, if any, whether the guest publishes its listeners, as under a policy that allows ingress or host-loopback access both -ways, and the guest's proxy URL; and the processor and memory targets. The -identity of a binding, of an export plan, or of a network is the SHA-256 digest -of a domain and of its serde form, never of a file's content. +ways, and the guest's proxy URL; and the processor and memory targets. The image must present the logical and physical block sizes of the template's placeholder and be no longer than it, since a restore presents every clone's @@ -1629,7 +1624,7 @@ with `BLKRRPART`, since the kernel created the block devices of partitions for the template's placeholder. Whatever the image, it requires a fresh upper, whose `upper` and `work` directories are missing or empty, in a bounded check that reads no file, while a cold sandbox's scratch keeps its upper across boots. It -ignores the record's workload identity, if it has one: each execution names its +uses no execution identity from the record, which carries none: each execution names its own identity, which `Entry::check` checks, as in a sandbox without a workload identity at a cold boot. With the record's generated machine ID, it then generates one, once, with `MachineId::generate`, after the repair's reseed, so @@ -2984,7 +2979,7 @@ sides of every record, frame, and payload: the codec decodes each vector of 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 +Ready activation of `tests/vectors/activation-v2.txt` to its values, encode them to its bytes, and refuse its invalid bytes. Property tests check that records and activation records round-trip, that the activation record and Ready activation decoders accept only canonical encodings, and that the diff --git a/nvx-rs/src/activation/codec.rs b/nvx-rs/src/activation/codec.rs index 91e6dc4e7..065c95cb9 100644 --- a/nvx-rs/src/activation/codec.rs +++ b/nvx-rs/src/activation/codec.rs @@ -1,18 +1,15 @@ //! The canonical encoding of an activation record, which the [module](super#encoding) describes: //! [`ActivationRecord::encode`] writes it, and [`ActivationRecord::decode`] reads it back. -use std::fmt; -use std::net::IpAddr; -use std::num::NonZeroU64; - use super::{ - ActivationError, ActivationRecord, Configuration, ExpectedResources, Export, ExportKind, - Identity, Image, Journal, MAX_RECORD_BYTES, MachineIdMode, Network, OverlayMode, Publication, - RestoreSection, StoreGeneration, TemplateReference, TransactionId, Upper, VERSION, - WorkloadIdentity, WorkloadView, + ActivationError, ActivationRecord, Configuration, ExpectedResources, Export, ExportKind, Image, + Journal, MAX_RECORD_BYTES, MachineIdMode, Network, OverlayMode, Publication, RestoreSection, + TransactionId, Upper, VERSION, View, }; use crate::protocol::root::ImageFormat; use crate::uuid::Uuid; +use std::fmt; +use std::net::IpAddr; #[cfg(test)] mod tests; @@ -33,20 +30,19 @@ const SCRATCH: u8 = 2; pub enum Field { /// `0x01`: the restore section of a clone. Restore, - /// `0x10`: the image's format, the identity of its binding, and a GPT disk's first-partition - /// bound. + /// `0x10`: the image's format and a GPT disk's first-partition bound. Image, /// `0x11`: the UUID or the disk GUID that the guest expects of the image. ImageIdentity, - /// `0x20`: the upper, and a scratch's binding and journal policy. + /// `0x20`: the upper, and its tmpfs cap or scratch journal policy. Upper, /// `0x30`: the overlay's mode. Overlay, - /// `0x40`: the export's shape, and the identity of its plan. + /// `0x40`: the export's shape. Export, /// `0x41`: the number of host mappings. Mappings, - /// `0x50`: the identity of the network's policy, and the publication of the listeners. + /// `0x50`: the publication of the listeners. Network, /// `0x51`: the address of the network's DNS server. Dns, @@ -56,15 +52,13 @@ pub enum Field { Hostname, /// `0x61`: the machine ID's mode. MachineId, - /// `0x62`: the workload identity. - WorkloadIdentity, /// `0x70`: the expected resources. Resources, } impl Field { /// Every field, in the order of their tags. - pub const ALL: [Self; 14] = [ + pub const ALL: [Self; 13] = [ Self::Restore, Self::Image, Self::ImageIdentity, @@ -77,7 +71,6 @@ impl Field { Self::Proxy, Self::Hostname, Self::MachineId, - Self::WorkloadIdentity, Self::Resources, ]; @@ -96,7 +89,6 @@ impl Field { Self::Proxy => 0x52, Self::Hostname => 0x60, Self::MachineId => 0x61, - Self::WorkloadIdentity => 0x62, Self::Resources => 0x70, } } @@ -123,7 +115,6 @@ impl fmt::Display for Field { Self::Proxy => "proxy", Self::Hostname => "hostname", Self::MachineId => "machine ID", - Self::WorkloadIdentity => "workload identity", Self::Resources => "resources", }) } @@ -192,61 +183,43 @@ impl ActivationRecord { pub fn encode(&self) -> Vec { let configuration = &self.configuration; let mut encoder = Encoder::new(); - if let Some(RestoreSection { - transaction, - template, - }) = &self.restore - { - encoder.field( - Field::Restore, - &[ - transaction.as_bytes(), - &template.generation.get().get().to_le_bytes(), - template.key.as_bytes(), - ], - ); + if let Some(RestoreSection { transaction }) = &self.restore { + encoder.field(Field::Restore, &[transaction.as_bytes()]); } let image = &configuration.image; let format = [image.format().code()]; match image { - Image::Erofs { binding, .. } => { - encoder.field(Field::Image, &[&format, binding.as_bytes()]); + Image::Erofs { .. } => { + encoder.field(Field::Image, &[&format]); } Image::Gpt { - binding, first_partition_bound, .. } => encoder.field( Field::Image, - &[ - &format, - binding.as_bytes(), - &first_partition_bound.to_le_bytes(), - ], + &[&format, &first_partition_bound.to_le_bytes()], ), } if let Some(expected) = image.expected() { encoder.field(Field::ImageIdentity, &[expected.as_bytes()]); } match configuration.upper { - Upper::Tmpfs => encoder.field(Field::Upper, &[&[TMPFS]]), - Upper::Scratch { binding, journal } => encoder.field( - Field::Upper, - &[&[SCRATCH], binding.as_bytes(), &[journal.code()]], - ), + Upper::Tmpfs { max_memory_percent } => { + encoder.field(Field::Upper, &[&[TMPFS, max_memory_percent]]); + } + Upper::Scratch { journal } => { + encoder.field(Field::Upper, &[&[SCRATCH, journal.code()]]); + } } encoder.field(Field::Overlay, &[&[configuration.overlay.code()]]); - if let Some(Export { kind, plan }) = configuration.export { - encoder.field(Field::Export, &[&[kind.code()], plan.as_bytes()]); + if let Some(Export { kind }) = configuration.export { + encoder.field(Field::Export, &[&[kind.code()]]); // A record with an export announces 1 to `MAX_MAPPINGS` mappings, which a `u16` holds. let mappings = configuration.mappings as u16; encoder.field(Field::Mappings, &[&mappings.to_le_bytes()]); } if let Some(network) = &configuration.network { - encoder.field( - Field::Network, - &[network.policy.as_bytes(), &[network.publication.code()]], - ); + encoder.field(Field::Network, &[&[network.publication.code()]]); match network.dns { Some(IpAddr::V4(address)) => encoder.field(Field::Dns, &[&address.octets()]), Some(IpAddr::V6(address)) => encoder.field(Field::Dns, &[&address.octets()]), @@ -256,15 +229,9 @@ impl ActivationRecord { encoder.field(Field::Proxy, &[proxy.as_bytes()]); } } - let workload = &configuration.workload; - encoder.field(Field::Hostname, &[&workload.hostname]); - encoder.field(Field::MachineId, &[&[workload.machine_id.code()]]); - if let Some(WorkloadIdentity { uid, gid }) = workload.identity { - encoder.field( - Field::WorkloadIdentity, - &[&uid.to_le_bytes(), &gid.to_le_bytes()], - ); - } + let view = &configuration.view; + encoder.field(Field::Hostname, &[&view.hostname]); + encoder.field(Field::MachineId, &[&[view.machine_id.code()]]); let ExpectedResources { cpus, memory } = configuration.resources; encoder.field( Field::Resources, @@ -280,7 +247,7 @@ impl ActivationRecord { /// then, field by field, a field whose tag and length end past the bytes, a field with an /// unknown tag, a repeated field, a field out of order, a value that ends past the bytes, and a /// value with another length than its contents take, with a code that the version does not - /// define, with a mapping count or a store generation of 0, or with a proxy endpoint that is + /// define, with a mapping count of 0, or with a proxy endpoint that is /// not UTF-8; then a missing field that every record holds, in the order of the tags; then a /// DNS server or a proxy without a network; then the first limit that [`Configuration::check`] /// finds broken, such as an export without mappings, or mappings without an export. So it reads @@ -368,8 +335,8 @@ struct Draft { export: Option, /// The number of mappings: 1 or more. mappings: Option, - /// The identity of the network's policy, and the publication of the listeners. - network: Option<(Identity, Publication)>, + /// The publication of the listeners. + network: Option, /// The address of the DNS server. dns: Option, /// The proxy endpoint. @@ -378,8 +345,6 @@ struct Draft { hostname: Option>, /// The machine ID's mode. machine_id: Option, - /// The workload identity. - identity: Option, /// The expected resources. resources: Option, } @@ -391,26 +356,13 @@ impl Draft { match field { Field::Restore => { let transaction = TransactionId::from_bytes(value.take()?); - // A store assigns no template the generation 0. - let generation = NonZeroU64::new(u64::from_le_bytes(value.take()?)) - .map(StoreGeneration::new) - .ok_or(ActivationError::FieldValue(field))?; - let key = Identity::from_bytes(value.take()?); - self.restore = Some(RestoreSection { - transaction, - template: TemplateReference { generation, key }, - }); + self.restore = Some(RestoreSection { transaction }); } Field::Image => { let format = value.code::()?; - let binding = Identity::from_bytes(value.take()?); self.image = Some(match format { - ImageFormat::Erofs => Image::Erofs { - binding, - uuid: None, - }, + ImageFormat::Erofs => Image::Erofs { uuid: None }, ImageFormat::Gpt => Image::Gpt { - binding, disk_guid: None, first_partition_bound: u64::from_le_bytes(value.take()?), }, @@ -419,9 +371,10 @@ impl Draft { Field::ImageIdentity => self.image_identity = Some(Uuid::from_bytes(value.take()?)), Field::Upper => { self.upper = Some(match value.take::<1>()? { - [TMPFS] => Upper::Tmpfs, + [TMPFS] => Upper::Tmpfs { + max_memory_percent: value.take::<1>()?[0], + }, [SCRATCH] => Upper::Scratch { - binding: Identity::from_bytes(value.take()?), journal: value.code()?, }, _ => return Err(ActivationError::FieldValue(field)), @@ -430,18 +383,14 @@ impl Draft { Field::Overlay => self.overlay = Some(value.code()?), Field::Export => { let kind = value.code()?; - let plan = Identity::from_bytes(value.take()?); - self.export = Some(Export { kind, plan }); + self.export = Some(Export { kind }); } Field::Mappings => match u16::from_le_bytes(value.take()?) { // A record without mappings leaves the field out. 0 => return Err(ActivationError::FieldValue(field)), mappings => self.mappings = Some(usize::from(mappings)), }, - Field::Network => { - let policy = Identity::from_bytes(value.take()?); - self.network = Some((policy, value.code()?)); - } + Field::Network => self.network = Some(value.code()?), Field::Dns => { // A DNS server's address is IPv4 or IPv6, which its length tells apart. self.dns = Some(if bytes.len() == 4 { @@ -457,11 +406,6 @@ impl Draft { } Field::Hostname => self.hostname = Some(value.rest().to_vec()), Field::MachineId => self.machine_id = Some(value.code()?), - Field::WorkloadIdentity => { - let uid = u32::from_le_bytes(value.take()?); - let gid = u32::from_le_bytes(value.take()?); - self.identity = Some(WorkloadIdentity { uid, gid }); - } Field::Resources => { let cpus = u32::from_le_bytes(value.take()?); let memory = u64::from_le_bytes(value.take()?); @@ -488,8 +432,7 @@ impl Draft { } } let network = match self.network { - Some((policy, publication)) => Some(Network { - policy, + Some(publication) => Some(Network { dns: self.dns, publication, proxy: self.proxy, @@ -509,10 +452,9 @@ impl Draft { export: self.export, mappings: self.mappings.unwrap_or(0), network, - workload: WorkloadView { + view: View { hostname, machine_id, - identity: self.identity, }, resources, }; diff --git a/nvx-rs/src/activation/codec/tests.rs b/nvx-rs/src/activation/codec/tests.rs index 6ed85c9a7..9e69c4ede 100644 --- a/nvx-rs/src/activation/codec/tests.rs +++ b/nvx-rs/src/activation/codec/tests.rs @@ -2,7 +2,6 @@ //! by field from the encoding's definition, never with the encoder that they check. use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use std::num::NonZeroU64; use proptest::prelude::*; @@ -26,19 +25,18 @@ fn encoding_of(version: u16, fields: &[RawField]) -> Vec { bytes } -/// Returns the encoding of version 1 with `fields`, in the order given. +/// Returns the encoding of version 2 with `fields`, in the order given. fn encoding(fields: &[RawField]) -> Vec { - encoding_of(1, fields) + encoding_of(2, fields) } /// Returns the fields of the cold record of the tests' [`configuration`], from the encoding's /// definition. fn cold_fields() -> Vec { vec![ - // An EROFS image, whose binding's identity is 0x11 bytes. - (0x10, [&[1][..], &[0x11; 32]].concat()), - // A tmpfs upper. - (0x20, vec![1]), + // An EROFS image and a tmpfs upper capped at 50 percent. + (0x10, vec![1]), + (0x20, vec![1, 50]), // Trusted lowers. (0x30, vec![1]), (0x60, b"nvx-sandbox".to_vec()), @@ -66,17 +64,15 @@ fn without(mut fields: Vec, tag: u8) -> Vec { fields } -/// Returns the fields of the cold record with an aggregate export, whose plan's identity is 0x55 -/// bytes, and with the mapping count `count`. +/// Returns the fields of the cold record with an aggregate export and mapping count `count`. fn with_export(count: [u8; 2]) -> Vec { - let fields = with(cold_fields(), 0x40, &[&[2][..], &[0x55; 32]].concat()); + let fields = with(cold_fields(), 0x40, &[2]); with(fields, 0x41, &count) } -/// Returns the fields of the cold record with a network, whose policy's identity is 0x66 bytes, -/// and whose listeners are not published. +/// Returns the fields of the cold record with a network whose listeners are not published. fn with_network() -> Vec { - with(cold_fields(), 0x50, &[&[0x66; 32][..], &[2]].concat()) + with(cold_fields(), 0x50, &[2]) } /// Returns what the decoder makes of `fields`. @@ -87,7 +83,7 @@ fn decode(fields: &[RawField]) -> Result { /// Returns the fields of an encoding, as tag and value, and checks the encoding's version, and /// that its fields hold nothing past their ends and come in the increasing order of their tags. fn fields_of(bytes: &[u8]) -> Vec { - assert_eq!(bytes[..2], [1, 0], "the version"); + assert_eq!(bytes[..2], [2, 0], "the version"); let mut fields: Vec = Vec::new(); let mut rest = &bytes[2..]; while let [tag, length, tail @ ..] = rest { @@ -118,7 +114,6 @@ fn the_fields_keep_their_tags_and_names() { (Field::Proxy, 0x52, "proxy"), (Field::Hostname, 0x60, "hostname"), (Field::MachineId, 0x61, "machine ID"), - (Field::WorkloadIdentity, 0x62, "workload identity"), (Field::Resources, 0x70, "resources"), ]; assert_eq!(Field::ALL.len(), expected.len()); @@ -168,8 +163,8 @@ fn a_cold_record_encodes_to_the_bytes_of_its_definition() { #[test] fn a_clone_record_holds_its_restore_section_first() { let record = ActivationRecord::new(Some(restore()), configuration()).unwrap(); - // The transaction ID, the store generation, and the identity of the template's key. - let section = [&[0x22; 16][..], &[0x33; 8], &[0x44; 32]].concat(); + // Nothing besides the transaction ID belongs to the restore section. + let section = [0x22; 16]; let bytes = encoding(&with(cold_fields(), 0x01, §ion)); assert_eq!(record.encode(), bytes); assert_eq!(ActivationRecord::decode(&bytes), Ok(record)); @@ -179,43 +174,32 @@ fn a_clone_record_holds_its_restore_section_first() { fn every_part_of_a_configuration_encodes_to_the_bytes_of_its_definition() { let mut configuration = configuration(); configuration.image = Image::Gpt { - binding: Identity::from_bytes([0x77; 32]), disk_guid: Some("fedcba98-7654-3210-fedc-ba9876543210".parse().unwrap()), first_partition_bound: 64 << 10, }; configuration.upper = Upper::Scratch { - binding: Identity::from_bytes([0x88; 32]), journal: Journal::Forbidden, }; configuration.overlay = OverlayMode::UntrustedLowers; configuration.export = Some(Export { kind: ExportKind::Directory, - plan: Identity::from_bytes([0x99; 32]), }); configuration.mappings = 0x0102; configuration.network = Some(Network { - policy: Identity::from_bytes([0xaa; 32]), dns: Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))), publication: Publication::Published, proxy: Some("http://10.0.0.1:3128".to_owned()), }); - configuration.workload = WorkloadView { + configuration.view = View { hostname: b"clone".to_vec(), machine_id: MachineIdMode::Generated, - identity: Some(WorkloadIdentity { - uid: 1000, - gid: 0x0102_0304, - }), }; configuration.resources = ExpectedResources { cpus: 4, memory: 0x0102_0304_0506_0708, }; let fields = vec![ - ( - 0x10, - [&[2][..], &[0x77; 32], &[0, 0, 1, 0, 0, 0, 0, 0]].concat(), - ), + (0x10, vec![2, 0, 0, 1, 0, 0, 0, 0, 0]), ( 0x11, vec![ @@ -223,16 +207,15 @@ fn every_part_of_a_configuration_encodes_to_the_bytes_of_its_definition() { 0x32, 0x10, ], ), - (0x20, [&[2][..], &[0x88; 32], &[2]].concat()), + (0x20, vec![2, 2]), (0x30, vec![2]), - (0x40, [&[1][..], &[0x99; 32]].concat()), + (0x40, vec![1]), (0x41, vec![2, 1]), - (0x50, [&[0xaa; 32][..], &[1]].concat()), + (0x50, vec![1]), (0x51, vec![10, 0, 0, 1]), (0x52, b"http://10.0.0.1:3128".to_vec()), (0x60, b"clone".to_vec()), (0x61, vec![2]), - (0x62, vec![0xe8, 3, 0, 0, 4, 3, 2, 1]), (0x70, vec![4, 0, 0, 0, 8, 7, 6, 5, 4, 3, 2, 1]), ]; let record = ActivationRecord::new(None, configuration.clone()).unwrap(); @@ -248,10 +231,10 @@ fn every_part_of_a_configuration_encodes_to_the_bytes_of_its_definition() { } #[test] -fn a_maximal_record_holds_602_bytes_within_the_limit() { +fn a_maximal_record_holds_424_bytes_within_the_limit() { let record = ActivationRecord::new(Some(restore()), maximal()).unwrap(); let bytes = record.encode(); - assert_eq!(bytes.len(), 602); + assert_eq!(bytes.len(), 424); assert!(bytes.len() <= MAX_RECORD_BYTES); assert_eq!( fields_of(&bytes) @@ -286,11 +269,11 @@ fn every_prefix_of_an_encoding_is_refused() { Err(ActivationError::Truncated) ); assert_eq!( - ActivationRecord::decode(&[1]), + ActivationRecord::decode(&[2]), Err(ActivationError::Truncated) ); assert_eq!( - ActivationRecord::decode(&[1, 0]), + ActivationRecord::decode(&[2, 0]), Err(ActivationError::MissingField(Field::Image)) ); } @@ -318,7 +301,7 @@ fn bytes_past_the_limit_are_refused_before_anything_else() { #[test] fn other_versions_are_refused() { - for version in [0, 2, 0x0100, u16::MAX] { + for version in [0, 1, 0x0100, u16::MAX] { assert_eq!( ActivationRecord::decode(&encoding_of(version, &cold_fields())), Err(ActivationError::Version(version)), @@ -378,7 +361,7 @@ fn fields_out_of_order_are_refused() { } // The restore section comes first. let mut fields = cold_fields(); - fields.push((0x01, [&[0x22; 16][..], &[0x33; 8], &[0x44; 32]].concat())); + fields.push((0x01, vec![0x22; 16])); assert_eq!( decode(&fields), Err(ActivationError::FieldOrder(Field::Restore)) @@ -407,7 +390,6 @@ fn a_dns_server_or_a_proxy_without_a_network_is_refused() { assert_eq!( network, Some(Network { - policy: Identity::from_bytes([0x66; 32]), dns: Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1))), publication: Publication::Unpublished, proxy: Some("10.0.0.1:3128".to_owned()), @@ -417,34 +399,20 @@ fn a_dns_server_or_a_proxy_without_a_network_is_refused() { #[test] fn values_of_another_length_are_refused() { - let identity = [0x12; 32]; let cases: Vec<(Field, Vec, usize)> = vec![ - (Field::Restore, with(cold_fields(), 0x01, &[1; 55]), 55), - (Field::Restore, with(cold_fields(), 0x01, &[1; 57]), 57), + (Field::Restore, with(cold_fields(), 0x01, &[1; 15]), 15), + (Field::Restore, with(cold_fields(), 0x01, &[1; 17]), 17), (Field::Image, with(cold_fields(), 0x10, &[]), 0), + (Field::Image, with(cold_fields(), 0x10, &[1, 0]), 2), ( Field::Image, - with(cold_fields(), 0x10, &[&[1][..], &[0; 31]].concat()), - 32, - ), - ( - Field::Image, - with(cold_fields(), 0x10, &[&[1][..], &[0; 33]].concat()), - 34, - ), - ( - Field::Image, - with(cold_fields(), 0x10, &[&[2][..], &identity].concat()), - 33, + with(cold_fields(), 0x10, &[&[2][..], &[1; 7]].concat()), + 8, ), ( Field::Image, - with( - cold_fields(), - 0x10, - &[&[2][..], &identity, &[1; 9]].concat(), - ), - 42, + with(cold_fields(), 0x10, &[&[2][..], &[1; 9]].concat()), + 10, ), ( Field::ImageIdentity, @@ -457,46 +425,22 @@ fn values_of_another_length_are_refused() { 17, ), (Field::Upper, with(cold_fields(), 0x20, &[]), 0), - (Field::Upper, with(cold_fields(), 0x20, &[1, 1]), 2), + (Field::Upper, with(cold_fields(), 0x20, &[1]), 1), + (Field::Upper, with(cold_fields(), 0x20, &[1, 50, 0]), 3), (Field::Upper, with(cold_fields(), 0x20, &[2]), 1), - ( - Field::Upper, - with(cold_fields(), 0x20, &[&[2][..], &identity].concat()), - 33, - ), - ( - Field::Upper, - with( - cold_fields(), - 0x20, - &[&[2][..], &identity, &[1, 1]].concat(), - ), - 35, - ), + (Field::Upper, with(cold_fields(), 0x20, &[2, 1, 1]), 3), (Field::Overlay, with(cold_fields(), 0x30, &[]), 0), (Field::Overlay, with(cold_fields(), 0x30, &[1, 1]), 2), - ( - Field::Export, - with(with_export([1, 0]), 0x40, &[&[1][..], &[0; 31]].concat()), - 32, - ), - ( - Field::Export, - with(with_export([1, 0]), 0x40, &[&[1][..], &[0; 33]].concat()), - 34, - ), + (Field::Export, with(with_export([1, 0]), 0x40, &[]), 0), + (Field::Export, with(with_export([1, 0]), 0x40, &[1, 1]), 2), (Field::Mappings, with(with_export([1, 0]), 0x41, &[1]), 1), ( Field::Mappings, with(with_export([1, 0]), 0x41, &[1, 0, 0]), 3, ), - (Field::Network, with(cold_fields(), 0x50, &[0; 32]), 32), - ( - Field::Network, - with(cold_fields(), 0x50, &[&[0; 32][..], &[1, 1]].concat()), - 34, - ), + (Field::Network, with(cold_fields(), 0x50, &[]), 0), + (Field::Network, with(cold_fields(), 0x50, &[1, 1]), 2), (Field::Dns, with(with_network(), 0x51, &[]), 0), (Field::Dns, with(with_network(), 0x51, &[10, 0, 0]), 3), (Field::Dns, with(with_network(), 0x51, &[10, 0, 0, 1, 0]), 5), @@ -504,16 +448,6 @@ fn values_of_another_length_are_refused() { (Field::Dns, with(with_network(), 0x51, &[0; 17]), 17), (Field::MachineId, with(cold_fields(), 0x61, &[]), 0), (Field::MachineId, with(cold_fields(), 0x61, &[1, 1]), 2), - ( - Field::WorkloadIdentity, - with(cold_fields(), 0x62, &[1; 7]), - 7, - ), - ( - Field::WorkloadIdentity, - with(cold_fields(), 0x62, &[1; 9]), - 9, - ), (Field::Resources, with(cold_fields(), 0x70, &[1; 11]), 11), (Field::Resources, with(cold_fields(), 0x70, &[1; 13]), 13), ]; @@ -528,35 +462,14 @@ fn values_of_another_length_are_refused() { #[test] fn unknown_codes_are_refused() { - let identity = [0x12; 32]; for code in [0, 3, u8::MAX] { let cases = [ - ( - Field::Image, - with(cold_fields(), 0x10, &[&[code][..], &identity].concat()), - ), + (Field::Image, with(cold_fields(), 0x10, &[code])), (Field::Upper, with(cold_fields(), 0x20, &[code])), - ( - Field::Upper, - with( - cold_fields(), - 0x20, - &[&[2][..], &identity, &[code]].concat(), - ), - ), + (Field::Upper, with(cold_fields(), 0x20, &[2, code])), (Field::Overlay, with(cold_fields(), 0x30, &[code])), - ( - Field::Export, - with( - with_export([1, 0]), - 0x40, - &[&[code][..], &identity].concat(), - ), - ), - ( - Field::Network, - with(cold_fields(), 0x50, &[&identity[..], &[code]].concat()), - ), + (Field::Export, with(with_export([1, 0]), 0x40, &[code])), + (Field::Network, with(cold_fields(), 0x50, &[code])), (Field::MachineId, with(cold_fields(), 0x61, &[code])), ]; for (field, fields) in cases { @@ -582,19 +495,16 @@ fn a_mapping_count_of_zero_is_refused() { } #[test] -fn a_store_generation_of_0_is_refused() { - let section = [&[0x22; 16][..], &[0; 8], &[0x44; 32]].concat(); - assert_eq!( - decode(&with(cold_fields(), 0x01, §ion)), - Err(ActivationError::FieldValue(Field::Restore)) - ); - // A store's first generation, 1, is read. - let section = [&[0x22; 16][..], &[1, 0, 0, 0, 0, 0, 0, 0], &[0x44; 32]].concat(); - let record = decode(&with(cold_fields(), 0x01, §ion)).unwrap(); - assert_eq!( - record.restore().map(|restore| restore.template.generation), - Some(StoreGeneration::new(NonZeroU64::MIN)) - ); +fn a_tmpfs_cap_round_trips_in_its_upper_field() { + for max_memory_percent in [1, 50, 90] { + let fields = with(cold_fields(), 0x20, &[1, max_memory_percent]); + let record = decode(&fields).unwrap(); + assert_eq!( + record.configuration().upper, + Upper::Tmpfs { max_memory_percent } + ); + assert_eq!(record.encode(), encoding(&fields)); + } } #[test] @@ -635,16 +545,23 @@ fn values_past_a_limit_are_refused() { #[test] fn values_that_a_configuration_refuses_are_refused() { - let identity = [0x12; 32]; let cases = [ ( - with( - cold_fields(), - 0x10, - &[&[2][..], &identity, &[0; 8]].concat(), - ), + with(cold_fields(), 0x10, &[&[2][..], &[0; 8]].concat()), ActivationError::FirstPartitionBound, ), + ( + with(cold_fields(), 0x20, &[1, 0]), + ActivationError::TmpfsMemoryPercent(0), + ), + ( + with(cold_fields(), 0x20, &[1, 91]), + ActivationError::TmpfsMemoryPercent(91), + ), + ( + with(cold_fields(), 0x20, &[1, 255]), + ActivationError::TmpfsMemoryPercent(255), + ), ( with(with_network(), 0x52, b""), ActivationError::ProxyEndpoint, @@ -670,22 +587,6 @@ fn values_that_a_configuration_refuses_are_refused() { with(cold_fields(), 0x60, b"nvx\0sandbox"), ActivationError::Hostname, ), - ( - with(cold_fields(), 0x62, &[0, 0, 0, 0, 1, 0, 0, 0]), - ActivationError::WorkloadIdentity, - ), - ( - with(cold_fields(), 0x62, &[1, 0, 0, 0, 0, 0, 0, 0]), - ActivationError::WorkloadIdentity, - ), - ( - with(cold_fields(), 0x62, &[0xff, 0xff, 0xff, 0xff, 1, 0, 0, 0]), - ActivationError::WorkloadIdentity, - ), - ( - with(cold_fields(), 0x62, &[1, 0, 0, 0, 0xff, 0xff, 0xff, 0xff]), - ActivationError::WorkloadIdentity, - ), ( with(cold_fields(), 0x70, &[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]), ActivationError::Cpus, @@ -772,8 +673,8 @@ fn refusals_name_the_rule_that_they_break() { "the record ends inside its version or one of its fields", ), ( - ActivationError::Version(2), - "the record's version is 2, not 1", + ActivationError::Version(1), + "the record's version is 1, not 2", ), ( ActivationError::UnknownField(0x0f), @@ -808,11 +709,6 @@ fn refusals_name_the_rule_that_they_break() { } } -/// Returns arbitrary identities. -fn identities() -> impl Strategy { - any::<[u8; 32]>().prop_map(Identity::from_bytes) -} - /// Returns arbitrary UUIDs, or none. fn uuids() -> impl Strategy> { any::>().prop_map(|bytes| bytes.map(Uuid::from_bytes)) @@ -821,22 +717,18 @@ fn uuids() -> impl Strategy> { /// Returns arbitrary valid configurations. fn configurations() -> impl Strategy { let image = prop_oneof![ - (identities(), uuids()).prop_map(|(binding, uuid)| Image::Erofs { binding, uuid }), - (identities(), uuids(), 1..=u64::MAX).prop_map(|(binding, disk_guid, bound)| { + uuids().prop_map(|uuid| Image::Erofs { uuid }), + (uuids(), 1..=u64::MAX).prop_map(|(disk_guid, bound)| { Image::Gpt { - binding, disk_guid, first_partition_bound: bound, } }), ]; let upper = prop_oneof![ - Just(Upper::Tmpfs), - ( - identities(), - prop_oneof![Just(Journal::Allowed), Just(Journal::Forbidden)] - ) - .prop_map(|(binding, journal)| Upper::Scratch { binding, journal }), + (1_u8..=90).prop_map(|max_memory_percent| Upper::Tmpfs { max_memory_percent }), + prop_oneof![Just(Journal::Allowed), Just(Journal::Forbidden)] + .prop_map(|journal| Upper::Scratch { journal }), ]; let overlay = prop_oneof![ Just(OverlayMode::TrustedLowers), @@ -846,10 +738,9 @@ fn configurations() -> impl Strategy { Just((None::, 0_usize)), ( prop_oneof![Just(ExportKind::Directory), Just(ExportKind::Aggregate)], - identities(), 1..=MAX_MAPPINGS, ) - .prop_map(|(kind, plan, mappings)| (Some(Export { kind, plan }), mappings)), + .prop_map(|(kind, mappings)| (Some(Export { kind }), mappings)), ]; let address = prop_oneof![ any::<[u8; 4]>().prop_map(IpAddr::from), @@ -863,41 +754,35 @@ fn configurations() -> impl Strategy { .prop_map(|bytes| String::from_utf8(bytes).unwrap()); let network = prop::option::of( ( - identities(), prop::option::of(address), prop_oneof![Just(Publication::Published), Just(Publication::Unpublished)], prop::option::of(proxy), ) - .prop_map(|(policy, dns, publication, proxy)| Network { - policy, + .prop_map(|(dns, publication, proxy)| Network { dns, publication, proxy, }), ); - let workload = ( + let view = ( prop::collection::vec(1_u8..=u8::MAX, 1..=MAX_HOSTNAME_BYTES), prop_oneof![Just(MachineIdMode::Image), Just(MachineIdMode::Generated)], - prop::option::of( - (1..u32::MAX, 1..u32::MAX).prop_map(|(uid, gid)| WorkloadIdentity { uid, gid }), - ), ) - .prop_map(|(hostname, machine_id, identity)| WorkloadView { + .prop_map(|(hostname, machine_id)| View { hostname, machine_id, - identity, }); let resources = (1..=u32::MAX, 1..=u64::MAX).prop_map(|(cpus, memory)| ExpectedResources { cpus, memory }); - (image, upper, overlay, export, network, workload, resources).prop_map( - |(image, upper, overlay, (export, mappings), network, workload, resources)| Configuration { + (image, upper, overlay, export, network, view, resources).prop_map( + |(image, upper, overlay, (export, mappings), network, view, resources)| Configuration { image, upper, overlay, export, mappings, network, - workload, + view, resources, }, ) @@ -905,17 +790,9 @@ fn configurations() -> impl Strategy { /// Returns arbitrary valid records, of cold boots and of clones. fn records() -> impl Strategy { - let restore = prop::option::of( - (any::<[u8; 16]>(), any::(), identities()).prop_map( - |(transaction, generation, key)| RestoreSection { - transaction: TransactionId::from_bytes(transaction), - template: TemplateReference { - generation: StoreGeneration::new(generation), - key, - }, - }, - ), - ); + let restore = prop::option::of(any::<[u8; 16]>().prop_map(|transaction| RestoreSection { + transaction: TransactionId::from_bytes(transaction), + })); (restore, configurations()) .prop_map(|(restore, configuration)| ActivationRecord::new(restore, configuration).unwrap()) } diff --git a/nvx-rs/src/activation/codec/tests/vectors.rs b/nvx-rs/src/activation/codec/tests/vectors.rs index b837f14c9..dbf5df9fd 100644 --- a/nvx-rs/src/activation/codec/tests/vectors.rs +++ b/nvx-rs/src/activation/codec/tests/vectors.rs @@ -2,14 +2,13 @@ //! 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, + ActivationError, ActivationRecord, Configuration, ExpectedResources, Export, ExportKind, Image, + Journal, MachineIdMode, Network, OverlayMode, Publication, RestoreSection, TransactionId, + Upper, View, }; -use crate::tests::{ACTIVATION_V1, Values, Vector, of_kind}; +use crate::tests::{ACTIVATION_V2, Values, Vector, of_kind}; use crate::uuid::Uuid; /// The names of the vectors of records, in order. @@ -29,11 +28,6 @@ fn array(values: &mut Values<'_>) -> [u8; N] { .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)) @@ -54,29 +48,22 @@ 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, + "TMPFS" => Upper::Tmpfs { + max_memory_percent: values.number(), + }, "SCRATCH" => Upper::Scratch { - binding: identity(&mut values), journal: choice( &mut values, &[ @@ -102,15 +89,13 @@ fn record_of(vector: &Vector) -> ActivationRecord { ("AGGREGATE", ExportKind::Aggregate), ], ); - let plan = identity(values); - (Export { kind, plan }, values.number()) + (Export { kind }, 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, &[ @@ -135,10 +120,6 @@ fn record_of(vector: &Vector) -> ActivationRecord { ("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(), @@ -151,10 +132,9 @@ fn record_of(vector: &Vector) -> ActivationRecord { export, mappings, network, - workload: WorkloadView { + view: View { hostname, machine_id, - identity: workload_identity, }, resources, }; @@ -164,7 +144,7 @@ fn record_of(vector: &Vector) -> ActivationRecord { #[test] fn records_match_their_vectors() { - for vector in of_kind(ACTIVATION_V1, "RECORD", &RECORDS) { + for vector in of_kind(ACTIVATION_V2, "RECORD", &RECORDS) { let name = vector.name; let record = record_of(&vector); assert_eq!(record.encode(), vector.bytes, "{name}"); @@ -183,7 +163,7 @@ fn invalid_records_are_refused() { "TRUNCATED_VERSION", "NO_FIELDS", "VERSION_0", - "VERSION_2", + "VERSION_1", "TOO_LONG", "TRUNCATED_FIELD_HEADER", "TRUNCATED_FIELD_VALUE", @@ -203,7 +183,12 @@ fn invalid_records_are_refused() { "MACHINE_ID_CODE_0", "DNS_OF_5_BYTES", "SHORT_RESOURCES", - "ZERO_GENERATION", + "RESTORE_WITH_TEMPLATE", + "IMAGE_WITH_BINDING", + "SCRATCH_WITH_BINDING", + "EXPORT_WITH_PLAN", + "NETWORK_WITH_POLICY", + "WORKLOAD_IDENTITY", "MISSING_IMAGE", "MISSING_RESOURCES", "EXPORT_WITHOUT_MAPPINGS", @@ -220,14 +205,12 @@ fn invalid_records_are_refused() { "HOSTNAME_WITH_NUL", "LONG_HOSTNAME", "ZERO_FIRST_PARTITION_BOUND", - "ZERO_UID", - "ZERO_GID", - "UNCHANGED_UID", - "UNCHANGED_GID", + "ZERO_TMPFS_CAP", + "LARGE_TMPFS_CAP", "ZERO_CPUS", "ZERO_MEMORY", ]; - for vector in of_kind(ACTIVATION_V1, "INVALID_RECORD", &names) { + for vector in of_kind(ACTIVATION_V2, "INVALID_RECORD", &names) { let refusal: Result<_, ActivationError> = ActivationRecord::decode(&vector.bytes); assert!(refusal.is_err(), "{} is admitted", vector.name); } diff --git a/nvx-rs/src/activation/mod.rs b/nvx-rs/src/activation/mod.rs index d66ca0523..80dc5fdf3 100644 --- a/nvx-rs/src/activation/mod.rs +++ b/nvx-rs/src/activation/mod.rs @@ -10,10 +10,8 @@ //! //! - [`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 -//! random bytes that the host picks for each restore and keeps across the restore's retries, -//! and the [`TemplateReference`] of the template that the clone restores: the template's -//! [`StoreGeneration`], the nonzero publication ID that its store assigned to it, and the -//! [`Identity`] of its key. A cold boot's record has no restore section. +//! random bytes that the host picks for each restore and keeps across the restore's retries. +//! 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: that it applied, which every @@ -31,22 +29,18 @@ //! //! A [`Configuration`] holds: //! -//! - the [`Image`]: its format, an EROFS filesystem or a GPT disk, the [`Identity`] of the -//! image's binding, and the EROFS filesystem's UUID or the GPT disk's GUID that the guest -//! expects, if any; -//! - the [`Upper`]: a tmpfs, or the scratch, with the [`Identity`] of the scratch's binding; -//! - the [`Export`], if any: one export, a directory or an aggregate, with the [`Identity`] of -//! its plan, which covers the export's whole shape, and the number of host mappings that the -//! host sends once the guest is ready, from 1 to [`MAX_MAPPINGS`]. A configuration without an -//! export has no mappings; -//! - the [`Network`] of a VM that has one: the [`Identity`] of its policy, the address of its DNS -//! server, if any, the [`Publication`] of the workload's listeners, and the endpoint of its -//! proxy, if any, which holds no credentials; -//! - the [`WorkloadView`]: the hostname, the [`MachineIdMode`], the image's own machine ID or one -//! that the guest generates, and the [`WorkloadIdentity`], if any: a UID and a GID, neither 0 -//! nor `u32::MAX`, which the guest ignores as it applies the record, since each execution of a -//! clone names its own identity, as CRI containers do. A VM's configuration names its hostname -//! and machine ID in a [`View`], which leaves the identity to each execution; +//! - the [`Image`]: its format, an EROFS filesystem or a GPT disk, and the EROFS filesystem's +//! UUID or the GPT disk's GUID that the guest expects, if any; +//! - the [`Upper`]: a tmpfs with its cap in percent of the VM's memory, or the scratch with its +//! journal policy; +//! - the [`Export`], if any: one export, a directory or an aggregate, and the number of host +//! mappings that the host sends once the guest is ready, from 1 to [`MAX_MAPPINGS`]. A +//! configuration without an export has no mappings; +//! - the [`Network`] of a VM that has one: the address of its DNS server, if any, the +//! [`Publication`] of the workload's listeners, and the endpoint of its proxy, if any, which +//! holds no credentials; +//! - the [`View`]: the hostname and the [`MachineIdMode`], the image's own machine ID or one that +//! the guest generates. Each execution names its own identity; //! - the root's settings: the [`OverlayMode`], the [`Journal`] policy of a scratch upper, and the //! bound of a GPT disk's first partition; //! - the [`ExpectedResources`]: the CPU count and the memory size that the guest must have, as a @@ -69,10 +63,9 @@ //! record's encoding within [`MAX_RECORD_BYTES`]. //! //! [`Configuration::check`] also refuses an export without mappings, mappings without an export, -//! a workload identity whose UID or GID is 0, or `u32::MAX`, which the kernel reads as an -//! unchanged ID, a hostname with a NUL, a proxy endpoint with anything other than visible ASCII -//! characters or with an `@`, which would start credentials, and a zero CPU count, memory size, -//! or bound of a GPT disk's first partition. +//! a tmpfs cap outside 1 to 90 percent, a hostname with a NUL, a proxy endpoint with anything +//! other than visible ASCII characters or with an `@`, which would start credentials, and a +//! zero CPU count, memory size, or bound of a GPT disk's first partition. //! //! # Encoding //! @@ -82,30 +75,29 @@ //! //! | Tag | [`Field`] | Value | Bytes | Present | //! | --- | --- | --- | --- | --- | -//! | `0x01` | [`Restore`](Field::Restore) | The transaction ID, the store generation, a `u64`, and the identity of the template's key | 56 | In a clone's record | -//! | `0x10` | [`Image`](Field::Image) | The format, `1` for EROFS or `2` for GPT, the identity of the image's binding, and a GPT disk's first-partition bound, a `u64` | 33 for EROFS, 41 for GPT | Always | +//! | `0x01` | [`Restore`](Field::Restore) | The transaction ID | 16 | In a clone's record | +//! | `0x10` | [`Image`](Field::Image) | The format, `1` for EROFS or `2` for GPT, and a GPT disk's first-partition bound, a `u64` | 1 for EROFS, 9 for GPT | Always | //! | `0x11` | [`ImageIdentity`](Field::ImageIdentity) | The UUID or the disk GUID that the guest expects, in the order of its canonical text | 16 | If the guest expects one | -//! | `0x20` | [`Upper`](Field::Upper) | `1` for a tmpfs, or `2` for the scratch, then the identity of the scratch's binding and its journal policy, `1` allowed or `2` forbidden | 1 for a tmpfs, 34 for the scratch | Always | +//! | `0x20` | [`Upper`](Field::Upper) | `1` for a tmpfs and its cap, 1 to 90 percent, or `2` for the scratch and its journal policy, `1` allowed or `2` forbidden | 2 | Always | //! | `0x30` | [`Overlay`](Field::Overlay) | `1` for trusted lowers, or `2` for untrusted lowers | 1 | Always | -//! | `0x40` | [`Export`](Field::Export) | The shape, `1` for a directory or `2` for an aggregate, and the identity of the export's plan | 33 | With an export | +//! | `0x40` | [`Export`](Field::Export) | The shape, `1` for a directory or `2` for an aggregate | 1 | With an export | //! | `0x41` | [`Mappings`](Field::Mappings) | The number of mappings, a `u16` from 1 to [`MAX_MAPPINGS`] | 2 | With an export | -//! | `0x50` | [`Network`](Field::Network) | The identity of the policy, and `1` if listeners are published, or `2` if not | 33 | With a network | +//! | `0x50` | [`Network`](Field::Network) | `1` if listeners are published, or `2` if not | 1 | With a network | //! | `0x51` | [`Dns`](Field::Dns) | The DNS server's IPv4 or IPv6 address | 4 or 16 | With a network that has one | //! | `0x52` | [`Proxy`](Field::Proxy) | The proxy endpoint | 1 to [`MAX_PROXY_ENDPOINT_BYTES`] | With a network that has one | //! | `0x60` | [`Hostname`](Field::Hostname) | The hostname | 1 to [`MAX_HOSTNAME_BYTES`] | Always | //! | `0x61` | [`MachineId`](Field::MachineId) | `1` for the image's machine ID, or `2` for a generated one | 1 | Always | -//! | `0x62` | [`WorkloadIdentity`](Field::WorkloadIdentity) | The UID and the GID, each a `u32` | 8 | With a workload identity | //! | `0x70` | [`Resources`](Field::Resources) | The CPU count, a `u32`, and the memory size, a `u64` | 12 | Always | //! //! Integers are little-endian, and identities, IDs, and addresses are their bytes, in order. The -//! largest record, with every field at its largest, holds 602 bytes. +//! largest record, with every field at its largest, holds 424 bytes. //! //! [`ActivationRecord::decode`] refuses bytes that are longer than [`MAX_RECORD_BYTES`], end //! inside the version or a field, or hold another version; a field with an unknown tag, a //! repeated field, and a field out of order; a value with another number of bytes than its //! contents take, or with a code that the version does not define; a mapping count of 0, since -//! a record without mappings leaves the field out, and a store generation of 0, which no store -//! assigns; a record without a field that is always present, or with a DNS server or a proxy but +//! a record without mappings leaves the field out; a record without a field that is always +//! present, or with a DNS server or a proxy but //! no network; and every configuration that [`Configuration::check`] refuses, such as one with //! an export without mappings, or with mappings without an export. A record therefore has //! exactly one encoding, which [`ActivationRecord::encode`] writes, and which the decoder reads @@ -123,48 +115,39 @@ //! [`ReadyActivation::CLONE_LEN`] bytes. A Ready that embeds these bytes therefore has the same //! encoding for a clone as for a cold boot. //! -//! The conformance vectors of `tests/vectors/activation-v1.txt` hold records, and what Ready +//! The conformance vectors of `tests/vectors/activation-v2.txt` hold records, and what Ready //! reports about their activations, in both encodings, and bytes that every decoder refuses. //! //! # Example //! //! ``` -//! use std::num::NonZeroU64; -//! //! use nvx::activation::{ -//! ActivationRecord, Configuration, ExpectedResources, Identity, Image, MachineIdMode, -//! OverlayMode, RestoreSection, StoreGeneration, TemplateReference, TransactionId, Upper, -//! WorkloadIdentity, WorkloadView, +//! ActivationRecord, Configuration, ExpectedResources, Image, MachineIdMode, +//! OverlayMode, RestoreSection, TransactionId, Upper, View, //! }; //! //! # fn main() -> Result<(), Box> { //! let configuration = Configuration { //! image: Image::Erofs { -//! binding: Identity::from_bytes([0x11; 32]), //! uuid: Some("0123abcd-4567-89ef-0123-456789abcdef".parse()?), //! }, -//! upper: Upper::Tmpfs, +//! upper: Upper::Tmpfs { max_memory_percent: 50 }, //! overlay: OverlayMode::UntrustedLowers, //! export: None, //! mappings: 0, //! network: None, -//! workload: WorkloadView { +//! view: View { //! hostname: b"sandbox".to_vec(), //! machine_id: MachineIdMode::Generated, -//! identity: Some(WorkloadIdentity { uid: 1000, gid: 1000 }), //! }, //! resources: ExpectedResources { cpus: 2, memory: 1 << 30 }, //! }; //! // A cold boot's record has no restore section. //! let cold = ActivationRecord::new(None, configuration.clone())?; //! assert!(cold.restore().is_none()); -//! // A clone's names its restore's transaction and the template that it restores. +//! // A clone's names only its restore's transaction. //! let restore = RestoreSection { //! transaction: TransactionId::from_bytes([0x22; 16]), -//! template: TemplateReference { -//! generation: StoreGeneration::new(NonZeroU64::MIN), -//! key: Identity::from_bytes([0x44; 32]), -//! }, //! }; //! let clone = ActivationRecord::new(Some(restore), configuration)?; //! assert_eq!(clone.restore(), Some(&restore)); @@ -192,7 +175,7 @@ pub use self::codec::Field; /// The version of the activation contract that this build speaks: the schema of an /// [`ActivationRecord`], which every record names. A record of another version is refused. -pub const VERSION: u16 = 1; +pub const VERSION: u16 = 2; /// The most bytes of a record's encoding, so that a restore packet could carry the same bytes. pub const MAX_RECORD_BYTES: usize = 1024; @@ -207,10 +190,8 @@ pub const MAX_HOSTNAME_BYTES: usize = 64; /// The longest proxy endpoint, in bytes. pub const MAX_PROXY_ENDPOINT_BYTES: usize = 255; -/// The identity of an object that a record names: 32 bytes that the host computes, such as a -/// SHA-256 digest of the object's canonical form, by which a record names the binding of its -/// image and of its scratch, the plan of its export, the policy of its network, and the key of a -/// clone's template. A record never holds or reads the objects themselves. +/// The identity of a stored object's canonical form: 32 bytes that the host computes, such as a +/// SHA-256 digest of a template's key. Activation records do not encode store identities. /// /// It displays as 64 lowercase hexadecimal digits. #[derive(Clone, Copy, PartialEq, Eq, Hash)] @@ -314,14 +295,11 @@ pub struct TemplateReference { pub key: Identity, } -/// The restore section of a clone's record: the restore's transaction ID, and the template that -/// the clone restores. +/// The restore section of a clone's record: only the restore's transaction ID. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct RestoreSection { /// The restore's transaction ID, which the clone's Ready reports. pub transaction: TransactionId, - /// The template that the clone restores. - pub template: TemplateReference, } /// The image of a layered root, on its `distro` block device, in the format that the record @@ -330,16 +308,12 @@ pub struct RestoreSection { pub enum Image { /// An EROFS filesystem, the overlay's one lower layer. Erofs { - /// The identity of the image's binding. - binding: Identity, /// The UUID that the filesystem's superblock must hold, if the guest expects one. uuid: Option, }, /// A GPT disk, whose first partition holds opaque bytes, and whose later partitions hold the /// overlay's lower layers. Gpt { - /// The identity of the image's binding. - binding: Identity, /// The GUID that the disk must have, if the guest expects one. disk_guid: Option, /// The most bytes that the disk's first partition may hold: at least 1, since a @@ -357,13 +331,6 @@ impl Image { } } - /// Returns the identity of the image's binding. - pub fn binding(&self) -> &Identity { - match self { - Self::Erofs { binding, .. } | Self::Gpt { binding, .. } => binding, - } - } - /// Returns what the guest expects of the image, if anything: the EROFS filesystem's UUID, or /// the GPT disk's GUID. pub fn expected(&self) -> Option { @@ -378,11 +345,12 @@ impl Image { #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Upper { /// A tmpfs, for a root that keeps no upper on a scratch. - Tmpfs, + Tmpfs { + /// The most that the upper may hold, in percent of the VM's memory: 1 to 90. + max_memory_percent: u8, + }, /// A whole-device ext4 filesystem on the `scratch` block device. Scratch { - /// The identity of the scratch's binding. - binding: Identity, /// Whether the scratch's filesystem may have a journal. journal: Journal, }, @@ -421,9 +389,6 @@ pub enum ExportKind { pub struct Export { /// The export's shape. pub kind: ExportKind, - /// The identity of the export's plan, which covers the export's whole shape, so that an - /// export of any shape has a record of the same form. - pub plan: Identity, } /// Whether the workload's listeners are published, through private-network ingress or the @@ -439,8 +404,6 @@ pub enum Publication { /// The network of a VM that has one. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Network { - /// The identity of the network's policy. - pub policy: Identity, /// The address of the DNS server that the guest's resolver configuration names, if any. pub dns: Option, /// Whether the workload's listeners are published. @@ -460,28 +423,6 @@ pub enum MachineIdMode { Generated, } -/// A workload identity that an activation record may carry: a UID and a GID, neither 0 nor -/// `u32::MAX`. The guest ignores it as it applies the record: each execution names its own. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct WorkloadIdentity { - /// The user ID: neither 0 nor `u32::MAX`. - pub uid: u32, - /// The group ID: neither 0 nor `u32::MAX`. - pub gid: u32, -} - -/// The view that the workload has of its VM. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct WorkloadView { - /// The hostname: 1 to [`MAX_HOSTNAME_BYTES`] bytes, without a NUL. - pub hostname: Vec, - /// The machine ID that the workload sees. - pub machine_id: MachineIdMode, - /// A workload identity, which the guest ignores as it applies the record: each execution - /// names its own. - pub identity: Option, -} - /// The view that a VM's workload has of it: its hostname and its machine ID, without an identity /// of the executions, which each execution names. #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -522,7 +463,7 @@ pub struct Configuration { /// The network, for a VM that has one. pub network: Option, /// The workload's view. - pub workload: WorkloadView, + pub view: View, /// The resources that the guest must have. pub resources: ExpectedResources, } @@ -531,12 +472,12 @@ impl Configuration { /// Checks that the configuration keeps every limit of the [module](self#limits), and returns /// the first that it breaks, in the order of the configuration's fields. /// - /// It refuses a GPT disk's first-partition bound of 0; an export without mappings, mappings + /// It refuses a GPT disk's first-partition bound of 0; a tmpfs cap outside 1 to 90 percent; + /// an export without mappings, mappings /// without an export, and more than [`MAX_MAPPINGS`] mappings; a proxy endpoint that is empty /// or longer than [`MAX_PROXY_ENDPOINT_BYTES`], or holds a byte other than a visible ASCII /// character, or an `@`; a hostname that is empty or longer than [`MAX_HOSTNAME_BYTES`], or - /// holds a NUL; a workload identity whose UID or GID is 0 or `u32::MAX`; and a CPU count or - /// memory size of 0. + /// holds a NUL; and a CPU count or memory size of 0. pub fn check(&self) -> Result<(), ActivationError> { if let Image::Gpt { first_partition_bound: 0, @@ -545,6 +486,11 @@ impl Configuration { { return Err(ActivationError::FirstPartitionBound); } + if let Upper::Tmpfs { max_memory_percent } = self.upper + && !(1..=90).contains(&max_memory_percent) + { + return Err(ActivationError::TmpfsMemoryPercent(max_memory_percent)); + } match (self.export.is_some(), self.mappings) { (true, 0) => return Err(ActivationError::ExportWithoutMappings), (true, mappings) if mappings > MAX_MAPPINGS => { @@ -561,15 +507,9 @@ impl Configuration { { return Err(ActivationError::ProxyEndpoint); } - if !is_hostname(&self.workload.hostname) { + if !is_hostname(&self.view.hostname) { return Err(ActivationError::Hostname); } - // The kernel reads `u32::MAX` as an unchanged ID, which the guest's entry refuses. - if let Some(WorkloadIdentity { uid, gid }) = self.workload.identity - && [uid, gid].into_iter().any(|id| id == 0 || id == u32::MAX) - { - return Err(ActivationError::WorkloadIdentity); - } if self.resources.cpus == 0 { return Err(ActivationError::Cpus); } @@ -639,6 +579,8 @@ pub struct ReadyActivation { pub enum ActivationError { /// A GPT disk's first-partition bound is 0. FirstPartitionBound, + /// The tmpfs cap is outside 1 to 90 percent: this value. + TmpfsMemoryPercent(u8), /// The configuration has an export, but no mappings. ExportWithoutMappings, /// The configuration has mappings, but no export. @@ -650,9 +592,6 @@ pub enum ActivationError { ProxyEndpoint, /// The hostname is empty, longer than [`MAX_HOSTNAME_BYTES`], or holds a NUL. Hostname, - /// The workload identity's UID or GID is 0, or `u32::MAX`, which the kernel reads as an - /// unchanged ID. - WorkloadIdentity, /// The expected CPU count is 0. Cpus, /// The expected memory size is 0. @@ -689,6 +628,10 @@ impl fmt::Display for ActivationError { Self::FirstPartitionBound => { formatter.write_str("the bound of a GPT disk's first partition is 0") } + Self::TmpfsMemoryPercent(percent) => write!( + formatter, + "the tmpfs cap of {percent}% of the VM's memory is not 1 to 90%" + ), Self::ExportWithoutMappings => formatter.write_str("the export has no mappings"), Self::MappingsWithoutExport => formatter.write_str("the mappings have no export"), Self::TooManyMappings(mappings) => write!( @@ -704,9 +647,6 @@ impl fmt::Display for ActivationError { formatter, "the hostname is not 1 to {MAX_HOSTNAME_BYTES} bytes without a NUL" ), - Self::WorkloadIdentity => { - formatter.write_str("the workload identity's UID or GID is 0 or 4294967295") - } Self::Cpus => formatter.write_str("the expected CPU count is 0"), Self::Memory => formatter.write_str("the expected memory size is 0"), Self::TooLong(length) => write!( diff --git a/nvx-rs/src/activation/ready/tests.rs b/nvx-rs/src/activation/ready/tests.rs index 728673f35..6ad974951 100644 --- a/nvx-rs/src/activation/ready/tests.rs +++ b/nvx-rs/src/activation/ready/tests.rs @@ -4,7 +4,7 @@ use proptest::prelude::*; use super::*; -use crate::tests::{ACTIVATION_V1, of_kind, vector}; +use crate::tests::{ACTIVATION_V2, of_kind, vector}; /// The names of the vectors of what Ready reports, in order: those of the records whose /// activations they report. @@ -72,7 +72,7 @@ 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) + let record = ActivationRecord::decode(&vector(ACTIVATION_V2, "RECORD", name).bytes) .unwrap_or_else(|error| panic!("RECORD {name}: {error}")); let ready = ReadyActivation::of(&record); assert_eq!( @@ -82,7 +82,7 @@ fn ready_reports_a_clone_s_transaction_and_nothing_more_for_a_cold_boot() { ); assert_eq!( ready.encode(), - vector(ACTIVATION_V1, "READY", name).bytes, + vector(ACTIVATION_V2, "READY", name).bytes, "{name}" ); } @@ -90,7 +90,7 @@ fn ready_reports_a_clone_s_transaction_and_nothing_more_for_a_cold_boot() { #[test] fn ready_activations_match_their_vectors() { - for vector in of_kind(ACTIVATION_V1, "READY", &READY) { + for vector in of_kind(ACTIVATION_V2, "READY", &READY) { let mut values = vector.values(); let transaction = values.optional(|values| TransactionId::from_bytes(values.bytes().try_into().unwrap())); @@ -117,7 +117,7 @@ fn invalid_ready_activations_are_refused() { "SHORT_TRANSACTION", "LONG_TRANSACTION", ]; - for vector in of_kind(ACTIVATION_V1, "INVALID_READY", &names) { + for vector in of_kind(ACTIVATION_V2, "INVALID_READY", &names) { assert_eq!( ReadyActivation::decode(&vector.bytes), Err(ActivationError::Ready), diff --git a/nvx-rs/src/activation/tests.rs b/nvx-rs/src/activation/tests.rs index 353c649c6..ec8afdef8 100644 --- a/nvx-rs/src/activation/tests.rs +++ b/nvx-rs/src/activation/tests.rs @@ -9,22 +9,20 @@ use super::*; const DISK_GUID: &str = "fedcba98-7654-3210-fedc-ba9876543210"; /// Returns the configuration of the tests: an EROFS image over a tmpfs, without an export, a -/// network, or a workload identity. +/// or network. pub(super) fn configuration() -> Configuration { Configuration { - image: Image::Erofs { - binding: Identity::from_bytes([0x11; 32]), - uuid: None, + image: Image::Erofs { uuid: None }, + upper: Upper::Tmpfs { + max_memory_percent: 50, }, - upper: Upper::Tmpfs, overlay: OverlayMode::TrustedLowers, export: None, mappings: 0, network: None, - workload: WorkloadView { + view: View { hostname: b"nvx-sandbox".to_vec(), machine_id: MachineIdMode::Image, - identity: None, }, resources: ExpectedResources { cpus: 1, @@ -36,37 +34,29 @@ pub(super) fn configuration() -> Configuration { /// Returns a configuration whose every part is at its limit: a GPT image over a scratch, an /// aggregate export of [`MAX_MAPPINGS`] mappings, a network with an IPv6 DNS server and a proxy /// endpoint of [`MAX_PROXY_ENDPOINT_BYTES`], a hostname of [`MAX_HOSTNAME_BYTES`], and the largest -/// IDs and resources. +/// resources. pub(super) fn maximal() -> Configuration { Configuration { image: Image::Gpt { - binding: Identity::from_bytes([0xff; 32]), disk_guid: Some(DISK_GUID.parse().unwrap()), first_partition_bound: u64::MAX, }, upper: Upper::Scratch { - binding: Identity::from_bytes([0xee; 32]), journal: Journal::Forbidden, }, overlay: OverlayMode::UntrustedLowers, export: Some(Export { kind: ExportKind::Aggregate, - plan: Identity::from_bytes([0xdd; 32]), }), mappings: MAX_MAPPINGS, network: Some(Network { - policy: Identity::from_bytes([0xcc; 32]), dns: Some(IpAddr::V6(Ipv6Addr::new(0xfd00, 0, 0, 0, 0, 0, 0xa00, 1))), publication: Publication::Published, proxy: Some("~".repeat(MAX_PROXY_ENDPOINT_BYTES)), }), - workload: WorkloadView { + view: View { hostname: vec![0xff; MAX_HOSTNAME_BYTES], machine_id: MachineIdMode::Generated, - identity: Some(WorkloadIdentity { - uid: u32::MAX - 1, - gid: u32::MAX - 1, - }), }, resources: ExpectedResources { cpus: u32::MAX, @@ -79,10 +69,6 @@ pub(super) fn maximal() -> Configuration { pub(super) fn restore() -> RestoreSection { RestoreSection { transaction: TransactionId::from_bytes([0x22; 16]), - template: TemplateReference { - generation: StoreGeneration::new(NonZeroU64::new(0x3333_3333_3333_3333).unwrap()), - key: Identity::from_bytes([0x44; 32]), - }, } } @@ -97,7 +83,6 @@ fn admit(change: impl FnOnce(&mut Configuration)) -> Result( ) -> Result, ApplyError> { let configuration = record.configuration(); let assembly = assembly(template, configuration); - let workload = &configuration.workload; + let view = &configuration.view; if let root::Image::Gpt { .. } = assembly.image { system .reread(assembly.distro, assembly.timeout, deadline) @@ -784,7 +784,7 @@ fn apply_with( let mut root = system .assemble(&assembly, deadline) .map_err(ApplyError::Assembly)?; - let machine_id = match workload.machine_id { + let machine_id = match view.machine_id { MachineIdMode::Image => MachineId::Image, MachineIdMode::Generated => { match system.generate(&template.runtime.join(MACHINE_ID_FILE)) { @@ -840,7 +840,7 @@ fn apply_with( None }; let view = View { - hostname: workload.hostname.clone(), + hostname: view.hostname.clone(), machine_id, resolver, }; @@ -956,9 +956,8 @@ fn assembly(template: &RootTemplate, configuration: &Configuration) -> Assembly }, }; let upper = match configuration.upper { - // The record names no cap, so the tmpfs takes the kernel's default size. - activation::Upper::Tmpfs => root::Upper::Tmpfs { - max_memory_percent: None, + activation::Upper::Tmpfs { max_memory_percent } => root::Upper::Tmpfs { + max_memory_percent: Some(max_memory_percent), }, activation::Upper::Scratch { journal, .. } => root::Upper::Scratch { device: template.scratch, diff --git a/nvx-rs/src/guest/clone/activation/apply/tests.rs b/nvx-rs/src/guest/clone/activation/apply/tests.rs index a66827faf..53fed6d64 100644 --- a/nvx-rs/src/guest/clone/activation/apply/tests.rs +++ b/nvx-rs/src/guest/clone/activation/apply/tests.rs @@ -20,7 +20,6 @@ use std::error::Error as _; use std::fs; use std::io; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; -use std::num::NonZeroU64; use std::os::unix::fs::{PermissionsExt, symlink}; use std::rc::Rc; use std::time::Instant; @@ -28,8 +27,7 @@ use std::time::Instant; use super::*; use crate::activation::{ ExpectedResources, Export, ExportKind, MachineIdMode, Network, Publication, ReadyActivation, - RestoreSection, StoreGeneration, TemplateReference, TransactionId, WorkloadIdentity, - WorkloadView, + RestoreSection, TransactionId, View as RecordView, }; use crate::guest::clone::{ Activation, ActivationAction, ActivationCause, ActivationFailure, ActivationInput, @@ -60,11 +58,6 @@ const IMAGE_UUID: Uuid = Uuid::from_bytes([0x5a; 16]); const DISK_GUID: Uuid = Uuid::from_bytes([0xd1; 16]); /// The most bytes that the first partition of the tests' GPT disks may hold. const FIRST_PARTITION_BOUND: u64 = 4096; -/// The tests' workload identity, whose account the tests' image holds. -const WORKLOAD: WorkloadIdentity = WorkloadIdentity { - uid: 1000, - gid: 1001, -}; /// The passwd database of the tests' image, which holds the workload identity's account. const PASSWD: &[u8] = b"root:x:0:0:root:/root:/bin/sh\n\ workload:x:1000:1001::/home/workload:/bin/sh\n"; @@ -534,17 +527,13 @@ fn template() -> RootTemplate { /// Returns an EROFS image, with the UUID `uuid` that the record expects of it, if any. fn erofs(uuid: Option) -> activation::Image { - activation::Image::Erofs { - binding: activation::Identity::from_bytes([0x11; activation::Identity::LEN]), - uuid, - } + activation::Image::Erofs { uuid } } /// Returns a GPT disk, with the GUID `disk_guid` that the record expects of it, if any, whose /// first partition holds at most [`FIRST_PARTITION_BOUND`] bytes. fn gpt(disk_guid: Option) -> activation::Image { activation::Image::Gpt { - binding: activation::Identity::from_bytes([0x11; activation::Identity::LEN]), disk_guid, first_partition_bound: FIRST_PARTITION_BOUND, } @@ -564,20 +553,16 @@ fn gpt_image(disk_guid: Option) -> root::Image { /// Returns the scratch upper, with the journal policy `journal`. fn scratch(journal: activation::Journal) -> activation::Upper { - activation::Upper::Scratch { - binding: activation::Identity::from_bytes([0x33; activation::Identity::LEN]), - journal, - } + activation::Upper::Scratch { journal } } /// Returns the configuration of a root of `image` over `upper`, in `overlay` mode, which names -/// the workload identity `identity`, if any, and expects the tests' clone's resources, without an +/// the view and expects the tests' clone's resources, without an /// export or a network. fn configuration( image: activation::Image, upper: activation::Upper, overlay: activation::OverlayMode, - identity: Option, ) -> Configuration { Configuration { image, @@ -586,10 +571,9 @@ fn configuration( export: None, mappings: 0, network: None, - workload: WorkloadView { + view: RecordView { hostname: HOSTNAME.to_vec(), machine_id: MachineIdMode::Image, - identity, }, resources: ExpectedResources { cpus: CPUS, @@ -602,22 +586,19 @@ fn configuration( fn clone_record(configuration: Configuration) -> ActivationRecord { let restore = RestoreSection { transaction: TRANSACTION, - template: TemplateReference { - generation: StoreGeneration::new(NonZeroU64::MIN), - key: activation::Identity::from_bytes([0x44; activation::Identity::LEN]), - }, }; ActivationRecord::new(Some(restore), configuration).unwrap() } /// Returns the record of the tests' Activate: an EROFS image, whose UUID it expects, over a -/// tmpfs, with untrusted lowers, which names the tests' workload identity. +/// tmpfs capped at 50 percent, with untrusted lowers. fn accepted() -> ActivationRecord { clone_record(configuration( erofs(Some(IMAGE_UUID)), - activation::Upper::Tmpfs, + activation::Upper::Tmpfs { + max_memory_percent: 50, + }, activation::OverlayMode::UntrustedLowers, - Some(WORKLOAD), )) } @@ -633,7 +614,7 @@ fn accepted_assembly() -> Assembly { uuid: Some(IMAGE_UUID), }, upper: root::Upper::Tmpfs { - max_memory_percent: None, + max_memory_percent: Some(50), }, overlay: root::OverlayMode::UntrustedLowers, timeout: template.timeout, @@ -644,23 +625,19 @@ fn accepted_assembly() -> Assembly { /// a machine ID that the guest generates. fn with_generated_id() -> ActivationRecord { let mut configuration = accepted().configuration().clone(); - configuration.workload.machine_id = MachineIdMode::Generated; + configuration.view.machine_id = MachineIdMode::Generated; clone_record(configuration) } -/// Returns an export of `kind`, of the tests' plan. +/// Returns an export of `kind`. fn export(kind: ExportKind) -> Export { - Export { - kind, - plan: activation::Identity::from_bytes([0x77; activation::Identity::LEN]), - } + Export { kind } } /// Returns a network whose resolver names `dns`, if any, and whose proxy is `proxy`, if any, /// with published listeners. fn network(dns: Option, proxy: Option<&str>) -> Network { Network { - policy: activation::Identity::from_bytes([0x88; activation::Identity::LEN]), dns, publication: Publication::Published, proxy: proxy.map(str::to_owned), @@ -931,12 +908,22 @@ fn assembly_inputs_are_taken_exactly_from_the_record() { (gpt(None), gpt_image(None), true), (gpt(Some(DISK_GUID)), gpt_image(Some(DISK_GUID)), true), ]; - // The record names no cap of a tmpfs, which then takes the kernel's default size. + // The root receives the record's cap, including both ends of its range. let uppers = [ ( - activation::Upper::Tmpfs, + activation::Upper::Tmpfs { + max_memory_percent: 1, + }, + root::Upper::Tmpfs { + max_memory_percent: Some(1), + }, + ), + ( + activation::Upper::Tmpfs { + max_memory_percent: 90, + }, root::Upper::Tmpfs { - max_memory_percent: None, + max_memory_percent: Some(90), }, ), ( @@ -967,50 +954,40 @@ fn assembly_inputs_are_taken_exactly_from_the_record() { for (image, root_image, reread) in images { for (upper, root_upper) in uppers { for (overlay, root_overlay) in overlays { - // The record's workload identity, if any, is no input: each execution names - // its own. - for identity in [None, Some(WORKLOAD)] { - let record = clone_record(configuration(image, upper, overlay, identity)); - let mut system = FakeSystem::new(); - let applied = apply_with(&mut system, &record, &template, None).unwrap(); - let assembly = Assembly { - runtime: template.runtime.clone(), - distro: template.distro, - image: root_image.clone(), - upper: root_upper, - overlay: root_overlay, - timeout: template.timeout, - }; - let mut effects = Vec::new(); - if reread { - effects.push(Effect::Reread(template.distro)); - } - effects.push(Effect::Assemble(assembly)); - effects.push(Effect::Bind(view(MachineId::Image))); - assert_eq!(system.effects, effects, "{record:?}"); - assert_eq!(applied, activated(&system, MachineId::Image)); + let record = clone_record(configuration(image, upper, overlay)); + let mut system = FakeSystem::new(); + let applied = apply_with(&mut system, &record, &template, None).unwrap(); + let assembly = Assembly { + runtime: template.runtime.clone(), + distro: template.distro, + image: root_image.clone(), + upper: root_upper, + overlay: root_overlay, + timeout: template.timeout, + }; + let mut effects = Vec::new(); + if reread { + effects.push(Effect::Reread(template.distro)); } + effects.push(Effect::Assemble(assembly)); + effects.push(Effect::Bind(view(MachineId::Image))); + assert_eq!(system.effects, effects, "{record:?}"); + assert_eq!(applied, activated(&system, MachineId::Image)); } } } // The record's other settings, which the application leaves alone, change no input: its - // bindings, its export's shape and plan, the number of its mappings, which the adapter's - // order takes, its network's policy, its resources, and its restore section. + // export's shape, the number of its mappings, which the adapter's order takes, its resources, + // and its restore section. let mut other = with_everything().configuration().clone(); other.image = activation::Image::Erofs { - binding: activation::Identity::from_bytes([0x66; activation::Identity::LEN]), uuid: Some(IMAGE_UUID), }; other.export = Some(Export { kind: ExportKind::Directory, - plan: activation::Identity::from_bytes([0x99; activation::Identity::LEN]), }); other.mappings = 7; - other.network = Some(Network { - policy: activation::Identity::from_bytes([0xaa; activation::Identity::LEN]), - ..network(Some(DNS), Some(PROXY)) - }); other.resources = ExpectedResources { cpus: 8, memory: 1 << 33, @@ -1040,7 +1017,6 @@ fn a_gpt_disk_s_partition_table_is_reread_before_its_root_is_assembled() { gpt(Some(DISK_GUID)), scratch(activation::Journal::Forbidden), activation::OverlayMode::UntrustedLowers, - Some(WORKLOAD), )); let mut adapter = Adapter::restricted(FakeSystem::new(), base); adapter @@ -1075,13 +1051,9 @@ fn a_gpt_disk_s_partition_table_is_reread_before_its_root_is_assembled() { } #[test] -fn the_application_checks_no_account_of_the_record_s_workload_identity() { - // The record's workload identity is no identity in which the executions run: each execution - // names its own, whose account the adapter's entry reads and checks before the execution - // runs, as in a sandbox without a workload identity at a cold boot. The application reads no - // account database, so a record that names a workload identity activates whether the image - // holds its account or not: one of its user ID whose primary group is its group ID, which the - // group database holds, and whose home is absolute. +fn the_application_leaves_the_images_accounts_to_each_execution() { + // Each execution names its own identity, whose entry checks its account. Activation reads + // no account database, including one that is absent or malformed. let base = Instant::now(); let record = accepted().encode(); let without_passwd = FakeSystem::new(); @@ -1825,9 +1797,10 @@ fn the_application_refuses_without_privileges_what_it_cannot_assemble() { // template's distro device, the reread fails at its one poll, with nothing mounted. let disk = clone_record(configuration( gpt(None), - activation::Upper::Tmpfs, + activation::Upper::Tmpfs { + max_memory_percent: 50, + }, activation::OverlayMode::TrustedLowers, - Some(WORKLOAD), )); let at_once = RootTemplate { timeout: Duration::ZERO, diff --git a/nvx-rs/src/guest/clone/activation/tests.rs b/nvx-rs/src/guest/clone/activation/tests.rs index 7554724aa..aad5dfe1a 100644 --- a/nvx-rs/src/guest/clone/activation/tests.rs +++ b/nvx-rs/src/guest/clone/activation/tests.rs @@ -5,13 +5,11 @@ use std::cell::Cell; use std::io; -use std::num::NonZeroU64; use std::rc::Rc; use super::*; use crate::activation::{ - Configuration, Identity, Image, MAX_RECORD_BYTES, MachineIdMode, OverlayMode, RestoreSection, - StoreGeneration, TemplateReference, Upper, WorkloadView, + Configuration, Image, MAX_RECORD_BYTES, MachineIdMode, OverlayMode, RestoreSection, Upper, View, }; /// The number of online CPUs of the clone. @@ -78,19 +76,17 @@ fn resources() -> ExpectedResources { /// `resources`. fn configuration(hostname: &[u8], resources: ExpectedResources) -> Configuration { Configuration { - image: Image::Erofs { - binding: Identity::from_bytes([0x11; Identity::LEN]), - uuid: None, + image: Image::Erofs { uuid: None }, + upper: Upper::Tmpfs { + max_memory_percent: 50, }, - upper: Upper::Tmpfs, overlay: OverlayMode::UntrustedLowers, export: None, mappings: 0, network: None, - workload: WorkloadView { + view: View { hostname: hostname.to_vec(), machine_id: MachineIdMode::Image, - identity: None, }, resources, } @@ -103,13 +99,7 @@ fn clone_record( hostname: &[u8], resources: ExpectedResources, ) -> ActivationRecord { - let restore = RestoreSection { - transaction, - template: TemplateReference { - generation: StoreGeneration::new(NonZeroU64::MIN), - key: Identity::from_bytes([0x44; Identity::LEN]), - }, - }; + let restore = RestoreSection { transaction }; ActivationRecord::new(Some(restore), configuration(hostname, resources)).unwrap() } @@ -469,7 +459,7 @@ fn an_activate_is_decoded_and_validated_before_any_side_effect() { let mut long = record(); long.resize(MAX_RECORD_BYTES + 1, 0); let mut version = record(); - version[0] = 2; + version[0] = 1; let cold = ActivationRecord::new(None, configuration(b"clone", resources())) .unwrap() .encode(); @@ -497,7 +487,7 @@ fn an_activate_is_decoded_and_validated_before_any_side_effect() { ) }), (version, |cause| { - matches!(cause, ActivationCause::Record(ActivationError::Version(2))) + matches!(cause, ActivationCause::Record(ActivationError::Version(1))) }), (cold, |cause| matches!(cause, ActivationCause::Cold)), ( diff --git a/nvx-rs/src/host/store/lease.rs b/nvx-rs/src/host/store/lease.rs index cb534e9fe..7059e74f7 100644 --- a/nvx-rs/src/host/store/lease.rs +++ b/nvx-rs/src/host/store/lease.rs @@ -233,8 +233,8 @@ impl Lease { &self.launch } - /// Returns the reference by which the activation record of the clone names the template that - /// it restores: the leased generation's store generation, and its key's identity. + /// Returns the reference that the clone's lease and durable records keep of its template: + /// the leased generation's store generation and key identity. pub fn template(&self) -> TemplateReference { TemplateReference { generation: self.generation, diff --git a/nvx-rs/src/host/template/clone.rs b/nvx-rs/src/host/template/clone.rs index 4cbb623db..c4776cb6a 100644 --- a/nvx-rs/src/host/template/clone.rs +++ b/nvx-rs/src/host/template/clone.rs @@ -39,7 +39,7 @@ use super::key::TemplateKey; use super::manifest::{BlockGeometry, PROCESSOR_COUNTS}; use super::plan::MEMORY_BLOCK_SIZE; use super::store::{Acquired, Generation, Lease, Store}; -use crate::activation::{ActivationRecord, Journal, OverlayMode, WorkloadView}; +use crate::activation::{ActivationRecord, Journal, OverlayMode, View}; use crate::contract; use crate::error::{Error, ErrorCode, ErrorKind, Result}; use crate::host::filesystem::{self, ExportPlan}; @@ -105,10 +105,9 @@ pub struct CloneRequest { /// The memory target, in bytes: the RAM of the clone's VM once restored, to which the restore /// expands the template's RAM in whole blocks. pub memory: u64, - /// The view that the clone's workload has of its VM: its hostname, the machine ID that it - /// sees, and a workload identity, which the clone's guest ignores: each execution names its - /// own. - pub workload: WorkloadView, + /// The view that the clone's workload has of its VM: its hostname and machine ID. Each + /// execution names its own identity. + pub view: View, /// The mode of the clone's overlay, which selects its options. pub overlay: OverlayMode, /// Whether the filesystem of the scratch image of an upper on one may have a journal. An @@ -249,7 +248,7 @@ pub enum CloneUpper { /// use std::path::Path; /// use std::time::{Duration, Instant}; /// -/// use nvx::activation::{Journal, MachineIdMode, OverlayMode, WorkloadView}; +/// use nvx::activation::{Journal, MachineIdMode, OverlayMode, View}; /// use nvx::host::sandbox::distro::{DISTRO_POLICY_REVISION, DistroBinding}; /// use nvx::host::sandbox::{SandboxDistro, SandboxPlan, SandboxUpper}; /// use nvx::host::store::{CallerFiles, RuntimeIdentity}; @@ -280,10 +279,9 @@ pub enum CloneUpper { /// export: None, /// processors: 2, /// memory: 8 * MEMORY_BLOCK_SIZE, -/// workload: WorkloadView { +/// view: View { /// hostname: b"clone".to_vec(), /// machine_id: MachineIdMode::Image, -/// identity: None, /// }, /// overlay: OverlayMode::UntrustedLowers, /// journal: Journal::Allowed, @@ -423,8 +421,8 @@ impl ClonePlan { /// - with [`ErrorKind::Unsupported`] for settings that no activation record holds, as /// [`Configuration::check`] finds them, such as a GPT disk's first-partition bound of 0, a /// hostname that is empty, longer than - /// [`MAX_HOSTNAME_BYTES`](crate::activation::MAX_HOSTNAME_BYTES), or holds a NUL, and a - /// workload identity whose UID or GID is 0, [`Evidence::InvalidActivate`], and with + /// [`MAX_HOSTNAME_BYTES`](crate::activation::MAX_HOSTNAME_BYTES), or holds a NUL, + /// [`Evidence::InvalidActivate`], and with /// [`ErrorKind::Other`] if the transaction ID of the clone's restore cannot be drawn, the /// host's evidence; /// - with [`ErrorKind::Unavailable`] for a link or a directory under a name that the sandbox's @@ -567,7 +565,7 @@ impl ClonePlan { let key = store.read_key(&generation).map_err(generation_fault)?; let snapshot = store.snapshot_dir(&generation).map_err(generation_fault)?; check_template(request, &key, &generation)?; - let activation = record::build(request, lease, &key)?; + let activation = record::build(request, &key)?; let owned = StoreFiles::of(locked).on_host()?; let image = check_image(request, &key, &owned, access)?; let capability = generate_capability().on_host()?; @@ -677,25 +675,19 @@ impl ClonePlan { /// Ready reports. /// /// The record holds the restore section, with the restore's transaction ID, 16 random bytes - /// that the plan drew, and the template that the [lease](Lease::template) names; the image, in - /// the format that its binding names, with the identity of its binding: an EROFS layer, with + /// that the plan drew; the image, in the format that its binding names: an EROFS layer, with /// the UUID that the guest expects of its filesystem, or a GPT disk, with the GUID that the /// guest expects of the disk, each if the adapter's plan names one, and the request's bound of - /// the disk's first partition; the upper, a tmpfs, or the scratch image, with the identity of - /// its binding and the request's journal policy; the request's overlay mode and workload view; - /// the export, if the request has one, an aggregate, with the identity of its plan, and the + /// the disk's first partition; the upper, a tmpfs capped at half of the VM's memory, or the + /// scratch image with the request's journal policy; the request's overlay mode and view; + /// the export, if the request has one, an aggregate, and the /// number of its mappings, the binds of the plan, from 1 to /// [`MAX_MAPPINGS`](crate::activation::MAX_MAPPINGS), or no export and no mappings; the - /// clone's network, if the template has a NIC, with the identity of the network, whose policy - /// the restore enforces on the NIC, the DNS server that OpenVMM serves the guest at a gateway + /// clone's network, if the template has a NIC: the DNS server that OpenVMM serves the guest at a gateway /// under that policy, if any, whether the guest publishes its listeners, as it does under a /// policy that allows ingress or host-loopback access both ways, and the URL of the policy's /// proxy as the guest sees it, if any; and the processor target, which the restore brings /// online, and the memory target, as the resources that the clone must have. - /// - /// The identity of a binding, of an export plan, or of a network is the SHA-256 digest of a - /// domain of its kind and of its serde form, a few hundred bytes of metadata: the plan reads - /// no content to compute it. pub fn activation(&self) -> &ActivationRecord { &self.activation } diff --git a/nvx-rs/src/host/template/clone/record.rs b/nvx-rs/src/host/template/clone/record.rs index 3f6abaf23..7ab1caaba 100644 --- a/nvx-rs/src/host/template/clone/record.rs +++ b/nvx-rs/src/host/template/clone/record.rs @@ -2,67 +2,37 @@ //! launch from the request and the checked generation: the record of the settings that the //! clone's guest applies in place of the configuration that its template captured, as //! [`nvx::activation`](crate::activation) describes it. -//! -//! A record names the bindings of the clone's image and scratch image, the plan of its export, and -//! its network, by their identities: the SHA-256 digest of a domain of each kind of object and of -//! the object's serde form, which its callers record. The digest covers metadata alone, a few -//! hundred bytes of a binding or of a network, and the host paths and guest targets of an export -//! plan, never the content of a file. A clone's launch computes each once. - -use serde::Serialize; -use sha2::{Digest, Sha256}; use super::CloneRequest; use super::failure::{Attribute, Evidence, Fault}; use crate::activation::{ - ActivationRecord, Configuration, ExpectedResources, Export, ExportKind, Identity, Image, - Network, Publication, RestoreSection, TransactionId, Upper, + ActivationRecord, Configuration, ExpectedResources, Export, ExportKind, Image, Network, + Publication, RestoreSection, TransactionId, Upper, }; use crate::error::{Error, Result}; -use crate::host::filesystem::ExportPlan; -use crate::host::sandbox::SandboxNetwork; -use crate::host::sandbox::distro::{DistroBinding, DistroFormat}; -use crate::host::sandbox::scratch::ScratchBinding; +use crate::host::config::UpperConfig; +use crate::host::sandbox::distro::DistroFormat; use crate::host::template::key::TemplateKey; -use crate::host::template::store::Lease; - -/// What the identity of an image's binding digests before the binding's serde form: a domain -/// that no other digest of this crate shares, and the NUL that ends it. -const DISTRO_DOMAIN: &[u8] = b"nvx distro binding\0"; - -/// What the identity of a scratch image's binding digests before the binding's serde form, as -/// [`DISTRO_DOMAIN`] does for an image's. -const SCRATCH_DOMAIN: &[u8] = b"nvx scratch binding\0"; - -/// What the identity of an export plan digests before the plan's serde form, as -/// [`DISTRO_DOMAIN`] does for an image's binding. -const EXPORT_DOMAIN: &[u8] = b"nvx export plan\0"; - -/// What the identity of a clone's network digests before the network's serde form, as -/// [`DISTRO_DOMAIN`] does for an image's binding. -const NETWORK_DOMAIN: &[u8] = b"nvx network policy\0"; /// Returns the activation record of the clone that `request` asks for, of the template whose key -/// is `key`, which `lease` leases, with a fresh transaction ID. +/// is `key`, with a fresh transaction ID. /// /// The record holds: /// -/// - the restore section: a transaction ID of 16 random bytes, and the template that `lease` -/// names, its generation and the identity of its key; -/// - the image, in the format that its binding names: an EROFS layer, with the identity of its -/// binding and the UUID of its filesystem, or a GPT disk, with the identity of its binding, the -/// GUID of the disk, and the request's bound of its first partition; the adapter's plan names -/// the UUID or the GUID, as the binding does; -/// - the upper: a tmpfs, or the scratch, with the identity of its binding and the request's +/// - the restore section: only a transaction ID of 16 random bytes; +/// - the image, in the format that its binding names: an EROFS layer with its filesystem UUID, +/// or a GPT disk with its GUID and the request's bound of its first partition; +/// - the upper: a tmpfs capped at half of the VM's memory, or the scratch with the request's /// journal policy; -/// - the request's overlay mode and workload view; -/// - the export, if the request has one: an aggregate, as every export plan is, with the identity -/// of its plan, and the number of its mappings, the plan's binds, from 1 to +/// - the request's overlay mode and view; +/// - the export, if the request has one: an aggregate, as every export plan is, and the number +/// of its mappings, the plan's binds, from 1 to /// [`MAX_MAPPINGS`](crate::activation::MAX_MAPPINGS); or no export, and no mappings; /// - the clone's network, if the template has a NIC, whose policy the restore enforces there in -/// place of the template's complete deny-all policy: the identity of the network, its guest's -/// address and policy; the DNS server that OpenVMM serves the guest at a gateway under the -/// policy, as [`SandboxNetwork::dns_server`] finds it with the IPv6 gateway of the template's +/// place of the template's complete deny-all policy: the DNS server that OpenVMM serves the +/// guest at a gateway under the +/// policy, as [`SandboxNetwork::dns_server`](crate::host::sandbox::SandboxNetwork::dns_server) +/// finds it with the IPv6 gateway of the template's /// NIC, [`TemplateKey::ipv6_gateway`], if it serves one, since the template's guest configured /// no resolver at boot under the complete deny-all policy; published listeners under a policy /// that publishes them, [`NetworkPolicy::publishes_listeners`], which allows ingress or @@ -74,35 +44,30 @@ const NETWORK_DOMAIN: &[u8] = b"nvx network policy\0"; /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a configuration that /// no record holds, as [`Configuration::check`] finds it, such as a GPT disk's first-partition /// bound of 0, a hostname that is empty or longer than -/// [`MAX_HOSTNAME_BYTES`](crate::activation::MAX_HOSTNAME_BYTES), or a workload identity whose -/// UID or GID is 0, [`Evidence::InvalidActivate`], and for a network whose policy does not lower +/// [`MAX_HOSTNAME_BYTES`](crate::activation::MAX_HOSTNAME_BYTES), [`Evidence::InvalidActivate`], +/// and for a network whose policy does not lower /// to OpenVMM's arguments, [`Evidence::InvalidSettings`]; and with -/// [`ErrorKind::Other`](crate::ErrorKind::Other) if the transaction ID cannot be drawn, or the -/// identity of a binding, of the export's plan, or of the network computed, the host's evidence. +/// [`ErrorKind::Other`](crate::ErrorKind::Other) if the transaction ID cannot be drawn, the +/// host's evidence. /// /// [`NetworkPolicy::publishes_listeners`]: crate::host::network::NetworkPolicy::publishes_listeners pub(super) fn build( request: &CloneRequest, - lease: &Lease, key: &TemplateKey, ) -> std::result::Result { - let binding = distro_identity(&request.binding).on_host()?; let expected = request.adapter.distro().image().identity; let image = match request.binding.format() { - DistroFormat::Erofs => Image::Erofs { - binding, - uuid: expected, - }, + DistroFormat::Erofs => Image::Erofs { uuid: expected }, DistroFormat::Gpt => Image::Gpt { - binding, disk_guid: expected, first_partition_bound: request.first_partition_bound, }, }; let upper = match &request.scratch { - None => Upper::Tmpfs, - Some(binding) => Upper::Scratch { - binding: scratch_identity(binding).on_host()?, + None => Upper::Tmpfs { + max_memory_percent: UpperConfig::DEFAULT_MAX_MEMORY_PERCENT, + }, + Some(_) => Upper::Scratch { journal: request.journal, }, }; @@ -110,7 +75,6 @@ pub(super) fn build( let network = match request.adapter.network().filter(|_| key.has_network()) { None => None, Some(network) => Some(Network { - policy: network_identity(network).on_host()?, dns: network .dns_server(key.ipv6_gateway()) .attribute(Evidence::InvalidSettings)?, @@ -127,7 +91,6 @@ pub(super) fn build( Some(plan) => ( Some(Export { kind: ExportKind::Aggregate, - plan: export_identity(plan).on_host()?, }), plan.binds.len(), ), @@ -139,7 +102,7 @@ pub(super) fn build( export, mappings, network, - workload: request.workload.clone(), + view: request.view.clone(), resources: ExpectedResources { cpus: request.processors, memory: request.memory, @@ -147,7 +110,6 @@ pub(super) fn build( }; let restore = RestoreSection { transaction: transaction().on_host()?, - template: lease.template(), }; ActivationRecord::new(Some(restore), configuration).map_err(|error| { Fault::new( @@ -159,49 +121,6 @@ pub(super) fn build( }) } -/// Returns the identity of `binding`, the binding of a clone's image, as the [module](self) -/// describes it. -pub(super) fn distro_identity(binding: &DistroBinding) -> Result { - identity(DISTRO_DOMAIN, binding, "binding of the clone's image") -} - -/// Returns the identity of `binding`, the binding of a clone's scratch image, as the -/// [module](self) describes it. -pub(super) fn scratch_identity(binding: &ScratchBinding) -> Result { - identity( - SCRATCH_DOMAIN, - binding, - "binding of the clone's scratch image", - ) -} - -/// Returns the identity of `plan`, the plan of a clone's export, as the [module](self) describes -/// it: it covers the export's whole shape, its directories and files, their policies, the binds, -/// and the owner. -pub(super) fn export_identity(plan: &ExportPlan) -> Result { - identity(EXPORT_DOMAIN, plan, "plan of the clone's export") -} - -/// Returns the identity of `network`, a clone's network, as the [module](self) describes it: it -/// covers the guest's address and the whole policy, its egress, rules, ingress, host-loopback -/// access and forwards, and proxy. -pub(super) fn network_identity(network: &SandboxNetwork) -> Result { - identity(NETWORK_DOMAIN, network, "network of the clone") -} - -/// Returns the SHA-256 digest of `domain` and of the serde form of `object`, a clone's binding, -/// export plan, or network, which messages call `what`. -fn identity(domain: &[u8], object: &impl Serialize, what: &str) -> Result { - let form = serde_json::to_vec(object).map_err(|error| { - Error::other(format!("cannot write the {what} in its serde form")).with_source(error) - })?; - let digest = Sha256::new() - .chain_update(domain) - .chain_update(form) - .finalize(); - Ok(Identity::from_bytes(digest.into())) -} - /// Returns a fresh transaction ID: 16 random bytes. fn transaction() -> Result { let mut bytes = [0; TransactionId::LEN]; diff --git a/nvx-rs/src/host/template/clone/tests.rs b/nvx-rs/src/host/template/clone/tests.rs index eea683429..e7bbdc651 100644 --- a/nvx-rs/src/host/template/clone/tests.rs +++ b/nvx-rs/src/host/template/clone/tests.rs @@ -6,18 +6,15 @@ mod launch; mod restore; -use std::collections::HashSet; use std::io::Read; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::time::Duration; -use sha2::{Digest, Sha256}; - use super::*; use crate::ErrorCode; use crate::activation::{ - Configuration, ExpectedResources, Export, ExportKind, Identity, Image, MachineIdMode, Network, - Publication, Upper, WorkloadIdentity, + Configuration, ExpectedResources, Export, ExportKind, Image, MachineIdMode, Network, + Publication, Upper, }; use crate::host::filesystem; use crate::host::network::{ @@ -194,7 +191,7 @@ fn gpt_layer(path: &Path) -> SandboxLayer { } /// Returns the request of a clone of the image at `image`, which `binding` binds, over an upper in -/// RAM, with the template's processors and RAM as its targets, and the settings of [`workload`], +/// RAM, with the template's processors and RAM as its targets, and the settings of [`view`], /// for untrusted lowers, and with a journal policy, [`Journal::Forbidden`], that its record leaves /// out, and for a GPT disk, the bound [`FIRST_PARTITION_BOUND`]. fn tmpfs_request(image: &Path, binding: &DistroBinding) -> CloneRequest { @@ -209,7 +206,7 @@ fn tmpfs_request(image: &Path, binding: &DistroBinding) -> CloneRequest { export: None, processors: PROCESSORS, memory: MEMORY, - workload: workload(), + view: view(), overlay: OverlayMode::UntrustedLowers, journal: Journal::Forbidden, first_partition_bound: FIRST_PARTITION_BOUND, @@ -217,14 +214,10 @@ fn tmpfs_request(image: &Path, binding: &DistroBinding) -> CloneRequest { } /// Returns the view that these tests' clones give their workloads of their VMs. -fn workload() -> WorkloadView { - WorkloadView { +fn view() -> View { + View { hostname: b"clone".to_vec(), machine_id: MachineIdMode::Generated, - identity: Some(WorkloadIdentity { - uid: 1000, - gid: 1000, - }), } } @@ -2242,16 +2235,6 @@ fn files_that_their_paths_do_not_name_as_checked_are_refused() { assert_untouched(&acquired); } -/// Returns the SHA-256 digest of `domain` and of the serde form of `binding`, as the identity of a -/// binding is documented. -fn digest_of(domain: &[u8], binding: &impl serde::Serialize) -> Identity { - let digest = Sha256::new() - .chain_update(domain) - .chain_update(serde_json::to_vec(binding).unwrap()) - .finalize(); - Identity::from_bytes(digest.into()) -} - #[test] fn the_activation_record_of_a_clone_in_ram_names_its_restore_image_and_settings() { let fixture = Fixture::new(); @@ -2261,21 +2244,21 @@ fn the_activation_record_of_a_clone_in_ram_names_its_restore_image_and_settings( let plan = fixture.plan(&mut acquired, &request, NO_CONTENT).unwrap(); let record = plan.activation(); let restore = record.restore().unwrap(); - assert_eq!(restore.template, acquired.lease.template()); assert_eq!( record.configuration(), &Configuration { image: Image::Erofs { - binding: digest_of(b"nvx distro binding\0", &binding), uuid: Some(Uuid::parse(UUID).unwrap()), }, // A clone in RAM has no journal policy, whatever the request's. - upper: Upper::Tmpfs, + upper: Upper::Tmpfs { + max_memory_percent: 50 + }, overlay: OverlayMode::UntrustedLowers, export: None, mappings: 0, network: None, - workload: workload(), + view: view(), resources: ExpectedResources { cpus: PROCESSORS, memory: MEMORY, @@ -2294,7 +2277,6 @@ fn the_activation_record_of_a_clone_in_ram_names_its_restore_image_and_settings( let again = fixture.plan(&mut acquired, &request, NO_CONTENT).unwrap(); let restored = again.activation().restore().unwrap(); assert_ne!(restored.transaction, transaction); - assert_eq!(restored.template, acquired.lease.template()); // A larger memory target is the resources that the clone must have. drop(again); let larger = CloneRequest { @@ -2309,7 +2291,7 @@ fn the_activation_record_of_a_clone_in_ram_names_its_restore_image_and_settings( } #[test] -fn the_activation_record_of_a_clone_on_a_scratch_image_names_its_binding_and_journal() { +fn the_activation_record_of_a_clone_on_a_scratch_image_names_only_its_journal() { let fixture = Fixture::new(); let (image, binding) = fixture.image("distro.erofs", IMAGE_LEN); let (scratch, scratch_binding) = fixture.scratch("scratch.ext4", SCRATCH_LEN); @@ -2322,27 +2304,20 @@ fn the_activation_record_of_a_clone_on_a_scratch_image_names_its_binding_and_jou }; let plan = fixture.plan(&mut acquired, &request, NO_CONTENT).unwrap(); let configuration = plan.activation().configuration(); - assert_eq!( - configuration.upper, - Upper::Scratch { - binding: digest_of(b"nvx scratch binding\0", &scratch_binding), - journal, - } - ); + assert_eq!(configuration.upper, Upper::Scratch { journal }); assert_eq!(configuration.overlay, OverlayMode::TrustedLowers); assert_eq!( - *configuration.image.binding(), - digest_of(b"nvx distro binding\0", &binding) + configuration.image.expected(), + Some(Uuid::parse(UUID).unwrap()) ); } - // The identities of two bindings differ, as their files do. + // Another binding does not change the guest-applied upper. let (other, other_binding) = fixture.scratch("other.ext4", SCRATCH_LEN); let request = scratch_request(&image, &binding, &other, &other_binding); let plan = fixture.plan(&mut acquired, &request, NO_CONTENT).unwrap(); - assert_ne!( + assert_eq!( plan.activation().configuration().upper, Upper::Scratch { - binding: digest_of(b"nvx scratch binding\0", &scratch_binding), journal: Journal::Forbidden, } ); @@ -2355,7 +2330,6 @@ fn the_activation_record_of_a_gpt_clone_names_its_disk_and_the_bound_of_its_firs let (scratch, scratch_binding) = fixture.scratch("scratch.ext4", SCRATCH_LEN); let mut acquired = fixture.acquire("sandbox"); let gpt = |first_partition_bound| Image::Gpt { - binding: digest_of(b"nvx distro binding\0", &binding), disk_guid: Some(Uuid::parse(UUID).unwrap()), first_partition_bound, }; @@ -2376,7 +2350,7 @@ fn the_activation_record_of_a_gpt_clone_names_its_disk_and_the_bound_of_its_firs assert!(plan.image().file().read(&mut byte).is_err()); let configuration = plan.activation().configuration(); assert_eq!(configuration.image, gpt(FIRST_PARTITION_BOUND)); - assert_eq!(configuration.workload, workload()); + assert_eq!(configuration.view, view()); } // The record keeps the request's bound, whatever it is from 1 on. for bound in [1, u64::MAX] { @@ -2417,10 +2391,9 @@ fn the_activation_record_of_a_clone_of_a_template_with_a_nic_names_its_own_netwo ]; assert_eq!(policies.len(), expected.len()); // Each clone's record names its own network, whatever the policy of the template's class: - // the identity of the network, the DNS server that OpenVMM serves the guest at a gateway + // the DNS server that OpenVMM serves the guest at a gateway // under its policy, whether the guest publishes its listeners, and the proxy's URL as the // guest sees it, at its gateway. - let mut identities = HashSet::new(); for (policy, (dns, publication, proxy)) in policies.into_iter().zip(expected) { let network = SandboxNetwork::new("10.0.0.2/24", policy).unwrap(); let request = with_network(tmpfs_request(&image, &binding), Some(network.clone())); @@ -2429,14 +2402,12 @@ fn the_activation_record_of_a_clone_of_a_template_with_a_nic_names_its_own_netwo assert_eq!( recorded, Some(Network { - policy: digest_of(b"nvx network policy\0", &network), dns, publication, proxy: proxy.map(str::to_owned), }), "{network:?}" ); - assert!(identities.insert(recorded.unwrap().policy)); } } @@ -2446,41 +2417,34 @@ fn settings_that_no_activation_record_holds_are_refused_before_anything_is_creat let (image, binding) = fixture.image("distro.erofs", IMAGE_LEN); let mut acquired = fixture.acquire("sandbox"); let request = tmpfs_request(&image, &binding); - let with_workload = |workload: WorkloadView| CloneRequest { - workload, + let with_view = |view: View| CloneRequest { + view, ..request.clone() }; - for (workload, problem) in [ + for (view, problem) in [ ( - WorkloadView { + View { hostname: Vec::new(), - ..workload() + ..view() }, "the hostname is not 1 to 64 bytes without a NUL", ), ( - WorkloadView { + View { hostname: vec![b'h'; 65], - ..workload() + ..view() }, "the hostname is not 1 to 64 bytes without a NUL", ), ( - WorkloadView { + View { hostname: b"clone\0".to_vec(), - ..workload() + ..view() }, "the hostname is not 1 to 64 bytes without a NUL", ), - ( - WorkloadView { - identity: Some(WorkloadIdentity { uid: 0, gid: 1000 }), - ..workload() - }, - "the workload identity's UID or GID is 0 or 4294967295", - ), ] { - let plan = fixture.plan(&mut acquired, &with_workload(workload), NO_CONTENT); + let plan = fixture.plan(&mut acquired, &with_view(view), NO_CONTENT); assert_eq!( refused(plan, Evidence::InvalidActivate), ( @@ -2577,14 +2541,12 @@ fn a_clone_with_an_export_records_its_plan_and_maps_from_its_children() { let request = with_export(tmpfs_request(&image, &binding), &export); let plan = fixture.plan(&mut acquired, &request, NO_CONTENT).unwrap(); assert_eq!(plan.export(), Some(&export)); - // The record names the export, an aggregate, by the identity of its plan, and announces its - // mappings, the plan's binds. + // The record names the export's shape and announces its mappings, not its plan identity. let configuration = plan.activation().configuration(); assert_eq!( configuration.export, Some(Export { kind: ExportKind::Aggregate, - plan: digest_of(b"nvx export plan\0", &export), }) ); assert_eq!(configuration.mappings, export.binds.len()); @@ -2597,15 +2559,17 @@ fn a_clone_with_an_export_records_its_plan_and_maps_from_its_children() { .map(|mapping| mapping.source) .collect(); assert_eq!(sources, ["0", "1", "2", "3", "0/work"]); - // The identity covers the whole plan: another aggregate's is another. + // Another aggregate still records only its shape and mapping count. drop(plan); let other = fixture.data_export("other"); let request = with_export(tmpfs_request(&image, &binding), &other); let plan = fixture.plan(&mut acquired, &request, NO_CONTENT).unwrap(); let recorded = plan.activation().configuration().export.unwrap(); assert_eq!(recorded.kind, ExportKind::Aggregate); - assert_eq!(recorded.plan, digest_of(b"nvx export plan\0", &other)); - assert_ne!(recorded.plan, digest_of(b"nvx export plan\0", &export)); + assert_eq!( + plan.activation().configuration().mappings, + other.binds.len() + ); assert_eq!(plan.table(), data_table()); // A clone without mappings has no export, and its record none, so its template's slot stays // dormant. diff --git a/nvx-rs/src/host/template/clone/tests/launch.rs b/nvx-rs/src/host/template/clone/tests/launch.rs index 655fa7b24..ffe9849f0 100644 --- a/nvx-rs/src/host/template/clone/tests/launch.rs +++ b/nvx-rs/src/host/template/clone/tests/launch.rs @@ -2071,12 +2071,12 @@ impl Crashed { .unwrap() } - /// Checks that the sandbox holds the clone's activation record, which names the restore of - /// the leased generation, and, on Linux, the restore-ready socket. + /// Checks that the sandbox holds the clone's activation record, with its restore + /// transaction, and, on Linux, the restore-ready socket. fn kept_its_files(&self, locked: &LockedStore) { let recorded = fs::read(locked.clone_activation_path()).unwrap(); let record = ActivationRecord::decode(&recorded).unwrap(); - assert_eq!(record.restore().unwrap().template, self.lease.template()); + assert!(record.restore().is_some()); if cfg!(target_os = "linux") { assert!(exists(&locked.restore_ready_path())); } diff --git a/nvx-rs/src/host/template/clone/tests/restore.rs b/nvx-rs/src/host/template/clone/tests/restore.rs index df33137b0..0a33cb1dd 100644 --- a/nvx-rs/src/host/template/clone/tests/restore.rs +++ b/nvx-rs/src/host/template/clone/tests/restore.rs @@ -662,7 +662,6 @@ fn a_retry_never_sends_another_record_nor_takes_another_activation() { let other = ActivationRecord::new( Some(RestoreSection { transaction: TransactionId::from_bytes([0x77; TransactionId::LEN]), - template: restore.template, }), original.configuration().clone(), ) diff --git a/nvx-rs/src/host/template/key.rs b/nvx-rs/src/host/template/key.rs index 54b63145b..5a6e0af7c 100644 --- a/nvx-rs/src/host/template/key.rs +++ b/nvx-rs/src/host/template/key.rs @@ -208,8 +208,8 @@ identity!( /// /// Its text form, which `Display` writes and `FromStr` parses, is 64 lowercase hexadecimal /// digits; parsing also accepts uppercase ones. It converts to and from the - /// [`activation::Identity`] by which a clone's activation record names the key of its - /// template, in its [`TemplateReference`](activation::TemplateReference), without loss. + /// [`activation::Identity`] by which a clone's lease and durable records name the key of its + /// template, in their [`TemplateReference`](activation::TemplateReference), without loss. KeyIdentity, "a template key's identity" ); diff --git a/nvx-rs/src/host/template/key/tests.rs b/nvx-rs/src/host/template/key/tests.rs index 284634d71..7c119ac8e 100644 --- a/nvx-rs/src/host/template/key/tests.rs +++ b/nvx-rs/src/host/template/key/tests.rs @@ -813,7 +813,7 @@ fn a_key_identity_is_the_identity_that_names_a_clones_template() { assert_eq!(named.as_bytes(), identity.as_bytes()); assert_eq!(named.to_string(), identity.to_string()); assert_eq!(KeyIdentity::from(named), identity); - // A clone's activation record names the template by its generation and this identity. + // A clone's lease and durable records name its template by generation and key identity. let reference = activation::TemplateReference { generation: activation::StoreGeneration::new(std::num::NonZeroU64::MIN), key: identity.into(), diff --git a/nvx-rs/src/host/template/store.rs b/nvx-rs/src/host/template/store.rs index 90493b48b..2a7cd7b91 100644 --- a/nvx-rs/src/host/template/store.rs +++ b/nvx-rs/src/host/template/store.rs @@ -298,8 +298,8 @@ impl Index { /// /// - The store generation, [`StoreGeneration`], is the host's publication ID: the store assigns /// one to each generation as it indexes it, from 1 on, in the order of publication, and never -/// assigns one twice, even once its generation is gone. A clone's activation record names the -/// generation that it restores by its store generation and the identity of its key, in its +/// assigns one twice, even once its generation is gone. A clone's lease and durable records name +/// the generation that it restores by its store generation and the identity of its key, in their /// [`TemplateReference`](crate::activation::TemplateReference). /// - The storage generation, [`StorageGeneration`], is the bound-block storage generation that a /// capture gives OpenVMM, which the storage owner keeps immutable for the blocks that it binds. diff --git a/nvx-rs/src/tests.rs b/nvx-rs/src/tests.rs index 13603041a..da7e11beb 100644 --- a/nvx-rs/src/tests.rs +++ b/nvx-rs/src/tests.rs @@ -5,9 +5,9 @@ use std::fmt::Debug; /// The conformance vectors of version 1 of the control protocol. pub(crate) const NVXC_V1: &str = include_str!("../tests/vectors/nvxc-v1.txt"); -/// The conformance vectors of version 1 of the activation record, and of what Ready reports about +/// The conformance vectors of version 2 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"); +pub(crate) const ACTIVATION_V2: &str = include_str!("../tests/vectors/activation-v2.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 08f8a646d..d8b5ffef8 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, and what Ready reports about an activation | +| [`activation-v2.txt`](activation-v2.txt) | Version 2 of the activation record of a layered root, and what Ready reports about an activation | | [`template-key-v1.txt`](template-key-v1.txt) | Version 1 of the canonical encodings of a template's compatibility key and of a capture class, with their identities | | [`openvmm/`](openvmm) | OpenVMM's vectors and cases of the `NVXS` session, and the excerpts of its sources that define the kernel command-line tokens of its platform tier and the error of a port forward that it cannot bind, unchanged | @@ -23,7 +23,7 @@ describe the protocols, and the documentation of `nvx::activation`, in ## Line format -`nvxc-v1.txt`, `nvxl-v1.txt`, `activation-v1.txt`, and `template-key-v1.txt` +`nvxc-v1.txt`, `nvxl-v1.txt`, `activation-v2.txt`, and `template-key-v1.txt` share one line format. A line that starts with `#` is a comment, and an empty line is ignored. Every other line is one vector, whose fields `|` separates: diff --git a/nvx-rs/tests/vectors/activation-v1.txt b/nvx-rs/tests/vectors/activation-v1.txt deleted file mode 100644 index 1b9a83402..000000000 --- a/nvx-rs/tests/vectors/activation-v1.txt +++ /dev/null @@ -1,115 +0,0 @@ -# Conformance vectors of version 1 of the activation record of a layered root, and of what Ready -# reports about an activation. The documentation of nvx::activation defines the encodings. -# README.md describes the line format and the rules that the vectors follow. -# -# RECORD|NAME|BYTES|RESTORE|IMAGE|UPPER|OVERLAY|EXPORT|NETWORK|HOSTNAME|MACHINE_ID|WORKLOAD|CPUS|MEMORY -# An activation record. RESTORE is absent, for a cold boot, or a clone's -# TRANSACTION|GENERATION|KEY, with GENERATION a number. IMAGE is EROFS|BINDING|UUID or -# GPT|BINDING|BOUND|GUID, with UUID and GUID absent where the guest expects none. UPPER is TMPFS, -# or SCRATCH|BINDING|JOURNAL with JOURNAL ALLOWED or FORBIDDEN. OVERLAY is TRUSTED_LOWERS or -# UNTRUSTED_LOWERS. EXPORT is absent, or DIRECTORY or AGGREGATE followed by PLAN|MAPPINGS. NETWORK -# is absent, or POLICY|PUBLICATION|DNS|PROXY, with PUBLICATION PUBLISHED or UNPUBLISHED, DNS the -# bytes of an IPv4 or IPv6 address, or absent, and PROXY a text, or absent. HOSTNAME is a text, -# MACHINE_ID IMAGE or GENERATED, and WORKLOAD absent, or UID|GID. Identities, IDs, UUIDs, and -# GUIDs are byte strings. -# READY|NAME|BYTES|TRANSACTION -# What Ready reports about the activation of the record of the same name: a clone's transaction -# ID, or absent for a cold boot. -# INVALID_RECORD|NAME|BYTES -# Bytes that every decoder refuses as an activation record. -# INVALID_READY|NAME|BYTES -# Bytes that every decoder refuses as what Ready reports about an activation. - -# The smallest record: a cold boot's EROFS image over a tmpfs, without an export, a -# network, or a workload identity. -RECORD|COLD|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000|-|EROFS|000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f|-|TMPFS|TRUSTED_LOWERS|-|-|nvx-sandbox|IMAGE|-|1|268435456 - -# The same configuration in a clone's record, with its restore section. -RECORD|CLONE|01000138a0a1a2a3a4a5a6a7a8a9aaabacadaeaf0100000000000000c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000|a0a1a2a3a4a5a6a7a8a9aaabacadaeaf|1|c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf|EROFS|000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f|-|TMPFS|TRUSTED_LOWERS|-|-|nvx-sandbox|IMAGE|-|1|268435456 - -# A cold boot's EROFS image with a filesystem UUID over the scratch, an aggregate export with -# the most mappings, and a network with its policy alone. -RECORD|SCRATCH_AGGREGATE|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f11100123456789abcdef0123456789abcdef202202202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f01300101402102404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f410200105021606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f026006776f726b6572610101700c040000000000008000000000|-|EROFS|000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f|0123456789abcdef0123456789abcdef|SCRATCH|202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f|ALLOWED|TRUSTED_LOWERS|AGGREGATE|404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f|4096|606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f|UNPUBLISHED|-|-|worker|IMAGE|-|4|2147483648 - -# A clone's GPT disk with a disk GUID over a tmpfs, a directory export with one mapping, a -# network with an IPv4 DNS server and a proxy, a generated machine ID, and a workload identity. -RECORD|GPT_DIRECTORY|010001380102030405060708090a0b0c0d0e0f1008070605040302012122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40102902808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f00001000000000001110fedcba9876543210fedcba9876543210200101300102402101e0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff4102010050214142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f600151040a0000015214687474703a2f2f31302e302e302e313a333132386007636c6f6e652d316101026208e8030000e9030000700c020000000000004000000000|0102030405060708090a0b0c0d0e0f10|0x102030405060708|2122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40|GPT|808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f|1048576|fedcba9876543210fedcba9876543210|TMPFS|UNTRUSTED_LOWERS|DIRECTORY|e0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff|1|4142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f60|PUBLISHED|0a000001|http://10.0.0.1:3128|clone-1|GENERATED|1000|1001|2|1073741824 - -# Every field at its largest: 602 bytes. The hostname holds the bytes 01 through 40. -RECORD|LARGEST|01000138f0f1f2f3f4f5f6f7f8f9fafbfcfdfeffffffffffffffffffc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf102902a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfffffffffffffffff1110ffffffffffffffffffffffffffffffff202202808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f02300102402102606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f410200105021404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f015110fd00000000000000000000000a00000152ff687474703a2f2f616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161612e746573743a3331323860400102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f406101026208fefffffffeffffff700cffffffffffffffffffffffff|f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff|0xffffffffffffffff|c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf|GPT|a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf|0xffffffffffffffff|ffffffffffffffffffffffffffffffff|SCRATCH|808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f|FORBIDDEN|UNTRUSTED_LOWERS|AGGREGATE|606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f|4096|404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f|PUBLISHED|fd00000000000000000000000a000001|http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.test:3128|%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f !"#$%25&'()*+,-./0123456789:;<=>?@|GENERATED|4294967294|4294967294|0xffffffff|0xffffffffffffffff - -# What Ready reports about the activation of each record. -READY|COLD|00|- -READY|CLONE|01a0a1a2a3a4a5a6a7a8a9aaabacadaeaf|a0a1a2a3a4a5a6a7a8a9aaabacadaeaf -READY|SCRATCH_AGGREGATE|00|- -READY|GPT_DIRECTORY|010102030405060708090a0b0c0d0e0f10|0102030405060708090a0b0c0d0e0f10 -READY|LARGEST|01f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff|f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff - -# Bytes that end inside the version, or hold no field. -INVALID_RECORD|EMPTY| -INVALID_RECORD|TRUNCATED_VERSION|01 -INVALID_RECORD|NO_FIELDS|0100 - -# Another version, and bytes past the limit. -INVALID_RECORD|VERSION_0|0000102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|VERSION_2|0200102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|TOO_LONG|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c01000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 - -# Fields that end past the bytes, are unknown, repeat, or come out of order. -INVALID_RECORD|TRUNCATED_FIELD_HEADER|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c01000000000000100000000071 -INVALID_RECORD|TRUNCATED_FIELD_VALUE|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c0100000000000010000000 -INVALID_RECORD|UNKNOWN_FIELD|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f120101200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|REPEATED_FIELD|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|UPPER_BEFORE_IMAGE|0100200101102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f300101600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|RESTORE_LAST|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c0100000000000010000000000138a0a1a2a3a4a5a6a7a8a9aaabacadaeaf0100000000000000c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf - -# Values of another length, with a code that version 1 does not define, or a store generation of 0. -INVALID_RECORD|EMPTY_OVERLAY|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013000600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|LONG_OVERLAY|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20010130020101600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|OVERLAY_CODE_0|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300100600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|OVERLAY_CODE_3|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300103600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|IMAGE_FORMAT_3|0100102103000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|GPT_WITHOUT_BOUND|0100102102000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|SHORT_RESTORE|01000137a0a1a2a3a4a5a6a7a8a9aaabacadaeaf0100000000000000c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcddde102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|SHORT_IMAGE_IDENTITY|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f110f000102030405060708090a0b0c0d0e200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|SCRATCH_WITHOUT_JOURNAL|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202102202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f300101600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|MACHINE_ID_CODE_0|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610100700c010000000000001000000000 -INVALID_RECORD|DNS_OF_5_BYTES|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013001015021606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f0251050a00000100600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|SHORT_RESOURCES|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700b0100000000000010000000 -INVALID_RECORD|ZERO_GENERATION|01000138a0a1a2a3a4a5a6a7a8a9aaabacadaeaf0000000000000000c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 - -# Missing fields, and fields that need another. -INVALID_RECORD|MISSING_IMAGE|0100200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|MISSING_RESOURCES|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101 -INVALID_RECORD|EXPORT_WITHOUT_MAPPINGS|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101402102404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|MAPPINGS_WITHOUT_EXPORT|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20010130010141020100600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|ZERO_MAPPINGS|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101402102404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f41020000600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|DNS_WITHOUT_NETWORK|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20010130010151040a000001600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|PROXY_WITHOUT_NETWORK|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013001015214687474703a2f2f31302e302e302e313a33313238600b6e76782d73616e64626f78610101700c010000000000001000000000 - -# Values past a limit, or that no guest can apply. -INVALID_RECORD|TOO_MANY_MAPPINGS|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101402102404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f41020110600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|EMPTY_PROXY|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013001015021606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f025200600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|PROXY_WITH_CREDENTIALS|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013001015021606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f025220687474703a2f2f757365723a7365637265744031302e302e302e313a33313238600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|PROXY_WITH_SPACE|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013001015021606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f025215687474703a2f2f31302e302e302e313a3331323820600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|PROXY_NOT_ASCII|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013001015021606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f02520e687474703a2f2fc3a13a33313238600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|EMPTY_HOSTNAME|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2001013001016000610101700c010000000000001000000000 -INVALID_RECORD|HOSTNAME_WITH_NUL|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76780073616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|LONG_HOSTNAME|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20010130010160416161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610101700c010000000000001000000000 -INVALID_RECORD|ZERO_FIRST_PARTITION_BOUND|0100102902000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f0000000000000000200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 -INVALID_RECORD|ZERO_UID|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101620800000000e8030000700c010000000000001000000000 -INVALID_RECORD|ZERO_GID|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f786101016208e803000000000000700c010000000000001000000000 -INVALID_RECORD|UNCHANGED_UID|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f786101016208ffffffffe8030000700c010000000000001000000000 -INVALID_RECORD|UNCHANGED_GID|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f786101016208e8030000ffffffff700c010000000000001000000000 -INVALID_RECORD|ZERO_CPUS|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c000000000000001000000000 -INVALID_RECORD|ZERO_MEMORY|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000000000000000 - -# What Ready reports: no bytes, a byte other than 0 and 1, a cold boot's with bytes after -# it, and a clone's whose transaction ID isn't 16 bytes. -INVALID_READY|EMPTY| -INVALID_READY|FLAG_2|02 -INVALID_READY|COLD_WITH_TRANSACTION|00a0a1a2a3a4a5a6a7a8a9aaabacadaeaf -INVALID_READY|COLD_WITH_TRAILING_BYTE|0000 -INVALID_READY|CLONE_WITHOUT_TRANSACTION|01 -INVALID_READY|SHORT_TRANSACTION|01a0a1a2a3a4a5a6a7a8a9aaabacadae -INVALID_READY|LONG_TRANSACTION|01a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0 diff --git a/nvx-rs/tests/vectors/activation-v2.txt b/nvx-rs/tests/vectors/activation-v2.txt new file mode 100644 index 000000000..5dde01709 --- /dev/null +++ b/nvx-rs/tests/vectors/activation-v2.txt @@ -0,0 +1,93 @@ +# Conformance vectors of version 2 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|CPUS|MEMORY +# RESTORE is absent, or a clone's TRANSACTION. IMAGE is EROFS|UUID or GPT|BOUND|GUID, with UUID +# and GUID absent where the guest expects none. UPPER is TMPFS|CAP, in percent of VM memory, +# or SCRATCH|JOURNAL, ALLOWED or FORBIDDEN. OVERLAY is TRUSTED_LOWERS or UNTRUSTED_LOWERS. +# EXPORT is absent, or DIRECTORY or AGGREGATE followed by MAPPINGS. NETWORK is absent, or +# 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 and MACHINE_ID IMAGE +# or GENERATED. Transaction IDs, UUIDs and GUIDs are byte strings. No store or planning +# identity or execution credential is encoded. +# READY|NAME|BYTES|TRANSACTION +# What Ready reports: 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. + +RECORD|COLD|020010010120020132300101600b6e76782d73616e64626f78610101700c010000000000001000000000|-|EROFS|-|TMPFS|50|TRUSTED_LOWERS|-|-|nvx-sandbox|IMAGE|1|268435456 +RECORD|CLONE|02000110a0a1a2a3a4a5a6a7a8a9aaabacadaeaf10010120020132300101600b6e76782d73616e64626f78610101700c010000000000001000000000|a0a1a2a3a4a5a6a7a8a9aaabacadaeaf|EROFS|-|TMPFS|50|TRUSTED_LOWERS|-|-|nvx-sandbox|IMAGE|1|268435456 +RECORD|SCRATCH_AGGREGATE|020010010111100123456789abcdef0123456789abcdef20020201300101400102410200105001026006776f726b6572610101700c040000000000008000000000|-|EROFS|0123456789abcdef0123456789abcdef|SCRATCH|ALLOWED|TRUSTED_LOWERS|AGGREGATE|4096|UNPUBLISHED|-|-|worker|IMAGE|4|2147483648 +RECORD|GPT_DIRECTORY|020001100102030405060708090a0b0c0d0e0f1010090200001000000000001110fedcba9876543210fedcba98765432102002015a3001024001014102010050010151040a0000015214687474703a2f2f31302e302e302e313a333132386007636c6f6e652d31610102700c020000000000004000000000|0102030405060708090a0b0c0d0e0f10|GPT|1048576|fedcba9876543210fedcba9876543210|TMPFS|90|UNTRUSTED_LOWERS|DIRECTORY|1|PUBLISHED|0a000001|http://10.0.0.1:3128|clone-1|GENERATED|2|1073741824 +RECORD|LARGEST|02000110f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff100902ffffffffffffffff1110ffffffffffffffffffffffffffffffff20020202300102400102410200105001015110fd00000000000000000000000a00000152ff687474703a2f2f616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161612e746573743a3331323860400102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f40610102700cffffffffffffffffffffffff|f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff|GPT|0xffffffffffffffff|ffffffffffffffffffffffffffffffff|SCRATCH|FORBIDDEN|UNTRUSTED_LOWERS|AGGREGATE|4096|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|0xffffffff|0xffffffffffffffff + +# What Ready reports about each record, unchanged by the record schema. +READY|COLD|00|- +READY|CLONE|01a0a1a2a3a4a5a6a7a8a9aaabacadaeaf|a0a1a2a3a4a5a6a7a8a9aaabacadaeaf +READY|SCRATCH_AGGREGATE|00|- +READY|GPT_DIRECTORY|010102030405060708090a0b0c0d0e0f10|0102030405060708090a0b0c0d0e0f10 +READY|LARGEST|01f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff|f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff + +# Invalid versions, framing, values, and removed store/planning fields. +INVALID_RECORD|EMPTY| +INVALID_RECORD|TRUNCATED_VERSION|02 +INVALID_RECORD|NO_FIELDS|0200 +INVALID_RECORD|VERSION_0|000010010120020132300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|VERSION_1|0100102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f200101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|TOO_LONG|020010010120020132300101600b6e76782d73616e64626f78610101700c0100000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +INVALID_RECORD|TRUNCATED_FIELD_HEADER|020010010120020132300101600b6e76782d73616e64626f78610101700c01000000000000100000000071 +INVALID_RECORD|TRUNCATED_FIELD_VALUE|020010010120020132300101600b6e76782d73616e64626f78610101700c0100000000000010000000 +INVALID_RECORD|UNKNOWN_FIELD|020010010112010120020132300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|REPEATED_FIELD|020010010120020132300101600b6e76782d73616e64626f78600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|UPPER_BEFORE_IMAGE|020020020132100101300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|RESTORE_LAST|020010010120020132300101600b6e76782d73616e64626f78610101700c0100000000000010000000000110a0a1a2a3a4a5a6a7a8a9aaabacadaeaf +INVALID_RECORD|EMPTY_OVERLAY|0200100101200201323000600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|LONG_OVERLAY|02001001012002013230020101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|OVERLAY_CODE_0|020010010120020132300100600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|OVERLAY_CODE_3|020010010120020132300103600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|IMAGE_FORMAT_3|020010010320020132300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|GPT_WITHOUT_BOUND|020010010220020132300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|SHORT_RESTORE|0200010fa0a1a2a3a4a5a6a7a8a9aaabacadae10010120020132300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|SHORT_IMAGE_IDENTITY|0200100101110f000102030405060708090a0b0c0d0e20020132300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|SCRATCH_WITHOUT_JOURNAL|0200100101200102300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|MACHINE_ID_CODE_0|020010010120020132300101600b6e76782d73616e64626f78610100700c010000000000001000000000 +INVALID_RECORD|DNS_OF_5_BYTES|02001001012002013230010150010251050a00000100600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|SHORT_RESOURCES|020010010120020132300101600b6e76782d73616e64626f78610101700b0100000000000010000000 +INVALID_RECORD|RESTORE_WITH_TEMPLATE|02000138a0a1a2a3a4a5a6a7a8a9aaabacadaeaf0100000000000000c0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedf10010120020132300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|IMAGE_WITH_BINDING|0200102101000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20020132300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|SCRATCH_WITH_BINDING|0200100101202202202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f01300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|EXPORT_WITH_PLAN|020010010120020132300101402102404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f41020100600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|NETWORK_WITH_POLICY|0200100101200201323001015021606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f02600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|WORKLOAD_IDENTITY|020010010120020132300101600b6e76782d73616e64626f786101016208e8030000e9030000700c010000000000001000000000 +INVALID_RECORD|MISSING_IMAGE|020020020132300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|MISSING_RESOURCES|020010010120020132300101600b6e76782d73616e64626f78610101 +INVALID_RECORD|EXPORT_WITHOUT_MAPPINGS|020010010120020132300101400102600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|MAPPINGS_WITHOUT_EXPORT|02001001012002013230010141020100600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|ZERO_MAPPINGS|02001001012002013230010140010241020000600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|DNS_WITHOUT_NETWORK|02001001012002013230010151040a000001600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|PROXY_WITHOUT_NETWORK|0200100101200201323001015214687474703a2f2f31302e302e302e313a33313238600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|TOO_MANY_MAPPINGS|02001001012002013230010140010241020110600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|EMPTY_PROXY|0200100101200201323001015001025200600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|PROXY_WITH_CREDENTIALS|0200100101200201323001015001025220687474703a2f2f757365723a7365637265744031302e302e302e313a33313238600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|PROXY_WITH_SPACE|0200100101200201323001015001025215687474703a2f2f31302e302e302e313a3331323820600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|PROXY_NOT_ASCII|020010010120020132300101500102520e687474703a2f2fc3a13a33313238600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|EMPTY_HOSTNAME|0200100101200201323001016000610101700c010000000000001000000000 +INVALID_RECORD|HOSTNAME_WITH_NUL|020010010120020132300101600b6e76780073616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|LONG_HOSTNAME|02001001012002013230010160416161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161610101700c010000000000001000000000 +INVALID_RECORD|ZERO_FIRST_PARTITION_BOUND|0200100902000000000000000020020132300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|ZERO_TMPFS_CAP|020010010120020100300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|LARGE_TMPFS_CAP|02001001012002015b300101600b6e76782d73616e64626f78610101700c010000000000001000000000 +INVALID_RECORD|ZERO_CPUS|020010010120020132300101600b6e76782d73616e64626f78610101700c000000000000001000000000 +INVALID_RECORD|ZERO_MEMORY|020010010120020132300101600b6e76782d73616e64626f78610101700c010000000000000000000000 + +# Invalid Ready encodings, unchanged by the record schema. +INVALID_READY|EMPTY| +INVALID_READY|FLAG_2|02 +INVALID_READY|COLD_WITH_TRANSACTION|00a0a1a2a3a4a5a6a7a8a9aaabacadaeaf +INVALID_READY|COLD_WITH_TRAILING_BYTE|0000 +INVALID_READY|CLONE_WITHOUT_TRANSACTION|01 +INVALID_READY|SHORT_TRANSACTION|01a0a1a2a3a4a5a6a7a8a9aaabacadae +INVALID_READY|LONG_TRANSACTION|01a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0 diff --git a/tests/runtime-adapter/README.md b/tests/runtime-adapter/README.md index 5a54b719e..c25cdcc81 100644 --- a/tests/runtime-adapter/README.md +++ b/tests/runtime-adapter/README.md @@ -837,10 +837,10 @@ after the repair's acknowledgement of the restore: its device timeout, the tag of the host filesystem device, `worker::capture::FILESYSTEM_TAG`, and the gateway, if the template has a NIC. The application assembles the root, an - EROFS image or a GPT disk over a tmpfs or the scratch, in the record's - overlay mode, waiting for each device until the activation's deadline at the - latest, so that it ends by then. It ignores the record's workload identity, - if it has one: each execution names its own. For a GPT disk, it first asks + EROFS image or a GPT disk over a tmpfs with the record's RAM cap or the + scratch, in the record's overlay mode, waiting for each device until the activation's deadline at the + latest, so that it ends by then. The record carries no execution identity: + each execution names its own. For a GPT disk, it first asks the kernel to reread the disk's partition table, so that the block devices of its partitions are the clone's disk's, not the template's placeholder's. The upper must be fresh: the assembly refuses one whose @@ -1573,8 +1573,8 @@ library side, `clone`, holds what the command's options choose of the clone, `clone::Request`: its image, an EROFS layer or a GPT disk no longer than the template's placeholder, its upper, on an ext4 scratch image or in RAM, its processor target, the template's processors online at boot unless given, its -memory target, the template's RAM unless given, the workload identity that its -record holds, which its guest ignores, its overlay mode, its scratch's journal +memory target, the template's RAM unless given, a legacy workload option that +activation records do not encode, its overlay mode, its scratch's journal policy, the bound of a GPT disk's first partition, the machine ID that its executions see, the host paths that it shares with its root, as a launch's [shares](#shares), and its [network](#networks), as a launch reads it, which @@ -1592,8 +1592,8 @@ clone, which a read-only bind holds at the root's `/etc/machine-id` for every execution, and the guest's defaults of the other settings, unless the request names them; for a GPT disk, the bound of its first partition is 64 KiB unless the request names one, as a launch's guest takes it by default. For a template -with a NIC, the record names the clone's network: the identity of its policy, -a digest of its serde form; the DNS server that OpenVMM serves the guest under +with a NIC, the record names the clone's guest-applied network settings: +the DNS server that OpenVMM serves the guest under the policy, if any, which NVX finds as OpenVMM does, since the template's guest learned none, and which the guest writes to a resolver file, of mode 0444, that a read-only bind holds at the root's `/etc/resolv.conf` for every execution, @@ -1692,8 +1692,8 @@ the adapter's clone order, `host::CloneOrder`, which sends the clone's activation record in an `Activate`, checks the guest agent's build in its `Ready`, sends the export's mapping table, which the record announces, in order, as a launch -sends its own, and then `Start`. The record names the export by the identity of -its plan, and the table holds its mappings, each from a source that names its +sends its own, and then `Start`. The record names the export's shape and mapping +count, and the table holds its mappings, each from a source that names its child by the child's index, as a cold launch's table does. A clone without shares has no export: its restore leaves the slot dormant, its record announces no mapping, @@ -1933,8 +1933,8 @@ Each command of `runtime-adapter-host` but `stage`, `list`, `inspect`, and the template's placeholder, whatever the placeholder's format, over the ext4 scratch image PATH, or RAM, with a processor target of N processors, the template's processors online at boot by default, a memory target of MIB - mebibytes, the template's RAM by default, the workload identity UID:GID, if - given, which the clone's record holds and its guest ignores, the overlay mode + mebibytes, the template's RAM by default, the legacy workload option UID:GID, if + given, which activation records do not encode, the overlay mode MODE and the scratch's journal policy POLICY, as a launch takes them, and for a GPT disk, the bound BYTES of its first partition, 64 KiB by default, as a launch's guest takes it. Every execution @@ -2481,9 +2481,8 @@ for every execution, each of which a test may fail. The tests cover: - an `Activate` of another transaction, or of the same transaction with another record, once the clone is ready, or its sandbox started, which fails the activation with the status of the host's session; -- the identity that each start names, whether the record names a workload - identity or not, while a start that names none is refused with `EINVAL`, and - the record's hostname, which every execution's entry takes; +- the identity that each start names, while a start that names none is refused + with `EINVAL`, and the record's hostname, which every execution's entry takes; - the `activation` probe, which the worker answers itself, with no root operation, executions running or not: the restore's reset, the host's first message, the clone's `Ready`, and its start, in that order, after the @@ -2960,7 +2959,7 @@ directory that the starts authorize: | `a_clone_refuses_a_scratch_image_whose_upper_an_earlier_clone_used` | Over a scratch image, in whose root the `contents` probe writes a file, then over a copy of that scratch image once the first clone stopped | The first started; then status 8, since the copy's upper is not fresh, and the record of a failure of the request, at the activation, once the VM exited, by the evidence of an unusable root, with the guest's status 54; nothing quarantined, and the lease released, with the records | | `clones_at_once_keep_their_own_uppers_and_machine_ids` | Three at once: over a scratch image and in RAM, each with a generated machine ID, and in RAM with the image's own; two executions in each, as user ID 0, that write a marker in the root, and try to write `/etc/machine-id` | In every execution, the hostname `runtime-adapter`, and no marker of another clone; a generated machine ID that differs between the clones, the same in each execution of a clone, held by one bind that is read-only, `nosuid`, and `nodev`, and that the write cannot change; with the image's own machine ID, none, since the image has none, no bind, and in the later execution, the ID that the first wrote; then each stop releases its lease | | `a_clone_whose_view_cannot_be_set_ends_before_it_is_ready` | Of a template of the EROFS image, a GPT disk whose layer holds a regular file `etc`, with the image's own machine ID, then with a generated one | The first started, with the disk's root; then status 8, since no bind can hold the machine ID below a file, and the record of a failure of the request, at the activation, once the VM exited, by the evidence of invalid settings, with the guest's status 55; nothing quarantined, and the lease released, with the records | -| `a_clone_runs_the_executions_that_name_their_identity_once_it_is_ready` | Over a scratch image, with the workload identity of nobody, which the guest ignores | An execution that names nobody's identity, which runs as nobody, one that names none refused with `EINVAL`, and the first execution after the clone's start | +| `a_clone_runs_the_executions_that_name_their_identity_once_it_is_ready` | Over a scratch image, with the legacy workload option of nobody, which no activation record encodes | An execution that names nobody's identity, which runs as nobody, one that names none refused with `EINVAL`, and the first execution after the clone's start | | `openvmm_reads_none_of_the_host_s_input_until_the_repair_acknowledges_the_restore` | Of a template whose clones wait 8 seconds before their repair, in RAM, which the host starts through NVX's public API, with the host program's clone order, over transports that watch their attachments, each of which they write a byte at a time; then a reattach through NVX's public API | The template's key, which names the delay; the attachment written within half the delay of the launch, the broker's first answer, and the attachment of the host, only after the delay; on Linux, `SIOCOUTQ` at the control socket risen by the same step with each byte that the host wrote, then as the write left it at every record before then, and zero once the broker answered, while a listener of the test's own process sees it fall once it read the first byte of such an attachment, and on Windows no record, since a named pipe tells its writer nothing; then the reattach's attachment answered within a quarter of the delay, and on Linux read whole by then; the guest's `repair-delay` of 8 seconds, and the events of its activation in order; and a stop that releases the lease | | `a_generation_is_eligible_by_the_clock_of_its_capture` | At the host's time, then with clock fixtures: UTC a day behind, another boot an hour after the capture by UTC, and the restore window passed by each clock, or UTC before the capture | The host's monotonic clock on the boot of the capture, whatever UTC says, UTC after a reboot, with the fixture's downtime, and, outside the window, status 10, with the record of the store's refusal of the selection, `expired` once the window passed, and `ineligible`, by the evidence `early`, before the capture, without a lease | | `a_clone_reads_no_more_of_its_disks_than_its_root_needs` | Over a scratch image, and in RAM | Less than an eighth of each disk read once the clone is ready, by the guest's statistics, and each disk's whole length once the guest read it to its end; on the host, the staging's count of the bytes that it read at least the image's length, which it hashed, the start's less than an eighth of it, and the VMM's less than an eighth of the disks whose files back the clone's, those of the image and of a scratch image, once the clone is ready; then, once the guest read each disk to its end, the VMM's count raised by at least what the guest asked of those files on Windows, and by less than an eighth of it on Linux, whose `rchar` misses OpenVMM's reads through io_uring | @@ -2970,12 +2969,12 @@ directory that the starts authorize: | `a_clone_whose_vm_exits_once_a_stop_found_it_fails_before_its_lease_is_released` | In RAM, then a `stop` that the step hook pauses at `found`, while the test terminates the clone's VMM | Status 8, `reattached=live` and `stopped=exited`, and the record of a failure of an unknown cause, at the run, by the evidence of the clone's lost status, without a status of the guest's, once the VM exited, which quarantines nothing; the lease released, with the records; then another clone that starts and stops | | `a_tombstone_leaves_the_running_clone_of_its_generation_to_its_lease` | Over a scratch image, then a tombstone of its generation, through NVX's store | A start whose selection the store refuses, with status 10 and the record of an `ineligible` generation, by the evidence of its tombstone, without a lease; the running clone, which reattaches, probes, runs an execution, and stops, which releases its lease; and the generation's snapshot unchanged | | `the_lease_of_a_start_that_ended_before_its_record_is_released_through_its_template_store` | In RAM, in a sandbox that NVX's store leased a generation to, as a start that ended before it recorded the clone leaves it: first moved, with another sandbox at its lease's path, which holds a lease of its own, then none; then in place, without a record of a clone, then with the record of an earlier clone, of another template | For the moved sandbox, a `stop` and a `reattach` that refuse it, since the sandbox at its lease's path does not hold its lease, and leave that sandbox's lease; in place, a stop that finds the clone `ended` at `planned`, and keeps the lease, whose template store no record names; a stop with another template store, which the release refuses; a `reattach` or a `stop` with `--template` naming the lease's own, which releases it; and a start and a stop in the sandbox after each | -| `a_clone_maps_an_aggregate_of_directories_and_files_beneath_its_root_before_its_sandbox_starts` | Over a scratch image, sharing directories and files of a host directory: a read-only directory, with a read-write directory inside it, at its target there, a read-only file, a read-write file, and a read-write directory, with a file of the first hidden, and of the last a directory hidden, but a file in it shown again | Started, with five mappings, and a record that names the export, an aggregate, by the identity of its plan, with five mappings; on Linux, the VMM's `--mount-aggregate` and one `--mount-child` per directory and file, in order, the first directory read-write, each file with its access and `file`, then the hidden paths, the one shown again, and the one that the guest may write, and no `--mount`; the export mounted once, read-write, `nosuid`, and `nodev`, below a `hostfs` of mode 0700 that root owns; the mappings below the root, and nothing else, in the table's order, each with its access, `nosuid`, and `nodev`, showing what the host holds but for the hidden files, and the hidden directory, which lists the file shown again alone, with the missing targets created empty in the upper; the same from an execution as user ID 0; and writes through the writable mappings alone, which the host's directory holds, before and after the stop | -| `clones_at_once_see_only_their_own_exports` | Four at once: one with a read-write export of its own and a generated machine ID, two over the same read-only export, over a scratch image and in RAM, and one without an export | Each with an export started with one mapping, and a record that names it, an aggregate, by the identity of its plan, the same for the clones of one export, and on Linux, the VMM's `--mount-aggregate`, with one `--mount-child`, read-write or read-only, as the mapping needs; each export mounted read-write, even where its mapping is read-only; below the root, the mapping, beside the bind of a generated machine ID; each clone's own directory, with its access; without an export, no export in the record, no mapping, none of OpenVMM's options of an export on Linux, no export mounted, and nothing below the root; the `entries` probe, whose helper writes the root's machine ID, failing with `EROFS` where the generated one is bound; the host's directories with what the writable export took alone; then each stop releases its lease | +| `a_clone_maps_an_aggregate_of_directories_and_files_beneath_its_root_before_its_sandbox_starts` | Over a scratch image, sharing directories and files of a host directory: a read-only directory, with a read-write directory inside it, at its target there, a read-only file, a read-write file, and a read-write directory, with a file of the first hidden, and of the last a directory hidden, but a file in it shown again | Started, with five mappings, and a record that names the export's aggregate shape and five mappings; on Linux, the VMM's `--mount-aggregate` and one `--mount-child` per directory and file, in order, the first directory read-write, each file with its access and `file`, then the hidden paths, the one shown again, and the one that the guest may write, and no `--mount`; the export mounted once, read-write, `nosuid`, and `nodev`, below a `hostfs` of mode 0700 that root owns; the mappings below the root, and nothing else, in the table's order, each with its access, `nosuid`, and `nodev`, showing what the host holds but for the hidden files, and the hidden directory, which lists the file shown again alone, with the missing targets created empty in the upper; the same from an execution as user ID 0; and writes through the writable mappings alone, which the host's directory holds, before and after the stop | +| `clones_at_once_see_only_their_own_exports` | Four at once: one with a read-write export of its own and a generated machine ID, two over the same read-only export, over a scratch image and in RAM, and one without an export | Each with an export started with one mapping, and a record that names its aggregate shape without a host plan identity, and on Linux, the VMM's `--mount-aggregate`, with one `--mount-child`, read-write or read-only, as the mapping needs; each export mounted read-write, even where its mapping is read-only; below the root, the mapping, beside the bind of a generated machine ID; each clone's own directory, with its access; without an export, no export in the record, no mapping, none of OpenVMM's options of an export on Linux, no export mounted, and nothing below the root; the `entries` probe, whose helper writes the root's machine ID, failing with `EROFS` where the generated one is bound; the host's directories with what the writable export took alone; then each stop releases its lease | | `a_start_refuses_an_export_whose_mapped_objects_changed_before_any_vmm_starts` | Sharing a read-write directory, a directory inside it at a target of its own, and a read-only file; while the step hook pauses each start at `checked`, a directory replaces the file, then another directory the one inside the read-write directory | Status 8, with NVX's reason, and the record of a failure of the request, at the launch, without a VM, by the evidence of invalid mappings, which quarantines nothing; no VMM's log, launch journal, runtime record, or record of the clone's launch in the sandbox, and the lease released | | `a_clone_whose_mapping_fails_ends_before_its_sandbox_starts` | Over a scratch image, sharing a host directory whose second mapping puts a directory onto a file of the image | Status 8, a `Ready`, but no start, and the record of a failure of an unknown cause, at the start, at the clone's `Ready`, by the evidence of the protocol, once the VM exited with the guest's success, or NVX terminated it, with the guest's report that the table failed after its first mapping; nothing quarantined, and the lease released, with the records; the host's directory unchanged | | `a_template_without_a_nic_serves_clones_without_a_network` | Two at once, without a network, and with one that lets nothing through; then, once they stopped, one of each network with a NIC of the template scenarios | The first two started, with no network in their records, on Linux no argument of a network of their VMMs, no network device, and the image's own `/etc/resolv.conf`, an empty file of mode 0644, which no bind holds, and root may write; then for each of the others, status 8, with NVX's reason, and the record of a failure of the request, at the plan, without a VM, by the evidence of invalid settings, which quarantines nothing; the lease released | -| `one_template_serves_the_clones_of_every_network_policy_at_its_guest_s_address` | Of a template of host-loopback access both ways, with a forward: in turn, one of each network with a NIC, of egress allowed, with outbound host-loopback access or without, of one TCP rule to the gateway, with outbound host-loopback access, of a proxy, of ingress allowed, of host-loopback access both ways, and of that access with a forward; then one at the guest's address `192.0.2.3/24`, one of a network that lets nothing through, and one without a network | Each of the first started, with one network device, on Linux the VMM's `--restore-network-policy`, then the arguments of the clone's own policy, without `--net`, and a record that names the policy, the gateway's DNS server for the networks with egress allowed, the proxy's URL for the proxy-only one, and a publication of the guest's listeners for those with ingress or host-loopback access both ways; an execution's TCP connections to the gateway, at the ports of the host's listeners on host loopback, that reach the allow rule's alone, the proxy's alone, every one with egress and outbound host-loopback access, and none otherwise, as the host's listeners confirm; a listener of the guest, as nobody, that the host reaches at its port of host loopback, with a line each way, exactly with host-loopback access both ways, once the clone's announcer announced it, and through the forward's port; and executions as user ID 0 that ask for `CAP_NET_BIND_SERVICE` or `CAP_NET_RAW` refused with `EPERM` where the listeners are published, and started elsewhere, as one that asks for `CAP_CHOWN` everywhere; then for each of the others, status 8, with NVX's reason, and the record of a failure of the request, at the plan, without a VM, by the evidence of invalid settings, which quarantines nothing; the lease released; each clone's policy differs from the others' | +| `one_template_serves_the_clones_of_every_network_policy_at_its_guest_s_address` | Of a template of host-loopback access both ways, with a forward: in turn, one of each network with a NIC, of egress allowed, with outbound host-loopback access or without, of one TCP rule to the gateway, with outbound host-loopback access, of a proxy, of ingress allowed, of host-loopback access both ways, and of that access with a forward; then one at the guest's address `192.0.2.3/24`, one of a network that lets nothing through, and one without a network | Each of the first started, with one network device, on Linux the VMM's `--restore-network-policy`, then the arguments of the clone's own policy, without `--net`, and a record that names the gateway's DNS server for the networks with egress allowed, the proxy's URL for the proxy-only one, and a publication of the guest's listeners for those with ingress or host-loopback access both ways, without a host policy identity; an execution's TCP connections to the gateway, at the ports of the host's listeners on host loopback, that reach the allow rule's alone, the proxy's alone, every one with egress and outbound host-loopback access, and none otherwise, as the host's listeners confirm; a listener of the guest, as nobody, that the host reaches at its port of host loopback, with a line each way, exactly with host-loopback access both ways, once the clone's announcer announced it, and through the forward's port; and executions as user ID 0 that ask for `CAP_NET_BIND_SERVICE` or `CAP_NET_RAW` refused with `EPERM` where the listeners are published, and started elsewhere, as one that asks for `CAP_CHOWN` everywhere; then for each of the others, status 8, with NVX's reason, and the record of a failure of the request, at the plan, without a VM, by the evidence of invalid settings, which quarantines nothing; the lease released | | `a_forward_whose_port_another_socket_holds_fails_the_clone_as_the_host_s` | Of a template of a network with egress allowed, one of host-loopback access both ways, with a forward of a port of host loopback that a listener of the host holds; then, once the listener closed, one of the same network | Status 8, with OpenVMM's cause, that it cannot create the forward's socket, and the record of a failure of the host, at the restore, once the VMM exited before its restore-ready event, by the evidence of the forward's bind, which quarantines nothing; the lease released, with the records; then a clone that starts and stops | | `a_clone_binds_the_resolver_of_its_policy_s_dns_server_read_only` | Of a template of a proxy-only network, two at once, of a network with egress allowed, with a generated machine ID, and with the image's own; then, once they stopped, a GPT disk whose layer holds a regular file `etc`, with the image's own machine ID | Each of the first two started, with a record that names the gateway's DNS server, one network device, and at `/etc/resolv.conf`, a file of mode 0444 that names the server, held by a bind that is read-only, `nosuid`, and `nodev`, after the bind of the generated machine ID, below the root, whose file system refuses a write as root, with nothing in the upper there; and a TCP connection to the server's DNS port, which the gateway's DNS proxy accepts without the host's resolver; then status 8, since no bind can hold the resolver configuration below a file, and the record of a failure of the request, at the activation, once the VM exited, by the evidence of invalid settings, with the guest's status 57; nothing quarantined, and the lease released, with the records | | `a_clone_s_executions_find_the_proxy_of_its_policy` | Of a template of a network with egress allowed, one of a proxy-only network, with an execution as user ID 0 that runs `env`, then one that names its own `HTTPS_PROXY` | A record that names the proxy's URL, `http://192.0.2.1:PORT`, at the port of the proxy's listener, and no DNS server; the first execution with its own entry, then the URL in `http_proxy`, `https_proxy`, `HTTP_PROXY`, and `HTTPS_PROXY`; the second with exactly its own environment; one network device, and the image's own `/etc/resolv.conf`, which no bind holds | @@ -3184,7 +3183,7 @@ layered roots, captures of templates, and the starts and runs of their clones. | `vm_clone` | `a_clone_of_a_template_with_a_nic_maps_its_export_beside_its_resolver` | A clone of a template with a NIC maps its export below the binds of its machine ID and of its resolver configuration | | `vm_clone` | `a_clone_reads_no_more_of_its_disks_than_its_root_needs` | A clone's restore reads less than an eighth of each disk, before the guest reads each to its end, by the guest's statistics, and as the host counts the reads of the start, which hashes and copies nothing, and of the VMM, whose count on Windows rises by what the guest asked of the disks' files, and on Linux, where it misses io_uring's reads, does not | | `vm_clone` | `a_clone_refuses_a_scratch_image_whose_upper_an_earlier_clone_used` | A scratch image whose upper an earlier clone used ends the clone before it is ready, with status 54 | -| `vm_clone` | `a_clone_runs_the_executions_that_name_their_identity_once_it_is_ready` | A clone runs the executions that name their identity once it is ready, whatever workload identity its record names, and refuses one that names none with `EINVAL` | +| `vm_clone` | `a_clone_runs_the_executions_that_name_their_identity_once_it_is_ready` | A clone runs the executions that name their identity once it is ready, and refuses one that names none with `EINVAL` | | `vm_clone` | `a_clone_s_executions_find_the_proxy_of_its_policy` | The executions of a clone of a proxy-only network find its proxy's URL in their environment, and the clone keeps the image's own `/etc/resolv.conf` | | `vm_clone` | `a_clone_s_time_settles_after_it_is_ready` | A clone's time settles once it is ready, without a violation, after the downtime that the host's monotonic clock measured | | `vm_clone` | `a_clone_whose_activation_fails_ends_before_it_is_ready` | A clone whose activation fails ends before it is ready, with the status of the failure's class and the failure's record, quarantines nothing, and releases its lease | diff --git a/tests/runtime-adapter/src/bin/runtime-adapter-host.rs b/tests/runtime-adapter/src/bin/runtime-adapter-host.rs index e080f6687..b6a88e20a 100644 --- a/tests/runtime-adapter/src/bin/runtime-adapter-host.rs +++ b/tests/runtime-adapter/src/bin/runtime-adapter-host.rs @@ -170,13 +170,12 @@ //! `ExportPlan::verify`, OpenVMM restores the generation's snapshot in place, with the clone's //! devices, through NVX's supervisor, as the launch that the lease reserves, and the restore //! sequence of `nvx::host::template` activates the clone through the adapter's clone order, -//! `runtime_adapter::host::CloneOrder`, with the clone's activation record: the workload -//! identity UID:GID, if given, which the clone's guest ignores as it applies the record, since -//! each execution names its own identity, the overlay mode MODE and the scratch's journal policy +//! `runtime_adapter::host::CloneOrder`, with the clone's activation record: the overlay mode +//! MODE and the scratch's journal policy //! POLICY, as a launch takes them, for a GPT disk, the bound BYTES of its first partition, 64 -//! KiB unless given, as a launch's guest takes it by default, the export, by the identity of -//! its plan, with the number of its mappings, and for a template with a NIC the clone's network: -//! the identity of its policy, the DNS server that OpenVMM serves the guest under the policy, if +//! KiB unless given, as a launch's guest takes it by default, the export's shape and number of +//! mappings, and for a template with a NIC the clone's network: +//! the DNS server that OpenVMM serves the guest under the policy, if //! any, which NVX finds as OpenVMM does, since the template's guest learned none, and whose //! address the clone's guest writes to a resolver file that it binds read-only over the root's //! `/etc/resolv.conf`, whether the policy publishes the workload's listeners, as it does with diff --git a/tests/runtime-adapter/src/clone.rs b/tests/runtime-adapter/src/clone.rs index c74949333..f17d88aba 100644 --- a/tests/runtime-adapter/src/clone.rs +++ b/tests/runtime-adapter/src/clone.rs @@ -3,8 +3,8 @@ //! //! A [`Request`] holds what the host program's options choose of a clone: its image, an EROFS //! layer or a GPT disk no longer than the template's placeholder, its upper, on an ext4 scratch -//! image or in RAM, its processor and memory targets, the workload identity of its record, which -//! its guest ignores, its overlay mode, its scratch's journal policy, the bound of a GPT disk's +//! image or in RAM, its processor and memory targets, its overlay mode, its scratch's journal +//! policy, the bound of a GPT disk's //! first partition, the machine ID that its executions see, the image's own or one that its //! guest generates, the host paths that it shares with its root, as a launch shares them, //! `sharing::HostFsSharing`, and its network, as a launch reads it, `layered::read_network`, @@ -35,8 +35,7 @@ use std::path::{Path, PathBuf}; use std::{env, fs}; use nvx::activation::{ - Journal, MachineIdMode, OverlayMode as RecordOverlay, StoreGeneration, WorkloadIdentity, - WorkloadView, + Journal, MachineIdMode, OverlayMode as RecordOverlay, StoreGeneration, View, }; use nvx::host::filesystem::ExportPlan; use nvx::host::process::BootId; @@ -84,8 +83,8 @@ pub struct Request { pub processors: Option, /// The memory target, in bytes, or `None` for the template's RAM. pub memory: Option, - /// The workload identity of the clone's record, if any: its user ID and its group ID. The - /// clone's guest ignores it: each execution names its own. + /// The legacy workload option, which activation records do not encode. Each execution names + /// its own identity. pub workload_identity: Option<(u32, u32)>, /// The overlay's mode, or `None` for the guest's default, which trusts the lowers. pub overlay_mode: Option, @@ -142,8 +141,8 @@ impl Request { /// sandbox's store stages for its images, `bindings`, as `staged::Staged::bindings` finds /// them, the processor target `processors`, and the memory target `memory`, in bytes. /// - /// The record of the clone's activation names the workload identity, if any, the - /// [`CLONE_HOSTNAME`], the request's machine ID, the overlay's mode, and the scratch's + /// The record of the clone's activation names the [`CLONE_HOSTNAME`], the request's machine + /// ID, the overlay's mode, and the scratch's /// journal policy, each the guest's default unless the request names it, as a root /// configuration's are, and the bound of a GPT disk's first partition, the default of a /// launch's guest, [`DEFAULT_FIRST_PARTITION_BOUND`], unless the request names one. The record @@ -188,12 +187,9 @@ impl Request { export, processors, memory, - workload: WorkloadView { + view: View { hostname: CLONE_HOSTNAME.as_bytes().to_vec(), machine_id: self.machine_id, - identity: self - .workload_identity - .map(|(uid, gid)| WorkloadIdentity { uid, gid }), }, overlay, journal, diff --git a/tests/runtime-adapter/src/worker.rs b/tests/runtime-adapter/src/worker.rs index dc166c88d..8bebc48ca 100644 --- a/tests/runtime-adapter/src/worker.rs +++ b/tests/runtime-adapter/src/worker.rs @@ -82,8 +82,8 @@ //! `nvx::guest::clone::apply`: it assembles the root from the template's devices, with the //! record's image, upper, and overlay mode, once the kernel reread the partition table of a //! GPT disk, over a fresh upper, waiting for each device until the activation's deadline at the -//! latest, so that the application ends by then. It ignores the record's workload identity, if -//! it has one. If the record asks for a generated machine ID, it then generates one, once, after +//! latest, so that the application ends by then. Each execution names its own identity. +//! If the record asks for a generated machine ID, it then generates one, once, after //! the repair's reseed. If the record has an export, it mounts the host export //! out of the root, through the template's host filesystem device, `capture::FILESYSTEM_TAG`, as //! the worker of a VM that boots cold mounts the one that its command line names, and if the @@ -489,8 +489,8 @@ pub trait RootOperations: EntryOperations { /// [`map`](Self::map) maps the host's mapping table, and the announcer of the workload's /// listeners that it started, if the record's network publishes them, whose /// [pidfd](Self::announcer) the worker polls: in place of a root configuration's - /// [layout](Self::command_line), assembly, and [export](Self::mount_export). It ignores the - /// record's workload identity: each execution of a clone names its own. The template's VM + /// [layout](Self::command_line), assembly, and [export](Self::mount_export). + /// Each execution of a clone names its own identity. The template's VM /// [bootstrapped](Self::bootstrap) the guest before its capture. /// /// Fails as `nvx::guest::clone::apply` does, with an [`ApplyError`]. diff --git a/tests/runtime-adapter/src/worker/activation.rs b/tests/runtime-adapter/src/worker/activation.rs index 364b8090b..97e45d260 100644 --- a/tests/runtime-adapter/src/worker/activation.rs +++ b/tests/runtime-adapter/src/worker/activation.rs @@ -318,7 +318,7 @@ impl CloneState { self.applied = Some(Applied { result, mappings, - hostname: configuration.workload.hostname.clone(), + hostname: configuration.view.hostname.clone(), publication, joined: 0, }); diff --git a/tests/runtime-adapter/src/worker/activation/tests.rs b/tests/runtime-adapter/src/worker/activation/tests.rs index cbc439cb4..ec555b0e9 100644 --- a/tests/runtime-adapter/src/worker/activation/tests.rs +++ b/tests/runtime-adapter/src/worker/activation/tests.rs @@ -1,10 +1,9 @@ use std::io; -use std::num::NonZeroU64; use std::path::PathBuf; use nvx::activation::{ - Configuration, ExpectedResources, Identity, Image, MachineIdMode, OverlayMode, RestoreSection, - StoreGeneration, TemplateReference, TransactionId, Upper, WorkloadIdentity, WorkloadView, + Configuration, ExpectedResources, Image, MachineIdMode, OverlayMode, RestoreSection, + TransactionId, Upper, View, }; use nvx::guest::clone::{AdmissionFence, NetworkError}; use nvx::guest::entry; @@ -22,32 +21,25 @@ const RESOURCES: ExpectedResources = ExpectedResources { memory: 1 << 30, }; -/// Returns a clone's record of the transaction `transaction`, with the workload identity of -/// `uid`. -fn record(transaction: u8, uid: u32) -> ActivationRecord { +/// Returns a clone's record of the transaction `transaction`, with `hostname`. +fn record(transaction: u8, hostname: &[u8]) -> ActivationRecord { let configuration = Configuration { - image: Image::Erofs { - binding: Identity::from_bytes([0x11; 32]), - uuid: None, + image: Image::Erofs { uuid: None }, + upper: Upper::Tmpfs { + max_memory_percent: 50, }, - upper: Upper::Tmpfs, overlay: OverlayMode::UntrustedLowers, export: None, mappings: 0, network: None, - workload: WorkloadView { - hostname: b"clone".to_vec(), + view: View { + hostname: hostname.to_vec(), machine_id: MachineIdMode::Image, - identity: Some(WorkloadIdentity { uid, gid: uid }), }, resources: RESOURCES, }; let restore = RestoreSection { transaction: TransactionId::from_bytes([transaction; 16]), - template: TemplateReference { - generation: StoreGeneration::new(NonZeroU64::MIN), - key: Identity::from_bytes([0x44; 32]), - }, }; ActivationRecord::new(Some(restore), configuration).unwrap() } @@ -72,7 +64,7 @@ fn restricted() -> Activation { /// Returns an activation that accepted the record of the transaction 1. fn activating() -> Activation { let mut activation = restricted(); - let bytes = record(1, 1000).encode(); + let bytes = record(1, b"clone").encode(); let action = activation.input(Instant::now(), ActivationInput::Activate(&bytes)); assert!( matches!(action, Ok(ActivationAction::Apply(_))), @@ -89,7 +81,7 @@ fn failed(result: Result) -> Activatio #[test] fn each_class_of_failure_ends_the_clone_with_its_own_status() { let now = Instant::now; - let bytes = record(1, 1000).encode(); + let bytes = record(1, b"clone").encode(); let mut cases = Vec::new(); // The deadline. @@ -104,21 +96,21 @@ fn each_class_of_failure_ends_the_clone_with_its_own_status() { cases.push((failure, status::ACTIVATION_PROTOCOL)); let early = activation(RESOURCES, ACTIVATION_TIMEOUT).input(now(), ActivationInput::Request); cases.push((failed(early), status::ACTIVATION_PROTOCOL)); - let other = record(2, 1000).encode(); + let other = record(2, b"clone").encode(); let failure = failed(activating().input(now(), ActivationInput::Activate(&other))); assert!(matches!( failure.cause(), ActivationCause::Transaction { .. } )); cases.push((failure, status::ACTIVATION_PROTOCOL)); - let changed = record(1, 1001).encode(); + let changed = record(1, b"another-clone").encode(); let failure = failed(activating().input(now(), ActivationInput::Activate(&changed))); assert!(matches!(failure.cause(), ActivationCause::Changed)); cases.push((failure, status::ACTIVATION_PROTOCOL)); // The record: one that does not decode, and a cold boot's. let failure = failed(restricted().input(now(), ActivationInput::Activate(b"\x01"))); cases.push((failure, status::ACTIVATION_RECORD)); - let cold = ActivationRecord::new(None, record(1, 1000).configuration().clone()).unwrap(); + let cold = ActivationRecord::new(None, record(1, b"clone").configuration().clone()).unwrap(); let cold = cold.encode(); let failure = failed(restricted().input(now(), ActivationInput::Activate(&cold))); assert!(matches!(failure.cause(), ActivationCause::Cold)); diff --git a/tests/runtime-adapter/tests/clone_start.rs b/tests/runtime-adapter/tests/clone_start.rs index 578dc06e5..eca730b9e 100644 --- a/tests/runtime-adapter/tests/clone_start.rs +++ b/tests/runtime-adapter/tests/clone_start.rs @@ -825,9 +825,8 @@ fn a_clone_s_request_takes_the_staged_bindings_and_the_guest_s_defaults() { clone.policy_revision, nvx::host::sandbox::distro::DISTRO_POLICY_REVISION ); - assert_eq!(clone.workload.hostname, b"runtime-adapter"); - assert_eq!(clone.workload.machine_id, MachineIdMode::Image); - assert_eq!(clone.workload.identity, None); + assert_eq!(clone.view.hostname, b"runtime-adapter"); + assert_eq!(clone.view.machine_id, MachineIdMode::Image); assert_eq!(clone.overlay, nvx::activation::OverlayMode::TrustedLowers); assert_eq!(clone.journal, nvx::activation::Journal::Allowed); assert_eq!(clone.first_partition_bound, DEFAULT_FIRST_PARTITION_BOUND); @@ -844,15 +843,8 @@ fn a_clone_s_request_takes_the_staged_bindings_and_the_guest_s_defaults() { let clone = named .clone_request(plan.clone(), None, &bindings, processors, memory) .unwrap(); - assert_eq!( - clone.workload.identity, - Some(nvx::activation::WorkloadIdentity { - uid: 65534, - gid: 65533 - }) - ); - assert_eq!(clone.workload.hostname, b"runtime-adapter"); - assert_eq!(clone.workload.machine_id, MachineIdMode::Generated); + assert_eq!(clone.view.hostname, b"runtime-adapter"); + assert_eq!(clone.view.machine_id, MachineIdMode::Generated); assert_eq!(clone.overlay, nvx::activation::OverlayMode::UntrustedLowers); assert_eq!(clone.journal, nvx::activation::Journal::Forbidden); assert_eq!(clone.first_partition_bound, 4096); diff --git a/tests/runtime-adapter/tests/guest_session.rs b/tests/runtime-adapter/tests/guest_session.rs index 61777bc98..1fcd34af2 100644 --- a/tests/runtime-adapter/tests/guest_session.rs +++ b/tests/runtime-adapter/tests/guest_session.rs @@ -29,7 +29,7 @@ //! activation, retries of the `Activate`, the order that the worker serves once the clone is //! ready, the status of each class of the activation's failures, which the worker answers with //! nothing, before any side effect where the class allows, its deadline, also once the stream -//! takes nothing more, the record's workload identity, as which every execution runs, the +//! takes nothing more, the identity that each execution names, the //! mapping table of the record's export, which the worker applies from the export that the //! application mounted, while it leaves the endpoint of the network's proxy to the host, and the //! announcer of the listeners of a record whose network publishes them, whose end ends the clone, @@ -1395,8 +1395,8 @@ impl RootOperations for FakeRoot { /// the gate, if any: for a GPT disk, it stands for the kernel's reread of the disk's partition /// table first, which fails as the kernel does while a partition is open if the test fails /// [`Operation::Reread`]; it then takes as long as the assembly, but no longer than until - /// `deadline`, as the waits for the devices do; like the application, it checks no account of - /// the record's workload identity. It then stands for the generation of the machine ID + /// `deadline`, as the waits for the devices do; like the application, it checks no account. + /// It then stands for the generation of the machine ID /// that the record asks for, if any, the mount of the record's export through the template's /// host filesystem device, if it has one, the write of the resolver file that names the DNS /// server of the record's network, if it names one, the start of the announcer of the @@ -1442,8 +1442,8 @@ impl RootOperations for FakeRoot { None => self.assembly, }; thread::sleep(assembly); - let workload = &configuration.workload; - if workload.machine_id == MachineIdMode::Generated { + let view = &configuration.view; + if view.machine_id == MachineIdMode::Generated { self.result(Operation::MachineId) .map_err(|problem| ApplyError::MachineId { error: io::Error::other(problem), @@ -4125,12 +4125,10 @@ fn the_worker_runs_executions_on_a_tty() { #[cfg(target_arch = "x86_64")] mod clones { use std::net::{IpAddr, Ipv4Addr}; - use std::num::NonZeroU64; use nvx::activation::{ self, ActivationRecord, Configuration, ExpectedResources, MachineIdMode, ReadyActivation, - RestoreSection, StoreGeneration, TemplateReference, TransactionId, WorkloadIdentity, - WorkloadView, + RestoreSection, TransactionId, View, }; use nvx::guest::clone::{Activation, AdmissionFence, RootTemplate}; use nvx::protocol::nvxs::{GuestEvent, GuestSession}; @@ -4165,36 +4163,27 @@ mod clones { /// The IPv4 gateway of the NIC of the clones' template. const GATEWAY: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1); - /// Returns the record of a clone's Activate of the transaction `transaction`, whose workload - /// runs as `identity`, if any: an EROFS image over a scratch, without an export, a mapping, or + /// Returns the record of a clone's Activate of the transaction `transaction`: + /// an EROFS image over a scratch, without an export, a mapping, or /// a network, which expects [`RESOURCES`]. - fn record(transaction: u8, identity: Option<(u32, u32)>) -> ActivationRecord { + fn record(transaction: u8) -> ActivationRecord { let configuration = Configuration { - image: activation::Image::Erofs { - binding: activation::Identity::from_bytes([0x11; 32]), - uuid: None, - }, + image: activation::Image::Erofs { uuid: None }, upper: activation::Upper::Scratch { - binding: activation::Identity::from_bytes([0x22; 32]), journal: activation::Journal::Forbidden, }, overlay: activation::OverlayMode::UntrustedLowers, export: None, mappings: 0, network: None, - workload: WorkloadView { + view: View { hostname: b"clone".to_vec(), machine_id: MachineIdMode::Image, - identity: identity.map(|(uid, gid)| WorkloadIdentity { uid, gid }), }, resources: RESOURCES, }; let restore = RestoreSection { transaction: TransactionId::from_bytes([transaction; 16]), - template: TemplateReference { - generation: StoreGeneration::new(NonZeroU64::MIN), - key: activation::Identity::from_bytes([0x44; 32]), - }, }; ActivationRecord::new(Some(restore), configuration).unwrap() } @@ -4215,7 +4204,6 @@ mod clones { changed(record, |configuration| { configuration.export = Some(activation::Export { kind: activation::ExportKind::Aggregate, - plan: activation::Identity::from_bytes([0x33; 32]), }); configuration.mappings = mappings; }) @@ -4226,7 +4214,6 @@ mod clones { fn networked(record: &ActivationRecord, proxy: Option<&str>) -> ActivationRecord { changed(record, |configuration| { configuration.network = Some(activation::Network { - policy: activation::Identity::from_bytes([0x55; 32]), dns: Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 3))), publication: activation::Publication::Unpublished, proxy: proxy.map(str::to_owned), @@ -4428,7 +4415,7 @@ mod clones { // The worker sends no attachment: it acknowledges the reset that the restore queued, with // its receive window, first. broker.reset(RESTORED); - let record = record(1, Some(WORKLOAD)); + let record = record(1); let replies = broker.exchange(&[activate(&record)], OUTER_MAX_PAYLOAD, 1); assert_eq!(replies, [activated(&record)]); let Message::Ready(body) = &replies[0] else { @@ -4526,7 +4513,7 @@ mod clones { fn a_clone_s_activation_fails_finally_with_the_status_of_the_failure_s_class() { // The activation refuses a record that does not decode, a cold boot's, and one that // expects other resources than the repair found, before anything has a side effect. - let clone = record(1, None); + let clone = record(1); let cold = ActivationRecord::new(None, clone.configuration().clone()).unwrap(); let more = changed(&clone, |configuration| configuration.resources.memory *= 2); let cases = [ @@ -4554,13 +4541,12 @@ mod clones { // Ready. let gpt = changed(&clone, |configuration| { configuration.image = activation::Image::Gpt { - binding: activation::Identity::from_bytes([0x11; 32]), disk_guid: None, first_partition_bound: 1 << 20, }; }); let generated = changed(&clone, |configuration| { - configuration.workload.machine_id = MachineIdMode::Generated; + configuration.view.machine_id = MachineIdMode::Generated; }); let cases = [ ( @@ -4609,7 +4595,7 @@ mod clones { // can have before it takes any byte of its body, and the activation takes it as a record // that long: it fails with the record's status, whether the clone is ready or not, and the // worker answers nothing more. - let accepted = record(1, Some(WORKLOAD)); + let accepted = record(1); let kind = activate(&accepted).kind(); for length in [MAX_ACTIVATE_BODY + 1, usize::try_from(u32::MAX).unwrap()] { let mut header = u32::try_from(length).unwrap().to_le_bytes().to_vec(); @@ -4651,7 +4637,7 @@ mod clones { // activation, before anything has a side effect. The broker sends what it sends before the // worker runs, which then takes it at once: a worker that runs only once the deadline // passed still acknowledges the reset, and fails with the deadline's status. - let record = record(1, Some(WORKLOAD)); + let record = record(1); let bytes = activate(&record).encode().unwrap(); let cases: [Option<&[u8]>; 3] = [None, Some(&[]), Some(&bytes[..bytes.len() / 2])]; for sent in cases { @@ -4703,7 +4689,7 @@ mod clones { let started = Instant::now(); let (mut broker, working) = start_clone(root, timeout); broker.reset(RESTORED); - let record = record(1, Some(WORKLOAD)); + let record = record(1); broker.send_data(&activate(&record).encode().unwrap(), OUTER_MAX_PAYLOAD); let root = unactivated(&mut broker, working, status::ACTIVATION_DEADLINE); let ended = Instant::now(); @@ -4724,7 +4710,7 @@ mod clones { // its Activate, before Ready, which the worker takes before it completes the activation: // another reset, which the worker acknowledges, or a request fails the activation, with // the status of the host's session, after one application, and with no Ready. - let record = record(1, Some(WORKLOAD)); + let record = record(1); let bytes = activate(&record).encode().unwrap(); for reset in [true, false] { let (root, gate) = FakeRoot::booting(&layout()).gated(); @@ -4803,7 +4789,7 @@ mod clones { // Whether the restore's reset came or not, or part of an Activate, a broker that closes // the stream before the clone is ready ends the clone with the status of the host's // session, with nothing applied, and its fence closed. - let record = record(1, Some(WORKLOAD)); + let record = record(1); let bytes = activate(&record).encode().unwrap(); let cases: [Option<&[u8]>; 3] = [None, Some(&[]), Some(&bytes[..bytes.len() / 2])]; for sent in cases { @@ -4880,11 +4866,14 @@ mod clones { // An Activate of another transaction, or of the same transaction with another record, once // the clone is ready or its sandbox started, fails the activation with the status of the // host's session. The worker leaves the root that it assembled to its operations. - let accepted = record(1, Some(WORKLOAD)); + let accepted = record(1); + let changed = changed(&accepted, |configuration| { + configuration.view.hostname = b"another-clone".to_vec(); + }); let cases = [ - (record(2, Some(WORKLOAD)), false), - (record(1, None), false), - (record(1, None), true), + (record(2), false), + (changed.clone(), false), + (changed, true), ]; for (other, started) in cases { let (mut broker, working) = @@ -4915,20 +4904,17 @@ mod clones { #[test] fn a_clone_s_executions_name_their_own_identity_and_take_the_hostname_of_its_record() { - // A clone has no workload identity of the whole sandbox, whether its record names one or - // not: each start names its execution's identity, as in a sandbox without a workload + // A clone has no workload identity of the whole sandbox: each start names its + // execution's identity, as in a sandbox without a workload // identity, and a start that names none is refused. Every execution's entry takes the // record's hostname. let refused = LaunchFailure::new(0, Errno::EINVAL as i32); let workload = exec::Identity::new(WORKLOAD.0, WORKLOAD.1); let identity = entry::Identity::new(WORKLOAD.0, WORKLOAD.1); - let renamed = changed(&record(1, None), |configuration| { - configuration.workload.hostname = b"another-clone".to_vec(); + let renamed = changed(&record(1), |configuration| { + configuration.view.hostname = b"another-clone".to_vec(); }); - let records = [ - (record(1, Some(WORKLOAD)), &b"clone"[..]), - (renamed, &b"another-clone"[..]), - ]; + let records = [(record(1), &b"clone"[..]), (renamed, &b"another-clone"[..])]; for (record, hostname) in records { let mut host = Host::activated(FakeRoot::booting(&layout()), &record); host.start(start_of(1, shell("exit 0")), Vec::new()); @@ -4956,9 +4942,9 @@ mod clones { Capability::CHOWN, ]; for (record, publication) in [ - (record(1, None), Publication::Unpublished), - (networked(&record(1, None), None), Publication::Unpublished), - (published(&record(1, None)), Publication::Published), + (record(1), Publication::Unpublished), + (networked(&record(1), None), Publication::Unpublished), + (published(&record(1)), Publication::Published), ] { let mut host = Host::activated(FakeRoot::booting(&layout()), &record); for (id, capability) in (1..).zip(capabilities) { @@ -5001,7 +4987,7 @@ mod clones { // with an execution running, ends the clone with the network's status, once the worker // dropped its executions: the gateway would keep publishing the listeners that the // announcer last announced, and learn of no later change of them. - let record = published(&record(1, None)); + let record = published(&record(1)); for started in [false, true] { let (root, ending) = FakeRoot::booting(&layout()).ending(); let (mut broker, working) = start_clone(root, ACTIVATION_TIMEOUT); @@ -5091,7 +5077,7 @@ mod clones { // executions running or not: the restore's reset came after the repair's acknowledgement, // then the host's first message, the clone's Activate, then its Ready and its start, and // no execution yet. - let record = record(1, Some(WORKLOAD)); + let record = record(1); let mut host = Host::activated(FakeRoot::booting(&layout()), &record); let probe = Message::Probe(ACTIVATION_PROBE.as_bytes().to_vec()); host.send(std::slice::from_ref(&probe)); @@ -5134,7 +5120,7 @@ mod clones { let table = table(3); let parts = parts(&table, 2); assert_eq!(parts.len(), 2); - let record = exported(&record(1, Some(WORKLOAD)), table.len()); + let record = exported(&record(1), table.len()); let record = networked(&record, Some("http://10.0.0.1:3128")); let mut host = Host::mapped(FakeRoot::booting(&layout()), &record, &parts); // The host tells the workloads the endpoint of the network's proxy, as after a cold boot: diff --git a/tests/runtime-adapter/tests/vm_clone.rs b/tests/runtime-adapter/tests/vm_clone.rs index 01241c00d..21b4414ce 100644 --- a/tests/runtime-adapter/tests/vm_clone.rs +++ b/tests/runtime-adapter/tests/vm_clone.rs @@ -66,10 +66,10 @@ //! child of its own, and whose mappings, each from a source that names its child by its index, //! keep their access, show what the host holds, but for the paths that the start hides, and //! those inside them that it shows again, and take what the guest, and the clone's workload, -//! write through the writable ones alone. The clone's record names the export, an aggregate, by -//! the identity of its plan, with the number of its mappings. +//! write through the writable ones alone. The clone's record names the export's aggregate +//! shape and the number of its mappings, without its host plan identity. //! - Clones of one generation at once see only their own exports: each its own directory, or two -//! the same one, which their records name by the same identity, and a clone without an export +//! the same one, without recording a host plan identity, and a clone without an export //! none, whose restore leaves the slot dormant. A clone mounts its export read-write, even where //! every mapping is read-only, mount tables list the bind of a generated machine ID below the //! root beside the mappings, and the bind holds even against the guest's root. @@ -79,9 +79,9 @@ //! the guest may write, that another directory replaced. //! - A clone whose guest fails a mapping of its table ends before its sandbox starts, nothing of //! it runs, and the host's directory stays as it was. -//! - A clone runs the executions that name their identity, once it is ready, whatever workload -//! identity its record names, and refuses one that names none: the repair acknowledged the -//! restore before the clone took its reset, the host's first message came after that, and the +//! - A clone runs the executions that name their identity, once it is ready, and refuses one that +//! names none: the repair acknowledged the restore before the clone took its reset, +//! the host's first message came after that, and the //! clone's Ready, its start, and its first execution came in that order. //! - OpenVMM reads none of the host's input until the clone's repair acknowledges its restore, as //! the host sees it: a template whose initramfs delays each clone's repair, for tests, holds a @@ -3285,7 +3285,7 @@ fn a_clone_whose_view_cannot_be_set_ends_before_it_is_ready() { #[ignore = "boots VMs; see the module documentation for what they need"] fn a_clone_runs_the_executions_that_name_their_identity_once_it_is_ready() { let template = Template::create("cl-ident"); - // The clone's record names a workload identity, which its guest ignores. + // The legacy workload option is not encoded; each execution names its own identity. let options = ["--workload-identity", NOBODY]; let start = StartOf { upper: Upper::Scratch, @@ -4376,8 +4376,8 @@ fn a_clone_maps_an_aggregate_of_directories_and_files_beneath_its_root_before_it let mut clone = template.start("cl-export-1", start, 0); clone.assert_mapped(EXPORT_SHARES.len()); - // The clone's record names the export, an aggregate, by the identity of its plan, and the - // number of its mappings, which the restore sequence sent once the clone was ready. NVX + // The clone's record names the export's aggregate shape and the number of its mappings, + // which the restore sequence sent once the clone was ready. NVX // attached the aggregate to the template's dormant filesystem slot as a cold launch attaches // one: its children, the outermost shares, in order, `dir` read-write, since the guest may // write `dir/inner` in it, and nothing else of it, each file with its access, and `out`, then @@ -4538,28 +4538,19 @@ fn clones_at_once_see_only_their_own_exports() { } without.assert_started(); - // Each record names its clone's export, an aggregate of one directory, by the identity of its - // plan, which the clones of the same export share, and one mapping; the clone without an - // export has none, and no mapping, and NVX restored it with none of OpenVMM's options of an + // Each record names its clone's export, an aggregate of one directory, and one mapping, + // without its host plan identity; the clone without an export has none, and no mapping, + // and NVX restored it with none of OpenVMM's options of an // export, which leaves the template's filesystem slot dormant. NVX attached each directory to // the slot as its aggregate's one child, with the access that its mapping needs. let exported = |clone: &Cloned| { let configuration = clone.configuration(); - let export = configuration - .export - .map(|export| (export.kind, export.plan)); + let export = configuration.export.map(|export| export.kind); (export, configuration.mappings) }; - let (own_export, own_mappings) = exported(with_own); - let (common_export, common_mappings) = exported(with_common); - let (own_export, common_export) = (own_export.unwrap(), common_export.unwrap()); - assert_eq!((own_export.0, own_mappings), (ExportKind::Aggregate, 1)); - assert_eq!( - (common_export.0, common_mappings), - (ExportKind::Aggregate, 1) - ); - assert_ne!(own_export.1, common_export.1); - assert_eq!(exported(with_same), (Some(common_export), 1)); + for clone in [&*with_own, &*with_common, &*with_same] { + assert_eq!(exported(clone), (Some(ExportKind::Aggregate), 1)); + } assert_eq!(exported(without), (None, 0)); with_own.assert_vmm_export(&own.attached(&[(".", "rw")])); with_common.assert_vmm_export(&common.attached(&[(".", "ro")])); @@ -4912,10 +4903,9 @@ fn one_template_serves_the_clones_of_every_network_policy_at_its_guest_s_address // policy of the network that its creation named, which publishes the guest's listeners, and // forwards a port of host loopback. let template = Template::in_network("cl-pol", Network::Forward); - let mut policies = Vec::new(); for network in Network::WITH_NIC { // The restore of a clone of each network installs the network's policy on the template's - // NIC, and the clone's record names the policy, the DNS server that OpenVMM serves the + // NIC, and the clone's record names the DNS server that OpenVMM serves the // guest under it, where it lets the guest reach the gateway's, the proxy, if any, and // whether it publishes the workload's listeners. let own = template.network_options(network, GUEST_NETWORK); @@ -4931,7 +4921,6 @@ fn one_template_serves_the_clones_of_every_network_policy_at_its_guest_s_address (&network.dns(), &network.proxy(), network.publication()), "{network:?}" ); - policies.push((network, recorded.policy)); cloned.assert_vmm_network(&network.restored(GUEST_NETWORK)); assert_eq!(cloned.script(NETWORK_VIEW)[0], "devices 1", "{network:?}"); @@ -4982,12 +4971,6 @@ fn one_template_serves_the_clones_of_every_network_policy_at_its_guest_s_address refused.assert_unserved(problem); } - // Each clone's record names a policy of its own. - for (index, (network, policy)) in policies.iter().enumerate() { - for (other, other_policy) in &policies[index + 1..] { - assert_ne!(policy, other_policy, "{network:?} and {other:?}"); - } - } assert!(template.leases().is_empty()); assert_payload(&template, false); } From 9eee18a58713a3843332337af58c620e7b583217 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Thu, 5 Nov 2026 20:22:00 -0800 Subject: [PATCH 2/3] activation: align the public API snapshots with record v2 List the transaction-only restore section, View and bounded tmpfs cap, and remove the fields and types that no activation record carries. Regenerate both platform snapshots from the shared crate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3532e012-2beb-4ff7-81ab-3795de501985 --- nvx-rs/public-api/linux.txt | 23 +++++------------------ nvx-rs/public-api/windows.txt | 23 +++++------------------ 2 files changed, 10 insertions(+), 36 deletions(-) diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 137011df4..a8830709f 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -108,6 +108,8 @@ variant nvx::activation::ActivationError::MissingField field nvx::activation::ActivationError::MissingField::0 variant nvx::activation::ActivationError::ProxyEndpoint variant nvx::activation::ActivationError::Ready +variant nvx::activation::ActivationError::TmpfsMemoryPercent +field nvx::activation::ActivationError::TmpfsMemoryPercent::0 variant nvx::activation::ActivationError::TooLong field nvx::activation::ActivationError::TooLong::0 variant nvx::activation::ActivationError::TooManyMappings @@ -119,7 +121,6 @@ variant nvx::activation::ActivationError::Version field nvx::activation::ActivationError::Version::0 variant nvx::activation::ActivationError::WithoutNetwork field nvx::activation::ActivationError::WithoutNetwork::0 -variant nvx::activation::ActivationError::WorkloadIdentity struct nvx::activation::ActivationRecord method nvx::activation::ActivationRecord::configuration method nvx::activation::ActivationRecord::decode @@ -136,13 +137,12 @@ field nvx::activation::Configuration::network field nvx::activation::Configuration::overlay field nvx::activation::Configuration::resources field nvx::activation::Configuration::upper -field nvx::activation::Configuration::workload +field nvx::activation::Configuration::view struct nvx::activation::ExpectedResources field nvx::activation::ExpectedResources::cpus field nvx::activation::ExpectedResources::memory struct nvx::activation::Export field nvx::activation::Export::kind -field nvx::activation::Export::plan enum nvx::activation::ExportKind variant nvx::activation::ExportKind::Aggregate variant nvx::activation::ExportKind::Directory @@ -161,7 +161,6 @@ variant nvx::activation::Field::Proxy variant nvx::activation::Field::Resources variant nvx::activation::Field::Restore variant nvx::activation::Field::Upper -variant nvx::activation::Field::WorkloadIdentity method nvx::activation::Field::from_tag method nvx::activation::Field::tag struct nvx::activation::Identity @@ -170,13 +169,10 @@ method nvx::activation::Identity::as_bytes method nvx::activation::Identity::from_bytes enum nvx::activation::Image variant nvx::activation::Image::Erofs -field nvx::activation::Image::Erofs::binding field nvx::activation::Image::Erofs::uuid variant nvx::activation::Image::Gpt -field nvx::activation::Image::Gpt::binding field nvx::activation::Image::Gpt::disk_guid field nvx::activation::Image::Gpt::first_partition_bound -method nvx::activation::Image::binding method nvx::activation::Image::expected method nvx::activation::Image::format enum nvx::activation::Journal @@ -191,7 +187,6 @@ variant nvx::activation::MachineIdMode::Generated variant nvx::activation::MachineIdMode::Image struct nvx::activation::Network field nvx::activation::Network::dns -field nvx::activation::Network::policy field nvx::activation::Network::proxy field nvx::activation::Network::publication enum nvx::activation::OverlayMode @@ -208,7 +203,6 @@ method nvx::activation::ReadyActivation::encode method nvx::activation::ReadyActivation::of field nvx::activation::ReadyActivation::transaction struct nvx::activation::RestoreSection -field nvx::activation::RestoreSection::template field nvx::activation::RestoreSection::transaction struct nvx::activation::StoreGeneration method nvx::activation::StoreGeneration::get @@ -222,20 +216,13 @@ method nvx::activation::TransactionId::as_bytes method nvx::activation::TransactionId::from_bytes enum nvx::activation::Upper variant nvx::activation::Upper::Scratch -field nvx::activation::Upper::Scratch::binding field nvx::activation::Upper::Scratch::journal variant nvx::activation::Upper::Tmpfs +field nvx::activation::Upper::Tmpfs::max_memory_percent const nvx::activation::VERSION struct nvx::activation::View field nvx::activation::View::hostname field nvx::activation::View::machine_id -struct nvx::activation::WorkloadIdentity -field nvx::activation::WorkloadIdentity::gid -field nvx::activation::WorkloadIdentity::uid -struct nvx::activation::WorkloadView -field nvx::activation::WorkloadView::hostname -field nvx::activation::WorkloadView::identity -field nvx::activation::WorkloadView::machine_id mod nvx::contract const nvx::contract::BOOT_CONTRACT_VERSION const nvx::contract::CONTROL_CONTRACT_REVISION @@ -2880,7 +2867,7 @@ field nvx::host::template::CloneRequest::overlay field nvx::host::template::CloneRequest::policy_revision field nvx::host::template::CloneRequest::processors field nvx::host::template::CloneRequest::scratch -field nvx::host::template::CloneRequest::workload +field nvx::host::template::CloneRequest::view enum nvx::host::template::CloneUpper variant nvx::host::template::CloneUpper::Scratch field nvx::host::template::CloneUpper::Scratch::0 diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index 10a1af6c6..ab8e7d184 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -108,6 +108,8 @@ variant nvx::activation::ActivationError::MissingField field nvx::activation::ActivationError::MissingField::0 variant nvx::activation::ActivationError::ProxyEndpoint variant nvx::activation::ActivationError::Ready +variant nvx::activation::ActivationError::TmpfsMemoryPercent +field nvx::activation::ActivationError::TmpfsMemoryPercent::0 variant nvx::activation::ActivationError::TooLong field nvx::activation::ActivationError::TooLong::0 variant nvx::activation::ActivationError::TooManyMappings @@ -119,7 +121,6 @@ variant nvx::activation::ActivationError::Version field nvx::activation::ActivationError::Version::0 variant nvx::activation::ActivationError::WithoutNetwork field nvx::activation::ActivationError::WithoutNetwork::0 -variant nvx::activation::ActivationError::WorkloadIdentity struct nvx::activation::ActivationRecord method nvx::activation::ActivationRecord::configuration method nvx::activation::ActivationRecord::decode @@ -136,13 +137,12 @@ field nvx::activation::Configuration::network field nvx::activation::Configuration::overlay field nvx::activation::Configuration::resources field nvx::activation::Configuration::upper -field nvx::activation::Configuration::workload +field nvx::activation::Configuration::view struct nvx::activation::ExpectedResources field nvx::activation::ExpectedResources::cpus field nvx::activation::ExpectedResources::memory struct nvx::activation::Export field nvx::activation::Export::kind -field nvx::activation::Export::plan enum nvx::activation::ExportKind variant nvx::activation::ExportKind::Aggregate variant nvx::activation::ExportKind::Directory @@ -161,7 +161,6 @@ variant nvx::activation::Field::Proxy variant nvx::activation::Field::Resources variant nvx::activation::Field::Restore variant nvx::activation::Field::Upper -variant nvx::activation::Field::WorkloadIdentity method nvx::activation::Field::from_tag method nvx::activation::Field::tag struct nvx::activation::Identity @@ -170,13 +169,10 @@ method nvx::activation::Identity::as_bytes method nvx::activation::Identity::from_bytes enum nvx::activation::Image variant nvx::activation::Image::Erofs -field nvx::activation::Image::Erofs::binding field nvx::activation::Image::Erofs::uuid variant nvx::activation::Image::Gpt -field nvx::activation::Image::Gpt::binding field nvx::activation::Image::Gpt::disk_guid field nvx::activation::Image::Gpt::first_partition_bound -method nvx::activation::Image::binding method nvx::activation::Image::expected method nvx::activation::Image::format enum nvx::activation::Journal @@ -191,7 +187,6 @@ variant nvx::activation::MachineIdMode::Generated variant nvx::activation::MachineIdMode::Image struct nvx::activation::Network field nvx::activation::Network::dns -field nvx::activation::Network::policy field nvx::activation::Network::proxy field nvx::activation::Network::publication enum nvx::activation::OverlayMode @@ -208,7 +203,6 @@ method nvx::activation::ReadyActivation::encode method nvx::activation::ReadyActivation::of field nvx::activation::ReadyActivation::transaction struct nvx::activation::RestoreSection -field nvx::activation::RestoreSection::template field nvx::activation::RestoreSection::transaction struct nvx::activation::StoreGeneration method nvx::activation::StoreGeneration::get @@ -222,20 +216,13 @@ method nvx::activation::TransactionId::as_bytes method nvx::activation::TransactionId::from_bytes enum nvx::activation::Upper variant nvx::activation::Upper::Scratch -field nvx::activation::Upper::Scratch::binding field nvx::activation::Upper::Scratch::journal variant nvx::activation::Upper::Tmpfs +field nvx::activation::Upper::Tmpfs::max_memory_percent const nvx::activation::VERSION struct nvx::activation::View field nvx::activation::View::hostname field nvx::activation::View::machine_id -struct nvx::activation::WorkloadIdentity -field nvx::activation::WorkloadIdentity::gid -field nvx::activation::WorkloadIdentity::uid -struct nvx::activation::WorkloadView -field nvx::activation::WorkloadView::hostname -field nvx::activation::WorkloadView::identity -field nvx::activation::WorkloadView::machine_id mod nvx::contract const nvx::contract::BOOT_CONTRACT_VERSION const nvx::contract::CONTROL_CONTRACT_REVISION @@ -1802,7 +1789,7 @@ field nvx::host::template::CloneRequest::overlay field nvx::host::template::CloneRequest::policy_revision field nvx::host::template::CloneRequest::processors field nvx::host::template::CloneRequest::scratch -field nvx::host::template::CloneRequest::workload +field nvx::host::template::CloneRequest::view enum nvx::host::template::CloneUpper variant nvx::host::template::CloneUpper::Scratch field nvx::host::template::CloneUpper::Scratch::0 From 02d4219ff07e74622fe3178ad9e7f5b7d1ea3975 Mon Sep 17 00:00:00 2001 From: Enrique Saurez Date: Fri, 6 Nov 2026 00:13:28 -0800 Subject: [PATCH 3/3] activation: correct the cold-boot fixture description Describe the fixture as having neither an export nor a network. This only repairs the test helper documentation; executable code and assertions are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3532e012-2beb-4ff7-81ab-3795de501985 --- nvx-rs/src/activation/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nvx-rs/src/activation/tests.rs b/nvx-rs/src/activation/tests.rs index ec8afdef8..172f71b36 100644 --- a/nvx-rs/src/activation/tests.rs +++ b/nvx-rs/src/activation/tests.rs @@ -8,8 +8,8 @@ use super::*; /// The disk GUID of the tests' GPT images. const DISK_GUID: &str = "fedcba98-7654-3210-fedc-ba9876543210"; -/// Returns the configuration of the tests: an EROFS image over a tmpfs, without an export, a -/// or network. +/// Returns the configuration of the tests: an EROFS image over a tmpfs, without an export or a +/// network. pub(super) fn configuration() -> Configuration { Configuration { image: Image::Erofs { uuid: None },