diff --git a/nvx-rs/README.md b/nvx-rs/README.md index bb5011c43..043b84870 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -670,8 +670,9 @@ which OpenVMM restores from the snapshot, alone; a memory target in whole blocks of `MEMORY_BLOCK_SIZE` from the template's RAM to its capacity; the clone's image, which the check of its `DistroBinding` opens; its upper, either the caller's scratch image, which the check of its `ScratchBinding` claims, and -which nothing formats, copies, or reads, or for an upper in RAM a fresh sparse -file, `clone-scratch.raw` in the sandbox's directory, which the release removes; +which nothing formats, copies, or reads, or RAM, whose restore declares the +template's scratch absent, so that OpenVMM attaches it with no file and the plan +creates none; its export, if it has one, an `ExportPlan` that `filesystem::plan` made for the container root, which must export exactly one directory, since the template's dormant filesystem slot binds one directory and no file, and which must neither @@ -706,12 +707,15 @@ and the guest's proxy URL; and the template's processors and the memory target. The identity of a binding or of an export plan is the SHA-256 digest of a domain and of its serde form, never of a file's content. -The image and the scratch device must present the `BlockGeometry` of the -template's, which a restore compares, and be, at the paths by which OpenVMM -opens them, the regular files that the checks found, which are not links or -other reparse points, and which no one else may change. Neither the image nor a -scratch image may be a file that the sandbox's store owns, such as -`clone-scratch.raw`, its log, or its records, by any path, since the store's +The image must present the logical and physical block sizes of the template's +placeholder and be no longer than it, since a restore presents every clone's +image at the placeholder's length, its capacity, and reads zeros past the end of +a shorter image, while a scratch image must present the `BlockGeometry` of the +template's scratch, its length included, which a restore compares; both must be, +at the paths by which OpenVMM opens them, the regular files that the checks +found, which are not links or other reparse points, and which no one else may +change. Neither the image nor a scratch image may be a file that the sandbox's +store owns, such as its log or its records, by any path, since the store's transitions replace or remove them, and a link or a directory under one of the store's names is refused. @@ -724,7 +728,8 @@ writes the clone's activation record in its canonical encoding, `clone-activation.bin`, from which a caller resends it unchanged; it listens for the VMM's restore-ready event, at the socket `restore.sock` on Linux, and at a named pipe of a random name on Windows; and it runs `supervisor::launch` with -OpenVMM's restore of the clone's devices and of the template's network, as its +OpenVMM's restore of the clone's devices, with `--restore-absent-scratch` for a +clone in RAM, and of the template's network, as its key records it, unchanged, its export bound to the dormant slot, `--mount` with the directory, and its hidden, re-exposed, and writable paths and its owner, `--restore-snapshot` with the generation's snapshot in place, which no clone @@ -745,8 +750,7 @@ claim's handle, which OpenVMM's open of the image would refuse. A payload holds the sockets of its clones only while they lease it: the VMM removes its socket as it exits, a stop or a recovery removes the one that an abrupt exit leaves, and the release makes the socket's absence durable before -the lease ends, and removes the record and `restore.sock`, as it removes -`clone-scratch.raw`. +the lease ends, and removes the record and `restore.sock`. `Restoring::activate` then runs the clone's restore sequence through the adapter's framing of its control session, a `CloneControl`, by a diff --git a/nvx-rs/src/host/sandbox/distro.rs b/nvx-rs/src/host/sandbox/distro.rs index 9d716e293..25b3329c7 100644 --- a/nvx-rs/src/host/sandbox/distro.rs +++ b/nvx-rs/src/host/sandbox/distro.rs @@ -223,9 +223,10 @@ impl CheckedDistro { /// its `st_blksize`, on Linux, of 4096 bytes on Windows, and of the logical block size on /// other hosts, which must be a power of two of at least 512 bytes. /// - /// It reads the file's metadata alone. A restore of a template compares the geometry of each - /// of its block devices with the one that the template recorded, so the image of a clone must - /// present the geometry of the template's `distro`, as + /// It reads the file's metadata alone. A restore of a template presents its `distro` at the + /// geometry that the template recorded, the capacity of every clone's image, over an image + /// that may be shorter, so the image of a clone must be no longer than the template's + /// `distro`, with the same logical and physical block sizes, as /// [`ClonePlan`](crate::host::template::ClonePlan) checks before the clone's launch. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a length or a diff --git a/nvx-rs/src/host/store/lifecycle/tests.rs b/nvx-rs/src/host/store/lifecycle/tests.rs index 9b09876a8..e39271463 100644 --- a/nvx-rs/src/host/store/lifecycle/tests.rs +++ b/nvx-rs/src/host/store/lifecycle/tests.rs @@ -413,7 +413,8 @@ fn a_removal_is_refused_while_a_vm_or_a_launch_is_recorded() { } } -/// Writes the files that a clone keeps of its launch in the sandbox `dir`, its binding included. +/// Writes the files that a clone keeps of its launch in the sandbox `dir`, its binding included, +/// and the file of the scratch device that an earlier version left for a clone in RAM. fn write_clone_files(dir: &Path) { for name in [ CLONE_BINDING_NAME, diff --git a/nvx-rs/src/host/store/mod.rs b/nvx-rs/src/host/store/mod.rs index 89ec87ac1..5db462a69 100644 --- a/nvx-rs/src/host/store/mod.rs +++ b/nvx-rs/src/host/store/mod.rs @@ -16,8 +16,8 @@ //! outcome.json the VMM's outcome report of the latest launch //! .tmpXXXXXX the outcome report while the VMM writes it, which it then publishes //! clone.json the binding of a clone to the generation of a template that it leases -//! clone-scratch.raw the file of the scratch device of a clone whose upper is in RAM, a -//! sparse file that the clone's plan creates +//! clone-scratch.raw the file of the scratch device that earlier versions created for a +//! clone whose upper is in RAM, which a release removes if one remains //! clone-activation.bin the activation record of a clone, which its launch writes //! clone-progress.log the progress log of a clone's launch and restore sequence //! clone-failure.json the record of a clone's failure @@ -297,11 +297,10 @@ pub const OUTCOME_NAME: &str = "outcome.json"; /// removes once its VM has exited. A [removal](self#removal) of the sandbox removes it first, and /// makes its absence durable before the sandbox goes. pub const CLONE_BINDING_NAME: &str = "clone.json"; -/// Name of the file of the `scratch` block device of a clone whose upper is in RAM, which the -/// clone's restore attaches in place of the template's scratch, and which the guest leaves -/// unused: a sparse file of the template's scratch length, which -/// [`template::ClonePlan::new`](crate::host::template::ClonePlan::new) creates under the lease, -/// replacing one that an earlier clone of the sandbox left, and which +/// Name of the file of the `scratch` block device that earlier versions of this crate created for +/// a clone whose upper is in RAM, a sparse file of the template's scratch length. Such a clone now +/// restores with its scratch absent, and nothing creates the file, but a sandbox's directory may +/// still hold one that an earlier version left, which /// [`template::Store::release`](crate::host::template::Store::release) removes once the clone's VM /// has exited, as a [removal](self#removal) of the sandbox does. pub const CLONE_SCRATCH_NAME: &str = "clone-scratch.raw"; @@ -332,9 +331,9 @@ pub const CLONE_PROGRESS_NAME: &str = "clone-progress.log"; /// [`template::Store::release`](crate::host::template::Store::release) removes, as a /// [removal](self#removal) of the sandbox does. pub const CLONE_FAILURE_NAME: &str = "clone-failure.json"; -/// The files that the clone of a sandbox keeps of its launch, which a release removes once the -/// clone's VM has exited, as a [removal](self#removal) of the sandbox does, before the clone -/// binding. +/// The files that the clone of a sandbox keeps of its launch, and the file of the scratch device +/// that an earlier version created for a clone in RAM, which a release removes once the clone's VM +/// has exited, as a [removal](self#removal) of the sandbox does, before the clone binding. const CLONE_FILE_NAMES: [&str; 5] = [ CLONE_SCRATCH_NAME, CLONE_ACTIVATION_NAME, @@ -583,12 +582,6 @@ impl SandboxStore { self.path(CLONE_BINDING_NAME) } - /// Returns the path of the file of the scratch device of a clone whose upper is in RAM, - /// [`CLONE_SCRATCH_NAME`]. - pub fn clone_scratch_path(&self) -> PathBuf { - self.path(CLONE_SCRATCH_NAME) - } - /// Returns the path of the activation record of a clone, [`CLONE_ACTIVATION_NAME`]. pub fn clone_activation_path(&self) -> PathBuf { self.path(CLONE_ACTIVATION_NAME) @@ -859,14 +852,14 @@ impl LockedStore { /// Removes what the clone of the sandbox kept of its launch in the sandbox's directory, whose /// VM has exited, through `directory`, the handle of the directory that /// [`open_dir`](Self::open_dir) returned: the file of its scratch device, - /// [`CLONE_SCRATCH_NAME`], its activation record, [`CLONE_ACTIVATION_NAME`], the socket of its - /// restore-ready event, [`RESTORE_READY_NAME`], its progress log, [`CLONE_PROGRESS_NAME`], and - /// the record of its failure, [`CLONE_FAILURE_NAME`]. A release, as a [removal](self#removal) - /// does, removes them before the clone binding, whose removal, once durable, makes theirs - /// durable too, since all are entries of the sandbox's directory. A file whose deletion is - /// pending, as a Windows file system without POSIX deletion leaves a removed file that another - /// handle holds, is removed already, so that a release that failed once it removed the file can - /// be retried. + /// [`CLONE_SCRATCH_NAME`], if an earlier version left one, its activation record, + /// [`CLONE_ACTIVATION_NAME`], the socket of its restore-ready event, [`RESTORE_READY_NAME`], + /// its progress log, [`CLONE_PROGRESS_NAME`], and the record of its failure, + /// [`CLONE_FAILURE_NAME`]. A release, as a [removal](self#removal) does, removes them before + /// the clone binding, whose removal, once durable, makes theirs durable too, since all are + /// entries of the sandbox's directory. A file whose deletion is pending, as a Windows file + /// system without POSIX deletion leaves a removed file that another handle holds, is removed + /// already, so that a release that failed once it removed the file can be retried. pub(crate) fn remove_clone_files(&self, directory: &platform::Directory) -> Result<()> { for name in CLONE_FILE_NAMES { directory.remove_file(name).map_err(io_error(format!( diff --git a/nvx-rs/src/host/store/tests.rs b/nvx-rs/src/host/store/tests.rs index 7b810d298..c039bdc26 100644 --- a/nvx-rs/src/host/store/tests.rs +++ b/nvx-rs/src/host/store/tests.rs @@ -408,7 +408,8 @@ fn the_files_that_a_clone_kept_of_its_launch_go_and_nothing_else() { let (_directory, store) = store(); let locked = store.lock(None).unwrap(); for path in [ - store.clone_scratch_path(), + // The file of the scratch device that an earlier version left for a clone in RAM. + store.dir().join(CLONE_SCRATCH_NAME), store.clone_activation_path(), store.clone_progress_path(), store.clone_failure_path(), diff --git a/nvx-rs/src/host/template/clone.rs b/nvx-rs/src/host/template/clone.rs index 769790fa0..542417467 100644 --- a/nvx-rs/src/host/template/clone.rs +++ b/nvx-rs/src/host/template/clone.rs @@ -52,11 +52,9 @@ use crate::host::sandbox::distro::{ use crate::host::sandbox::plan::bound_image; use crate::host::sandbox::scratch::{self, CheckedScratch, ScratchBinding}; use crate::host::sandbox::{ - LayerRole, SandboxLayer, SandboxPlan, SandboxUpper, block_geometry, reject_container_path, - reject_disk_path, + LayerRole, SandboxLayer, SandboxPlan, reject_container_path, reject_disk_path, }; use crate::host::store::LockedStore; -use crate::host::store::files::{create_new, remove_if_present}; use crate::host::store::regular::{identity_of, is_refused, open_regular, was_removed}; use crate::protocol::{CAPABILITY_LEN, HostMap}; @@ -67,9 +65,6 @@ const MIB: u64 = 1024 * 1024; /// Windows host, 63, follows in one. const MAX_PATH_LINKS: usize = 64; -/// How messages name the file of the `scratch` block device of a clone whose upper is in RAM. -const SPARSE_WHAT: &str = "scratch device of the clone"; - /// What the clone of a sandbox restores of the generation of a template that it leased: the /// adapter's plan of the clone's image and upper, the binding of the image, the binding of the /// scratch image of an upper on one, the export of the host paths that the clone shares with its @@ -121,19 +116,14 @@ pub struct CloneRequest { pub first_partition_bound: u64, } -/// The upper of a clone's plan, with the file of the clone's `scratch` block device, which every -/// restore of a template attaches in place of the template's scratch. +/// The upper of a clone's plan, which decides how the clone's restore attaches the template's +/// `scratch` block device. #[derive(Debug)] pub enum CloneUpper { - /// An upper in RAM, a tmpfs, which leaves the `scratch` block device unused: its file is a - /// sparse file of the template's scratch length, - /// [`CLONE_SCRATCH_NAME`](crate::host::store::CLONE_SCRATCH_NAME) in the sandbox's directory, - /// which the plan created, and which [`Store::release`] removes once the clone's VM has - /// exited, as a [removal](crate::host::store#removal) of the sandbox does. - Tmpfs { - /// The sparse file. - path: PathBuf, - }, + /// An upper in RAM, a tmpfs, which leaves the `scratch` block device unused: the restore + /// declares the device absent, and OpenVMM attaches it with no file, at the geometry that the + /// template recorded, as [`ClonePlan::launch`] describes, so the plan creates no file for it. + Tmpfs, /// An upper on the caller's scratch image, which the plan checked and claimed: the claim /// lasts as long as the checked image, as [`ScratchBinding::check`] describes. Scratch(CheckedScratch), @@ -145,34 +135,39 @@ pub enum CloneUpper { /// that acquired the lease, before the clone's launch. /// /// A restore of a template attaches the template's two block devices, its `distro` and its -/// `scratch`, with files of the clone's, which OpenVMM opens itself, by their paths, as -/// [Block devices by path](Store#block-devices-by-path) describes, and whose geometries it -/// compares with those that the template recorded. The plan checks each of the clone's inputs as -/// OpenVMM will find it, and holds what the checks opened: +/// `scratch`, at the geometries that the template recorded, as OpenVMM presents them: the +/// `distro`, the clone's image, at the placeholder's, the capacity of every clone's image, over an +/// image that may be shorter, whose blocks past its end read as zeros, and the `scratch` at the +/// template's scratch, over the clone's scratch image, of exactly that geometry, for an upper on +/// one, or with no file, for an upper in RAM, whose restore declares it absent. OpenVMM opens the +/// clone's files itself, by their paths, as [Block devices by path](Store#block-devices-by-path) +/// describes. The plan checks each of the clone's inputs as OpenVMM will find it, and holds what +/// the checks opened: /// /// - The image, the clone's `distro`, an EROFS layer or a GPT disk, as its binding names it, must -/// pass the check of its binding, [`DistroBinding::check`], and present the geometry of the -/// template's placeholder, as its key records it: the placeholder's length, and the same logical -/// and physical block sizes, which [`CheckedDistro::geometry`] reads through the checked file, -/// whatever the placeholder's format. At its path, it must be a regular file, not a link, nor on -/// Windows any other reparse point, that is the file that the check found, that belongs to the -/// current user or to an administrator, and that no one else may change. The plan holds the -/// file that the check opened, for reading alone. As at a cold launch, the guest reads and -/// checks a GPT disk's partition table, with `nvx::gpt`, before it assembles the root, so the -/// plan reads none of it. +/// pass the check of its binding, [`DistroBinding::check`], and fit the capacity of the +/// template's placeholder, as its key records it, [`TemplateKey::distro`]: no longer than the +/// placeholder, with the same logical and physical block sizes, which +/// [`CheckedDistro::geometry`] reads through the checked file, whatever the placeholder's +/// format. On Linux, a file's physical block size is the I/O block size of its filesystem, so +/// the image must lie on a filesystem of the placeholder's. At its path, it must be a regular +/// file, not a link, nor on Windows any other reparse point, that is the file that the check +/// found, that belongs to the current user or to an administrator, and that no one else may +/// change. The plan holds the file that the check opened, for reading alone. Nothing resizes, +/// pads, or writes the image. As at a cold launch, the guest reads and checks a GPT disk's +/// partition table, with `nvx::gpt`, before it assembles the root, so the plan reads none of +/// it: of a disk shorter than the capacity, `nvx::gpt` requires only that its backup header lie +/// inside the capacity, as it does at the disk's end. /// - Neither the image nor a scratch image may be a file that the sandbox's store owns, by any /// path, through a link among its directories, say, or a hard link: the store's transitions -/// replace or remove its files, as a plan replaces the file of the scratch device of a clone in -/// RAM, [`CLONE_SCRATCH_NAME`](crate::host::store::CLONE_SCRATCH_NAME), a launch its log, and a -/// release the clone binding. The store creates nothing but regular files under its names, and -/// its control socket, so the plan also refuses a link or a directory under one of them, -/// through which the path of an image could pass. +/// replace or remove its files, as a launch replaces its log and the clone's activation record, +/// and a release removes the clone binding. The store creates nothing but regular files under +/// its names, and its control socket, so the plan also refuses a link or a directory under one +/// of them, through which the path of an image could pass. /// - An upper in RAM, a tmpfs, leaves the `scratch` block device unused, which the restore -/// attaches all the same: the plan creates its file, -/// [`CLONE_SCRATCH_NAME`](crate::host::store::CLONE_SCRATCH_NAME) in the sandbox's directory, a -/// sparse file of the template's scratch length, private to the current user, in place of one -/// that an earlier clone of the sandbox left, and closes it. The filesystem of the sandbox's -/// directory must present the template's scratch geometry. +/// attaches all the same, since the template's guest negotiated it: the restore declares it +/// absent, and OpenVMM attaches it with no file, as [`launch`](Self::launch) describes. The plan +/// creates nothing for it. /// - An upper on the caller's scratch image must pass the check of its binding, /// [`ScratchBinding::check`], which checks the image's identity, kind, and length and claims it /// for the clone, and present the geometry of the template's scratch, its length included, @@ -320,17 +315,14 @@ pub struct ClonePlan { export: Option, } -/// How a plan reaches the clone's files: as the checks of their bindings reach them, and as it -/// creates the file of the `scratch` block device of an upper in RAM; or, in tests, through opens -/// that read no content, or that reach files on filesystems of other block sizes. +/// How a plan reaches the clone's files: as the checks of their bindings reach them, or, in tests, +/// through opens that read no content, or that reach files on filesystems of other block sizes. #[derive(Clone, Copy)] struct Access<'a> { /// How the check of the image's binding reaches the image. distro: distro::Access<'a>, /// How the check of the scratch image's binding reaches the image. scratch: scratch::Access<'a>, - /// Creates the file of the `scratch` block device of an upper in RAM, open for writing. - create: &'a dyn Fn(&Path) -> io::Result, } impl Access<'static> { @@ -338,7 +330,6 @@ impl Access<'static> { const CHECKED: Self = Self { distro: distro::Access::GUARDED, scratch: scratch::Access::WRITABLE, - create: &create_sparse, }; } @@ -349,18 +340,18 @@ impl ClonePlan { /// /// The plan is made under the lock that acquired the lease, which `acquired` holds, before the /// launch that the lease reserves, [`Lease::launch`], which the caller then runs under the - /// same lock, with [`launch`](Self::launch): the sandbox records no VM and no launch, so no VM of an earlier clone holds a - /// file that the plan replaces. It takes `acquired` exclusively while it plans, since it may - /// create a file in the sandbox's directory, as the transitions of a sandbox's store take its - /// lock, and borrows nothing of it once it returns. It reads the generation's key, + /// same lock, with [`launch`](Self::launch), while the sandbox records no VM and no launch. It + /// takes `acquired` exclusively while it plans, as the transitions of a sandbox's store take + /// its lock, since it checks the files of the sandbox's store, and borrows nothing of it once + /// it returns. It creates nothing in the sandbox's directory. It reads the generation's key, /// [`Store::read_key`], without computing its identity again, and checks the generation's /// snapshot, [`Store::snapshot_dir`], then checks the clone's files, as the [type](Self) /// describes. /// - /// Fails with a [`CloneFailure`] at [`Stage::Plan`], and holds nothing, keeping no file that - /// it created. The failure's record holds the evidence of the check that failed, as below, no - /// transaction, and no VM, [`Teardown::NoVm`], and the plan records it nowhere: the caller - /// holds it, and releases the lease. The evidence of a failure of the host's own operations is + /// Fails with a [`CloneFailure`] at [`Stage::Plan`], and holds nothing. The failure's record + /// holds the evidence of the check that failed, as below, no transaction, and no VM, + /// [`Teardown::NoVm`], and the plan records it nowhere: the caller holds it, and releases the + /// lease. The evidence of a failure of the host's own operations is /// [`Evidence::ResourceExhaustion`] if an I/O error among the error's sources says that the /// host ran out of storage, memory, or descriptors, and [`Evidence::HostFailure`] otherwise. /// The plan checks in this order: @@ -433,16 +424,11 @@ impl ClonePlan { /// user or an administrator, and that no one else may change, /// [`Evidence::SuppliedIdentity`], and with [`ErrorKind::Io`] if the image cannot be opened /// at its path otherwise, or identified or examined once open, the host's evidence; with - /// [`ErrorKind::Unsupported`] for an image of another length than the template's - /// placeholder, [`Evidence::SuppliedLength`], or of other block sizes, or of a geometry that - /// OpenVMM refuses, [`Evidence::SuppliedGeometry`]; and with - /// [`ErrorKind::Unsupported`] for an image that is a file of the sandbox's store, - /// [`Evidence::InvalidSettings`]; - /// - for an upper in RAM, with [`ErrorKind::Unsupported`] for a sandbox whose path holds a - /// comma or a semicolon, [`Evidence::HostFailure`], or whose filesystem presents another - /// geometry than the template's scratch, [`Evidence::IncompatibleHost`], and with - /// [`ErrorKind::Io`] if the sparse file cannot be created, sized, or checked, the host's - /// evidence; + /// [`ErrorKind::Unsupported`] for an image of other block sizes than the template's + /// placeholder, or of a geometry that OpenVMM refuses, [`Evidence::SuppliedGeometry`], or + /// longer than the placeholder, the capacity of the template's clones' images, + /// [`Evidence::SuppliedLength`]; and with [`ErrorKind::Unsupported`] for an image that is a + /// file of the sandbox's store, [`Evidence::InvalidSettings`]; /// - for an upper on a scratch image, with [`ErrorKind::Io`] if the image's location cannot be /// compared with the template store's directory, or its names cannot be counted, the host's /// evidence, and as [`ScratchBinding::check`] and [`CheckedScratch::geometry`] fail: with @@ -578,11 +564,9 @@ impl ClonePlan { deadline, access, )?), - _ => CloneUpper::Tmpfs { - path: create_scratch(locked, &key, access)?, - }, + _ => CloneUpper::Tmpfs, }; - let devices = devices_of(request, &upper); + let devices = devices_of(request); Ok(Self { lease: lease.clone(), generation, @@ -626,18 +610,19 @@ impl ClonePlan { &self.image } - /// Returns the clone's upper, with the file of its `scratch` block device. + /// Returns the clone's upper, which decides how its restore attaches the `scratch` block + /// device. pub fn upper(&self) -> &CloneUpper { &self.upper } /// Returns the file of the clone's `scratch` block device, which OpenVMM opens at this path: - /// the sparse file [`CLONE_SCRATCH_NAME`](crate::host::store::CLONE_SCRATCH_NAME) in the - /// sandbox's directory for an upper in RAM, and the caller's scratch image otherwise. - pub fn scratch_path(&self) -> &Path { + /// the caller's scratch image, or `None` for an upper in RAM, whose restore declares the + /// device absent. + pub fn scratch_path(&self) -> Option<&Path> { match &self.upper { - CloneUpper::Tmpfs { path } => path, - CloneUpper::Scratch(checked) => checked.path(), + CloneUpper::Tmpfs => None, + CloneUpper::Scratch(checked) => Some(checked.path()), } } @@ -718,17 +703,13 @@ impl fmt::Debug for ClonePlan { } } -/// Returns the devices of the clone of `request`, whose upper is `upper`: the adapter's plan of its -/// image, over the file of its `scratch` block device, which every restore attaches, the sparse -/// file of an upper in RAM included. The restore attaches the template's network, which its key -/// records, rather than the plan's, which the plan's check found to be the same. -fn devices_of(request: &CloneRequest, upper: &CloneUpper) -> SandboxPlan { +/// Returns the devices of the clone of `request`: the adapter's plan of its image over its upper, +/// which renders the `scratch` block device of an upper on a scratch image alone, since the +/// restore declares that of an upper in RAM absent. The restore attaches the template's network, +/// which its key records, rather than the plan's, which the plan's check found to be the same. +fn devices_of(request: &CloneRequest) -> SandboxPlan { let adapter = &request.adapter; - let scratch = match upper { - CloneUpper::Tmpfs { path } => SandboxUpper::scratch_at(paths::normalized(path)), - CloneUpper::Scratch(_) => adapter.upper().clone(), - }; - SandboxPlan::adapter(adapter.distro().clone(), scratch) + SandboxPlan::adapter(adapter.distro().clone(), adapter.upper().clone()) } /// Refuses a request that no clone takes, as [`ClonePlan::new`] describes, without reading or @@ -1005,9 +986,9 @@ fn check_template( } /// Checks the image of `request` against its binding, through `access`, and then its geometry, -/// against the placeholder's that `key` records, the image at its path, and that it is none of -/// `owned`, the files of the sandbox's store, as [`ClonePlan`] describes, and returns the file -/// that the binding's check opened. +/// against the capacity of the placeholder that `key` records, the image at its path, and that it +/// is none of `owned`, the files of the sandbox's store, as [`ClonePlan`] describes, and returns +/// the file that the binding's check opened. fn check_image( request: &CloneRequest, key: &TemplateKey, @@ -1047,13 +1028,7 @@ fn check_image( Fault::new(evidence, error) })?; let geometry = checked.geometry().map_err(supplied_geometry_fault)?; - check_geometry( - geometry, - key.distro(), - &format!("the image {shown}"), - "placeholder", - Mismatch::SUPPLIED, - )?; + check_capacity(geometry, key.distro(), &format!("the image {shown}"))?; let identity = identity_of(checked.file()).map_err(|error| { Fault::host(Error::io(format!("the image {shown} cannot be identified")).with_source(error)) })?; @@ -1098,7 +1073,6 @@ fn check_scratch( key.scratch(), &format!("the scratch image {shown}"), "scratch", - Mismatch::SUPPLIED, )?; let identity = identity_of(checked.file()).map_err(|error| { Fault::host( @@ -1255,9 +1229,9 @@ struct StoreFiles(Vec<(PathBuf, (u64, u128))>); impl StoreFiles { /// Returns the regular files that the store of the sandbox of `locked` owns now, as - /// [`LockedStore::owned_entries`] lists its entries: its files, among them the file of the - /// scratch device of a clone in RAM, which a plan replaces, its log, which a launch replaces, - /// and the clone binding, which a release removes, and its temporary files. + /// [`LockedStore::owned_entries`] lists its entries: its files, among them its log and the + /// clone's activation record, which a launch replaces, and the clone binding, which a release + /// removes, and its temporary files. /// /// The store creates nothing but regular files under its names, and its control socket, so a /// link or a directory among its entries is refused: the path of an image could pass through @@ -1326,123 +1300,62 @@ impl StoreFiles { } } -/// Creates the file of the `scratch` block device of a clone whose upper is in RAM, in the -/// sandbox of `locked`, through `access`: a sparse file of the length of the template's scratch, -/// which `key` records, whose geometry must be the scratch's, and which is checked at its path as -/// the capture of a template checks its own, as [`ClonePlan`] describes. Returns its path, once -/// the file is closed. It removes the file if any step fails, its creation included, which can -/// fail once it made the file, as a file that cannot be made sparse does. -fn create_scratch( - locked: &LockedStore, - key: &TemplateKey, - access: Access<'_>, -) -> Result { - let path = locked.clone_scratch_path(); - let shown = path.display(); - reject_disk_path(&path, &shown.to_string()).attribute(Evidence::HostFailure)?; - let failed = |error: io::Error| { - Fault::host( - Error::io(format!("cannot create the {SPARSE_WHAT} {shown}")).with_source(error), - ) - }; - let created = (|| { - let file = (access.create)(&path).map_err(&failed)?; - let scratch = key.scratch(); - file.set_len(scratch.length).map_err(&failed)?; - let geometry = - block_geometry(&file, &path, SPARSE_WHAT).map_err(|error| match error.kind() { - // The sandbox's filesystem presents a geometry that OpenVMM refuses. - ErrorKind::Unsupported => Fault::new(Evidence::IncompatibleHost, error), - _ => Fault::host(error), - })?; - check_geometry( - geometry, - scratch, - &format!("the {SPARSE_WHAT} {shown}, in the sandbox's filesystem,"), - "scratch", - Mismatch::HOST, - )?; - let identity = identity_of(&file).map_err(&failed)?; - drop(file); - // The host made the file, so what its path shows of it is the host's. - check_at_path(&path, identity, &format!("the {SPARSE_WHAT} {shown}")).map_err(|fault| { - let (_, message, source) = fault.error.into_parts(); - let error = Error::io(message); - Fault::host(match source { - Some(source) => error.with_source(source), - None => error, - }) - }) - })(); - match created { - Ok(()) => Ok(path), - Err(fault) => { - let _ = remove_if_present(&path); - Err(fault) - } - } -} - -/// Creates the file of the `scratch` block device of a clone whose upper is in RAM at `path`, a -/// sparse file private to the current user, open for writing, in place of one that an earlier -/// clone of the sandbox left. -fn create_sparse(path: &Path) -> io::Result { - match fs::remove_file(path) { - Err(error) if error.kind() != io::ErrorKind::NotFound => return Err(error), - _ => {} +/// Refuses `found`, the geometry of a clone's image, which messages call `what`, unless it fits +/// `capacity`, the geometry of the template's placeholder, which a restore presents over the +/// image: of other block sizes, [`Evidence::SuppliedGeometry`], or longer, +/// [`Evidence::SuppliedLength`]. A shorter image fills the capacity's first blocks, and the +/// restore reads zeros past its end. +fn check_capacity(found: BlockGeometry, capacity: BlockGeometry, what: &str) -> Result<(), Fault> { + check_blocks(found, capacity, what, "placeholder")?; + if found.length > capacity.length { + return Err(Fault::new( + Evidence::SuppliedLength, + Error::unsupported(format!( + "{what} is {} bytes long, longer than the template's placeholder, whose {} bytes \ + are the capacity of every clone's image", + found.length, capacity.length + )), + )); } - let file = create_new(path)?; - platform::make_sparse(&file)?; - Ok(file) -} - -/// The evidence of a file of a clone's block device whose geometry is not the one that the -/// template's device presented. -#[derive(Clone, Copy)] -struct Mismatch { - /// The evidence of another length. - length: Evidence, - /// The evidence of other block sizes. - blocks: Evidence, -} - -impl Mismatch { - /// A file that the request supplied. - const SUPPLIED: Self = Self { - length: Evidence::SuppliedLength, - blocks: Evidence::SuppliedGeometry, - }; - - /// The sparse file of an upper in RAM, which the plan sizes as the template's scratch, and - /// whose block sizes the sandbox's filesystem presents: the host's. - const HOST: Self = Self { - length: Evidence::IncompatibleHost, - blocks: Evidence::IncompatibleHost, - }; + Ok(()) } -/// Refuses `found`, the geometry of the file of a clone's block device, which messages call -/// `what`, unless it is `expected`, the geometry that the template's `device` presented, with the -/// evidence that `mismatch` gives. +/// Refuses `found`, the geometry of the file of a clone's block device that the request supplied, +/// which messages call `what`, unless it is `expected`, the geometry that the template's `device` +/// presented: of another length, [`Evidence::SuppliedLength`], or of other block sizes, +/// [`Evidence::SuppliedGeometry`]. fn check_geometry( found: BlockGeometry, expected: BlockGeometry, what: &str, device: &str, - mismatch: Mismatch, ) -> Result<(), Fault> { if found.length != expected.length { return Err(Fault::new( - mismatch.length, + Evidence::SuppliedLength, Error::unsupported(format!( "{what} is {} bytes long, not the {} bytes of the template's {device}", found.length, expected.length )), )); } - if found != expected { + check_blocks(found, expected, what, device) +} + +/// Refuses `found`, the geometry of the file of a clone's block device that the request supplied, +/// which messages call `what`, unless it presents the logical and physical block sizes of +/// `expected`, the geometry that the template's `device` presented, [`Evidence::SuppliedGeometry`]. +fn check_blocks( + found: BlockGeometry, + expected: BlockGeometry, + what: &str, + device: &str, +) -> Result<(), Fault> { + if found.logical_block_size != expected.logical_block_size + || found.physical_block_size != expected.physical_block_size + { return Err(Fault::new( - mismatch.blocks, + Evidence::SuppliedGeometry, Error::unsupported(format!( "{what} presents logical blocks of {} bytes and physical blocks of {} bytes, but \ the template's {device} presented {} and {} bytes", diff --git a/nvx-rs/src/host/template/clone/failure.rs b/nvx-rs/src/host/template/clone/failure.rs index f3aec70d6..dbdef5610 100644 --- a/nvx-rs/src/host/template/clone/failure.rs +++ b/nvx-rs/src/host/template/clone/failure.rs @@ -119,7 +119,9 @@ pub enum Evidence { /// changed, or removed after its binding was made, or its path names another file than the /// check found, or one that others may change. SuppliedIdentity, - /// A file that the request supplied has another length than the template's device. + /// A file that the request supplied is of a length that the template's device cannot take: an + /// image longer than the template's placeholder, or a scratch image of another length than the + /// template's scratch. SuppliedLength, /// A file that the request supplied presents other block sizes than the template's device, or /// a geometry that OpenVMM refuses. @@ -133,10 +135,9 @@ pub enum Evidence { /// The host ran out of a resource, such as its storage, its memory, or its descriptors. ResourceExhaustion, /// The host cannot restore the template: this build speaks other contracts than the template, - /// or reads no key of the template's encoding version, or the sandbox's filesystem presents - /// another geometry than the template's scratch, or OpenVMM refused to restore the snapshot - /// with a time ABI code that says that the host cannot present the template's CPU profile, - /// TSC, or LAPIC timer, as [the attribution](Self#attribution) describes. + /// or reads no key of the template's encoding version, or OpenVMM refused to restore the + /// snapshot with a time ABI code that says that the host cannot present the template's CPU + /// profile, TSC, or LAPIC timer, as [the attribution](Self#attribution) describes. IncompatibleHost, /// The clone runs, or would launch, under another runtime identity than the template's or the /// caller's, which the clone's guest might not speak. diff --git a/nvx-rs/src/host/template/clone/launch.rs b/nvx-rs/src/host/template/clone/launch.rs index 97223c772..cc989906e 100644 --- a/nvx-rs/src/host/template/clone/launch.rs +++ b/nvx-rs/src/host/template/clone/launch.rs @@ -14,20 +14,18 @@ use super::failure::{ host_evidence, }; use super::records::{End, ProgressLog, Recorder, Tracker, abandon}; -use super::{ClonePlan, CloneUpper, MIB, Mismatch, SPARSE_WHAT, check_geometry}; +use super::{ClonePlan, CloneUpper, MIB}; use crate::activation::ActivationRecord; use crate::error::{Error, ErrorKind, Result}; use crate::host::artifacts; use crate::host::filesystem; use crate::host::platform::{self, ReadyListener}; +use crate::host::sandbox::SandboxPlan; use crate::host::sandbox::distro::CheckedDistro; -use crate::host::sandbox::{SandboxPlan, block_geometry}; use crate::host::store::files::write_private; -use crate::host::store::regular::open_regular; use crate::host::store::{LockedStore, Recovery, RuntimeIdentity}; use crate::host::supervisor::{self, Inherited, Starting, VmmCommand}; use crate::host::template::key::TemplateKey; -use crate::host::template::manifest::BlockGeometry; use crate::host::template::store::{Acquired, Lease, SOCKET_PATH_LIMIT}; use crate::protocol::{CAPABILITY_LEN, HostMap}; @@ -87,10 +85,7 @@ impl ClonePlan { /// sandbox's clone binding records the lease: the acquisition made the binding durable, and no /// VMM of the clone starts without it, and that the sandbox records no failure of the clone: /// a lease's clone fails once, and the record of its failure lasts until the release, even - /// for a launch that failed before it began, which leaves the lock's reservation. For an upper - /// in RAM, it checks that the file of the scratch device that the plan created is still - /// there: a later plan under the same acquisition replaces it, and removes it if that plan - /// fails. It then: + /// for a launch that failed before it began, which leaves the lock's reservation. It then: /// /// 1. Creates the clone's progress log, /// [`CLONE_PROGRESS_NAME`](crate::host::store::CLONE_PROGRESS_NAME) in the sandbox's @@ -128,9 +123,14 @@ impl ClonePlan { /// pipe, as every launch does. [`Starting::endpoint`] returns it. /// /// The VMM's arguments are those of the clone's devices: the microVM machine, the clone's - /// image as the read-only `distro` block device and the file of its `scratch` block device, - /// which OpenVMM opens at their paths, as [Block devices by - /// path](crate::host::template::Store#block-devices-by-path) describes, and the template's + /// image as the read-only `distro` block device, which OpenVMM opens at its path, as [Block + /// devices by path](crate::host::template::Store#block-devices-by-path) describes, and presents + /// at the capacity that the template's placeholder fixed, reading zeros past the end of a + /// shorter image; for an upper on a scratch image, the image as the `scratch` block device, + /// which OpenVMM opens alike, and for an upper in RAM, `--restore-absent-scratch`, which + /// declares the template's scratch absent, so that OpenVMM attaches it with no file, at the + /// geometry that the template recorded, as the template's guest negotiated it: its reads return + /// zeros and its writes fail, and the clone's guest never uses it; and the template's /// network, as its key records it, [`TemplateKey::network_arguments`], the policy of the /// template's NIC, unchanged, if it has one, without `--net` and the guest's address, which /// OpenVMM takes from the snapshot. The clone's export, if it has one, follows, bound to the @@ -187,12 +187,7 @@ impl ClonePlan { /// [`Lease::of`] fails, or the record of a failure cannot be examined, the host's evidence; /// with /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a runtime identity other - /// than the template's, [`Evidence::RuntimeMismatch`]; for an upper in RAM, with - /// [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) if the file of its scratch - /// device, which the plan created, cannot be opened, as when a later plan of the clone - /// replaced it and failed, the host's evidence, and with - /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) if it no longer presents the - /// template's scratch geometry, [`Evidence::IncompatibleHost`]; on Linux, with + /// than the template's, [`Evidence::RuntimeMismatch`]; on Linux, with /// [`ErrorKind::Other`](crate::ErrorKind::Other) if the control socket that the lease /// reserves is not beside the plan's snapshot, as for a plan and a lease that name the /// template store by different directories, [`Evidence::HostFailure`], with @@ -327,7 +322,7 @@ impl ClonePlan { // On Windows, this closes the claim's handle, and keeps the image's guard. let claim = match upper { CloneUpper::Scratch(checked) => Some(checked.into_file()), - CloneUpper::Tmpfs { .. } => None, + CloneUpper::Tmpfs => None, }; // No spawn passes a descriptor of a standard stream, on which the claim is if the caller // had closed that stream: it moves above them, with its open file description. @@ -415,8 +410,8 @@ impl ClonePlan { /// Refuses a launch of the plan through `lease` and `locked` that is not the one that the /// plan's lease reserves, or whose binding the sandbox does not record, one under `runtime` - /// that the template does not take, one of a plan in RAM whose scratch device's file is gone, - /// and one of an export whose host objects changed since its plan found them, as + /// that the template does not take, and one of an export whose host objects changed since its + /// plan found them, as /// [`launch`](Self::launch) describes, without writing anything. fn check_launch( &self, @@ -490,9 +485,6 @@ impl ClonePlan { )), )); } - if let CloneUpper::Tmpfs { path } = &self.upper { - check_sparse(path, self.key.scratch())?; - } if cfg!(target_os = "linux") && let Some(socket) = locked.reserved_socket() { @@ -620,42 +612,18 @@ impl ClonePlan { } } -/// Refuses the file of the `scratch` block device of a clone whose upper is in RAM, at `path`, -/// which the clone's plan created, unless it is still a regular file that presents the template's -/// scratch geometry, `scratch`: OpenVMM opens it by its path, and a later plan under the same -/// acquisition replaces it, or removes it if that plan fails. The file is the host's, as is the -/// evidence of its refusal. -fn check_sparse(path: &Path, scratch: BlockGeometry) -> Result<(), Fault> { - let shown = path.display(); - let file = open_regular(path).map_err(|error| { - Fault::host( - Error::unavailable(format!( - "the {SPARSE_WHAT} {shown}, which the clone's plan created, cannot be opened, as \ - when a later plan of the clone replaced it and failed" - )) - .with_source(error), - ) - })?; - check_geometry( - block_geometry(&file, path, SPARSE_WHAT).map_err(|error| match error.kind() { - // The sandbox's filesystem presents a geometry that OpenVMM refuses. - crate::ErrorKind::Unsupported => Fault::new(Evidence::IncompatibleHost, error), - _ => Fault::host(error), - })?, - scratch, - &format!("the {SPARSE_WHAT} {shown}"), - "scratch", - Mismatch::HOST, - ) -} - /// Returns OpenVMM's arguments of the devices of a clone's restore: those of the clone's devices, -/// `devices`, which [`SandboxPlan::openvmm_arguments`] renders, then the template's network, as -/// `key` records it, [`TemplateKey::network_arguments`], without `--net` and the guest's address, -/// which OpenVMM takes from the snapshot, and refuses beside it. The options of the network's -/// policy stay: OpenVMM compares the policy that they name with the one of the snapshot's NIC. +/// `devices`, which [`SandboxPlan::openvmm_arguments`] renders, with `--restore-absent-scratch` +/// for an upper in RAM, whose plan renders no `scratch` block device, then the template's network, +/// as `key` records it, [`TemplateKey::network_arguments`], without `--net` and the guest's +/// address, which OpenVMM takes from the snapshot, and refuses beside it. The options of the +/// network's policy stay: OpenVMM compares the policy that they name with the one of the +/// snapshot's NIC. fn device_arguments(devices: &SandboxPlan, key: &TemplateKey) -> Result> { let mut arguments = devices.openvmm_arguments()?; + if devices.upper().scratch_path().is_none() { + arguments.push("--restore-absent-scratch".to_owned()); + } // A key's network attaches its NIC first, with `--net` and the guest's address. arguments.extend_from_slice(key.network_arguments().get(2..).unwrap_or_default()); Ok(arguments) diff --git a/nvx-rs/src/host/template/clone/tests.rs b/nvx-rs/src/host/template/clone/tests.rs index f125f08da..49d063618 100644 --- a/nvx-rs/src/host/template/clone/tests.rs +++ b/nvx-rs/src/host/template/clone/tests.rs @@ -1,7 +1,7 @@ //! Tests of the plans of clones: the checks of their images, uppers, exports, and memory targets //! against the generation that they lease, which read no content, the claims of their scratch -//! images, and the sparse scratch device of an upper in RAM; in [`launch`], of their launches; and -//! in [`restore`], of their restore sequences. +//! images, and the uppers in RAM, whose scratch devices their restores declare absent; in +//! [`launch`], of their launches; and in [`restore`], of their restore sequences. mod launch; mod restore; @@ -27,7 +27,9 @@ use crate::host::sandbox::distro::{DISTRO_POLICY_REVISION, PolicyVerdict}; use crate::host::sandbox::scratch::CLAIM_RELEASE_WAIT; use crate::host::sandbox::{SandboxDistro, SandboxNetwork, SandboxSettings, SandboxUpper}; use crate::host::seal::{FileSeal, open_metadata_only}; -use crate::host::store::{CLONE_SCRATCH_NAME, CallerFiles, LOG_NAME, SandboxStore}; +use crate::host::store::{ + CLONE_ACTIVATION_NAME, CLONE_SCRATCH_NAME, CallerFiles, LOG_NAME, SandboxStore, +}; use crate::host::template::store::{CAPTURES_NAME, SNAPSHOT_NAME, TRANSACTION_NAME}; use crate::host::template::test_support::{MIB, ManifestSpec, class, network_class}; use crate::host::template::{CaptureClass, HostTime}; @@ -64,7 +66,6 @@ const NO_CONTENT: Access<'static> = Access { seal: &FileSeal::of, }, scratch: WRITE_ONLY, - create: &create_sparse, }; /// Opens a scratch image for writing alone, as [`NO_CONTENT`] does. @@ -540,21 +541,21 @@ impl Fixture { } } -/// Returns the path of the scratch device's file of a clone in RAM in the sandbox of `acquired`. -fn sparse_path(acquired: &Acquired) -> PathBuf { - acquired.locked.dir().join(CLONE_SCRATCH_NAME) +/// Returns the names of the entries of the directory `dir`, in order. +fn entries(dir: &Path) -> Vec { + let mut names: Vec = fs::read_dir(dir) + .unwrap() + .map(|entry| entry.unwrap().file_name().into_string().unwrap()) + .collect(); + names.sort(); + names } -/// Asserts that the sandbox of `acquired` holds no scratch device's file of a clone in RAM, and -/// no record of a clone's progress or failure, and that its lease still reserves its launch, as -/// after a plan that failed. +/// Asserts that the sandbox of `acquired` holds no record of a clone's progress or failure, and +/// that its lease still reserves its launch, as after a plan that failed. fn assert_untouched(acquired: &Acquired) { let locked = &acquired.locked; - for path in [ - sparse_path(acquired), - locked.clone_progress_path(), - locked.clone_failure_path(), - ] { + for path in [locked.clone_progress_path(), locked.clone_failure_path()] { assert!(!path.exists(), "{}", path.display()); } assert_eq!(locked.reserved(), Some(acquired.lease.launch())); @@ -638,10 +639,12 @@ fn link_dir(target: &Path, link: &Path) { } #[test] -fn a_clone_in_ram_restores_its_lease_with_its_image_and_a_sparse_scratch_device() { +fn a_clone_in_ram_restores_its_lease_with_its_image_and_no_scratch_file() { let fixture = Fixture::new(); let (image, binding) = fixture.image("distro.erofs", IMAGE_LEN); let mut acquired = fixture.acquire("sandbox"); + let sandbox = acquired.locked.dir().to_owned(); + let before = entries(&sandbox); let request = tmpfs_request(&image, &binding); let plan = fixture.plan(&mut acquired, &request, NO_CONTENT).unwrap(); let store = &fixture.store; @@ -661,35 +664,11 @@ fn a_clone_in_ram_restores_its_lease_with_its_image_and_a_sparse_scratch_device( ); let mut byte = [0]; assert!(plan.image().file().read(&mut byte).is_err()); - // The scratch device's file is a sparse file of the template's scratch length, private to the - // current user, in the sandbox's directory, which the plan closed. - let sparse = sparse_path(&acquired); - assert!(matches!(plan.upper(), CloneUpper::Tmpfs { path } if *path == sparse)); - assert_eq!(plan.scratch_path(), sparse.as_path()); - let metadata = fs::metadata(&sparse).unwrap(); - assert_eq!(metadata.len(), SCRATCH_LEN); - #[cfg(target_os = "linux")] - { - use std::os::unix::fs::MetadataExt; - - assert_eq!(metadata.mode() & 0o777, 0o600); - assert!( - metadata.blocks() * 512 < SCRATCH_LEN, - "{}", - metadata.blocks() - ); - } - #[cfg(windows)] - { - use std::os::windows::fs::MetadataExt; - - /// `FILE_ATTRIBUTE_SPARSE_FILE`. - const SPARSE: u32 = 0x200; - assert_ne!(metadata.file_attributes() & SPARSE, 0); - } - let opened = File::open(&sparse).unwrap(); - assert_eq!(platform::foreign_writer_problem(&opened).unwrap(), None); - drop(opened); + // The restore declares the `scratch` block device of an upper in RAM absent, so the plan has + // no file for it, and creates nothing in the sandbox's directory. + assert!(matches!(plan.upper(), CloneUpper::Tmpfs)); + assert_eq!(plan.scratch_path(), None); + assert_eq!(entries(&sandbox), before); // The capability is fresh, and never shown. let capability = *plan.capability(); assert_ne!(capability, [0; CAPABILITY_LEN]); @@ -706,84 +685,15 @@ fn a_clone_in_ram_restores_its_lease_with_its_image_and_a_sparse_scratch_device( // The access of launches plans alike, with another capability. let again = ClonePlan::new(store, &mut acquired, &request, Instant::now()).unwrap(); assert_ne!(*again.capability(), capability); - assert_eq!(again.scratch_path(), sparse.as_path()); -} - -#[test] -fn the_scratch_device_of_a_clone_in_ram_is_fresh_and_goes_with_its_lease() { - let fixture = Fixture::new(); - let (image, binding) = fixture.image("distro.erofs", IMAGE_LEN); - let mut acquired = fixture.acquire("sandbox"); - let request = tmpfs_request(&image, &binding); - // An earlier clone of the sandbox left its file, which the plan replaces. - let sparse = sparse_path(&acquired); - fs::write(&sparse, b"left by an earlier clone").unwrap(); - let plan = fixture.plan(&mut acquired, &request, NO_CONTENT).unwrap(); - assert_eq!(plan.scratch_path(), sparse.as_path()); - let mut content = vec![1; 64]; - File::open(&sparse) - .unwrap() - .read_exact(&mut content) - .unwrap(); - assert_eq!(content, [0; 64]); - drop(plan); - // The release of the lease, once the clone's VM has exited, removes it with the binding. - assert_eq!( - fixture.store.release(&mut acquired.locked).unwrap(), - Some(acquired.lease.clone()) - ); - assert!(!sparse.exists()); - assert!(!acquired.locked.binding_path().exists()); - // The removal of the sandbox removes one that remains, as one of the store's files. - fs::write(&sparse, b"").unwrap(); - let Acquired { locked, .. } = acquired; - assert!(locked.remove().unwrap().is_none()); - assert!(!sparse.exists()); -} - -#[test] -fn a_scratch_device_that_the_plan_cannot_initialize_goes() { - let fixture = Fixture::new(); - let (image, binding) = fixture.image("distro.erofs", IMAGE_LEN); - let mut acquired = fixture.acquire("sandbox"); - let request = tmpfs_request(&image, &binding); - // A creation that makes the file, and then fails to make it sparse, as one on a volume that - // has no sparse files does. - let create = |path: &Path| -> io::Result { - drop(create_new(path)?); - Err(io::Error::other("the volume has no sparse files")) - }; - let access = Access { - create: &create, - ..NO_CONTENT - }; - let error = fixture.plan(&mut acquired, &request, access).unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Io, "{error}"); - assert_eq!( - error.message(), - format!( - "cannot create the scratch device of the clone {}", - shown(&sparse_path(&acquired)) - ) - ); - assert_eq!( - std::error::Error::source(&error).unwrap().to_string(), - "the volume has no sparse files" - ); - assert_untouched(&acquired); - // The next plan creates it afresh. - let plan = fixture.plan(&mut acquired, &request, NO_CONTENT).unwrap(); - assert_eq!( - fs::metadata(plan.scratch_path()).unwrap().len(), - SCRATCH_LEN - ); + assert_eq!(again.scratch_path(), None); + assert_eq!(entries(&sandbox), before); } #[test] fn files_of_the_sandboxs_store_are_refused_as_images() { let fixture = Fixture::new(); let mut acquired = fixture.acquire("sandbox"); - let sparse = sparse_path(&acquired); + let record = acquired.locked.clone_activation_path(); // The image or the scratch image of a request is a file of the sandbox's store: the request's // settings are invalid. let store_file = |result: std::result::Result| { @@ -801,19 +711,33 @@ fn files_of_the_sandboxs_store_are_refused_as_images() { ) }; let seal_of = |path: &Path| FileSeal::of(&File::open(path).unwrap()).unwrap(); - // An image that is the file of the scratch device of a clone in RAM, which a plan would - // replace, is refused before anything replaces it... - stage(&sparse, IMAGE_LEN); - let binding = DistroBinding::import(&layer(&sparse), PolicyVerdict::ACCEPTED).unwrap(); - let staged = seal_of(&sparse); + // An image that is the clone's activation record, which a launch would replace, is refused + // before anything replaces it... + stage(&record, IMAGE_LEN); + let binding = DistroBinding::import(&layer(&record), PolicyVerdict::ACCEPTED).unwrap(); + let staged = seal_of(&record); assert_eq!( - store_file(fixture.plan(&mut acquired, &tmpfs_request(&sparse, &binding), NO_CONTENT)), - refusal("image", &sparse, &sparse) + store_file(fixture.plan(&mut acquired, &tmpfs_request(&record, &binding), NO_CONTENT)), + refusal("image", &record, &record) + ); + assert_eq!(seal_of(&record), staged); + // So is the scratch device's file that an earlier version left for a clone in RAM, which a + // release removes. + let left = acquired.locked.dir().join(CLONE_SCRATCH_NAME); + stage(&left, IMAGE_LEN); + let left_binding = DistroBinding::import(&layer(&left), PolicyVerdict::ACCEPTED).unwrap(); + assert_eq!( + store_file(fixture.plan( + &mut acquired, + &tmpfs_request(&left, &left_binding), + NO_CONTENT + )), + refusal("image", &left, &left) ); - assert_eq!(seal_of(&sparse), staged); + fs::remove_file(&left).unwrap(); // ...by whatever path the request names it: through a hard link... let hard = fixture.path("images", "hard.erofs"); - fs::hard_link(&sparse, &hard).unwrap(); + fs::hard_link(&record, &hard).unwrap(); let hard_binding = DistroBinding::import(&layer(&hard), PolicyVerdict::ACCEPTED).unwrap(); assert_eq!( store_file(fixture.plan( @@ -821,12 +745,12 @@ fn files_of_the_sandboxs_store_are_refused_as_images() { &tmpfs_request(&hard, &hard_binding), NO_CONTENT )), - refusal("image", &hard, &sparse) + refusal("image", &hard, &record) ); // ...or through a link among its directories. let alias = fixture.path("images", "alias"); if symlink_dir(acquired.locked.dir(), &alias) { - let aliased = alias.join(CLONE_SCRATCH_NAME); + let aliased = alias.join(CLONE_ACTIVATION_NAME); let aliased_binding = DistroBinding::import(&layer(&aliased), PolicyVerdict::ACCEPTED).unwrap(); assert_eq!( @@ -835,36 +759,36 @@ fn files_of_the_sandboxs_store_are_refused_as_images() { &tmpfs_request(&aliased, &aliased_binding), NO_CONTENT )), - refusal("image", &aliased, &sparse) + refusal("image", &aliased, &record) ); } // A release of the lease removes that file, so a clone on a scratch image takes it neither // as its image, which is bound again, since the hard link changed its change time on // Linux... - let binding = DistroBinding::import(&layer(&sparse), PolicyVerdict::ACCEPTED).unwrap(); - let staged = seal_of(&sparse); + let binding = DistroBinding::import(&layer(&record), PolicyVerdict::ACCEPTED).unwrap(); + let staged = seal_of(&record); let (image, image_binding) = fixture.image("distro.erofs", IMAGE_LEN); let (scratch, scratch_binding) = fixture.scratch("scratch.ext4", SCRATCH_LEN); - let request = scratch_request(&sparse, &binding, &scratch, &scratch_binding); + let request = scratch_request(&record, &binding, &scratch, &scratch_binding); assert_eq!( store_file(fixture.plan(&mut acquired, &request, NO_CONTENT)), - refusal("image", &sparse, &sparse) + refusal("image", &record, &record) ); - assert_eq!(seal_of(&sparse), staged); + assert_eq!(seal_of(&record), staged); // ...nor as its scratch image, whose claim the refused plan releases. - fs::remove_file(&sparse).unwrap(); - stage(&sparse, SCRATCH_LEN); - let sparse_binding = ScratchBinding::bind(&sparse).unwrap(); - let request = scratch_request(&image, &image_binding, &sparse, &sparse_binding); + fs::remove_file(&record).unwrap(); + stage(&record, SCRATCH_LEN); + let record_binding = ScratchBinding::bind(&record).unwrap(); + let request = scratch_request(&image, &image_binding, &record, &record_binding); assert_eq!( store_file(fixture.plan(&mut acquired, &request, NO_CONTENT)), - refusal("scratch image", &sparse, &sparse) + refusal("scratch image", &record, &record) ); - assert_eq!(fs::metadata(&sparse).unwrap().len(), SCRATCH_LEN); - assert!(released(|| sparse_binding - .check(&sparse, Instant::now()) + assert_eq!(fs::metadata(&record).unwrap().len(), SCRATCH_LEN); + assert!(released(|| record_binding + .check(&record, Instant::now()) .is_ok())); - fs::remove_file(&sparse).unwrap(); + fs::remove_file(&record).unwrap(); // The store's other files are refused alike, such as its log, which a launch replaces, and // the temporary files of its records. for name in [LOG_NAME, ".runtime.json.1.tmp"] { @@ -886,7 +810,7 @@ fn links_and_directories_under_the_names_of_the_sandboxs_store_are_refused() { let fixture = Fixture::new(); let (image, binding) = fixture.image("distro.erofs", IMAGE_LEN); let mut acquired = fixture.acquire("sandbox"); - let sparse = sparse_path(&acquired); + let record = acquired.locked.clone_activation_path(); // The sandbox's store holds an entry that it never creates: the host's state is not as the // store keeps it. let store_entry = |result: std::result::Result| { @@ -902,8 +826,8 @@ fn links_and_directories_under_the_names_of_the_sandboxs_store_are_refused() { ) }; let request = tmpfs_request(&image, &binding); - // A directory, which a plan could not replace, nor a release or a launch remove. - for name in [CLONE_SCRATCH_NAME, LOG_NAME] { + // A directory, which a launch could not replace, nor a release remove. + for name in [CLONE_ACTIVATION_NAME, LOG_NAME] { let owned = acquired.locked.dir().join(name); fs::create_dir(&owned).unwrap(); assert_eq!( @@ -914,28 +838,28 @@ fn links_and_directories_under_the_names_of_the_sandboxs_store_are_refused() { fs::remove_dir(&owned).unwrap(); } // A link to the directory of the images, through which the path of an image can pass, and - // which a plan would replace, leaving that path naming nothing. - if symlink_dir(image.parent().unwrap(), &sparse) { - let through = sparse.join("distro.erofs"); + // which a launch would replace, leaving that path naming nothing. + if symlink_dir(image.parent().unwrap(), &record) { + let through = record.join("distro.erofs"); let through_binding = DistroBinding::import(&layer(&through), PolicyVerdict::ACCEPTED).unwrap(); for request in [tmpfs_request(&through, &through_binding), request.clone()] { assert_eq!( store_entry(fixture.plan(&mut acquired, &request, NO_CONTENT)), - refusal(&sparse) + refusal(&record) ); } assert!( - fs::symlink_metadata(&sparse) + fs::symlink_metadata(&record) .unwrap() .file_type() .is_symlink() ); assert!(fs::metadata(&through).unwrap().is_file()); #[cfg(unix)] - fs::remove_file(&sparse).unwrap(); + fs::remove_file(&record).unwrap(); #[cfg(windows)] - fs::remove_dir(&sparse).unwrap(); + fs::remove_dir(&record).unwrap(); } assert_untouched(&acquired); // A control socket that a launch left is no image, and the launch removes it. @@ -944,9 +868,9 @@ fn links_and_directories_under_the_names_of_the_sandboxs_store_are_refused() { acquired.locked.dir().join(crate::host::store::SOCKET_NAME), ) .unwrap(); - // The plan creates the file of the scratch device once nothing else is there. + // Once nothing else is there, the clone is planned. let plan = fixture.plan(&mut acquired, &request, NO_CONTENT).unwrap(); - assert!(fs::symlink_metadata(plan.scratch_path()).unwrap().is_file()); + assert_eq!(plan.scratch_path(), None); } #[test] @@ -1174,7 +1098,7 @@ fn names_that_the_file_system_takes_for_those_of_the_stores_files_are_the_stores }; // An image under the name of one of the store's files in other letter cases is that file // where the file system ignores case, as Windows' do, and another file where it does not. - for name in [CLONE_SCRATCH_NAME, LOG_NAME] { + for name in [CLONE_ACTIVATION_NAME, LOG_NAME] { let cased = dir.join(name.to_ascii_uppercase()); stage(&cased, IMAGE_LEN); let binding = DistroBinding::import(&layer(&cased), PolicyVerdict::ACCEPTED).unwrap(); @@ -1191,18 +1115,17 @@ fn names_that_the_file_system_takes_for_those_of_the_stores_files_are_the_stores } // So is a scratch image, which a release would remove as the store's file where the file // system takes it for that file, and keeps where it does not. - let sparse = sparse_path(&acquired); - let _ = fs::remove_file(&sparse); - let cased = dir.join(CLONE_SCRATCH_NAME.to_ascii_uppercase()); + let record = acquired.locked.clone_activation_path(); + let cased = dir.join(CLONE_ACTIVATION_NAME.to_ascii_uppercase()); stage(&cased, SCRATCH_LEN); let binding = ScratchBinding::bind(&cased).unwrap(); let request = scratch_request(&image, &image_binding, &cased, &binding); - let same = same_file(&cased, &sparse); + let same = same_file(&cased, &record); let result = fixture.plan(&mut acquired, &request, NO_CONTENT); if same { assert_eq!( store_file(result), - refusal("scratch image", &cased, &sparse) + refusal("scratch image", &cased, &record) ); } else { drop(result.unwrap()); @@ -1421,16 +1344,15 @@ fn a_clone_on_a_scratch_image_claims_it_and_reads_no_content() { panic!("the clone's upper is not on its scratch image: {plan:?}"); }; assert_eq!(checked.path().to_str().unwrap(), shown(&scratch)); - assert_eq!(plan.scratch_path(), checked.path()); + assert_eq!(plan.scratch_path(), Some(checked.path())); assert_eq!( checked.geometry().unwrap(), geometry(SCRATCH_LEN, fixture.physical) ); - // What the checks hold reads nothing, and the plan creates no scratch device of its own. + // What the checks hold reads nothing. let mut byte = [0]; assert!(checked.file().read(&mut byte).is_err()); assert!(plan.image().file().read(&mut byte).is_err()); - assert!(!sparse_path(&acquired).exists()); // The plan holds the claim: another check finds the image claimed. let error = scratch_binding.check(&scratch, Instant::now()).unwrap_err(); assert_eq!(error.code(), ErrorCode::ScratchClaimed, "{error}"); @@ -1537,33 +1459,61 @@ fn scratch_images_whose_identity_or_kind_changed_are_refused() { } #[test] -fn images_of_another_length_or_geometry_are_refused() { +fn images_no_longer_than_the_placeholder_are_served_and_longer_ones_refused() { let fixture = Fixture::new(); let mut acquired = fixture.acquire("sandbox"); - let mut refusal = |length: u64, evidence: Evidence| { - let (image, binding) = fixture.image("other.erofs", length); + let physical = fixture.physical; + // A template serves images of either format of any length up to its placeholder's, the + // capacity that the restore presents over each, reading zeros past a shorter one's end. + for length in [512, IMAGE_LEN / 2, IMAGE_LEN - 512, IMAGE_LEN] { + for gpt in [false, true] { + let (image, binding) = if gpt { + fixture.gpt_image("served.gpt", length) + } else { + fixture.image("served.erofs", length) + }; + let plan = fixture + .plan(&mut acquired, &tmpfs_request(&image, &binding), NO_CONTENT) + .unwrap(); + assert_eq!(plan.image().geometry().unwrap(), geometry(length, physical)); + assert_eq!(plan.key().distro(), geometry(IMAGE_LEN, physical)); + drop(plan); + fs::remove_file(&image).unwrap(); + } + } + let mut refusal = |length: u64, gpt: bool, evidence: Evidence| { + let (image, binding) = if gpt { + fixture.gpt_image("other.gpt", length) + } else { + fixture.image("other.erofs", length) + }; let plan = fixture.plan(&mut acquired, &tmpfs_request(&image, &binding), NO_CONTENT); let failed = refused(plan, evidence); fs::remove_file(&image).unwrap(); (shown(&image), failed) }; - // A clone's image has the placeholder's length. - for length in [IMAGE_LEN - 512, IMAGE_LEN + 512, 2 * IMAGE_LEN] { - let (image, failed) = refusal(length, Evidence::SuppliedLength); + // A longer image, of either format, is refused. + for (length, gpt) in [ + (IMAGE_LEN + 512, false), + (2 * IMAGE_LEN, false), + (IMAGE_LEN + 512, true), + ] { + let (image, failed) = refusal(length, gpt, Evidence::SuppliedLength); assert_eq!( failed, ( ErrorKind::Unsupported, format!( - "the image {image} is {length} bytes long, not the {IMAGE_LEN} bytes of the \ - template's placeholder" + "the image {image} is {length} bytes long, longer than the template's \ + placeholder, whose {IMAGE_LEN} bytes are the capacity of every clone's image" ) ) ); } - // OpenVMM attaches no block device of another length. - for length in [0, IMAGE_LEN + 1] { - let (image, failed) = refusal(length, Evidence::SuppliedGeometry); + // OpenVMM attaches no block device whose length is not a nonzero multiple of 512 bytes, however + // short. + for length in [0, IMAGE_LEN - 1, IMAGE_LEN + 1] { + let (image, failed) = refusal(length, false, Evidence::SuppliedGeometry); assert_eq!( failed, ( @@ -1575,50 +1525,34 @@ fn images_of_another_length_or_geometry_are_refused() { ) ); } - // A GPT disk has the placeholder's length too. - let (gpt, gpt_binding) = fixture.gpt_image("other.gpt", IMAGE_LEN + 512); - let plan = fixture.plan( - &mut acquired, - &tmpfs_request(&gpt, &gpt_binding), - NO_CONTENT, - ); - assert_eq!( - refused(plan, Evidence::SuppliedLength), - ( - ErrorKind::Unsupported, - format!( - "the image {} is {} bytes long, not the {IMAGE_LEN} bytes of the template's \ - placeholder", - shown(&gpt), - IMAGE_LEN + 512 - ) - ) - ); assert_untouched(&acquired); // A template whose placeholder presented physical blocks of another size serves no image of - // the filesystem of this one. + // the filesystem of this one, whatever its length. let fixture = Fixture::with(|class, spec, physical| { let scratch = class.scratch; let distro = geometry(IMAGE_LEN, other_physical(physical)); set_geometries(class, spec, distro, scratch); }); let mut acquired = fixture.acquire("sandbox"); - let (image, binding) = fixture.image("distro.erofs", IMAGE_LEN); let physical = fixture.physical; - let plan = fixture.plan(&mut acquired, &tmpfs_request(&image, &binding), NO_CONTENT); - assert_eq!( - refused(plan, Evidence::SuppliedGeometry), - ( - ErrorKind::Unsupported, - format!( - "the image {} presents logical blocks of 512 bytes and physical blocks of \ - {physical} bytes, but the template's placeholder presented 512 and {} bytes", - shown(&image), - other_physical(physical) + for length in [IMAGE_LEN / 2, IMAGE_LEN, 2 * IMAGE_LEN] { + let (image, binding) = fixture.image("distro.erofs", length); + let plan = fixture.plan(&mut acquired, &tmpfs_request(&image, &binding), NO_CONTENT); + assert_eq!( + refused(plan, Evidence::SuppliedGeometry), + ( + ErrorKind::Unsupported, + format!( + "the image {} presents logical blocks of 512 bytes and physical blocks of \ + {physical} bytes, but the template's placeholder presented 512 and {} bytes", + shown(&image), + other_physical(physical) + ) ) - ) - ); + ); + fs::remove_file(&image).unwrap(); + } assert_untouched(&acquired); } @@ -1673,9 +1607,11 @@ fn scratch_images_of_another_length_or_geometry_are_refused() { } #[test] -fn sandboxes_whose_filesystem_presents_another_scratch_geometry_are_refused() { +fn a_template_serves_clones_in_ram_whatever_geometry_its_scratch_presented() { // A template whose scratch presented physical blocks of another size than the filesystem of - // the sandboxes serves no clone in RAM there, nor on a scratch image of that filesystem. + // the sandboxes and of the scratch images: the restore of a clone in RAM attaches the scratch + // with no file, at the template's geometry, so the template serves clones in RAM there, while + // a scratch image must present that geometry, which no file of that filesystem does. let fixture = Fixture::with(|class, spec, physical| { let distro = class.distro; let scratch = geometry(SCRATCH_LEN, other_physical(physical)); @@ -1684,22 +1620,12 @@ fn sandboxes_whose_filesystem_presents_another_scratch_geometry_are_refused() { let (image, binding) = fixture.image("distro.erofs", IMAGE_LEN); let mut acquired = fixture.acquire("sandbox"); let (physical, other) = (fixture.physical, other_physical(fixture.physical)); - // The sandbox's filesystem is the host's, which cannot restore the template. - let plan = fixture.plan(&mut acquired, &tmpfs_request(&image, &binding), NO_CONTENT); - assert_eq!( - refused(plan, Evidence::IncompatibleHost), - ( - ErrorKind::Unsupported, - format!( - "the scratch device of the clone {}, in the sandbox's filesystem, presents \ - logical blocks of 512 bytes and physical blocks of {physical} bytes, but the \ - template's scratch presented 512 and {other} bytes", - shown(&sparse_path(&acquired)) - ) - ) - ); - // The refused plan removed the file that it created. - assert_untouched(&acquired); + let plan = fixture + .plan(&mut acquired, &tmpfs_request(&image, &binding), NO_CONTENT) + .unwrap(); + assert_eq!(plan.scratch_path(), None); + assert_eq!(plan.key().scratch(), geometry(SCRATCH_LEN, other)); + drop(plan); let (scratch, scratch_binding) = fixture.scratch("scratch.ext4", SCRATCH_LEN); let request = scratch_request(&image, &binding, &scratch, &scratch_binding); assert_eq!( @@ -1720,7 +1646,7 @@ fn sandboxes_whose_filesystem_presents_another_scratch_geometry_are_refused() { } /// Files on hugetlbfs, whose I/O block size is a huge page, present physical blocks of 2 MiB, -/// unlike the template's: an image, a scratch image, and the scratch device of a clone in RAM, +/// unlike the template's: an image, as long as the placeholder or shorter, and a scratch image, /// which each reach such a file. #[cfg(target_os = "linux")] #[test] @@ -1735,7 +1661,11 @@ fn files_on_a_filesystem_of_another_block_size_are_refused() { eprintln!("the files of the test's directory present physical blocks of a huge page"); return; } - let Some(huge_image) = huge_file(IMAGE_LEN) else { + let Some(huge_images) = [IMAGE_LEN, HUGE_PAGE] + .into_iter() + .map(huge_file) + .collect::>>() + else { return; }; let mut acquired = fixture.acquire("sandbox"); @@ -1749,29 +1679,32 @@ fn files_on_a_filesystem_of_another_block_size_are_refused() { ), ) }; - // The image: its binding and the check of the binding reach a file on hugetlbfs at its path. + // The image, as long as the placeholder or shorter: its binding and the check of the binding + // reach a file on hugetlbfs at its path. let image = fixture.path("images", "huge.erofs"); - let open = |path: &Path| { - if path == image { - huge_image.try_clone() - } else { - Err(io::Error::from(io::ErrorKind::NotFound)) - } - }; - let huge_distro = distro::Access { - open: &open, - seal: &FileSeal::of, - }; - let binding = import_with(&layer(&image), PolicyVerdict::ACCEPTED, huge_distro).unwrap(); - let access = Access { - distro: huge_distro, - ..NO_CONTENT - }; - let plan = fixture.plan(&mut acquired, &tmpfs_request(&image, &binding), access); - assert_eq!( - refused(plan, Evidence::SuppliedGeometry), - presented(format!("the image {}", shown(&image)), "placeholder") - ); + for huge_image in &huge_images { + let open = |path: &Path| { + if path == image { + huge_image.try_clone() + } else { + Err(io::Error::from(io::ErrorKind::NotFound)) + } + }; + let huge_distro = distro::Access { + open: &open, + seal: &FileSeal::of, + }; + let binding = import_with(&layer(&image), PolicyVerdict::ACCEPTED, huge_distro).unwrap(); + let access = Access { + distro: huge_distro, + ..NO_CONTENT + }; + let plan = fixture.plan(&mut acquired, &tmpfs_request(&image, &binding), access); + assert_eq!( + refused(plan, Evidence::SuppliedGeometry), + presented(format!("the image {}", shown(&image)), "placeholder") + ); + } // The scratch image: its binding and its check reach a file on hugetlbfs at its path. let (image, binding) = fixture.image("distro.erofs", IMAGE_LEN); let huge_scratch = huge_file(SCRATCH_LEN).unwrap(); @@ -1791,24 +1724,6 @@ fn files_on_a_filesystem_of_another_block_size_are_refused() { ), presented(format!("the scratch image {}", shown(&scratch)), "scratch") ); - // The scratch device of a clone in RAM, which a sandbox on hugetlbfs creates there. - let huge_device = huge_file(SCRATCH_LEN).unwrap(); - let create = |_: &Path| huge_device.try_clone(); - let access = Access { - create: &create, - ..NO_CONTENT - }; - let plan = fixture.plan(&mut acquired, &tmpfs_request(&image, &binding), access); - assert_eq!( - refused(plan, Evidence::IncompatibleHost), - presented( - format!( - "the scratch device of the clone {}, in the sandbox's filesystem,", - shown(&sparse_path(&acquired)) - ), - "scratch" - ) - ); assert_untouched(&acquired); } @@ -2148,7 +2063,6 @@ fn clones_are_planned_under_the_lock_that_acquired_their_lease_before_their_laun message.starts_with("a clone is planned under the lock that acquired its lease"), "{message}" ); - assert!(!sparse_path(&relocked).exists()); // The lock that acquired a lease, once the launch that the lease reserves has begun. let mut acquired = fixture.acquire("begun"); let key = fixture.store.read_key(&fixture.generation).unwrap(); @@ -2161,7 +2075,6 @@ fn clones_are_planned_under_the_lock_that_acquired_their_lease_before_their_laun let plan = fixture.plan(&mut acquired, &request, NO_CONTENT); let (kind, message) = refused(plan, Evidence::HostFailure); assert_eq!(kind, ErrorKind::Other, "{message}"); - assert!(!sparse_path(&acquired).exists()); } #[test] diff --git a/nvx-rs/src/host/template/clone/tests/launch.rs b/nvx-rs/src/host/template/clone/tests/launch.rs index ed492836c..3610ad1a5 100644 --- a/nvx-rs/src/host/template/clone/tests/launch.rs +++ b/nvx-rs/src/host/template/clone/tests/launch.rs @@ -22,8 +22,8 @@ use crate::host::process::{BootId, Process, ProcessRef}; use crate::host::store::files::{sync_dir, write_private}; use crate::host::store::{ CLONE_ACTIVATION_NAME, CLONE_BINDING_NAME, CLONE_FAILURE_NAME, CLONE_PROGRESS_NAME, - CLONE_SCRATCH_NAME, JOURNAL_NAME, LaunchState, LockedStore, OUTCOME_NAME, RESTORE_READY_NAME, - Recovery, RuntimeIdentity, RuntimeRecord, + JOURNAL_NAME, LaunchState, LockedStore, OUTCOME_NAME, RESTORE_READY_NAME, Recovery, + RuntimeIdentity, RuntimeRecord, }; use crate::host::supervisor::{self, Reattached, StopOutcome, Vmm, VmmCommand}; use crate::host::template::Retirement; @@ -186,7 +186,6 @@ fn assert_unlaunched(fixture: &Fixture, acquired: &Acquired) { /// leaves for its release. pub(super) fn assert_released(sandbox: &Path) { for name in [ - CLONE_SCRATCH_NAME, CLONE_ACTIVATION_NAME, CLONE_PROGRESS_NAME, CLONE_FAILURE_NAME, @@ -751,9 +750,10 @@ fn a_clone_restores_its_leased_snapshot_with_its_devices_and_activation_record() ); assert_eq!(made.working_dir, sandbox); assert_eq!(made.inherited, 0); - // It restores the generation's snapshot in place, with the clone's devices: its image, and - // the sparse file of its `scratch` block device. On Linux, its control console listens beside - // the snapshot, at the socket that the lease reserved, named after the launch. + // It restores the generation's snapshot in place, with the clone's devices: its image, and its + // `scratch` block device, which an upper in RAM declares absent, so that OpenVMM attaches it + // with no file. On Linux, its control console listens beside the snapshot, at the socket that + // the lease reserved, named after the launch. let endpoint = restoring.starting().endpoint().to_owned(); let ready = restoring.ready_endpoint().to_owned(); let socket = fixture.store.clone_socket_path(&lease); @@ -778,7 +778,6 @@ fn a_clone_restores_its_leased_snapshot_with_its_devices_and_activation_record() } assert_eq!(restoring.starting().journal().endpoint(), endpoint); let distro = format!("distro:file:{},ro", shown(&image)); - let scratch = format!("scratch:file:{}", shown(&sandbox.join(CLONE_SCRATCH_NAME))); let (profile, processors) = (key.cpu_profile(), key.processors().to_string()); let listen = format!("listen={endpoint}"); let report = sandbox.join(OUTCOME_NAME); @@ -787,8 +786,7 @@ fn a_clone_restores_its_leased_snapshot_with_its_devices_and_activation_record() "microvm", "--microvm-sandbox-block", &distro, - "--microvm-sandbox-block", - &scratch, + "--restore-absent-scratch", "--single-process", "--hypervisor", key.hypervisor().as_str(), @@ -1034,7 +1032,8 @@ fn a_clone_of_a_template_with_a_nic_restores_its_policy_without_its_address() { vmm.reference = Some(restoring.starting().process().reference().clone()); // The devices end with the template's network, as its key records it, the arguments of the // network's policy, without the guest's address, which OpenVMM takes from the snapshot, and - // refuses beside it. + // refuses beside it: after the image, and the clone's scratch, which its upper in RAM + // declares absent. let mut policy = attached.openvmm_arguments().unwrap(); assert_eq!(template, policy); assert_eq!(policy[..2], ["--net", "10.0.0.2/24"]); @@ -1044,7 +1043,8 @@ fn a_clone_of_a_template_with_a_nic_restores_its_policy_without_its_address() { .iter() .position(|argument| argument == "--single-process") .unwrap(); - assert_eq!(arguments[6..devices], policy); + assert_eq!(arguments[4], "--restore-absent-scratch"); + assert_eq!(arguments[5..devices], policy); assert!(!arguments.iter().any(|argument| argument == "--net")); } @@ -1073,11 +1073,20 @@ fn a_clone_on_a_scratch_image_passes_its_claim_to_its_vmm() { vmm.reference = Some(reference.clone()); vmm.started(); // The VMM opens the image at its path, as the `scratch` block device, and inherits the one - // descriptor or handle that holds the claim, which no argument names. + // descriptor or handle that holds the claim, which no argument names. The scratch is not + // absent. let made = made.take().unwrap(); assert_eq!(made.inherited, 1); let device = format!("scratch:file:{}", shown(&scratch)); assert!(made.arguments.contains(&device), "{:?}", made.arguments); + assert!( + !made + .arguments + .iter() + .any(|argument| argument == "--restore-absent-scratch"), + "{:?}", + made.arguments + ); // On Windows, no handle that may write the image is open any more, so OpenVMM's open, which // shares no writing, succeeds. #[cfg(windows)] @@ -1132,8 +1141,9 @@ fn a_clone_with_an_export_binds_it_to_its_template_s_dormant_filesystem_slot() { let reference = restoring.starting().process().reference().clone(); vmm.reference = Some(reference.clone()); vmm.started(); - // The export follows the clone's block devices: its directory on the dormant slot, writable, - // since the guest writes a directory in it, to which the export narrows the guest's writes. + // The clone in RAM declares its scratch absent, and the export follows the clone's block + // device: its directory on the dormant slot, writable, since the guest writes a directory in + // it, to which the export narrows the guest's writes. let data = &export.children[0].root; let mount = format!("{},{},rw", filesystem::GUEST_EXPORT, data.display()); let work = data.join("work"); @@ -1143,8 +1153,9 @@ fn a_clone_with_an_export_binds_it_to_its_template_s_dormant_filesystem_slot() { .position(|argument| argument == "--single-process") .unwrap(); assert_eq!( - arguments[6..devices], + arguments[4..devices], [ + "--restore-absent-scratch", "--mount", mount.as_str(), "--mount-write", @@ -1365,33 +1376,6 @@ fn a_clone_launches_only_as_the_launch_that_its_durable_binding_reserves() { "{message}" ); assert_unlaunched(&fixture, &first); - // Nor a plan in RAM whose scratch device's file a later plan of the clone, which failed, - // removed: OpenVMM would find no file at the path of its scratch device. - let stale = fixture.plan(&mut first, &request, NO_CONTENT).unwrap(); - let failing = Access { - create: &|path: &Path| -> io::Result { - fs::remove_file(path)?; - Err(io::Error::other("the disk failed")) - }, - ..NO_CONTENT - }; - assert!(fixture.plan(&mut first, &request, failing).is_err()); - let sparse = first.locked.clone_scratch_path(); - assert!(!exists(&sparse)); - let (kind, message) = refused_at( - stale.launch_with(&mut first, openvmm, &runtime, hooks), - Stage::Launch, - Evidence::HostFailure, - ); - assert_eq!(kind, ErrorKind::Unavailable, "{message}"); - assert!( - message.starts_with(&format!( - "the scratch device of the clone {}, which the clone's plan created, cannot be opened", - sparse.display() - )), - "{message}" - ); - assert_unlaunched(&fixture, &first); // On Linux, a sandbox whose restore-ready socket's path would not fit a Unix socket's // address is refused. #[cfg(target_os = "linux")] diff --git a/nvx-rs/src/host/template/mod.rs b/nvx-rs/src/host/template/mod.rs index db80abf57..917a9687f 100644 --- a/nvx-rs/src/host/template/mod.rs +++ b/nvx-rs/src/host/template/mod.rs @@ -36,9 +36,9 @@ //! against its binding, its upper, in RAM or on the caller's scratch image, which it claims, its //! memory target against the template, and its export, if it has one, a plan of one directory, //! which the template's dormant filesystem slot binds, and refuses a request that the template -//! cannot serve, such as an image of another length or block geometry than the template's -//! placeholder, a network other than the template's, which its key records, or an export of -//! several directories or of a file, before any VMM starts. +//! cannot serve, such as an image longer than the template's placeholder, the capacity of every +//! clone's image, or of other block sizes, a network other than the template's, which its key +//! records, or an export of several directories or of a file, before any VMM starts. //! - [`ClonePlan::launch`] launches the clone, as the launch that its lease reserves, once the //! objects of its export are still those that its plan found: OpenVMM restores the generation's //! snapshot in place with the clone's devices and export, and the clone's activation record diff --git a/nvx-rs/src/host/template/store.rs b/nvx-rs/src/host/template/store.rs index a491c9ba0..662453da2 100644 --- a/nvx-rs/src/host/template/store.rs +++ b/nvx-rs/src/host/template/store.rs @@ -367,7 +367,8 @@ impl Index { /// opened for reading, which on Windows lets no one write it, until the VM has ended. The /// directories above the files must be ones that only the current user, or an administrator, may /// change, since OpenVMM resolves each path again. A restore of a clone, whose block devices -/// OpenVMM also opens by their paths, takes the same rule. +/// OpenVMM also opens by their paths, takes the same rule; a restore of a clone in RAM declares its +/// scratch absent, which OpenVMM attaches with no file. /// /// ## What the VM leaves /// diff --git a/nvx-rs/src/host/template/store/leases.rs b/nvx-rs/src/host/template/store/leases.rs index 820779440..347e04318 100644 --- a/nvx-rs/src/host/template/store/leases.rs +++ b/nvx-rs/src/host/template/store/leases.rs @@ -368,9 +368,9 @@ impl Store { /// the reservation of the launch that `locked` holds, if it acquired the lease, so that one /// that fails part of the way, once it removed files that a launch of the clone's plan needs, /// leaves no launch that the lock may begin. It then removes what the clone kept of its - /// launch: the file of the scratch device of a clone whose upper is in RAM, - /// [`CLONE_SCRATCH_NAME`](crate::host::store::CLONE_SCRATCH_NAME), which the clone's - /// [plan](crate::host::template::ClonePlan) created, the clone's activation record, + /// launch: the file of the scratch device that an earlier version created for a clone in RAM, + /// [`CLONE_SCRATCH_NAME`](crate::host::store::CLONE_SCRATCH_NAME), if one remains, the + /// clone's activation record, /// [`CLONE_ACTIVATION_NAME`](crate::host::store::CLONE_ACTIVATION_NAME), the socket of its /// restore-ready event, [`RESTORE_READY_NAME`](crate::host::store::RESTORE_READY_NAME), and /// its records, which last as long as the lease, its progress log, @@ -462,12 +462,12 @@ impl Store { checked(); // A plan of the clone launches under the reservation that the lease's acquisition left in // the lock, which the release ends before it removes anything, so that a release that - // fails once it removed the clone's files, such as the scratch device's file of an upper - // in RAM, leaves no launch that the lock may begin. + // fails once it removed the clone's files, such as the record of its failure, which ends + // the clone's launches until the release, leaves no launch that the lock may begin. locked.reserve(None); // The clone's VM has exited, and the sandbox launches nothing else under the lease, so no - // VM holds the scratch device's file of a RAM upper, or listens at the clone's control - // socket. The binding's removal makes the removal of the clone's files durable too, since + // VM listens at the clone's control socket. The binding's removal makes the removal of the + // clone's files durable too, since // all are entries of the sandbox's directory, but not that of the socket, an entry of the // leased generation's payload, which must be durable before the lease ends, so that a // generation whose leases have drained holds no clone's socket. diff --git a/nvx-rs/src/host/template/store/leases/tests.rs b/nvx-rs/src/host/template/store/leases/tests.rs index 25066d514..886e5a8e4 100644 --- a/nvx-rs/src/host/template/store/leases/tests.rs +++ b/nvx-rs/src/host/template/store/leases/tests.rs @@ -841,7 +841,8 @@ fn a_release_makes_the_removal_of_its_binding_durable_before_it_removes_its_reco let payload = store.payload_dir(generation); let socket = store.clone_socket_path(&lease); // Each sync notes its directory, and whether any file that the clone kept of its launch, the - // control socket of its VMM included, the binding, and the record existed then. + // control socket of its VMM and the scratch file that an earlier version left for a clone in + // RAM included, the binding, and the record existed then. let kept = [ CLONE_SCRATCH_NAME, CLONE_ACTIVATION_NAME, @@ -897,9 +898,9 @@ fn a_release_makes_the_removal_of_its_binding_durable_before_it_removes_its_reco assert!(!record.exists()); assert!(fs::symlink_metadata(&socket).is_err()); // A release under the lock that acquired the lease ends the lock's reservation before it - // removes the clone's files, such as the scratch device's file of an upper in RAM, which a - // launch of the clone's plan needs, so that one that fails after their removal leaves no - // launch to begin. + // removes the clone's files, such as the record of its failure, which ends the clone's + // launches until the release, so that one that fails after their removal leaves no launch to + // begin. let Acquired { lease: again, mut locked, @@ -913,8 +914,8 @@ fn a_release_makes_the_removal_of_its_binding_durable_before_it_removes_its_reco ) .unwrap(); assert_eq!(locked.reserved(), Some(again.launch())); - let scratch = sandbox.join(CLONE_SCRATCH_NAME); - fs::write(&scratch, b"").unwrap(); + let failure = sandbox.join(CLONE_FAILURE_NAME); + fs::write(&failure, b"").unwrap(); // A release that cannot make the socket's removal durable keeps the binding, and the lease. if cfg!(unix) { let error = store @@ -932,7 +933,7 @@ fn a_release_makes_the_removal_of_its_binding_durable_before_it_removes_its_reco assert_eq!(cause(&error), "the disk failed"); assert_eq!(binding(&sandbox), Some(again.clone())); assert_eq!(locked.reserved(), None); - assert!(fs::symlink_metadata(&scratch).is_err()); + assert!(fs::symlink_metadata(&failure).is_err()); } // A release that cannot make the binding's removal durable keeps the record, which a // retirement removes once no binding names it. @@ -942,7 +943,7 @@ fn a_release_makes_the_removal_of_its_binding_durable_before_it_removes_its_reco }) .unwrap_err(); assert_eq!(locked.reserved(), None); - assert!(fs::symlink_metadata(&scratch).is_err()); + assert!(fs::symlink_metadata(&failure).is_err()); drop(locked); assert_eq!( error.message(), diff --git a/tests/runtime-adapter/README.md b/tests/runtime-adapter/README.md index dcb17dd3f..27d662109 100644 --- a/tests/runtime-adapter/README.md +++ b/tests/runtime-adapter/README.md @@ -1480,7 +1480,7 @@ state, and hash nothing. They take no lock, so each record is as they read it. `runtime-adapter-host start` starts a clone of a generation of a template store in a sandbox store, through NVX's public API, `nvx::host::template`. Its library side, `clone`, holds what the command's options choose of the clone, -`clone::Request`: its image, an EROFS layer or a GPT disk of the length of the +`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 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 @@ -1554,8 +1554,10 @@ template attaches unchanged, or, for a template without a NIC, none, or one that lets nothing through, so that a clone of another network policy finds no template in it, the bindings of its images, which no one else may change, the geometry of its -image, which must be the placeholder's, its length included, and that of its -scratch image, which must be the template's scratch's, its memory target, +image, which must have the placeholder's block sizes, and be no longer than +the placeholder, whose length is the capacity that the restore presents, +reading zeros past the image's end, and that of its scratch image, which must +be the template's scratch's, its length included, its memory target, whole blocks of 128 MiB, from the template's RAM to its capacity, and its export, if it has one: the template's filesystem slot, which its capture left dormant, binds one directory, so the export must have exactly one, the @@ -1572,7 +1574,11 @@ template store and the generation, so that a later stop finds the store whose lease it releases. Once NVX checked the export again, with `ExportPlan::verify`, OpenVMM restores the generation's snapshot in place, through NVX's supervisor, as the launch that the -lease reserves, with the export's directory on the template's filesystem slot, +lease reserves, with the clone's image at the capacity of the template's +placeholder, over the scratch image, or, for a clone in RAM, with the +template's scratch absent, which OpenVMM attaches with no file, so that no +file of the host backs it, with the export's directory on the template's +filesystem slot, and the template's NIC, if it has one, under the template's network, and the restore sequence activates the clone through the adapter's clone order, `host::CloneOrder`, which sends the clone's activation @@ -1809,8 +1815,8 @@ Each command of `runtime-adapter-host` but `stage`, `list`, `inspect`, and TEMPLATES in the sandbox store STORE, which it creates if needed, and whose `stage` staged its images, which no one else may change: the image PATH, an EROFS file system, `erofs`, or a GPT disk, `gpt`, of which the guest expects - the identity UUID, the file system's UUID or the disk's GUID, of the length - of the template's placeholder, whatever the placeholder's format, over the + the identity UUID, the file system's UUID or the disk's GUID, no longer than + the template's placeholder, whatever the placeholder's format, over the ext4 scratch image PATH, or RAM, with 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 @@ -2793,7 +2799,8 @@ in the host-only configuration, through `runtime-adapter-host start` and captures a template of its own, of 1 processor and 256 MiB of RAM with a capacity of 1 GiB, in a template store of its own, of a copy of NVX's Ubuntu EROFS image that a directory of the platform holds, which the clones stage as -their image, or of a GPT disk of its length that the platform holds beside it, +their image, or of a GPT disk 16 MiB longer than it that the platform holds +beside it, without a NIC unless its row names a network, whose guest is at `192.0.2.2/24`, as in the [template scenarios](#template-scenarios), and starts its clones, each in a sandbox store of its own, over a copy of the @@ -2808,8 +2815,8 @@ directory that the starts authorize: | Test | Clones | Expects | | --- | --- | --- | | `one_generation_serves_clones_of_each_upper_and_memory_target` | Two at once, over a scratch image at 256 MiB, and in RAM at 512 MiB with untrusted lowers, then one over a scratch image at 1 GiB | Each started, by the host's monotonic clock, with its own lease, which the store lists with the clone's sandbox, its memory target online, its upper, overlay mode, and image's contents, and the events of its activation in order; a stop releases each lease; the generation's snapshot unchanged, and its payload with nothing but the capture's files once the clones stopped | -| `one_generation_serves_clones_of_either_format_over_either_upper` | Of a template whose placeholder is a GPT disk of two layers, four at once: the EROFS image over a scratch image and in RAM, a GPT disk of one layer in RAM, and one of three layers 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; 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 image 4 KiB longer, one 4 KiB shorter, and one 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 | +| `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_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 | @@ -3026,7 +3033,7 @@ layered roots, captures of templates, and the starts and runs of their clones. | `vm_clone` | `clones_at_once_see_only_their_own_exports` | Clones of one generation at once see only their own exports, and a clone without one none | | `vm_clone` | `each_command_reattaches_to_the_clone_from_a_new_process` | `reattach`, `probe`, and `exec` each reattach to the clone from a new process, in a later epoch, without a second activation | | `vm_clone` | `one_generation_serves_clones_of_each_upper_and_memory_target` | One generation serves clones over a scratch image and in RAM, at each memory target up to its capacity, each with its own lease, and leaves its snapshot unchanged | -| `vm_clone` | `one_generation_serves_clones_of_either_format_over_either_upper` | A template of a GPT placeholder serves EROFS and GPT clones of other layouts over either upper, and a first partition over its bound ends a clone with status 54 | +| `vm_clone` | `one_generation_serves_clones_of_either_format_over_either_upper` | A template of a GPT placeholder serves EROFS and GPT clones of other layouts, shorter than the placeholder, at its length, over either upper, those in RAM with no scratch file, and a first partition over its bound ends a clone with status 54 | | `vm_clone` | `the_lease_of_a_start_that_ended_before_its_record_is_released_through_its_template_store` | The lease of a start that ended before it recorded its clone is released only through its own template store, which `--template` names | ### Running the VM tests diff --git a/tests/runtime-adapter/src/bin/runtime-adapter-host.rs b/tests/runtime-adapter/src/bin/runtime-adapter-host.rs index fa09fd5cb..288d30247 100644 --- a/tests/runtime-adapter/src/bin/runtime-adapter-host.rs +++ b/tests/runtime-adapter/src/bin/runtime-adapter-host.rs @@ -140,9 +140,10 @@ //! NVX's `ClonePlan` checks the clone against the generation, from the metadata of its files //! alone: its network, which must be the one that the template's key records, its image, the //! raw file PATH, an EROFS filesystem, `erofs`, or a GPT disk, `gpt`, of -//! which the guest expects the identity UUID, of the length of the template's placeholder, -//! whatever the placeholder's format, its upper, the ext4 scratch image PATH, of the geometry of -//! the template's scratch, or RAM, with the bindings that STORE stages for the images, as +//! which the guest expects the identity UUID, no longer than the template's placeholder, +//! whatever the placeholder's format, and of its block sizes, its upper, the ext4 scratch image +//! PATH, of the geometry of the template's scratch, or RAM, which needs no file, with the +//! bindings that STORE stages for the images, as //! `stage` stages them, its memory target, in whole blocks, from the template's RAM to its //! capacity, and its export, if it has one, which must export exactly one directory, which //! neither lies inside the template store's directory nor holds it: the export's root, which diff --git a/tests/runtime-adapter/src/clone.rs b/tests/runtime-adapter/src/clone.rs index 06f1c2b82..073e0b74b 100644 --- a/tests/runtime-adapter/src/clone.rs +++ b/tests/runtime-adapter/src/clone.rs @@ -2,7 +2,7 @@ //! NVX's template store leases to a sandbox, as `runtime-adapter-host start` asks. //! //! A [`Request`] holds what the host program's options choose of a clone: its image, an EROFS -//! layer or a GPT disk of the length of the template's placeholder, its upper, on an ext4 scratch +//! layer or a GPT disk no longer than the template's placeholder, its upper, on an ext4 scratch //! image or in RAM, its memory target, 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 image's own or one that its guest generates, the host @@ -68,7 +68,8 @@ const CLOCKS: [HostClock; 2] = [HostClock::LinuxBoottime, HostClock::WindowsInte /// What the host program asks of a clone. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Request { - /// The image, which the guest expects of the template's placeholder's length. + /// The image, which the guest expects no longer than the template's placeholder, whose length + /// is the capacity that the clone's restore presents. pub distro: PathBuf, /// The image's format. pub format: ImageFormat, diff --git a/tests/runtime-adapter/tests/vm_clone.rs b/tests/runtime-adapter/tests/vm_clone.rs index 00a810461..cc03d6232 100644 --- a/tests/runtime-adapter/tests/vm_clone.rs +++ b/tests/runtime-adapter/tests/vm_clone.rs @@ -5,7 +5,7 @@ //! //! Each scenario captures a [`Template`] of its own, with 256 MiB of RAM, of a capacity of //! 1 GiB, in a template store of its own, as `vm_template` does, of the platform's copy of the -//! fixtures' Ubuntu EROFS image, or of a GPT [`Disk`] of the image's length, without a NIC unless +//! fixtures' Ubuntu EROFS image, or of a GPT [`Disk`] longer than the image, without a NIC unless //! the scenario gives it a [`Network`], with the guest at `192.0.2.2/24`, and starts its //! clones, each in a sandbox store of its own, which stages the clone's images, as a launch's //! does: that copy, or a GPT disk, and for a clone over a scratch image, a copy of the scratch @@ -23,16 +23,21 @@ //! and than each other's. The guest of a GPT clone rereads its disk's partition table before it //! assembles the root, since its kernel created the partitions of the placeholder, so each //! clone's root stacks its own disk's layers, from the partitions that its disk's table names. -//! A disk whose first partition is larger than the clone's bound ends the clone before it is -//! ready. +//! The placeholder is longer than the platform's image, so every clone's image is shorter than +//! the capacity that its restore presents: the platform's image, a GPT disk of its length, and +//! one as short as its layout allows. The guest reads each image's device to the capacity's end, +//! past the image's. The restore of a clone in RAM declares the template's scratch absent, so no +//! file backs the clone's scratch device, which the guest still finds, and reads to its end, at +//! the template's scratch length. A disk whose first partition is larger than the clone's bound +//! ends the clone before it is ready. //! - The store and NVX's plan refuse a clone that the template cannot serve before any VMM starts, -//! each a failure of the request, as NVX attributes it by its evidence, with no VM: an image or a -//! scratch image of another length than the template's, an image of a geometry that OpenVMM -//! refuses, a memory target beyond its capacity or of a part of a block. The start then releases -//! the lease, as it does once the adapter refuses images that the sandbox does not stage. A -//! generation whose key is gone fails the plan too, as the template's failure, and the start -//! quarantines it through its tombstone, so that the store refuses a later start's selection of -//! it, as ineligible. +//! each a failure of the request, as NVX attributes it by its evidence, with no VM: an image of +//! either format longer than the template's placeholder, a scratch image of another length than +//! the template's scratch, an image of a geometry that OpenVMM refuses, a memory target beyond +//! its capacity or of a part of a block. The start then releases the lease, as it does once the +//! adapter refuses images that the sandbox does not stage. A generation whose key is gone fails +//! the plan too, as the template's failure, and the start quarantines it through its tombstone, +//! 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 @@ -358,6 +363,10 @@ const DNS_REACH: &str = "server=$(sed -n 's/^nameserver //p' /etc/resolv.conf); const REACH: &str = "gateway=$1; shift; for target in \"$@\"; do \ nc -z -w 2 \"$gateway\" \"${target#*:}\" /dev/null 2>&1; \ echo \"${target%%:*} $?\"; done"; +/// How much longer than the platform's image the GPT placeholder that [`Template::of_disk`] +/// captures is, so that the image of each of the template's clones, the platform's image or a GPT +/// disk of its length or shorter, is shorter than the capacity that the clone's restore presents. +const PLACEHOLDER_SLACK: u64 = 16 * MIB; /// The placeholder of the template of the scenarios of GPT disks: a disk of two layers, whose /// partitions start at multiples of 1 MiB. const PLACEHOLDER: Disk = Disk { @@ -416,10 +425,11 @@ const ETC_FILE: Disk = Disk { top: &[("etc", b"not a directory\n")], }; -/// A GPT disk of the scenarios, of the length of the platform's image, which -/// [`Template::of_disk`] captures as the template's placeholder, or [`Template::disk`] writes for -/// a clone. Its partitions follow each other, each at a multiple of its alignment: its first -/// partition, then a layer, a [`small_layer`], in each of the others, empty but the top one. +/// A GPT disk of the scenarios, which [`Template::of_disk`] captures as the template's +/// placeholder, longer than the platform's image, or [`Template::disk`] writes for a clone, of the +/// image's length or shorter. Its partitions follow each other, each at a multiple of its +/// alignment: its first partition, then a layer, a [`small_layer`], in each of the others, empty +/// but the top one. struct Disk { /// The disk's GUID, which the guest expects of it. guid: &'static str, @@ -438,8 +448,10 @@ struct Disk { } impl Disk { - /// Writes the disk at `path`, of `length` bytes, a file that no one else may change. - fn write(&self, path: &Path, length: u64) { + /// Writes the disk at `path`, of `length` bytes, or, for `None`, as short as its layout + /// allows, its partitions and its backup array and header alone, a file that no one else may + /// change. + fn write(&self, path: &Path, length: Option) { let (layer, top) = (small_layer(&[], &[]), small_layer(self.top, &[])); let partitions = self .partitions @@ -466,7 +478,7 @@ impl Disk { guid: self.guid, partitions: &partitions, alignment: self.alignment, - length: Some(length), + length, }; assert_eq!(gpt::write_disk(path, &layout), self.first); } @@ -676,7 +688,7 @@ fn write_network(directory: &Path, network: Network, guest: &str) -> PathBuf { /// The template of a scenario: the generation that a template store of its own captured, in the /// scenario's sandbox, of the platform's copy of the fixtures' Ubuntu EROFS image, or of a GPT -/// [`Disk`] of its length, with [`TEMPLATE_MEMORY`] MiB of RAM, of a capacity of +/// [`Disk`] longer than it, with [`TEMPLATE_MEMORY`] MiB of RAM, of a capacity of /// [`TEMPLATE_CAPACITY`] MiB, and a NIC exactly when its [`Network`] lets something through. struct Template { /// The scenario's sandbox, whose store is the template store, until the template drops. @@ -686,6 +698,9 @@ struct Template { image: PathBuf, /// The image's identity. identity: String, + /// The placeholder's length, in bytes: the capacity of every clone's image, which a restore + /// presents over it. + capacity: u64, /// The fixtures. fixtures: Fixtures, } @@ -702,8 +717,8 @@ impl Template { } /// Creates the sandbox of the scenario `name`, stages the platform's copy of the image, and - /// captures the template of the GPT disk `placeholder`, of the image's length, which the - /// platform stages beside it, as [`create`](Self::create) does. + /// captures the template of the GPT disk `placeholder`, [`PLACEHOLDER_SLACK`] longer than the + /// image, which the platform stages beside it, as [`create`](Self::create) does. fn of_disk(name: &str, placeholder: &Disk) -> Self { Self::capture(name, Some(placeholder), None) } @@ -716,21 +731,25 @@ impl Template { } /// Creates the sandbox of the scenario `name`, stages the platform's copy of the image, and - /// captures the template of the GPT disk `placeholder`, if any, and of the image otherwise, - /// with the network `network`, if any, with the guest at [`GUEST_NETWORK`]. + /// captures the template of the GPT disk `placeholder`, if any, [`PLACEHOLDER_SLACK`] longer + /// than the image, and of the image otherwise, with the network `network`, if any, with the + /// guest at [`GUEST_NETWORK`]. fn capture(name: &str, placeholder: Option<&Disk>, network: Option) -> Self { let mut sandbox = Sandbox::new(name); let fixtures = Fixtures::find(); let image = stage_platform(&sandbox.root.join("platform"), &fixtures); - let (distro, identity) = match placeholder { - None => (erofs(&image), fixtures.identity.as_str()), + let length = fs::metadata(&image).unwrap().len(); + let (distro, identity, capacity) = match placeholder { + None => (erofs(&image), fixtures.identity.as_str(), length), Some(disk) => { let path = image.with_file_name("placeholder.gpt"); - disk.write(&path, fs::metadata(&image).unwrap().len()); - (prefixed("gpt:", &path), disk.guid) + let capacity = length + PLACEHOLDER_SLACK; + disk.write(&path, Some(capacity)); + (prefixed("gpt:", &path), disk.guid, capacity) } }; - let (memory, capacity) = (TEMPLATE_MEMORY.to_string(), TEMPLATE_CAPACITY.to_string()); + let memory = TEMPLATE_MEMORY.to_string(); + let memory_capacity = TEMPLATE_CAPACITY.to_string(); let network = network.map(|network| write_network(&sandbox.root, network, GUEST_NETWORK)); let mut options = vec![ OsStr::new("--distro"), @@ -740,7 +759,7 @@ impl Template { OsStr::new("--memory"), OsStr::new(&memory), OsStr::new("--memory-capacity"), - OsStr::new(&capacity), + OsStr::new(&memory_capacity), ]; if let Some(network) = &network { options.extend([OsStr::new("--network"), network.as_os_str()]); @@ -752,6 +771,7 @@ impl Template { identity: fixtures.identity.clone(), sandbox: Some(sandbox), image, + capacity, fixtures, } } @@ -814,8 +834,14 @@ impl Template { /// Writes the GPT disk `disk`, of the length of the platform's image, which a clone restores /// in place of the template's placeholder, beside the image, at `name`, and returns its path. fn disk(&self, name: &str, disk: &Disk) -> PathBuf { + self.disk_of(name, disk, Some(fs::metadata(&self.image).unwrap().len())) + } + + /// Writes the GPT disk `disk`, of `length` bytes, or, for `None`, as short as its layout + /// allows, as [`disk`](Self::disk) does, and returns its path. + fn disk_of(&self, name: &str, disk: &Disk, length: Option) -> PathBuf { let path = self.image.with_file_name(name); - disk.write(&path, fs::metadata(&self.image).unwrap().len()); + disk.write(&path, length); path } @@ -1338,6 +1364,44 @@ impl Cloned { let _ = policy; } + /// Checks the arguments with which NVX started the clone's VMM, on Linux, as + /// [`assert_vmm_mount`](Self::assert_vmm_mount) does, for a clone over `upper`: the image as + /// the `distro` sandbox block device, then, over a scratch image, the image as the `scratch` + /// one, or, in RAM, none, but `--restore-absent-scratch`, which declares the template's + /// scratch absent, so that OpenVMM attaches it with no file. + fn assert_vmm_scratch(&self, upper: Upper<'_>) { + #[cfg(target_os = "linux")] + { + let arguments = self.vmm_arguments(); + let roles = arguments + .windows(2) + .filter(|pair| pair[0] == "--microvm-sandbox-block") + .map(|pair| pair[1].split(':').next().unwrap_or_default()) + .collect::>(); + let absent = arguments + .iter() + .any(|argument| argument == "--restore-absent-scratch"); + let expected = match upper { + Upper::Ram => (vec!["distro"], true), + _ => (vec!["distro", "scratch"], false), + }; + assert_eq!((roles, absent), expected, "{arguments:?}"); + } + #[cfg(not(target_os = "linux"))] + let _ = upper; + } + + /// Checks that no file of the clone's sandbox is as long as the template's scratch, so that + /// none backs the scratch device of a clone in RAM, whose restore declares the template's + /// scratch absent. + fn assert_scratch_unbacked(&self) { + let long = files_below(&self.sandbox.root) + .into_iter() + .filter(|(_, length, _)| *length >= DEFAULT_SCRATCH_LENGTH) + .collect::>(); + assert!(long.is_empty(), "{long:?}"); + } + /// Returns the arguments with which NVX started the clone's VMM, which the test, as root, /// reads on Linux. #[cfg(target_os = "linux")] @@ -1601,20 +1665,20 @@ fn names(path: &Path) -> Vec { names } -/// Returns each file below the directory `snapshot`, by its path in it, with its length and its +/// Returns each file below the directory `directory`, by its path in it, with its length and its /// modification time. -fn snapshot_files(snapshot: &Path) -> Vec<(PathBuf, u64, SystemTime)> { +fn files_below(directory: &Path) -> Vec<(PathBuf, u64, SystemTime)> { let mut files = Vec::new(); - let mut directories = vec![snapshot.to_owned()]; - while let Some(directory) = directories.pop() { - for entry in fs::read_dir(&directory).unwrap() { + let mut directories = vec![directory.to_owned()]; + while let Some(below) = directories.pop() { + for entry in fs::read_dir(&below).unwrap() { let entry = entry.unwrap(); let metadata = entry.metadata().unwrap(); if metadata.is_dir() { directories.push(entry.path()); continue; } - let path = entry.path().strip_prefix(snapshot).unwrap().to_owned(); + let path = entry.path().strip_prefix(directory).unwrap().to_owned(); files.push((path, metadata.len(), metadata.modified().unwrap())); } } @@ -1795,6 +1859,34 @@ fn disks(fact: &str) -> BTreeMap { .collect() } +/// Checks the disks of a clone, as its `devices` and `read-disks` probes found them: its image's, +/// read-only, at `capacity`, the length of the template's placeholder, and its scratch device, +/// writable, at the template's scratch length, each of which the guest read to its end, whatever +/// the length of the image, and whether a file backs the scratch device. +fn assert_capacity(facts: &Printed, capacity: u64) { + let read = disks(&facts["read-disks.disks"]); + let mut found = facts["devices.disks"] + .split(' ') + .map(|disk| { + let parts = disk.split(':').collect::>(); + let &[name, sectors, access] = parts.as_slice() else { + panic!("{disk:?} in {facts:?}"); + }; + let length = sectors.parse::().unwrap() * SECTOR; + (access, length, read.get(name).copied()) + }) + .collect::>(); + found.sort_unstable(); + assert_eq!( + found, + [ + ("ro", capacity, Some(capacity)), + ("rw", DEFAULT_SCRATCH_LENGTH, Some(DEFAULT_SCRATCH_LENGTH)) + ], + "{facts:?}" + ); +} + /// Returns a boot of this host other than `boot`. fn other_boot(boot: BootId) -> BootId { let text = boot.to_string(); @@ -1963,7 +2055,7 @@ fn limits() -> Limits { fn one_generation_serves_clones_of_each_upper_and_memory_target() { let template = Template::create("cl-reuse"); let snapshot = template.payload().join(SNAPSHOT_NAME); - let captured = snapshot_files(&snapshot); + let captured = files_below(&snapshot); // Two clones at once: one over a scratch image, at the template's RAM, which trusts its // lowers, as the guest does by default, and one in RAM, at twice the template's RAM, which @@ -2044,16 +2136,19 @@ fn one_generation_serves_clones_of_each_upper_and_memory_target() { // The clones changed nothing of the generation's snapshot, which they restored in place, and // left nothing in its payload. - assert_eq!(snapshot_files(&snapshot), captured); + assert_eq!(files_below(&snapshot), captured); assert_payload(&template, false); } #[test] #[ignore = "boots VMs; see the module documentation for what they need"] fn one_generation_serves_clones_of_either_format_over_either_upper() { + // The placeholder is longer than the platform's image, so every clone's image is shorter than + // the capacity that its restore presents: the platform's image, a GPT disk of its length, and + // one as short as its layout allows. let template = Template::of_disk("cl-gpt", &PLACEHOLDER); let one = template.disk("one.gpt", &ONE_LAYER); - let three = template.disk("three.gpt", &THREE_LAYERS); + let three = template.disk_of("three.gpt", &THREE_LAYERS, None); // At once: the platform's image over a scratch image and in RAM, and two GPT disks, of other // layouts than the placeholder's and than each other's, one in RAM, and one over a scratch @@ -2090,7 +2185,14 @@ fn one_generation_serves_clones_of_either_format_over_either_upper() { assert_eq!(template.leases().len(), clones.len()); assert_payload(&template, true); for (image, upper, cloned) in &mut clones { - let facts = cloned.probe(&["devices", "mounts", "contents", "first-partition"]); + let probes = [ + "devices", + "mounts", + "contents", + "first-partition", + "read-disks", + ]; + let facts = cloned.probe(&probes); match image { Some(Image::Gpt(disk, _)) => assert_gpt_root(&facts, disk, *upper), _ => { @@ -2098,6 +2200,13 @@ fn one_generation_serves_clones_of_either_format_over_either_upper() { assert_eq!(facts["first-partition.length"], "none", "{facts:?}"); } } + // The restore presents the capacity over each image, and the template's scratch over the + // scratch image, or, in RAM, with no file. + assert_capacity(&facts, template.capacity); + cloned.assert_vmm_scratch(*upper); + if *upper == Upper::Ram { + cloned.assert_scratch_unbacked(); + } } for (_, _, cloned) in &mut clones { cloned.stop(); @@ -2124,11 +2233,14 @@ fn a_start_refuses_what_the_template_cannot_serve_before_any_vmm_starts() { let template = Template::create("cl-refuse"); let length = fs::metadata(&template.image).unwrap().len(); let longer = template.variant("longer.erofs", length + 4096); - let shorter = template.variant("shorter.erofs", length - 4096); + let longer_disk = template.disk_of("longer.gpt", &ONE_LAYER, Some(length + 4096)); // OpenVMM attaches block devices of whole 512-byte sectors alone. let ragged = template.variant("ragged.erofs", length + 100); let placeholder = |found: u64| { - format!("is {found} bytes long, not the {length} bytes of the template's placeholder") + format!( + "is {found} bytes long, longer than the template's placeholder, whose {length} bytes \ + are the capacity of every clone's image" + ) }; let resized = 2 * DEFAULT_SCRATCH_LENGTH; let beyond = ["--memory", "2048"]; @@ -2144,12 +2256,12 @@ fn a_start_refuses_what_the_template_cannot_serve_before_any_vmm_starts() { ), ( StartOf { - image: Some(Image::Erofs(&shorter)), + image: Some(Image::Gpt(&ONE_LAYER, &longer_disk)), upper: Upper::Scratch, ..StartOf::default() }, "supplied-length", - placeholder(length - 4096), + placeholder(length + 4096), ), ( StartOf { @@ -2867,7 +2979,7 @@ fn a_clone_whose_vm_exits_once_a_stop_found_it_fails_before_its_lease_is_release fn a_tombstone_leaves_the_running_clone_of_its_generation_to_its_lease() { let template = Template::create("cl-tomb"); let snapshot = template.payload().join(SNAPSHOT_NAME); - let captured = snapshot_files(&snapshot); + let captured = files_below(&snapshot); let start = StartOf { upper: Upper::Scratch, ..StartOf::default() @@ -2901,7 +3013,7 @@ fn a_tombstone_leaves_the_running_clone_of_its_generation_to_its_lease() { assert!(template.leases().is_empty()); // The tombstone changed nothing of the generation's snapshot. - assert_eq!(snapshot_files(&snapshot), captured); + assert_eq!(files_below(&snapshot), captured); assert_payload(&template, false); }