diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 6bc521ddb..69d2093ce 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -1429,12 +1429,10 @@ For a GPT disk, it first asks the kernel to reread the disk's partition table 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. With -the record's workload identity, it checks its account in the assembled root -before Ready, as NVX's init checks it at boot: `entry::Identity::new(uid, gid)`, -without supplementary groups or capabilities, in existing-account mode, as which -every execution runs, while without one each execution names its own identity, -which `Entry::check` checks. With the record's generated machine ID, it then +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 +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 that no two clones share it, to `MACHINE_ID_FILE` in the template's runtime directory. With the record's export, it mounts the host export through that diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 3bfd1226d..31219c675 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -675,7 +675,6 @@ struct nvx::guest::clone::Applied (hidden) method nvx::guest::clone::Applied::announcer (hidden) method nvx::guest::clone::Applied::announcer_mut (hidden) method nvx::guest::clone::Applied::hostname (hidden) -method nvx::guest::clone::Applied::identity (hidden) method nvx::guest::clone::Applied::into_parts (hidden) method nvx::guest::clone::Applied::machine_id (hidden) method nvx::guest::clone::Applied::proxy (hidden) @@ -688,9 +687,6 @@ struct nvx::guest::clone::AppliedTeardownError (hidden) method nvx::guest::clone::AppliedTeardownError::binds (hidden) method nvx::guest::clone::AppliedTeardownError::root (hidden) enum nvx::guest::clone::ApplyError (hidden) -variant nvx::guest::clone::ApplyError::Account (hidden) -field nvx::guest::clone::ApplyError::Account::error (hidden) -field nvx::guest::clone::ApplyError::Account::teardown (hidden) variant nvx::guest::clone::ApplyError::Assembly (hidden) field nvx::guest::clone::ApplyError::Assembly::0 (hidden) variant nvx::guest::clone::ApplyError::Export (hidden) diff --git a/nvx-rs/src/activation/mod.rs b/nvx-rs/src/activation/mod.rs index c47ee54b3..9a2972203 100644 --- a/nvx-rs/src/activation/mod.rs +++ b/nvx-rs/src/activation/mod.rs @@ -44,10 +44,8 @@ //! 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`, whose account the image must hold, without supplementary groups or -//! capabilities, in which every execution runs, as each execution of a layered sandbox runs -//! today. An adapter whose executions name their own identity, as CRI containers do, leaves it -//! out; +//! 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; //! - 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 @@ -461,8 +459,8 @@ pub enum MachineIdMode { Generated, } -/// The identity in which every execution of a workload runs: a UID and a GID, neither 0 nor -/// `u32::MAX`, whose account the image must hold, without supplementary groups or capabilities. +/// 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`. @@ -478,7 +476,8 @@ pub struct WorkloadView { pub hostname: Vec, /// The machine ID that the workload sees. pub machine_id: MachineIdMode, - /// The identity in which every execution runs, unless the executions name their own. + /// A workload identity, which the guest ignores as it applies the record: each execution + /// names its own. pub identity: Option, } diff --git a/nvx-rs/src/guest/clone.rs b/nvx-rs/src/guest/clone.rs index 48c82c908..ccfa542a3 100644 --- a/nvx-rs/src/guest/clone.rs +++ b/nvx-rs/src/guest/clone.rs @@ -165,12 +165,10 @@ //! at a cold boot. The upper must be fresh: the assembly refuses one whose `upper` or `work` //! directory exists and is not empty, since a clone's root starts as its image alone, while a cold //! sandbox's scratch keeps its upper across boots; the check reads at most one entry of each, and -//! no file. If the record has a workload identity, it then checks its account in the assembled -//! root, before Ready, as NVX's init checks it at boot: the identity is -//! [`Identity::new`](super::entry::Identity::new) of the record's user and group IDs, without -//! supplementary groups or capabilities, in existing-account mode, and every execution runs as it. -//! Without one, the adapter gives each execution its own identity, which -//! [`Entry::check`](super::entry::Entry::check) checks. If the record asks for a generated machine +//! no file. It takes no identity in which the executions run from the record, whose workload +//! identity, if any, has no effect: the adapter gives each execution the identity that the +//! execution names, which [`Entry::check`](super::entry::Entry::check) checks, as for a sandbox +//! without a workload identity at a cold boot. If the record asks for a generated machine //! ID, it then generates one, once, with //! [`MachineId::generate`](super::entry::MachineId::generate), after the repair's reseed, so that //! no two clones of a template share it, to [`MACHINE_ID_FILE`] in the template's runtime diff --git a/nvx-rs/src/guest/clone/activation/apply.rs b/nvx-rs/src/guest/clone/activation/apply.rs index 45ee8bba8..96247341b 100644 --- a/nvx-rs/src/guest/clone/activation/apply.rs +++ b/nvx-rs/src/guest/clone/activation/apply.rs @@ -21,12 +21,8 @@ use std::time::{Duration, Instant}; use super::super::Devices; #[cfg(doc)] use super::Activation; -use crate::activation::{ - self, ActivationRecord, Configuration, MachineIdMode, Publication, WorkloadIdentity, -}; +use crate::activation::{self, ActivationRecord, Configuration, MachineIdMode, Publication}; use crate::gpt::Expected; -#[cfg(doc)] -use crate::guest::entry::AccountMode; use crate::guest::entry::{self, Entry, Identity, MachineId, RuntimeBinds}; use crate::guest::ingress::{Announcer, StartError}; use crate::guest::mappings::{self, Access, SourceRoot}; @@ -81,13 +77,14 @@ pub struct RootTemplate { /// A clone's root and workload, which [`apply`](fn@apply) activated: the [`Root`] that it /// assembled, which owns the root's mounts, the host export's among them, the root of the host /// export, if the record has one, the runtime's binds that it made on the root, in the caller's -/// mount namespace, the identity in which every execution runs, if the record has a workload -/// identity, the workload's hostname and machine ID, the record's, the resolver file that names -/// the DNS server of the record's network, if it names one, the endpoint of its proxy, if any, and -/// the announcer of the workload's listeners, if the record's network publishes them. +/// mount namespace, the workload's hostname and machine ID, the record's, the resolver file that +/// names the DNS server of the record's network, if it names one, the endpoint of its proxy, if +/// any, and the announcer of the workload's listeners, if the record's network publishes them. /// -/// Without a workload identity, the adapter gives each execution its own identity, which -/// [`Entry::check`] checks when the adapter prepares the execution's entry, as at a cold boot. +/// It holds no identity in which the executions run: the adapter gives each execution the +/// identity that the execution names, which [`Entry::check`] checks, and whose account +/// [`Entry::prepare`] reads in existing-account mode, when the adapter prepares the execution's +/// entry, as for a sandbox without a workload identity at a cold boot. /// Every execution's entry takes the [hostname](Self::hostname), through [`Entry::hostname`], and /// sees the [machine ID](Self::machine_id) and the [resolver file](Self::resolver) through the /// runtime's binds, which each execution's mount namespace copies from the caller's. The adapter @@ -113,8 +110,6 @@ pub struct Applied { binds: Binds, /// The assembled root. root: Root, - /// The identity in which every execution runs, if the record has a workload identity. - identity: Option, /// The hostname of every execution: the record's. hostname: Vec, /// The machine ID that every execution sees at `/etc/machine-id`. @@ -138,14 +133,6 @@ impl Applied { &mut self.root } - /// Returns the identity in which every execution runs: [`Identity::new`] of the record's - /// workload identity, without supplementary groups or capabilities, in existing-account mode, - /// whose account the root holds. Returns `None` if the record has no workload identity, so - /// that each execution names its own. - pub fn identity(&self) -> Option<&Identity> { - self.identity.as_ref() - } - /// Returns the hostname of every execution, the record's, which the adapter gives each /// execution's entry through [`Entry::hostname`], and which each execution then sets in a UTS /// namespace of its own. @@ -223,18 +210,16 @@ impl Applied { teardown_with(&mut LiveSystem, announcer, export, binds.take(), root) } - /// Returns the assembled root, the runtime's binds on it, and the identity in which every - /// execution runs, if any, for an adapter that keeps them apart: it then removes the binds - /// with [`RuntimeBinds::remove`] before it tears the root down, in the same mount namespace, - /// since dropping them leaves them mounted. It stops the announcer of the workload's - /// listeners, if the application started one, and closes the root of the host export, unless - /// the adapter [took](Self::take_export) it. - pub fn into_parts(self) -> (Root, RuntimeBinds, Option) { + /// Returns the assembled root and the runtime's binds on it, for an adapter that keeps them + /// apart: it then removes the binds with [`RuntimeBinds::remove`] before it tears the root + /// down, in the same mount namespace, since dropping them leaves them mounted. It stops the + /// announcer of the workload's listeners, if the application started one, and closes the root + /// of the host export, unless the adapter [took](Self::take_export) it. + pub fn into_parts(self) -> (Root, RuntimeBinds) { let Self { export, mut binds, root, - identity, announcer, .. } = self; @@ -242,7 +227,7 @@ impl Applied { let _ = announcer.stop(); } drop(export); - (root, binds.take(), identity) + (root, binds.take()) } } @@ -319,7 +304,9 @@ impl std::error::Error for AppliedTeardownError {} /// or the scratch, with the record's journal policy; and its overlay mode; and, from /// `template`, the runtime directory, the devices, and the timeout. It takes the workload's /// hostname and machine-ID mode, whether it has an export, and its network's DNS server, -/// proxy endpoint, and publication of the workload's listeners from the record too; +/// proxy endpoint, and publication of the workload's listeners from the record too. It takes +/// no identity in which the executions run: the record's workload identity, if any, has no +/// effect, and each execution names its own identity, which its entry checks; /// 2. for a GPT disk, asks the kernel to reread the disk's partition table, once the distro /// device appeared, so that the block devices of the disk's partitions are those of the /// clone's disk, rather than those of the template's placeholder, which the kernel created @@ -338,19 +325,13 @@ impl std::error::Error for AppliedTeardownError {} /// the assembly, as at its timeout, so that the application ends by the activation's /// deadline, past which [`Activation::complete`] fails the activation as expired, whatever /// the result; -/// 4. if the record has a workload identity, checks its account in the assembled root, before -/// Ready, as NVX's init checks it at boot: the identity is [`Identity::new`] of the record's -/// user and group IDs, without supplementary groups or capabilities, in -/// [existing-account](AccountMode::Existing) mode, whose account [`Entry::prepare`] reads and -/// checks below the root. A refusal fails with [`ApplyError::Account`], once the application -/// tore the root down; -/// 5. if the record asks for a generated machine ID, generates it, once, with +/// 4. if the record asks for a generated machine ID, generates it, once, with /// [`MachineId::generate`], from the kernel's random number generator, which the clone's /// repair reseeded before the activation accepted the record, so that no two clones of a /// template share it, to [`MACHINE_ID_FILE`] in the template's runtime directory. A file there /// already, as once a boot generated its ID, or one that cannot be created or written, fails /// with [`ApplyError::MachineId`], once the application tore the root down; -/// 6. if the record has an export, mounts the host export, through the template's host filesystem +/// 5. if the record has an export, mounts the host export, through the template's host filesystem /// device, which [`RootTemplate::filesystem_tag`] names, with [`Root::mount_export`], whatever /// the export's shape: at `hostfs/root` in the runtime directory, outside the root, where only /// the guest's root user reaches it, read-write. The record names no mode of the mount, and none @@ -361,19 +342,19 @@ impl std::error::Error for AppliedTeardownError {} /// inside a directory that a writable export of another sandbox or clone holds. A failure fails /// with [`ApplyError::Export`], once the application tore the root down, which unmounts an /// export that it mounted; -/// 7. if the record's network names a DNS server, writes a resolver file that names it, as +/// 6. if the record's network names a DNS server, writes a resolver file that names it, as /// [`write_resolver`] writes one, to [`RESOLVER_FILE`] in the template's runtime directory: a /// new file, of mode 0444, whatever the umask, so that every execution may read it. A file /// there already, or one that cannot be created or written, fails with /// [`ApplyError::Network`], once the application tore the root down; -/// 8. if the record's network publishes the workload's listeners, starts their announcer, to +/// 7. if the record's network publishes the workload's listeners, starts their announcer, to /// the gateway of [`RootTemplate::gateway`], with [`Announcer::start`]: it binds a UDP socket /// to a privileged source port, and spawns `/sbin/nvx-ingress inherit GATEWAY_IPV4` with the /// socket as its standard input, and waits until the program runs, but for no announcement. /// The template captured its NIC under a policy that publishes nothing, so no announcer ran /// in it. A template without a gateway, a socket that cannot be bound, and a program that does /// not run fail with [`ApplyError::Network`], once the application tore the root down; -/// 9. binds the runtime's files into the root, for every execution, with one +/// 8. binds the runtime's files into the root, for every execution, with one /// [`Entry::bind_runtime`], in the caller's mount namespace, of which each execution's starts /// as a copy: a generated machine ID's file, read-only over the root's `/etc/machine-id`, then /// the resolver file, read-only over the root's `/etc/resolv.conf`, and, with the image's own @@ -410,7 +391,6 @@ pub fn apply( export: activated.export, binds: Binds(Some(activated.binds)), root: activated.root, - identity: activated.identity, hostname: activated.hostname, machine_id: activated.machine_id, resolver: activated.resolver, @@ -437,16 +417,6 @@ pub enum ApplyError { /// the error holds the unmounts that failed. It fails with [`root::Error::NotFresh`] if the /// upper's `upper` or `work` directory exists and is not empty. Assembly(AssemblyError), - /// Existing-account mode refused the record's workload identity in the assembled root, which - /// the application then tore down. - Account { - /// Why the identity was refused: the root holds no account of its user ID whose primary - /// group is its group ID, and which its group database holds, or its account databases - /// cannot be read. - error: entry::Error, - /// The unmounts of the root's teardown that failed, if any, whose mounts stay. - teardown: Option, - }, /// The machine ID that the record asks the guest to generate could not be written to /// [`MACHINE_ID_FILE`] in the template's runtime directory, as this error tells: a file is /// there already, as once a boot generated its ID, or the file cannot be created or written. @@ -501,10 +471,6 @@ impl fmt::Display for ApplyError { Self::Assembly(error) => { return write!(formatter, "the root's assembly failed: {error}"); } - Self::Account { error, teardown } => { - write!(formatter, "the root refuses the workload identity: {error}")?; - teardown - } Self::MachineId { error, teardown } => { write!( formatter, @@ -540,7 +506,7 @@ impl std::error::Error for ApplyError { match self { Self::Gpt(error) | Self::Export { error, .. } => Some(error), Self::Assembly(error) => Some(error), - Self::Account { error, .. } | Self::View { error, .. } => Some(error), + Self::View { error, .. } => Some(error), Self::MachineId { error, .. } => Some(error), Self::Network { error, .. } => Some(error), } @@ -631,9 +597,6 @@ trait System { deadline: Option, ) -> Result; - /// Reads and checks the account of `identity` in `root`, as [`check_account`] does. - fn account(&mut self, root: &Self::Root, identity: &Identity) -> Result<(), entry::Error>; - /// Generates a machine ID, and writes it to a new file at `path`, as [`MachineId::generate`] /// does. fn generate(&mut self, path: &Path) -> io::Result; @@ -698,10 +661,6 @@ impl System for LiveSystem { root::assemble_fresh(assembly, deadline) } - fn account(&mut self, root: &Root, identity: &Identity) -> Result<(), entry::Error> { - check_account(root.path(), identity) - } - fn generate(&mut self, path: &Path) -> io::Result { MachineId::generate(path) } @@ -745,14 +704,9 @@ impl System for LiveSystem { } /// The workload's view of its root, which the application sets for every execution: the -/// identity of the entry through which it binds the runtime's files, the hostname, the machine -/// ID, and the resolver file. +/// hostname, the machine ID, and the resolver file. #[derive(Clone, Debug, PartialEq, Eq)] struct View { - /// The identity of the entry that binds the runtime's files: the record's workload identity, - /// or, without one, the user and group ID 0, since the entry binds the same files whatever - /// its identity. - identity: Identity, /// The hostname of every execution: the record's. hostname: Vec, /// The machine ID that every execution sees. @@ -764,9 +718,11 @@ struct View { impl View { /// Returns the entry into the root at `root` that binds the view's runtime files, and checks - /// its hostname, with [`Entry::bind_runtime`]. + /// its hostname, with [`Entry::bind_runtime`]. The entry binds the same files whatever its + /// identity, and no execution runs through it, so it takes the user and group ID 0, which + /// it checks as it checks any identity. fn entry(&self, root: &Path) -> Entry { - let entry = Entry::new(root, self.identity.clone()) + let entry = Entry::new(root, Identity::new(0, 0)) .hostname(self.hostname.clone()) .machine_id(self.machine_id.clone()); match &self.resolver { @@ -777,9 +733,9 @@ impl View { } /// What an application activated through a system, which an [`Applied`] holds in a VM: the -/// root, the root of the host export, if any, the runtime's binds on the root, the identity in -/// which every execution runs, if any, the workload's hostname and machine ID, the resolver file, -/// if any, the proxy's endpoint, if any, and the announcer of the workload's listeners, if any. +/// root, the root of the host export, if any, the runtime's binds on the root, the workload's +/// hostname and machine ID, the resolver file, if any, the proxy's endpoint, if any, and the +/// announcer of the workload's listeners, if any. #[derive(Debug)] #[cfg_attr(test, derive(PartialEq, Eq))] struct Activated { @@ -789,8 +745,6 @@ struct Activated { export: Option, /// The runtime's binds on the root. binds: B, - /// The identity in which every execution runs, if the record has a workload identity. - identity: Option, /// The hostname of every execution. hostname: Vec, /// The machine ID that every execution sees. @@ -822,7 +776,6 @@ fn apply_with( let configuration = record.configuration(); let assembly = assembly(template, configuration); let workload = &configuration.workload; - let identity = workload.identity.map(workload_identity); if let root::Image::Gpt { .. } = assembly.image { system .reread(assembly.distro, assembly.timeout, deadline) @@ -831,12 +784,6 @@ fn apply_with( let mut root = system .assemble(&assembly, deadline) .map_err(ApplyError::Assembly)?; - if let Some(identity) = &identity - && let Err(error) = system.account(&root, identity) - { - let teardown = system.teardown(root).err(); - return Err(ApplyError::Account { error, teardown }); - } let machine_id = match workload.machine_id { MachineIdMode::Image => MachineId::Image, MachineIdMode::Generated => { @@ -893,7 +840,6 @@ fn apply_with( None }; let view = View { - identity: identity.clone().unwrap_or_else(|| Identity::new(0, 0)), hostname: workload.hostname.clone(), machine_id, resolver, @@ -917,7 +863,6 @@ fn apply_with( root, export, binds, - identity, hostname: view.hostname, machine_id: view.machine_id, resolver: view.resolver, @@ -1033,16 +978,3 @@ fn assembly(template: &RootTemplate, configuration: &Configuration) -> Assembly timeout: template.timeout, } } - -/// Returns the identity in which every execution of a workload with `identity` runs: its user -/// and group IDs, without supplementary groups or capabilities, whose account an entry checks in -/// existing-account mode. -fn workload_identity(identity: WorkloadIdentity) -> Identity { - Identity::new(identity.uid, identity.gid) -} - -/// Reads and checks the account of `identity` below the root at `root`, as an entry does in -/// existing-account mode before each execution: through [`Entry::prepare`]. -fn check_account(root: &Path, identity: &Identity) -> Result<(), entry::Error> { - Entry::new(root, identity.clone()).prepare().map(drop) -} diff --git a/nvx-rs/src/guest/clone/activation/apply/tests.rs b/nvx-rs/src/guest/clone/activation/apply/tests.rs index 98afac7dd..b3b02609d 100644 --- a/nvx-rs/src/guest/clone/activation/apply/tests.rs +++ b/nvx-rs/src/guest/clone/activation/apply/tests.rs @@ -1,7 +1,7 @@ //! Tests of the application of a clone's activation record, through a fake system, which records //! every call of the application, and whose assembly returns a temporary directory that stands //! for the root, the image's view through the overlay, whose account databases the application -//! reads and checks for real, and which generates no machine ID, writes no resolver file, starts +//! leaves alone, and which generates no machine ID, writes no resolver file, starts //! no announcer, and mounts, binds, and unmounts nothing; and, with the clone's activation, //! through an adapter of //! the tests' own, which takes what the host sends, applies the record of the Activate that the @@ -28,13 +28,13 @@ use std::time::Instant; use super::*; use crate::activation::{ ExpectedResources, Export, ExportKind, MachineIdMode, Network, Publication, ReadyActivation, - RestoreSection, StoreGeneration, TemplateReference, TransactionId, WorkloadView, + RestoreSection, StoreGeneration, TemplateReference, TransactionId, WorkloadIdentity, + WorkloadView, }; use crate::guest::clone::{ Activation, ActivationAction, ActivationCause, ActivationFailure, ActivationInput, ActivationState, AdmissionFence, }; -use crate::guest::entry::{AccountError, AccountMode, Capabilities, Database}; use crate::guest::mappings::MappingError; use crate::guest::sys::{self, Errno, Mode, MountFlags, OpenFlags}; use crate::protocol::root::{DISTRO_MMIO_BASE, SCRATCH_MMIO_BASE}; @@ -82,9 +82,6 @@ const GATEWAY: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 1); /// Returns whether a failure's cause is the one that a test expects. type Expected = fn(&ActivationCause) -> bool; -/// Returns whether an account's refusal is the one that a test expects. -type Refusal = fn(&AccountError) -> bool; - /// Returns whether an application's failure is the one that a test expects. type Failed = fn(&ApplyError) -> bool; @@ -97,9 +94,6 @@ enum Effect { Reread(Device), /// The application assembled the root of this assembly, with a fresh upper, which mounts it. Assemble(Assembly), - /// The application checked this identity's account in the root, which reads the image's - /// account databases. - Account(Identity), /// The application generated a machine ID, which it wrote to a new file at this path. Generate(PathBuf), /// The application mounted the host export, the virtio-fs device of this tag, with this @@ -314,11 +308,6 @@ impl System for FakeSystem { Ok(self.root()) } - fn account(&mut self, root: &PathBuf, identity: &Identity) -> Result<(), entry::Error> { - self.effects.push(Effect::Account(identity.clone())); - check_account(root, identity) - } - fn generate(&mut self, path: &Path) -> io::Result { self.effects.push(Effect::Generate(path.to_owned())); match self.generating { @@ -439,9 +428,8 @@ struct Adapter { /// The system through which the adapter applies the record, which records what the clone did. system: FakeSystem, /// What the application activated: the root, the export's root, if any, the runtime's binds - /// on the root, the identity in which every execution runs, if any, the workload's hostname - /// and machine ID, the resolver file, if any, the proxy's endpoint, if any, and the gateway of - /// the announcer of the workload's listeners, if any. + /// on the root, the workload's hostname and machine ID, the resolver file, if any, the proxy's + /// endpoint, if any, and the gateway of the announcer of the workload's listeners, if any. applied: Option>, /// What the adapter's latest Ready reported. ready: Option, @@ -582,9 +570,9 @@ fn scratch(journal: activation::Journal) -> activation::Upper { } } -/// Returns the configuration of a root of `image` over `upper`, in `overlay` mode, whose workload -/// runs as `identity`, if any, which expects the tests' clone's resources, without an export or -/// a network. +/// 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 +/// export or a network. fn configuration( image: activation::Image, upper: activation::Upper, @@ -623,7 +611,7 @@ fn clone_record(configuration: Configuration) -> ActivationRecord { } /// Returns the record of the tests' Activate: an EROFS image, whose UUID it expects, over a -/// tmpfs, with untrusted lowers, whose workload runs as the tests' workload identity. +/// tmpfs, with untrusted lowers, which names the tests' workload identity. fn accepted() -> ActivationRecord { clone_record(configuration( erofs(Some(IMAGE_UUID)), @@ -650,11 +638,6 @@ fn accepted_assembly() -> Assembly { } } -/// Returns the identity in which the executions of the tests' workload run. -fn workload() -> Identity { - Identity::new(WORKLOAD.uid, WORKLOAD.gid) -} - /// Returns the record of the tests' Activate, as [`accepted`] returns it, but whose workload sees /// a machine ID that the guest generates. fn with_generated_id() -> ActivationRecord { @@ -718,38 +701,31 @@ fn resolver_path() -> PathBuf { } /// Returns the view that the application binds for a record of the tests' hostname, whose -/// workload runs as `identity`, if any, and sees `machine_id`, without a resolver file. -fn view(identity: Option, machine_id: MachineId) -> View { +/// workload sees `machine_id`, without a resolver file. +fn view(machine_id: MachineId) -> View { View { - identity: identity.unwrap_or_else(|| Identity::new(0, 0)), hostname: HOSTNAME.to_vec(), machine_id, resolver: None, } } -/// Returns the view that the application binds for [`with_everything`]'s record: of its -/// workload identity, with the generated machine ID and the resolver file. +/// Returns the view that the application binds for [`with_everything`]'s record: with the +/// generated machine ID and the resolver file. fn full_view() -> View { View { resolver: Some(resolver_path()), - ..view(Some(workload()), generated_id()) + ..view(generated_id()) } } /// Returns what the application activates through `system` for a record of the tests' hostname, -/// whose workload runs as `identity`, if any, and sees `machine_id`, without an export or a -/// network. -fn activated( - system: &FakeSystem, - identity: Option, - machine_id: MachineId, -) -> Activated { +/// whose workload sees `machine_id`, without an export or a network. +fn activated(system: &FakeSystem, machine_id: MachineId) -> Activated { Activated { root: system.root(), export: None, binds: (), - identity, hostname: HOSTNAME.to_vec(), machine_id, resolver: None, @@ -763,7 +739,6 @@ fn activated( fn applied_everything() -> Vec { vec![ Effect::Assemble(accepted_assembly()), - Effect::Account(workload()), Effect::Generate(machine_id_path()), Effect::MountExport(TAG.to_owned(), Access::ReadWrite), Effect::Resolver(resolver_path(), DNS), @@ -876,9 +851,9 @@ fn no_mount_spawn_or_id_generation_happens_before_the_gate() { .unwrap_err(); adapter.assert_untouched(&failure, |cause| matches!(cause, ActivationCause::Expired)); - // Only a valid Activate mounts the root, reads its account databases, generates the machine - // ID, mounts the export, writes the resolver file, starts the announcer, and binds the - // runtime's files, before Ready, and an execution spawns only once Ready opened the fence. + // Only a valid Activate mounts the root, generates the machine ID, mounts the export, writes + // the resolver file, starts the announcer, and binds the runtime's files, before Ready, and + // an execution spawns only once Ready opened the fence. let mut adapter = Adapter::restricted(FakeSystem::new(), base); adapter.take(base, ActivationInput::Flow).unwrap(); assert!(adapter.system.effects.is_empty()); @@ -899,9 +874,8 @@ fn a_valid_activate_activates_the_root_and_the_workload_once_before_ready() { adapter .take(base, ActivationInput::Activate(&record)) .unwrap(); - // The application assembled the root with the record's settings, and checked the workload - // identity's account in it, before Ready, which reports that the activation applied, with the - // record's transaction. + // The application assembled the root with the record's settings, before Ready, which reports + // that the activation applied, with the record's transaction. assert_eq!(adapter.activation.state(), ActivationState::Ready); assert_eq!( adapter.ready, @@ -911,20 +885,9 @@ fn a_valid_activate_activates_the_root_and_the_workload_once_before_ready() { ); assert_eq!( adapter.applied, - Some(activated( - &adapter.system, - Some(workload()), - MachineId::Image - )) + Some(activated(&adapter.system, MachineId::Image)) ); assert!(adapter.fence.is_open()); - // Every execution runs as the workload identity, which has no supplementary groups or - // capabilities, and whose account an entry checks in existing-account mode. - let identity = workload(); - assert_eq!((identity.uid(), identity.gid()), (1000, 1001)); - assert!(identity.supplementary_groups().is_empty()); - assert_eq!(identity.capability_set(), Capabilities::EMPTY); - assert_eq!(identity.account_mode(), AccountMode::Existing); // A later session, and a retry of the Activate in it, get the established activation // without applying the record again; requests spawn executions. @@ -940,8 +903,7 @@ fn a_valid_activate_activates_the_root_and_the_workload_once_before_ready() { adapter.system.effects, [ Effect::Assemble(accepted_assembly()), - Effect::Account(workload()), - Effect::Bind(view(Some(workload()), MachineId::Image)), + Effect::Bind(view(MachineId::Image)), Effect::Spawn, Effect::Spawn, ] @@ -951,7 +913,7 @@ fn a_valid_activate_activates_the_root_and_the_workload_once_before_ready() { } #[test] -fn assembly_and_identity_inputs_are_taken_exactly_from_the_record() { +fn assembly_inputs_are_taken_exactly_from_the_record() { let template = template(); // Each image, with what the assembly expects of it, and whether the kernel rereads its // partition table first. @@ -997,6 +959,8 @@ fn assembly_and_identity_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(); @@ -1009,16 +973,14 @@ fn assembly_and_identity_inputs_are_taken_exactly_from_the_record() { overlay: root_overlay, timeout: template.timeout, }; - let identity = identity.map(|_| workload()); let mut effects = Vec::new(); if reread { effects.push(Effect::Reread(template.distro)); } effects.push(Effect::Assemble(assembly)); - effects.extend(identity.clone().map(Effect::Account)); - effects.push(Effect::Bind(view(identity.clone(), MachineId::Image))); + effects.push(Effect::Bind(view(MachineId::Image))); assert_eq!(system.effects, effects, "{record:?}"); - assert_eq!(applied, activated(&system, identity, MachineId::Image)); + assert_eq!(applied, activated(&system, MachineId::Image)); } } } @@ -1093,8 +1055,7 @@ fn a_gpt_disk_s_partition_table_is_reread_before_its_root_is_assembled() { [ Effect::Reread(template.distro), Effect::Assemble(assembly), - Effect::Account(workload()), - Effect::Bind(view(Some(workload()), MachineId::Image)), + Effect::Bind(view(MachineId::Image)), ] ); // The reread waited for the distro device, and the assembly for each device, until the @@ -1106,91 +1067,46 @@ fn a_gpt_disk_s_partition_table_is_reread_before_its_root_is_assembled() { } #[test] -fn a_workload_identity_without_an_account_in_the_image_is_refused_before_ready() { +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. let base = Instant::now(); let record = accepted().encode(); - // Images that hold no account of the workload identity's user ID whose primary group is its - // group ID, which the group database holds, and whose home is absolute. let without_passwd = FakeSystem::new(); fs::remove_file(without_passwd.image.path().join("etc/passwd")).unwrap(); - let systems: [(FakeSystem, Refusal); 5] = [ - ( - FakeSystem::with(b"root:x:0:0:root:/root:/bin/sh\n", GROUP), - |error| matches!(error, AccountError::Missing), - ), - ( - FakeSystem::with(b"workload:x:1000:1002::/home/workload:/bin/sh\n", GROUP), - |error| matches!(error, AccountError::PrimaryGroup(1002)), - ), - (FakeSystem::with(PASSWD, b"root:x:0:\n"), |error| { - matches!(error, AccountError::Group) - }), - ( - FakeSystem::with(b"workload:x:1000:1001::home:/bin/sh\n", GROUP), - |error| matches!(error, AccountError::Home), - ), - (without_passwd, |error| { - matches!(error, AccountError::Read(Database::Passwd, _)) - }), + let systems = [ + FakeSystem::new(), + FakeSystem::with(b"root:x:0:0:root:/root:/bin/sh\n", GROUP), + FakeSystem::with(b"workload:x:1000:1002::/home/workload:/bin/sh\n", GROUP), + FakeSystem::with(PASSWD, b"root:x:0:\n"), + FakeSystem::with(b"workload:x:1000:1001::home:/bin/sh\n", GROUP), + FakeSystem::with(b"", b""), + without_passwd, ]; - for (system, refusal) in systems { + for system in systems { let mut adapter = Adapter::restricted(system, base); - let failure = adapter + adapter .take(base, ActivationInput::Activate(&record)) - .unwrap_err(); - assert_eq!(failure.state(), ActivationState::Activating); - let ApplyError::Account { - error: entry::Error::Account(error), - teardown: None, - } = apply_error(&failure) - else { - panic!("{failure}"); - }; - assert!(refusal(error), "{error}"); - // The root was assembled, its account databases read, and the root torn down, without - // a Ready, and behind the closed fence. + .unwrap(); + assert_eq!(adapter.activation.state(), ActivationState::Ready); + assert_eq!(adapter.ready, Some(ReadyActivation::of(&accepted()))); + assert_eq!( + adapter.applied, + Some(activated(&adapter.system, MachineId::Image)) + ); assert_eq!( adapter.system.effects, [ Effect::Assemble(accepted_assembly()), - Effect::Account(workload()), - Effect::Teardown, + Effect::Bind(view(MachineId::Image)), ] ); - assert_eq!(adapter.activation.state(), ActivationState::Failed); - assert_eq!(adapter.applied, None); - assert_eq!(adapter.ready, None); - assert!(!adapter.fence.is_open()); - // The failure is final: no retry applies the record again, and nothing is served. - for input in [ - ActivationInput::Activate(&record), - ActivationInput::Request, - ActivationInput::Reset, - ] { - assert!(adapter.take(base, input).is_err()); - } - assert_eq!(adapter.system.effects.len(), 3); + assert!(adapter.fence.is_open()); } - - // Without a workload identity, the application reads no account: each execution names its - // own identity, which the adapter's entry checks. The entry that binds the runtime's files - // binds the same files whatever its identity. - let anyone = clone_record(configuration( - erofs(Some(IMAGE_UUID)), - activation::Upper::Tmpfs, - activation::OverlayMode::UntrustedLowers, - None, - )); - let mut system = FakeSystem::with(b"", b""); - let applied = apply_with(&mut system, &anyone, &template(), None).unwrap(); - assert_eq!(applied.identity, None); - assert_eq!( - system.effects, - [ - Effect::Assemble(accepted_assembly()), - Effect::Bind(view(None, MachineId::Image)), - ] - ); } #[test] @@ -1227,8 +1143,8 @@ fn a_failed_assembly_is_final_and_reads_and_tears_down_nothing() { #[test] fn a_generated_machine_id_is_generated_once_after_the_gate_for_every_execution() { // The application generates the machine ID once the activation accepted the record, after - // the root's assembly and the account's check, in the template's runtime directory, and - // binds it for every execution, through the entry of the record's hostname. + // the root's assembly, in the template's runtime directory, and binds it for every + // execution, through the entry of the record's hostname. let base = Instant::now(); let record = with_generated_id(); let bytes = record.encode(); @@ -1238,14 +1154,13 @@ fn a_generated_machine_id_is_generated_once_after_the_gate_for_every_execution() .unwrap(); let expected = [ Effect::Assemble(accepted_assembly()), - Effect::Account(workload()), Effect::Generate(machine_id_path()), - Effect::Bind(view(Some(workload()), generated_id())), + Effect::Bind(view(generated_id())), ]; assert_eq!(adapter.system.effects, expected); assert_eq!( adapter.applied, - Some(activated(&adapter.system, Some(workload()), generated_id())) + Some(activated(&adapter.system, generated_id())) ); assert_eq!(adapter.ready, Some(ReadyActivation::of(&record))); assert_eq!(machine_id_path(), Path::new("/run/nvx/workload-machine-id")); @@ -1274,10 +1189,7 @@ fn a_generated_machine_id_is_generated_once_after_the_gate_for_every_execution() let mut system = FakeSystem::new(); let applied = apply_with(&mut system, &accepted(), &template(), None).unwrap(); assert_eq!(system.generated(), 0); - assert_eq!( - applied, - activated(&system, Some(workload()), MachineId::Image) - ); + assert_eq!(applied, activated(&system, MachineId::Image)); } #[test] @@ -1288,7 +1200,7 @@ fn the_view_s_entry_takes_the_record_s_hostname_and_machine_id() { let view_entry = |hostname: &[u8]| { let named = View { hostname: hostname.to_vec(), - ..view(None, MachineId::Image) + ..view(MachineId::Image) }; named.entry(Path::new(RUNTIME)) }; @@ -1305,13 +1217,13 @@ fn the_view_s_entry_takes_the_record_s_hostname_and_machine_id() { // machine ID without a resolver file: only the former open the root, which a temporary // directory, not the root of a mount, fails. let directory = tempfile::tempdir().unwrap(); - let image = view(None, MachineId::Image).entry(directory.path()); + let image = view(MachineId::Image).entry(directory.path()); image.bind_runtime().unwrap().remove().unwrap(); let resolver = View { resolver: Some(resolver_path()), - ..view(None, MachineId::Image) + ..view(MachineId::Image) }; - for bound in [view(None, generated_id()), resolver, full_view()] { + for bound in [view(generated_id()), resolver, full_view()] { let error = bound.entry(directory.path()).bind_runtime().unwrap_err(); assert!(matches!(error, entry::Error::Root(_)), "{bound:?}: {error}"); } @@ -1324,7 +1236,7 @@ fn a_machine_id_or_a_view_that_fails_fails_the_activation_once_the_root_is_torn_ // A file at the machine ID's path already, as once a boot generated its ID, and a bind that // the image refuses, as one whose `/etc/machine-id` is a directory. let generation = Effect::Generate(machine_id_path()); - let bind = Effect::Bind(view(Some(workload()), generated_id())); + let bind = Effect::Bind(view(generated_id())); let cases = [ ( FakeSystem::generating(io::ErrorKind::AlreadyExists), @@ -1352,12 +1264,8 @@ fn a_machine_id_or_a_view_that_fails_fails_the_activation_once_the_root_is_torn_ } => {} error => panic!("{error}"), } - // The root was assembled, its account checked, and the root torn down, without a Ready, - // and behind the closed fence. - let mut expected = vec![ - Effect::Assemble(accepted_assembly()), - Effect::Account(workload()), - ]; + // The root was assembled, then torn down, without a Ready, and behind the closed fence. + let mut expected = vec![Effect::Assemble(accepted_assembly())]; expected.extend(effects); expected.push(Effect::Teardown); assert_eq!(adapter.system.effects, expected); @@ -1429,8 +1337,8 @@ fn the_runtime_s_binds_are_removed_before_the_root_is_torn_down() { #[test] fn an_export_is_mounted_through_the_template_s_device_and_handed_to_the_adapter() { // A record's export, of either shape, is mounted once, before Ready, read-write, through the - // template's filesystem device, once the workload's identity was checked, and the adapter - // gets its root, to map the host's mapping table from. + // template's filesystem device, once the root was assembled, and the adapter gets its root, + // to map the host's mapping table from. let base = Instant::now(); for kind in [ExportKind::Directory, ExportKind::Aggregate] { let mut configuration = accepted().configuration().clone(); @@ -1443,16 +1351,15 @@ fn an_export_is_mounted_through_the_template_s_device_and_handed_to_the_adapter( .unwrap(); let expected = [ Effect::Assemble(accepted_assembly()), - Effect::Account(workload()), Effect::MountExport(TAG.to_owned(), Access::ReadWrite), - Effect::Bind(view(Some(workload()), MachineId::Image)), + Effect::Bind(view(MachineId::Image)), ]; assert_eq!(adapter.system.effects, expected, "{kind:?}"); assert_eq!( adapter.applied, Some(Activated { export: Some(()), - ..activated(&adapter.system, Some(workload()), MachineId::Image) + ..activated(&adapter.system, MachineId::Image) }) ); // A retry and an execution mount nothing again. @@ -1491,11 +1398,10 @@ fn a_network_s_dns_server_is_written_to_a_resolver_file_that_every_execution_see let applied = apply_with(&mut system, &record, &template(), None).unwrap(); let bound = View { resolver: Some(resolver_path()), - ..view(Some(workload()), MachineId::Image) + ..view(MachineId::Image) }; let expected = [ Effect::Assemble(accepted_assembly()), - Effect::Account(workload()), Effect::Resolver(resolver_path(), dns), Effect::Bind(bound), ]; @@ -1505,7 +1411,7 @@ fn a_network_s_dns_server_is_written_to_a_resolver_file_that_every_execution_see Activated { resolver: Some(resolver_path()), proxy: Some(PROXY.to_owned()), - ..activated(&system, Some(workload()), MachineId::Image) + ..activated(&system, MachineId::Image) } ); } @@ -1521,15 +1427,14 @@ fn a_network_s_dns_server_is_written_to_a_resolver_file_that_every_execution_see let applied = apply_with(&mut system, &record, &template(), None).unwrap(); let expected = [ Effect::Assemble(accepted_assembly()), - Effect::Account(workload()), - Effect::Bind(view(Some(workload()), MachineId::Image)), + Effect::Bind(view(MachineId::Image)), ]; assert_eq!(system.effects, expected, "{proxy:?}"); assert_eq!( applied, Activated { proxy: proxy.map(str::to_owned), - ..activated(&system, Some(workload()), MachineId::Image) + ..activated(&system, MachineId::Image) } ); } @@ -1593,7 +1498,7 @@ fn an_export_or_a_network_that_fails_fails_the_activation_once_the_root_is_torn_ let cases: [(FakeSystem, Vec, Failed); 6] = [ ( FakeSystem::unmountable(), - failed(4, false, false), + failed(3, false, false), |error| { matches!( error, @@ -1609,7 +1514,7 @@ fn an_export_or_a_network_that_fails_fails_the_activation_once_the_root_is_torn_ ), ( FakeSystem::unwritable(io::ErrorKind::AlreadyExists), - failed(5, false, true), + failed(4, false, true), |error| { matches!( error, @@ -1622,7 +1527,7 @@ fn an_export_or_a_network_that_fails_fails_the_activation_once_the_root_is_torn_ ), ( FakeSystem::unannounced(|| StartError::NoPrivilegedPort), - failed(6, false, true), + failed(5, false, true), |error| { matches!( error, @@ -1635,7 +1540,7 @@ fn an_export_or_a_network_that_fails_fails_the_activation_once_the_root_is_torn_ ), ( FakeSystem::unbindable(|| entry::Error::Resolver(MappingError::TargetKind)), - failed(7, true, true), + failed(6, true, true), |error| { matches!( error, @@ -1652,7 +1557,7 @@ fn an_export_or_a_network_that_fails_fails_the_activation_once_the_root_is_torn_ error: Box::new(entry::Error::Resolver(MappingError::TargetKind)), teardown: mappings::TeardownError::of("/etc/machine-id", MappingError::MountPoint), }), - failed(7, true, true), + failed(6, true, true), |error| { matches!( error, @@ -1665,7 +1570,7 @@ fn an_export_or_a_network_that_fails_fails_the_activation_once_the_root_is_torn_ ), ( FakeSystem::unbindable(|| entry::Error::MachineId(MappingError::TargetKind)), - failed(7, true, true), + failed(6, true, true), |error| { matches!( error, @@ -1703,11 +1608,8 @@ fn a_network_that_publishes_the_workload_s_listeners_starts_their_announcer_to_i let published = clone_record(configuration.clone()); let mut system = FakeSystem::new(); let applied = apply_with(&mut system, &published, &template(), None).unwrap(); - let bound = Effect::Bind(view(Some(workload()), MachineId::Image)); - let assembled = [ - Effect::Assemble(accepted_assembly()), - Effect::Account(workload()), - ]; + let bound = Effect::Bind(view(MachineId::Image)); + let assembled = [Effect::Assemble(accepted_assembly())]; assert_eq!( system.effects, [&assembled[..], &[Effect::Announce(GATEWAY), bound.clone()]].concat() @@ -1717,7 +1619,7 @@ fn a_network_that_publishes_the_workload_s_listeners_starts_their_announcer_to_i Activated { proxy: Some(PROXY.to_owned()), announcer: Some(GATEWAY), - ..activated(&system, Some(workload()), MachineId::Image) + ..activated(&system, MachineId::Image) } ); configuration.network = Some(unpublished(None, Some(PROXY))); @@ -1858,7 +1760,7 @@ fn a_generated_machine_id_and_a_resolver_file_keep_the_root_busy_until_their_bin assert_eq!(again.kind(), io::ErrorKind::AlreadyExists); let bound = View { resolver: Some(resolver), - ..view(Some(workload()), machine_id) + ..view(machine_id) }; let binds = bound.entry(&live.root).bind_runtime().unwrap(); let targets = [ @@ -1969,21 +1871,6 @@ fn application_errors_name_their_cause() { ); assert_eq!(error.source().unwrap().to_string(), refusal); - let error = ApplyError::Account { - error: entry::Error::Account(AccountError::Missing), - teardown: None, - }; - assert_eq!( - error.to_string(), - "the root refuses the workload identity: the account is unavailable: no passwd entry has \ - the user ID" - ); - assert_eq!( - error.source().unwrap().to_string(), - "the account is unavailable: no passwd entry has the user ID" - ); - assert!(format!("{error:?}").starts_with("Account { error: Account(Missing)")); - let exists = io::Error::from(io::ErrorKind::AlreadyExists); let error = ApplyError::MachineId { error: io::ErrorKind::AlreadyExists.into(), @@ -2088,15 +1975,20 @@ fn application_errors_name_their_cause() { // The adapter hands the error to the activation, whose failure names it. let base = Instant::now(); - let mut adapter = Adapter::restricted(FakeSystem::with(b"", GROUP), base); + let generating = FakeSystem::generating(io::ErrorKind::AlreadyExists); + let mut adapter = Adapter::restricted(generating, base); let failure = adapter - .take(base, ActivationInput::Activate(&accepted().encode())) + .take( + base, + ActivationInput::Activate(&with_generated_id().encode()), + ) .unwrap_err(); assert_eq!( failure.to_string(), - "the activation failed in its activating state: the record's application failed: the \ - root refuses the workload identity: the account is unavailable: no passwd entry has the \ - user ID" + format!( + "the activation failed in its activating state: the record's application failed: the \ + workload's machine ID cannot be generated: {exists}" + ) ); assert!(failure.source().unwrap().is::()); } diff --git a/nvx-rs/src/host/template/clone.rs b/nvx-rs/src/host/template/clone.rs index 6292aee49..4cbb623db 100644 --- a/nvx-rs/src/host/template/clone.rs +++ b/nvx-rs/src/host/template/clone.rs @@ -106,7 +106,7 @@ pub struct CloneRequest { /// 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 the identity in which every execution runs, unless the executions name their + /// sees, and a workload identity, which the clone's guest ignores: each execution names its /// own. pub workload: WorkloadView, /// The mode of the clone's overlay, which selects its options. diff --git a/nvx-rs/src/host/template/clone/reattach.rs b/nvx-rs/src/host/template/clone/reattach.rs index 95b4c4a33..61aa70aeb 100644 --- a/nvx-rs/src/host/template/clone/reattach.rs +++ b/nvx-rs/src/host/template/clone/reattach.rs @@ -78,14 +78,14 @@ pub enum ReattachedClone { /// - A VMM that runs under `runtime`, whose launch is the one that the clone's lease reserves, is /// the clone's, which the reattach checks in a new session, as the restore sequence of the /// clone's launch does, [`Restoring::activate`](super::Restoring::activate), sending the -/// sandbox's activation record, byte for byte, which must still be the canonical encoding of a -/// record of the restore of the lease's generation: the clone answers with the Ready that it -/// answered once, and the reattach checks the transaction ID that it reports, which -/// [`ReattachedClone::Live`] returns with the session. The clone's launch committed once the -/// clone applied its mapping table, if it has one, and started its sandbox, so the reattach -/// sends neither again, and no launch that it ends gets either. A check that fails, by the -/// evidence that the restore sequence gives it, ends the VMM through its verified identity, as -/// [`Vmm::terminate`] does within `bound`, and the clone fails at [`Stage::Reattach`]. +/// sandbox's activation record, byte for byte, which must still be the canonical encoding of the +/// record of a restore: the clone answers with the Ready that it answered once, and the reattach +/// checks the transaction ID that it reports, which [`ReattachedClone::Live`] returns with the +/// session. The clone's launch committed once the clone applied its mapping table, if it has one, +/// and started its sandbox, so the reattach sends neither again, and no launch that it ends gets +/// either. A check that fails, by the evidence that the restore sequence gives it, ends the VMM +/// through its verified identity, as [`Vmm::terminate`] does within `bound`, and the clone fails +/// at [`Stage::Reattach`]. /// - A VMM that runs under another runtime identity, which might not speak the caller's /// protocol, is ended the same way, without a word to its guest, and the clone fails at /// [`Stage::Reattach`] with [`Evidence::RuntimeMismatch`], and @@ -257,7 +257,7 @@ pub(super) fn reattach_clone_syncing( let uncertain = launched && !certain; let logged = logged.map(|logged| logged.progress); let progress = progress_of(&lease, journal.as_ref(), committed, logged); - let activation = read_activation(&locked.clone_activation_path(), &lease); + let activation = read_activation(&locked.clone_activation_path()); // A failure that the reattach records has the transaction that the clone's activation record // names, while the record of an earlier failure keeps its own. let transaction = activation @@ -473,13 +473,14 @@ fn exit_fault( Some((Fault::new(evidence, exited(progress, status)), status)) } -/// Reads the clone's activation record at `path`, which must be the canonical encoding of a -/// record of the restore of the generation that `lease` holds, as the clone's launch wrote it, -/// and returns its bytes and the record, or the fault, the host's, of one that is not. -fn read_activation( - path: &Path, - lease: &Lease, -) -> std::result::Result<(Vec, ActivationRecord), Fault> { +/// Reads the clone's activation record at `path`, which must be the canonical encoding of the +/// record of a restore, as the clone's launch wrote it, and returns its bytes and the record, or +/// the fault, the host's, of one that is not. +/// +/// It compares nothing of the record with the clone's lease: the reattach matches the clone's +/// records with the lease as [`CloneRecords`](super::CloneRecords) does, through +/// [`read_failure_of`], which refuses the record of the failure of another template's clone. +fn read_activation(path: &Path) -> std::result::Result<(Vec, ActivationRecord), Fault> { let bytes = open_regular(path) .and_then(|file| read_at_most(file, MAX_RECORD_BYTES as u64, "an activation record")) .map_err(|error| { @@ -506,16 +507,10 @@ fn read_activation( malformed("is not in its canonical encoding, in which the clone's launch wrote it"), )); } - if !record - .restore() - .is_some_and(|restore| restore.template == lease.template()) - { + if record.restore().is_none() { return Err(Fault::new( Evidence::HostFailure, - malformed(&format!( - "does not restore the generation {} that the clone's lease holds", - lease.generation() - )), + malformed("is not the record of a restore"), )); } Ok((bytes, record)) diff --git a/nvx-rs/src/host/template/clone/tests/launch.rs b/nvx-rs/src/host/template/clone/tests/launch.rs index 428c02535..655fa7b24 100644 --- a/nvx-rs/src/host/template/clone/tests/launch.rs +++ b/nvx-rs/src/host/template/clone/tests/launch.rs @@ -2689,6 +2689,77 @@ fn a_reattach_fails_a_clone_whose_sandbox_records_the_failure_of_another_clone() assert!(store.tombstones().unwrap().is_empty()); } +#[test] +fn a_reattach_fails_a_clone_whose_record_of_a_failure_is_of_another_template() { + // A clone whose sandbox records no failure is healthy: the reattach takes it over, and needs + // no record of a failure to match its lease. + let crashed = Crashed::at("committed"); + let mut locked = crashed.lock(); + let mut control = ReattachControl::answering(Some); + let vmm = match crashed.take_over(&mut locked, &crashed.runtime, &mut control) { + ReattachedClone::Live { vmm, .. } => vmm, + other => panic!("the reattach found {}", describe(&other)), + }; + let records = CloneRecords::read(&locked).unwrap().unwrap(); + assert_eq!(records.failure, None); + assert_eq!( + vmm.terminate(&mut locked, TIMEOUT).unwrap(), + StopOutcome::Forced + ); + crashed.releases(&mut locked); + // A record of a failure of the clone's own store, lease, and launch, but of another template: + // of another generation, or of another key of the leased generation. It tells nothing of this + // clone, whose activation record the reattach no longer compares with the lease, so the + // reattach takes it as a record that it cannot read, as it takes another lease's. + let leased = crashed.lease.template(); + let mut key = *leased.key.as_bytes(); + key[0] ^= 0xff; + let cases = [ + ( + leased.generation.get().checked_add(1).unwrap(), + leased.key, + "the leased generation", + ), + ( + leased.generation.get(), + crate::activation::Identity::from_bytes(key), + "the key of the leased generation", + ), + ]; + for (generation, key, difference) in cases { + let crashed = Crashed::at("committed"); + let mut locked = crashed.lock(); + let lease = &crashed.lease; + let other = Lease::new( + lease.id(), + lease.store(), + StoreGeneration::new(generation), + key, + lease.sandbox().to_owned(), + lease.launch().clone(), + ); + let defect = Error::io("the test's generation is malformed"); + let at = ((Stage::Restore, Progress::Spawned), None); + let template = (Evidence::MalformedGeneration, Teardown::Exited); + record_earlier(&other, &locked, at, template, &defect); + let mut control = ReattachControl::answering(Some); + let failure = failed(crashed.take_over(&mut locked, &crashed.runtime, &mut control)); + assert_eq!(failure.code(), ErrorCode::Io, "{difference}: {failure}"); + let differs = format!("they differ in {difference};"); + assert!(failure.message().contains(&differs), "{failure}"); + let reattach = (Stage::Reattach, Progress::Committed); + let (host, terminated) = (Evidence::HostFailure, Teardown::Terminated); + assert_recorded(&failure, lease, &locked, reattach, host, terminated); + assert!(control.capabilities.is_empty(), "{difference}"); + assert!(locked.runtime().unwrap().is_none(), "{difference}"); + crashed.releases(&mut locked); + drop(locked); + let store = &crashed.fixture.store; + assert!(!failure.quarantine(store, None).unwrap()); + assert!(store.tombstones().unwrap().is_empty()); + } +} + #[test] fn a_record_of_a_failure_that_cannot_be_read_is_a_failure_whatever_the_reattach_finds() { let reattach = (Stage::Reattach, Progress::Committed); diff --git a/tests/runtime-adapter/README.md b/tests/runtime-adapter/README.md index 1ab0f58e9..d28a31e78 100644 --- a/tests/runtime-adapter/README.md +++ b/tests/runtime-adapter/README.md @@ -837,10 +837,9 @@ after the repair's acknowledgement of the restore: 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, and checks the account of the record's - workload identity, if it has one, in the assembled root, in existing-account - mode. For a GPT disk, it first asks the kernel to reread the disk's partition - table, so that 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 + 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 `upper` or `work` directory exists and is not empty. If the record asks for @@ -874,9 +873,9 @@ after the repair's acknowledgement of the restore: waits for `Ready` before it sends the mapping table or the start. 5. It then serves the host's order, as after a cold boot: the mapping table that the record announces, which it applies from the record's export, the start, - the executions, the probes, and the stop. The record's workload identity, if - it has one, is the whole sandbox's, as a root configuration's is: every - execution runs as it, and a start that names an identity is refused. Every + the executions, the probes, and the stop. A clone has no workload identity of + the whole sandbox: each execution names its own, and a start that names none + is refused, as in a sandbox whose root configuration names none. Every execution takes the record's hostname. The host tells the workloads the endpoint of the network's proxy, as after a cold boot: the host program's `exec` adds it to each execution's environment, so the worker leaves each @@ -941,7 +940,7 @@ ends with one of these statuses, after a diagnostic on standard error: | 52 | The `Activate`'s record does not decode, breaks a limit, or is a cold boot's | | 53 | The record expects other resources than the clone has | | 54 | The root's assembly failed, its upper not fresh among its causes, or the kernel did not reread the partition table of the record's GPT disk before it | -| 55 | The clone's workload cannot be activated: the image refuses the record's workload identity, as it holds no account of the identity, or not its group, or its account databases cannot be read, or the machine ID that the record asks for cannot be generated, or the record's hostname or that machine ID cannot be set for every execution | +| 55 | The clone's workload cannot be activated: the machine ID that the record asks for cannot be generated, or the record's hostname or that machine ID cannot be set for every execution | | 56 | The record's host export cannot be mounted | | 57 | The record's network settings cannot be applied: the resolver file that names its DNS server cannot be written, or set for every execution, or the announcer of the listeners that its network publishes cannot start, as without the template's gateway; or that announcer ended | | 58 | The worker's own order misused the activation: it completed an activation that did not run, or started the sandbox before the clone was ready, or again | @@ -1572,10 +1571,10 @@ 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 as which -its executions run, 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 +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 +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 must be at the guest's address of the template's NIC, under a policy of its own. @@ -1933,9 +1932,8 @@ Each command of `runtime-adapter-host` but `stage`, `list`, `inspect`, and 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, as which every execution runs, and whose account the guest checks - before the clone is - ready, the overlay mode MODE and the scratch's journal policy POLICY, as a + given, which the clone's record holds and its guest ignores, 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 of the clone sees the hostname `runtime-adapter`, and the machine ID that @@ -2427,9 +2425,8 @@ epoch of its broker's first reset, and the clone's activation, root applies each record as `nvx::guest::clone::apply` does, without a root: for a GPT disk, it stands for the kernel's reread of the disk's partition table, which a test may fail; it takes as long as a test asks, but no longer than until -the activation's deadline, as the waits for the devices do, checks the account -of the record's workload identity in the tests' directory of account databases, -and stands for the generation of a machine ID that the record asks for, the +the activation's deadline, as the waits for the devices do, and stands for the +generation of a machine ID that the record asks for, the mount of the record's export, the write of the resolver file that names its network's DNS server, and the setting of the record's hostname and machine ID for every execution, each of which a test may fail. The tests cover: @@ -2447,11 +2444,10 @@ for every execution, each of which a test may fail. The tests cover: nothing answered or applied; - a record that does not decode, a cold boot's, and one that expects other resources, which fail the activation with nothing applied, and a GPT disk - whose partition table the kernel does not reread, a workload identity whose - account or group the image lacks, a machine ID that cannot be generated, a - hostname or machine ID that cannot be set, an export that cannot be mounted, - and a resolver file that cannot be written, which fail its application, each - with the status of its class, and no `Ready`; + whose partition table the kernel does not reread, a machine ID that cannot be + generated, a hostname or machine ID that cannot be set, an export that cannot + be mounted, and a resolver file that cannot be written, which fail its + application, each with the status of its class, and no `Ready`; - an `Activate` whose header announces more than 1 KiB, which the framing refuses before it takes any byte of its body, and which fails the activation with the record's status, before the clone is ready and once it is; @@ -2483,10 +2479,9 @@ 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 record's workload identity, as which every execution runs, while a start - that names an identity is refused with `EINVAL`, as is a start that names - none once the record has none, and the record's hostname, which every - execution's entry takes; +- 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 `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 @@ -2959,11 +2954,11 @@ directory that the starts authorize: | `a_generation_refuses_clones_of_fewer_processors_than_its_template_booted_with` | Of a template of 4 processors, 2 of them online at boot: a target of 1, then in turn one without a processor target, and one of 4 | 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 targets; then each started, with CPUs 0 and 1 online by default, which the restore brought none of online, and all 4 online for the target of 4, of which the restore brought CPUs 2 and 3 online | | `one_generation_serves_clones_of_either_format_over_either_upper` | Of a template whose placeholder is a GPT disk of two layers, longer than the EROFS image, four at once, each image shorter than the placeholder: the EROFS image over a scratch image and in RAM, a GPT disk of one layer, of the EROFS image's length, in RAM, and one of three layers, as short as its layout allows, over a scratch image, with a bound that its first partition meets exactly; then the disk of one layer, with a bound one byte smaller than its first partition | Each started, with its upper and the overlay's options; the EROFS image's root, without a first partition; each GPT disk's layers, ext4 file systems mounted read-only once each, from the partitions that its own table names, stacked in order, and the bytes of its own first partition; the image's read-only disk at the placeholder's length, and the scratch device at the template's scratch length, each of which the guest reads to its end; for a clone in RAM, no file of its sandbox as long as the template's scratch, and on Linux `--restore-absent-scratch` and no `scratch` block device among its VMM's arguments, which name one over a scratch image; then status 8, 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 | | `a_start_refuses_what_the_template_cannot_serve_before_any_vmm_starts` | An EROFS image and a GPT disk 4 KiB longer, and an EROFS image 100 bytes longer, than the placeholder, a scratch image of twice the template's scratch, memory targets of 2 GiB and of 320 MiB, unstaged images, and a generation that the store does not hold, then the generation once its key is gone | For those that NVX's plan refuses, 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 a length that the request supplied, of the image's geometry, which OpenVMM cannot attach, or of invalid targets, which quarantines nothing; for unstaged images, status 1, without a record; for the generation, status 10, with the record of a selection that names no template, which the store never published, without a lease; any lease released, and no VMM's log or record in the sandbox; for the generation without its key, status 8, and the record of a failure of the template, at the plan, by the evidence of a malformed generation, which quarantines the generation, so that the store refuses a later start's selection, with status 10, as `ineligible`, by the evidence of its tombstone, without a lease | -| `a_clone_whose_activation_fails_ends_before_it_is_ready` | A scratch image of zeros, a journal that the record forbids, a workload identity whose account the image lacks, and, through NVX's restore sequence, a ping, a start, or the start of an execution before the `Activate` | Status 8, no `Ready`, and the record of a failure, at the activation, once the VM exited: of the request, by the evidence of an unusable root, with the guest's status 54, or of invalid settings, with 55, or, before the `Activate`, of an unknown cause, by the protocol's, with 51, which the sandbox records until the release; nothing quarantined, and the lease released, with the records | +| `a_clone_whose_activation_fails_ends_before_it_is_ready` | A scratch image of zeros, a journal that the record forbids, and, through NVX's restore sequence, a ping, a start, or the start of an execution before the `Activate` | Status 8, no `Ready`, and the record of a failure, at the activation, once the VM exited: of the request, by the evidence of an unusable root, with the guest's status 54, or, before the `Activate`, of an unknown cause, by the protocol's, with 51, which the sandbox records until the release; nothing quarantined, and the lease released, with the records | | `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_its_executions_as_its_workload_identity_once_it_is_ready` | Over a scratch image, as nobody | An execution that runs as nobody, one that names an identity 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 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 | | `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 | @@ -2984,7 +2979,7 @@ directory that the starts authorize: | `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 | | `a_clone_of_a_template_with_a_nic_maps_its_export_beside_its_resolver` | Of a template of a network with egress allowed, one with a generated machine ID, sharing a host directory read-write | Started, with one mapping, and a record that names the export and the gateway's DNS server; on Linux, the VMM's `--mount-aggregate`, with one `--mount-child`, read-write, and its `--restore-network-policy`, then the arguments of the clone's policy; below the root, the binds of the machine ID and of the resolver configuration, then the export's mapping; the export's directory, and a write through it, which the host's directory holds after the stop | | `a_selection_that_names_no_template_leases_nothing` | A start that names a template store that does not exist, then, once `remove` retired the generation, which no lease kept, a start of it | Status 10, with the record of a selection that names no template, by the evidence `no-store`, then `retired`, without a lease or a VM; and a store that indexes no generation | -| `a_run_runs_one_execution_in_its_clone_then_tears_the_clone_down` | A run over a scratch image, as nobody, whose execution names its environment and its working directory, and ends with status 3, then a run in RAM, as nobody, whose execution names an identity of its own | Each with status 0, in one command: the preflight's `staged`, the clone started as a start starts one, the execution, and a stop that releases the lease, without a VM; the first execution's output, of the Ubuntu image's root, as nobody, in its working directory, with its environment, and its status; the second refused with `EINVAL`; no lease left, and the payload with nothing but the capture's files | +| `a_run_runs_one_execution_in_its_clone_then_tears_the_clone_down` | A run over a scratch image, whose execution names nobody's identity, its environment, and its working directory, and ends with status 3, then a run in RAM, whose execution names no identity | Each with status 0, in one command: the preflight's `staged`, the clone started as a start starts one, the execution, and a stop that releases the lease, without a VM; the first execution's output, of the Ubuntu image's root, as nobody, in its working directory, with its environment, and its status; the second refused with `EINVAL`; no lease left, and the payload with nothing but the capture's files | | `a_run_whose_clone_does_not_start_runs_nothing` | A run that names a template store that does not exist, one with a clock fixture past the restore window, and one over a scratch image of zeros | Once the preflight staged the images, status 10, with the record of a selection that names no template, by the evidence `no-store`, then of an `expired` generation, without a lease or a VM; then status 8, with the record of a failure of the request at the activation, once the VM exited, the lease released, and no execution or stop | | `a_run_in_a_template_of_a_network_finds_its_policy_s_proxy` | Of a template of a proxy-only network, a run of its network, whose execution, as user ID 0, runs `env` with an entry of its own, then a run of a network that lets nothing through | The first with status 0, its clone started and stopped as a run's, and the execution's own entry, then the proxy's URL, `http://192.0.2.1:PORT`, at the port of the proxy's listener, in `http_proxy`, `https_proxy`, `HTTP_PROXY`, and `HTTPS_PROXY`; then, once the preflight staged the images, 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, and no execution or stop | | `a_run_s_execution_finds_its_shares_beneath_the_root` | A run over a scratch image, sharing a host directory read-write, whose execution, as user ID 0, reads a file of the directory and writes another | Status 0, its clone started with one mapping and stopped as a run's; the file's contents in the execution's output; and the written file in the host's directory once the run tore the clone down | @@ -3187,7 +3182,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_its_executions_as_its_workload_identity_once_it_is_ready` | A clone runs its executions as its record's workload identity once it is ready, and refuses one that names an identity 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, whatever workload identity its record names, 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/scenarios.txt b/tests/runtime-adapter/scenarios.txt index a79cf517f..476988d64 100644 --- a/tests/runtime-adapter/scenarios.txt +++ b/tests/runtime-adapter/scenarios.txt @@ -142,7 +142,7 @@ vm_clone a_clone_maps_an_aggregate_of_directories_and_files_beneath_its_root_bef vm_clone a_clone_of_a_template_with_a_nic_maps_its_export_beside_its_resolver vm_clone a_clone_reads_no_more_of_its_disks_than_its_root_needs vm_clone a_clone_refuses_a_scratch_image_whose_upper_an_earlier_clone_used -vm_clone a_clone_runs_its_executions_as_its_workload_identity_once_it_is_ready +vm_clone a_clone_runs_the_executions_that_name_their_identity_once_it_is_ready vm_clone a_clone_s_executions_find_the_proxy_of_its_policy vm_clone a_clone_s_time_settles_after_it_is_ready vm_clone a_clone_whose_activation_fails_ends_before_it_is_ready diff --git a/tests/runtime-adapter/src/bin/runtime-adapter-host.rs b/tests/runtime-adapter/src/bin/runtime-adapter-host.rs index 417047869..e080f6687 100644 --- a/tests/runtime-adapter/src/bin/runtime-adapter-host.rs +++ b/tests/runtime-adapter/src/bin/runtime-adapter-host.rs @@ -171,8 +171,8 @@ //! 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, as which every execution runs, whose account the guest checks in -//! the clone's root before it is ready, the overlay mode MODE and the scratch's journal policy +//! 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 //! 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: diff --git a/tests/runtime-adapter/src/clone.rs b/tests/runtime-adapter/src/clone.rs index d49eca366..c74949333 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 as which its -//! executions run, its overlay mode, its scratch's journal policy, the bound of a GPT disk's +//! 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 //! 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`, @@ -84,7 +84,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 as which every execution runs, if any: its user ID and its group ID. + /// 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. 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, diff --git a/tests/runtime-adapter/src/worker.rs b/tests/runtime-adapter/src/worker.rs index 104d6e129..a0aa84bdd 100644 --- a/tests/runtime-adapter/src/worker.rs +++ b/tests/runtime-adapter/src/worker.rs @@ -82,10 +82,9 @@ //! `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, and checks the account of the record's workload -//! identity, if it has one, in the assembled root, as the worker of a VM that boots cold does -//! for a root configuration. 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 +//! 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 +//! 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 //! record's network names a DNS server, it writes a resolver file that names it. If the record's @@ -106,8 +105,8 @@ //! the start. //! 5. It then serves the host's order as after a cold boot: the mapping table that the record //! announces, which it maps from the record's export, the start, the executions, the probes, and -//! the stop. The record's workload identity, if it has one, is the whole sandbox's, as a root -//! configuration's is, so that executions name none, every execution takes the record's +//! the stop. A clone has no workload identity of the whole sandbox: each execution names its +//! own, as in a sandbox whose root configuration names none. Every execution takes the record's //! hostname, and a clone's executions run in the cgroups of the template's bootstrap. On the //! stop, the worker stops the announcer, closes the export's root, and removes the binds of the //! application before it tears the root down, since they keep the root's mounts busy, through @@ -490,19 +489,18 @@ 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, [export](Self::mount_export), and account check. - /// The template's VM [bootstrapped](Self::bootstrap) the guest before its capture. + /// [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 + /// [bootstrapped](Self::bootstrap) the guest before its capture. /// - /// Returns the identity in which every execution runs, that of the record's workload identity, - /// whose account the application checked in the assembled root, if the record has one. Fails - /// as `nvx::guest::clone::apply` does, with an [`ApplyError`]. + /// Fails as `nvx::guest::clone::apply` does, with an [`ApplyError`]. #[cfg(target_arch = "x86_64")] fn apply( &mut self, record: &ActivationRecord, template: &RootTemplate, deadline: Option, - ) -> Result, ApplyError>; + ) -> Result<(), ApplyError>; } /// A mapping that [`RootOperations::map`] refused or failed to apply, once it applied those before @@ -1431,12 +1429,11 @@ impl RootOperations for LibraryRoot { record: &ActivationRecord, template: &RootTemplate, deadline: Option, - ) -> Result, ApplyError> { + ) -> Result<(), ApplyError> { let mut applied = clone::apply(record, template, deadline)?; - let identity = applied.identity().cloned(); self.export = applied.take_export(); self.root = Some(Assembled::Clone(applied)); - Ok(identity) + Ok(()) } } diff --git a/tests/runtime-adapter/src/worker/activation.rs b/tests/runtime-adapter/src/worker/activation.rs index 131aaf103..364b8090b 100644 --- a/tests/runtime-adapter/src/worker/activation.rs +++ b/tests/runtime-adapter/src/worker/activation.rs @@ -35,7 +35,7 @@ use nvx::guest::clone::{ Activation, ActivationAction, ActivationCause, ActivationFailure, ActivationInput, ApplyError, RootTemplate, }; -use nvx::guest::entry::{Identity, Publication}; +use nvx::guest::entry::Publication; use nvx::protocol::status; use super::capture::Admission; @@ -78,8 +78,7 @@ pub const ACTIVATION_PROBE: &str = "activation"; /// is not fresh among its causes, or the kernel did not reread the partition table of the /// record's GPT disk before it, as [`ApplyError::Assembly`] and [`ApplyError::Gpt`] tell, or /// the application failed otherwise, as this build of the adapter does not know; -/// - [`ACTIVATION_WORKLOAD`](status::ACTIVATION_WORKLOAD): the image refuses the record's -/// workload identity, as [`ApplyError::Account`] tells, the machine ID that the record asks the +/// - [`ACTIVATION_WORKLOAD`](status::ACTIVATION_WORKLOAD): the machine ID that the record asks the /// guest to generate cannot be written, as [`ApplyError::MachineId`] tells, or the record's /// hostname or that machine ID cannot be set for every execution, as [`ApplyError::View`] /// tells; @@ -105,9 +104,9 @@ pub fn failure_status(failure: &ActivationFailure) -> u8 { ActivationCause::Resources { .. } => status::ACTIVATION_RESOURCES, ActivationCause::Failed(error) => match error.downcast_ref::() { Some(ApplyError::Gpt(_) | ApplyError::Assembly(_)) => status::ACTIVATION_ROOT, - Some( - ApplyError::Account { .. } | ApplyError::MachineId { .. } | ApplyError::View { .. }, - ) => status::ACTIVATION_WORKLOAD, + Some(ApplyError::MachineId { .. } | ApplyError::View { .. }) => { + status::ACTIVATION_WORKLOAD + } Some(ApplyError::Export { .. }) => status::ACTIVATION_EXPORT, Some(ApplyError::Network { .. }) => status::ACTIVATION_NETWORK, // A failure of the application that this build does not know. @@ -138,9 +137,8 @@ pub(super) struct CloneState { /// The application of the record of the Activate that a clone's activation accepted, which ran, /// and which awaits the activation's completion. struct Applied { - /// The identity in which every execution runs, if the record has a workload identity, or why - /// the application failed. - result: Result, ApplyError>, + /// Whether the application succeeded, or why it failed. + result: Result<(), ApplyError>, /// The number of mappings that the record announces. mappings: u32, /// The hostname of every execution: the record's. @@ -330,26 +328,23 @@ impl CloneState { /// worker took what the session received while it ran, which comes before `Ready`, and /// returns the worker's step: `Ready`, which reports the activation, for the `Activate` and /// for each retry that joined its application, once the clone is ready, after which - /// `executions` run in the cgroups that the template's bootstrap prepared, as the record's - /// workload identity, if it has one, with the record's hostname and publication of listeners, - /// and the worker's order awaits the record's mappings; or the end, once the activation - /// failed. + /// `executions` run in the cgroups that the template's bootstrap prepared, each as the identity + /// that it names, with the record's hostname and publication of listeners, and the worker's + /// order awaits the record's mappings; or the end, once the activation failed. pub(super) fn settle( &mut self, operations: &mut dyn RootOperations, executions: &mut Executions, ) -> Option> { let applied = self.applied.take()?; - let (result, identity) = match applied.result { - Ok(identity) => (Ok(()), identity), - Err(error) => (Err(error.into()), None), - }; + let result = applied.result.map_err(Into::into); Some(match self.activation.complete(Instant::now(), result) { Ok(activation) => { self.timeline.ready = Some(Instant::now()); - let identity = identity.map(|identity| (identity.uid(), identity.gid())); + // A clone has no workload identity of the whole sandbox: each execution names + // its own. let sandbox = - Sandbox::new(identity, applied.publication).with_hostname(applied.hostname); + Sandbox::new(None, applied.publication).with_hostname(applied.hostname); executions.run_in(operations.cgroups(), sandbox); self.order = Some(Order::new(applied.mappings)); Step::Continue(vec![ready(activation); 1 + applied.joined]) diff --git a/tests/runtime-adapter/src/worker/activation/tests.rs b/tests/runtime-adapter/src/worker/activation/tests.rs index 049dc2f9f..e2ac0c156 100644 --- a/tests/runtime-adapter/src/worker/activation/tests.rs +++ b/tests/runtime-adapter/src/worker/activation/tests.rs @@ -157,12 +157,6 @@ fn each_class_of_failure_ends_the_clone_with_its_own_status() { let assembly = ApplyError::Assembly(root::assemble(&relative).unwrap_err()); let root = activating().complete(now(), Err(assembly.into())); cases.push((failed(root), status::ACTIVATION_ROOT)); - let refused = ApplyError::Account { - error: entry::Error::Root(io::Error::other("no account")), - teardown: None, - }; - let workload = activating().complete(now(), Err(refused.into())); - cases.push((failed(workload), status::ACTIVATION_WORKLOAD)); let generated = ApplyError::MachineId { error: io::ErrorKind::AlreadyExists.into(), teardown: None, diff --git a/tests/runtime-adapter/tests/guest_session.rs b/tests/runtime-adapter/tests/guest_session.rs index 06626e089..4980e9e67 100644 --- a/tests/runtime-adapter/tests/guest_session.rs +++ b/tests/runtime-adapter/tests/guest_session.rs @@ -1395,9 +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, and checks the account of the record's workload - /// identity, if it has one, in existing-account mode, in the [accounts' root](accounts), which - /// stands for the root that it assembled. 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 of + /// the record's workload identity. 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 @@ -1412,7 +1411,7 @@ impl RootOperations for FakeRoot { record: &nvx::activation::ActivationRecord, template: &nvx::guest::clone::RootTemplate, deadline: Option, - ) -> Result, nvx::guest::clone::ApplyError> { + ) -> Result<(), nvx::guest::clone::ApplyError> { use nvx::activation::{self, MachineIdMode}; use nvx::guest::clone::{ApplyError, NetworkError}; use nvx::guest::ingress::StartError; @@ -1444,17 +1443,6 @@ impl RootOperations for FakeRoot { }; thread::sleep(assembly); let workload = &configuration.workload; - let identity = workload - .identity - .map(|identity| entry::Identity::new(identity.uid, identity.gid)); - if let Some(identity) = &identity - && let Err(error) = entry::Entry::new(accounts(), identity.clone()).prepare() - { - return Err(ApplyError::Account { - error, - teardown: None, - }); - } if workload.machine_id == MachineIdMode::Generated { self.result(Operation::MachineId) .map_err(|problem| ApplyError::MachineId { @@ -1510,7 +1498,7 @@ impl RootOperations for FakeRoot { teardown: None, } })?; - Ok(identity) + Ok(()) } } @@ -4554,8 +4542,7 @@ mod clones { } // The application fails once the kernel refused to reread a GPT disk's partition table, - // and refuses a workload identity whose account the image lacks, or whose primary group - // it lacks, a machine ID that it cannot generate, and a hostname or machine ID that it + // and refuses a machine ID that it cannot generate, and a hostname or machine ID that it // cannot set for every execution, an export that it cannot mount, a resolver file that it // cannot write, and an announcer of the workload's listeners that it cannot start, before // Ready. @@ -4575,16 +4562,6 @@ mod clones { FakeRoot::failing(Operation::Reread, "the disk is busy"), status::ACTIVATION_ROOT, ), - ( - record(1, Some((4242, 4242))), - FakeRoot::booting(&layout()), - status::ACTIVATION_WORKLOAD, - ), - ( - record(1, Some(ORPHAN)), - FakeRoot::booting(&layout()), - status::ACTIVATION_WORKLOAD, - ), ( generated.clone(), FakeRoot::failing(Operation::MachineId, "a machine ID exists already"), @@ -4931,39 +4908,33 @@ mod clones { } #[test] - fn a_clone_s_executions_run_as_the_workload_identity_and_with_the_hostname_of_its_record() { - // The record's workload identity is the whole sandbox's, as a root configuration's is: - // every execution runs as it, and a start that names an identity is refused. Every - // execution's entry takes the record's hostname. + 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 + // 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 mut host = Host::activated(FakeRoot::booting(&layout()), &record(1, Some(WORKLOAD))); - host.start(start_of(1, shell("exit 0")), Vec::new()); - let named = start_of(2, shell("exit 0")).with_identity(workload.clone()); - host.start(named, Vec::new()); - host.run_until(|host| (1..=2).all(|id| host.outcome(id).is_some())); - check_lossless(&host, 1, Status::Exited(0)); - assert_eq!(host.outcome(2).unwrap().launch_failure(), Some(refused)); - let (root, worked) = host.stop(); - assert_eq!(worked.unwrap(), Worked::Stopped); let identity = entry::Identity::new(WORKLOAD.0, WORKLOAD.1); - assert_eq!(entrances(&root), [cloned(identity.clone(), b"clone")]); - - // Without one, each start names its own, and takes the hostname of a record that names - // another. let renamed = changed(&record(1, None), |configuration| { configuration.workload.hostname = b"another-clone".to_vec(); }); - let mut host = Host::activated(FakeRoot::booting(&layout()), &renamed); - host.start(start_of(1, shell("exit 0")), Vec::new()); - let named = start_of(2, shell("exit 0")).with_identity(workload); - host.start(named, Vec::new()); - host.run_until(|host| (1..=2).all(|id| host.outcome(id).is_some())); - assert_eq!(host.outcome(1).unwrap().launch_failure(), Some(refused)); - check_lossless(&host, 2, Status::Exited(0)); - let (root, worked) = host.stop(); - assert_eq!(worked.unwrap(), Worked::Stopped); - assert_eq!(entrances(&root), [cloned(identity, b"another-clone")]); + let records = [ + (record(1, Some(WORKLOAD)), &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()); + let named = start_of(2, shell("exit 0")).with_identity(workload.clone()); + host.start(named, Vec::new()); + host.run_until(|host| (1..=2).all(|id| host.outcome(id).is_some())); + assert_eq!(host.outcome(1).unwrap().launch_failure(), Some(refused)); + check_lossless(&host, 2, Status::Exited(0)); + let (root, worked) = host.stop(); + assert_eq!(worked.unwrap(), Worked::Stopped); + assert_eq!(entrances(&root), [cloned(identity.clone(), hostname)]); + } } #[test] @@ -5128,7 +5099,11 @@ mod clones { assert!(events.is_sorted(), "{facts:?}"); // The start of the first execution came once the sandbox started. - host.start(start_of(1, shell("sleep 1")), Vec::new()); + let workload = exec::Identity::new(WORKLOAD.0, WORKLOAD.1); + host.start( + start_of(1, shell("sleep 1")).with_identity(workload), + Vec::new(), + ); host.send(&[probe]); host.run_until(|host| !host.replies.is_empty()); let facts = reported(&host.replies.remove(0)); @@ -5158,7 +5133,10 @@ mod clones { 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: // an execution's environment is exactly its start's, which names no proxy here. - let printing = start_of(1, vec![b"/usr/bin/env".to_vec()]).with_environment(["NAME=value"]); + let workload = exec::Identity::new(WORKLOAD.0, WORKLOAD.1); + let printing = start_of(1, vec![b"/usr/bin/env".to_vec()]) + .with_environment(["NAME=value"]) + .with_identity(workload); host.start(printing, Vec::new()); host.run_until(|host| host.outcome(1).is_some()); check_lossless(&host, 1, Status::Exited(0)); diff --git a/tests/runtime-adapter/tests/vm_clone.rs b/tests/runtime-adapter/tests/vm_clone.rs index f5bc2acd5..01241c00d 100644 --- a/tests/runtime-adapter/tests/vm_clone.rs +++ b/tests/runtime-adapter/tests/vm_clone.rs @@ -47,9 +47,9 @@ //! so that the store refuses a later start's selection of it, as ineligible. //! - A clone whose activation fails ends before it is ready, with the status of the failure's //! class, which attributes the failure, and the VMM's exit: a scratch image that holds no ext4 -//! file system, a journal that the clone's record forbids, a workload identity whose account the -//! image lacks, and a host that sends anything but the clone's Activate first, a ping, a start, or -//! an execution. The sandbox records the failure until the release of its lease. +//! file system, a journal that the clone's record forbids, and a host that sends anything but the +//! clone's Activate first, a ping, a start, or an execution. The sandbox records the failure +//! until the release of its lease. //! - A clone refuses a scratch image whose upper is not fresh, as an earlier clone left it, and //! ends before it is ready. //! - Clones at once each keep a root of their own, and every execution of a clone sees its @@ -79,9 +79,10 @@ //! 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 its executions as its record's workload identity, once it is ready: 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. +//! - 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 +//! 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 //! clone's restore unacknowledged for that long, and is of a class of its own, whose key names @@ -114,10 +115,9 @@ //! `remove` retired. //! - A run runs one execution in its clone, then tears the clone down, in one command: its //! preflight stages the clone's images, it starts the clone as a start does, runs the execution -//! in the clone's root, with the environment and the working directory that its start names, as -//! the clone's workload identity, whatever its program's status, and stops the clone, which -//! releases the lease. An execution that the guest refuses leaves the run to tear the clone down -//! as well. +//! in the clone's root, with the identity, the environment, and the working directory that its +//! start names, whatever its program's status, and stops the clone, which releases the lease. An +//! execution that the guest refuses leaves the run to tear the clone down as well. //! - A run whose clone does not start runs nothing: once the preflight staged the clone's images, //! the store refuses a selection that names no template, or of a generation whose restore window //! passed, without a lease, and a clone whose activation fails ends the run with the record of @@ -279,10 +279,8 @@ const TEMPLATE_MEMORY: u64 = 256; const TEMPLATE_CAPACITY: u64 = 1024; /// The generation of each scenario's template store that its template is. const GENERATION: StoreGeneration = StoreGeneration::new(NonZeroU64::MIN); -/// The workload identity of the clones that run as one: nobody's, in the Ubuntu image. +/// Nobody's identity, whose account the Ubuntu image holds. const NOBODY: &str = "65534:65534"; -/// A workload identity whose account the Ubuntu image lacks. -const STRANGER: &str = "4242:4242"; /// Bytes in a sector of the guest's disks, which their statistics count. const SECTOR: u64 = 512; /// How long a clone's time may take to settle once the clone is ready. @@ -3114,7 +3112,6 @@ fn a_start_refuses_what_the_template_cannot_serve_before_any_vmm_starts() { fn a_clone_whose_activation_fails_ends_before_it_is_ready() { let template = Template::create("cl-unready"); let forbidden = ["--scratch-journal", "forbidden"]; - let stranger = ["--workload-identity", STRANGER]; let cases = [ ( StartOf { @@ -3131,13 +3128,6 @@ fn a_clone_whose_activation_fails_ends_before_it_is_ready() { }, ("unusable-root", status::ACTIVATION_ROOT), ), - ( - StartOf { - options: &stranger, - ..StartOf::default() - }, - ("invalid-settings", status::ACTIVATION_WORKLOAD), - ), ]; for (start, (evidence, expected)) in cases { let ended = template.start("cl-u", start, CLONE_FAILED); @@ -3293,8 +3283,9 @@ fn a_clone_whose_view_cannot_be_set_ends_before_it_is_ready() { #[test] #[ignore = "boots VMs; see the module documentation for what they need"] -fn a_clone_runs_its_executions_as_its_workload_identity_once_it_is_ready() { +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. let options = ["--workload-identity", NOBODY]; let start = StartOf { upper: Upper::Scratch, @@ -3307,16 +3298,16 @@ fn a_clone_runs_its_executions_as_its_workload_identity_once_it_is_ready() { let events = assert_timeline(&facts); assert_eq!(facts["activation.execution"], "none", "{facts:?}"); - // Every execution runs as the record's workload identity, and one that names its own is - // refused before its program starts. - let ran = cloned.exec(&["--", "/bin/sh", "-c", "id -u; id -g"]); + // An execution runs as the identity that it names, and one that names none is refused before + // its program starts. + let ran = cloned.exec(&["--identity", NOBODY, "--", "/bin/sh", "-c", "id -u; id -g"]); let facts = ["launch", "status", "stdout", "stderr"].map(|key| &ran[&format!("exec.1.{key}")]); assert_eq!( facts, ["started", "exit 0", "65534\\n65534\\n", ""], "{ran:?}" ); - let refused = cloned.exec(&["--identity", "1000:1000", "--", "/bin/true"]); + let refused = cloned.exec(&["--", "/bin/true"]); let launch = format!("failed 0 {EINVAL}"); assert_eq!(refused["exec.1.launch"], launch, "{refused:?}"); @@ -4187,17 +4178,18 @@ fn a_selection_that_names_no_template_leases_nothing() { #[ignore = "boots VMs; see the module documentation for what they need"] fn a_run_runs_one_execution_in_its_clone_then_tears_the_clone_down() { let template = Template::create("cl-run"); - // The execution's program, its arguments, its environment, and its working directory reach - // the guest in its start, and it runs in the clone's root, the Ubuntu image's, as the clone's - // workload identity. The run ends with success whatever the program's status. - let options = ["--workload-identity", NOBODY]; + // The execution's program, its arguments, its identity, its environment, and its working + // directory reach the guest in its start, and it runs in the clone's root, the Ubuntu + // image's, as the identity that it names. The run ends with success whatever the program's + // status. let start = StartOf { upper: Upper::Scratch, - options: &options, ..StartOf::default() }; let script = ". /etc/os-release; echo \"$GREETING $ID $(id -u) $(pwd)\"; echo done >&2; exit 3"; let execution = [ + "--identity", + NOBODY, "--env", "GREETING=hello", "--cwd", @@ -4235,14 +4227,10 @@ fn a_run_runs_one_execution_in_its_clone_then_tears_the_clone_down() { ); assert!(template.leases().is_empty()); - // An execution that the guest refuses before its program starts, one that names an identity - // in a clone that runs as its own, leaves the run to tear the clone down as well. - let start = StartOf { - options: &options, - ..StartOf::default() - }; - let execution = ["--identity", "1000:1000", "--", "/bin/true"]; - let ran = template.run("cl-r", start, &execution, 0); + // An execution that the guest refuses before its program starts, one that names no + // identity, leaves the run to tear the clone down as well. + let execution = ["--", "/bin/true"]; + let ran = template.run("cl-r", StartOf::default(), &execution, 0); ran.assert_ran(); let printed = &ran.started; let launch = format!("failed 0 {EINVAL}");