diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 37b40d53f..abc8e4856 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -923,18 +923,12 @@ host namespace policy or skips the identity assertions. ### `nvx::host::artifacts` -`Artifacts` (also `nvx::Artifacts`): the OpenVMM executable, guest kernel, and -Alpine initramfs, from a release, a repository checkout, the environment, or a -directory next to the executable. Artifacts located in a release or checkout -keep its `ArtifactDir`, and `Artifacts::runtime_identity` reads their -`RuntimeIdentity` from its install metadata; artifacts named by path have none. - `ArtifactDir` and `Guest`: every artifact of the guests that `nvx.py` runs, the OpenVMM executable, the kernel, the Alpine, Ubuntu, and Azure Linux initramfs images and their package manifests, and the Ubuntu distro layer and its manifest, in a checkout's `build/` and `openvmm/target/release/` or a release's -`guest/` and `bin/`, checked in the order and with the messages of `nvx.py`. -`ArtifactPaths` lists them all and serializes as JSON. +`guest/` and `bin/`, checked with the messages of `nvx.py`. `ArtifactPaths` +lists them all and serializes as JSON. `ArtifactDir::runtime_identity` reads the `RuntimeIdentity` of the OpenVMM executable, kernel, and Alpine initramfs from the install metadata, a release's @@ -1107,20 +1101,13 @@ distro layer, the targets of `nvx-rs run` shares, and the managed exec environment; see [Sandbox input contract](#sandbox-input-contract). `distro`: the `DistroBinding` of a distro layer, an EROFS layer or a GPT disk, -which explicit verification makes or a staging caller imports, and its check -from metadata alone, which returns the layer open; see -[Distro bindings](#distro-bindings). +which explicit verification makes, and its check from metadata alone, which +returns the layer open; see [Distro bindings](#distro-bindings). `scratch`: the `ScratchBinding` of a scratch image, its identity, kind, and length, and its check from metadata alone, which claims the image for one sandbox at a time and returns it open. -`CheckedDistro::geometry` and `CheckedScratch::geometry` read, through the -checked files, the `BlockGeometry` that OpenVMM presents for them: their length, -a nonzero multiple of 512 bytes, logical blocks of 512 bytes, and physical -blocks of the I/O block size of their filesystem on Linux, and of 4096 bytes on -Windows. - ### `nvx::guest` The programs that run in Linux guests; see [Guest binaries](#guest-binaries). @@ -1695,17 +1682,15 @@ manifest, or to the manifest's absence, to the revision and result of the distro policy, `DISTRO_POLICY_REVISION`, and to the layer's format and the identity that the guest expects of it: the UUID of an EROFS layer, or the disk GUID of a GPT disk, which `SandboxLayer::gpt` names. It comes from explicit -verification, or from `DistroBinding::import`, which a staging caller that -verified the layer itself calls to bind it without hashing it again. `check` -fails for a binding of another version, layer, format, or policy revision, a verdict -that rejects the layer, a layer whose identity, kind, length, or times changed, -and a manifest that appeared, disappeared, or changed. A binding of a layer -without a manifest holds only while nothing is at the manifest's path: unlike -verification, which finds a manifest as `Path.exists` does, `check` and -`import` refuse a path whose lookup fails for another reason than a missing -file. A missing or stale binding fails: no runtime operation registers, -verifies, or hashes a layer on its own. Hashing belongs where layers are built, -installed, or staged, and to diagnostics. +verification. `check` fails for a binding of another version, layer, format, +or policy revision, a verdict that rejects the layer, a layer whose identity, +kind, length, or times changed, and a manifest that appeared, disappeared, or +changed. A binding of a layer without a manifest holds only while nothing is at +the manifest's path: unlike verification, which finds a manifest as +`Path.exists` does, `check` refuses a path whose lookup fails for another +reason than a missing file. A missing or stale binding fails: no runtime +operation registers, verifies, or hashes a layer on its own. Hashing belongs +where layers are built, installed, or staged, and to diagnostics. On Linux, a seal holds `st_dev`, `st_ino`, the file type, `st_size`, and `st_mtime` and `st_ctime` with their nanoseconds; on Windows, the volume serial @@ -2542,21 +2527,28 @@ restores one, and finds one again after its caller restarts. use std::path::Path; use std::time::{Duration, Instant}; +use nvx::Hypervisor; use nvx::activation::{MachineIdMode, View}; use nvx::host::config::{Boot, ConsoleAttachment, RootConfig, VmConfig}; -use nvx::host::runtime::{LaunchOptions, Runtime}; +use nvx::host::runtime::{ArtifactIdentity, LaunchOptions, Runtime, RuntimeIdentity}; use nvx::host::vm::{self, VmDir}; -use nvx::{Artifacts, Hypervisor}; fn main() -> Result<(), Box> { - // Locates OpenVMM, the guest kernel, and the Alpine initramfs, with the identity that the - // install metadata of their release or checkout gives them. - let artifacts = Artifacts::discover()?; + // The caller locates OpenVMM, the guest kernel, and the Alpine initramfs, and identifies each + // by the SHA-256 digest that its install metadata records, such as the `SHA256SUMS` of an NVX + // release: here, from the command line. + let arguments: Vec = std::env::args().skip(1).collect(); + let [openvmm, kernel, initramfs, openvmm_sha256, kernel_sha256, initramfs_sha256] = + <[String; 6]>::try_from(arguments).map_err(|_| "expected three paths and three digests")?; let runtime = Runtime { - identity: artifacts.runtime_identity()?, - openvmm: artifacts.openvmm, - kernel: artifacts.kernel, - initramfs: artifacts.initrd, + identity: RuntimeIdentity::new( + ArtifactIdentity::sha256(&openvmm_sha256)?, + ArtifactIdentity::sha256(&kernel_sha256)?, + ArtifactIdentity::sha256(&initramfs_sha256)?, + ), + openvmm: openvmm.into(), + kernel: kernel.into(), + initramfs: initramfs.into(), hypervisor: Hypervisor::from_env_or_default()?, cpu_profile: None, windows_breakaway: false, diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 98eff7faa..bf87e14ef 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -1,21 +1,4 @@ mod nvx -struct nvx::Artifacts -const nvx::Artifacts::BESIDE_EXE_DIR -const nvx::Artifacts::ENV_DIR -const nvx::Artifacts::ENV_INITRD -const nvx::Artifacts::ENV_KERNEL -const nvx::Artifacts::ENV_OPENVMM -method nvx::Artifacts::artifact_dir -method nvx::Artifacts::discover -method nvx::Artifacts::from_dir -method nvx::Artifacts::from_release_dir -method nvx::Artifacts::from_repo_layout -field nvx::Artifacts::initrd -field nvx::Artifacts::kernel -method nvx::Artifacts::new -field nvx::Artifacts::openvmm -method nvx::Artifacts::probe -method nvx::Artifacts::runtime_identity struct nvx::Error method nvx::Error::code method nvx::Error::exited @@ -1624,33 +1607,14 @@ method nvx::host::artifacts::ArtifactDir::release method nvx::host::artifacts::ArtifactDir::repository method nvx::host::artifacts::ArtifactDir::require_initramfs method nvx::host::artifacts::ArtifactDir::require_kernel -method nvx::host::artifacts::ArtifactDir::require_openvmm method nvx::host::artifacts::ArtifactDir::root method nvx::host::artifacts::ArtifactDir::runtime_identity -method nvx::host::artifacts::ArtifactDir::sandbox_artifacts struct nvx::host::artifacts::ArtifactPaths field nvx::host::artifacts::ArtifactPaths::guests field nvx::host::artifacts::ArtifactPaths::kernel field nvx::host::artifacts::ArtifactPaths::layout field nvx::host::artifacts::ArtifactPaths::openvmm field nvx::host::artifacts::ArtifactPaths::root -struct nvx::host::artifacts::Artifacts -const nvx::host::artifacts::Artifacts::BESIDE_EXE_DIR -const nvx::host::artifacts::Artifacts::ENV_DIR -const nvx::host::artifacts::Artifacts::ENV_INITRD -const nvx::host::artifacts::Artifacts::ENV_KERNEL -const nvx::host::artifacts::Artifacts::ENV_OPENVMM -method nvx::host::artifacts::Artifacts::artifact_dir -method nvx::host::artifacts::Artifacts::discover -method nvx::host::artifacts::Artifacts::from_dir -method nvx::host::artifacts::Artifacts::from_release_dir -method nvx::host::artifacts::Artifacts::from_repo_layout -field nvx::host::artifacts::Artifacts::initrd -field nvx::host::artifacts::Artifacts::kernel -method nvx::host::artifacts::Artifacts::new -field nvx::host::artifacts::Artifacts::openvmm -method nvx::host::artifacts::Artifacts::probe -method nvx::host::artifacts::Artifacts::runtime_identity const nvx::host::artifacts::CHECKSUMS_NAME const nvx::host::artifacts::DISTRO_MANIFEST_SUFFIX enum nvx::host::artifacts::Guest @@ -1662,7 +1626,6 @@ method nvx::host::artifacts::Guest::default_memory_mib method nvx::host::artifacts::Guest::distribution method nvx::host::artifacts::Guest::distro_layer_name method nvx::host::artifacts::Guest::distro_manifest_name -method nvx::host::artifacts::Guest::from_name method nvx::host::artifacts::Guest::initramfs_name method nvx::host::artifacts::Guest::name method nvx::host::artifacts::Guest::package_manifest_name @@ -1683,7 +1646,6 @@ const nvx::host::artifacts::MANIFEST_NAME const nvx::host::artifacts::OPENVMM_PROVENANCE_NAME const nvx::host::artifacts::PACKAGE_MANIFEST_SUFFIX fn nvx::host::artifacts::absolute -fn nvx::host::artifacts::check_manifest fn nvx::host::artifacts::openvmm_executable mod nvx::host::cli (hidden) fn nvx::host::cli::main (hidden) @@ -2163,19 +2125,9 @@ method nvx::host::sandbox::SandboxMount::with_kind mod nvx::host::sandbox::distro const nvx::host::sandbox::distro::BINDING_VERSION struct nvx::host::sandbox::distro::CheckedDistro -method nvx::host::sandbox::distro::CheckedDistro::file -method nvx::host::sandbox::distro::CheckedDistro::geometry -method nvx::host::sandbox::distro::CheckedDistro::into_file -method nvx::host::sandbox::distro::CheckedDistro::path const nvx::host::sandbox::distro::DISTRO_POLICY_REVISION struct nvx::host::sandbox::distro::DistroBinding method nvx::host::sandbox::distro::DistroBinding::check -method nvx::host::sandbox::distro::DistroBinding::format -method nvx::host::sandbox::distro::DistroBinding::identity -method nvx::host::sandbox::distro::DistroBinding::image -method nvx::host::sandbox::distro::DistroBinding::import -method nvx::host::sandbox::distro::DistroBinding::manifest -method nvx::host::sandbox::distro::DistroBinding::policy method nvx::host::sandbox::distro::DistroBinding::verify enum nvx::host::sandbox::distro::DistroFormat variant nvx::host::sandbox::distro::DistroFormat::Erofs @@ -2305,18 +2257,10 @@ mod nvx::host::sandbox::scratch const nvx::host::sandbox::scratch::CLAIM_RELEASE_WAIT struct nvx::host::sandbox::scratch::CheckedScratch method nvx::host::sandbox::scratch::CheckedScratch::file -method nvx::host::sandbox::scratch::CheckedScratch::geometry -method nvx::host::sandbox::scratch::CheckedScratch::into_file -method nvx::host::sandbox::scratch::CheckedScratch::path const nvx::host::sandbox::scratch::SCRATCH_BINDING_VERSION struct nvx::host::sandbox::scratch::ScratchBinding method nvx::host::sandbox::scratch::ScratchBinding::bind method nvx::host::sandbox::scratch::ScratchBinding::check -method nvx::host::sandbox::scratch::ScratchBinding::device -method nvx::host::sandbox::scratch::ScratchBinding::file_id -method nvx::host::sandbox::scratch::ScratchBinding::kind -method nvx::host::sandbox::scratch::ScratchBinding::length -method nvx::host::sandbox::scratch::ScratchBinding::scheme mod nvx::host::seal enum nvx::host::seal::FileKind variant nvx::host::seal::FileKind::Block @@ -2327,12 +2271,10 @@ variant nvx::host::seal::FileKind::Other variant nvx::host::seal::FileKind::Regular variant nvx::host::seal::FileKind::Socket struct nvx::host::seal::FileSeal -method nvx::host::seal::FileSeal::changed method nvx::host::seal::FileSeal::device method nvx::host::seal::FileSeal::file_id method nvx::host::seal::FileSeal::kind method nvx::host::seal::FileSeal::length -method nvx::host::seal::FileSeal::modified method nvx::host::seal::FileSeal::of method nvx::host::seal::FileSeal::scheme const nvx::host::seal::SEAL_VERSION diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index 00f6f4132..c0c08db40 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -1,21 +1,4 @@ mod nvx -struct nvx::Artifacts -const nvx::Artifacts::BESIDE_EXE_DIR -const nvx::Artifacts::ENV_DIR -const nvx::Artifacts::ENV_INITRD -const nvx::Artifacts::ENV_KERNEL -const nvx::Artifacts::ENV_OPENVMM -method nvx::Artifacts::artifact_dir -method nvx::Artifacts::discover -method nvx::Artifacts::from_dir -method nvx::Artifacts::from_release_dir -method nvx::Artifacts::from_repo_layout -field nvx::Artifacts::initrd -field nvx::Artifacts::kernel -method nvx::Artifacts::new -field nvx::Artifacts::openvmm -method nvx::Artifacts::probe -method nvx::Artifacts::runtime_identity struct nvx::Error method nvx::Error::code method nvx::Error::exited @@ -352,33 +335,14 @@ method nvx::host::artifacts::ArtifactDir::release method nvx::host::artifacts::ArtifactDir::repository method nvx::host::artifacts::ArtifactDir::require_initramfs method nvx::host::artifacts::ArtifactDir::require_kernel -method nvx::host::artifacts::ArtifactDir::require_openvmm method nvx::host::artifacts::ArtifactDir::root method nvx::host::artifacts::ArtifactDir::runtime_identity -method nvx::host::artifacts::ArtifactDir::sandbox_artifacts struct nvx::host::artifacts::ArtifactPaths field nvx::host::artifacts::ArtifactPaths::guests field nvx::host::artifacts::ArtifactPaths::kernel field nvx::host::artifacts::ArtifactPaths::layout field nvx::host::artifacts::ArtifactPaths::openvmm field nvx::host::artifacts::ArtifactPaths::root -struct nvx::host::artifacts::Artifacts -const nvx::host::artifacts::Artifacts::BESIDE_EXE_DIR -const nvx::host::artifacts::Artifacts::ENV_DIR -const nvx::host::artifacts::Artifacts::ENV_INITRD -const nvx::host::artifacts::Artifacts::ENV_KERNEL -const nvx::host::artifacts::Artifacts::ENV_OPENVMM -method nvx::host::artifacts::Artifacts::artifact_dir -method nvx::host::artifacts::Artifacts::discover -method nvx::host::artifacts::Artifacts::from_dir -method nvx::host::artifacts::Artifacts::from_release_dir -method nvx::host::artifacts::Artifacts::from_repo_layout -field nvx::host::artifacts::Artifacts::initrd -field nvx::host::artifacts::Artifacts::kernel -method nvx::host::artifacts::Artifacts::new -field nvx::host::artifacts::Artifacts::openvmm -method nvx::host::artifacts::Artifacts::probe -method nvx::host::artifacts::Artifacts::runtime_identity const nvx::host::artifacts::CHECKSUMS_NAME const nvx::host::artifacts::DISTRO_MANIFEST_SUFFIX enum nvx::host::artifacts::Guest @@ -390,7 +354,6 @@ method nvx::host::artifacts::Guest::default_memory_mib method nvx::host::artifacts::Guest::distribution method nvx::host::artifacts::Guest::distro_layer_name method nvx::host::artifacts::Guest::distro_manifest_name -method nvx::host::artifacts::Guest::from_name method nvx::host::artifacts::Guest::initramfs_name method nvx::host::artifacts::Guest::name method nvx::host::artifacts::Guest::package_manifest_name @@ -411,7 +374,6 @@ const nvx::host::artifacts::MANIFEST_NAME const nvx::host::artifacts::OPENVMM_PROVENANCE_NAME const nvx::host::artifacts::PACKAGE_MANIFEST_SUFFIX fn nvx::host::artifacts::absolute -fn nvx::host::artifacts::check_manifest fn nvx::host::artifacts::openvmm_executable mod nvx::host::cli (hidden) fn nvx::host::cli::main (hidden) @@ -891,19 +853,9 @@ method nvx::host::sandbox::SandboxMount::with_kind mod nvx::host::sandbox::distro const nvx::host::sandbox::distro::BINDING_VERSION struct nvx::host::sandbox::distro::CheckedDistro -method nvx::host::sandbox::distro::CheckedDistro::file -method nvx::host::sandbox::distro::CheckedDistro::geometry -method nvx::host::sandbox::distro::CheckedDistro::into_file -method nvx::host::sandbox::distro::CheckedDistro::path const nvx::host::sandbox::distro::DISTRO_POLICY_REVISION struct nvx::host::sandbox::distro::DistroBinding method nvx::host::sandbox::distro::DistroBinding::check -method nvx::host::sandbox::distro::DistroBinding::format -method nvx::host::sandbox::distro::DistroBinding::identity -method nvx::host::sandbox::distro::DistroBinding::image -method nvx::host::sandbox::distro::DistroBinding::import -method nvx::host::sandbox::distro::DistroBinding::manifest -method nvx::host::sandbox::distro::DistroBinding::policy method nvx::host::sandbox::distro::DistroBinding::verify enum nvx::host::sandbox::distro::DistroFormat variant nvx::host::sandbox::distro::DistroFormat::Erofs @@ -1032,19 +984,11 @@ fn nvx::host::sandbox::require_file mod nvx::host::sandbox::scratch const nvx::host::sandbox::scratch::CLAIM_RELEASE_WAIT struct nvx::host::sandbox::scratch::CheckedScratch -method nvx::host::sandbox::scratch::CheckedScratch::file -method nvx::host::sandbox::scratch::CheckedScratch::geometry method nvx::host::sandbox::scratch::CheckedScratch::into_file -method nvx::host::sandbox::scratch::CheckedScratch::path const nvx::host::sandbox::scratch::SCRATCH_BINDING_VERSION struct nvx::host::sandbox::scratch::ScratchBinding method nvx::host::sandbox::scratch::ScratchBinding::bind method nvx::host::sandbox::scratch::ScratchBinding::check -method nvx::host::sandbox::scratch::ScratchBinding::device -method nvx::host::sandbox::scratch::ScratchBinding::file_id -method nvx::host::sandbox::scratch::ScratchBinding::kind -method nvx::host::sandbox::scratch::ScratchBinding::length -method nvx::host::sandbox::scratch::ScratchBinding::scheme mod nvx::host::seal enum nvx::host::seal::FileKind variant nvx::host::seal::FileKind::Block @@ -1055,12 +999,10 @@ variant nvx::host::seal::FileKind::Other variant nvx::host::seal::FileKind::Regular variant nvx::host::seal::FileKind::Socket struct nvx::host::seal::FileSeal -method nvx::host::seal::FileSeal::changed method nvx::host::seal::FileSeal::device method nvx::host::seal::FileSeal::file_id method nvx::host::seal::FileSeal::kind method nvx::host::seal::FileSeal::length -method nvx::host::seal::FileSeal::modified method nvx::host::seal::FileSeal::of method nvx::host::seal::FileSeal::scheme const nvx::host::seal::SEAL_VERSION diff --git a/nvx-rs/src/host/artifacts/dir.rs b/nvx-rs/src/host/artifacts/dir.rs index 5654c22ba..33a35cbb7 100644 --- a/nvx-rs/src/host/artifacts/dir.rs +++ b/nvx-rs/src/host/artifacts/dir.rs @@ -9,9 +9,9 @@ use serde::Serialize; use super::identity::{provenance_identity, read_metadata, sha256_identity}; use super::require::required; use super::{ - Artifacts, CHECKSUMS_NAME, Guest, INITRAMFS_PROVENANCE_NAME, KERNEL_NAME, - KERNEL_PROVENANCE_NAME, OPENVMM_PROVENANCE_NAME, RELEASE_BIN_DIR, RELEASE_GUEST_DIR, - REPO_BUILD_DIR, REPO_OPENVMM_DIR, absolute, is_release_dir, openvmm_executable, + CHECKSUMS_NAME, Guest, INITRAMFS_PROVENANCE_NAME, KERNEL_NAME, KERNEL_PROVENANCE_NAME, + OPENVMM_PROVENANCE_NAME, RELEASE_BIN_DIR, RELEASE_GUEST_DIR, REPO_BUILD_DIR, REPO_OPENVMM_DIR, + absolute, is_release_dir, openvmm_executable, }; use crate::error::{Error, Result}; use crate::host::runtime::{ArtifactIdentity, RuntimeIdentity}; @@ -33,8 +33,8 @@ pub enum Layout { /// `scripts/nvx_tools/common.py` give for a checkout. /// /// Paths are joined to the directory, made absolute, without checking that they exist; the -/// `require_*` methods check them with the messages of `nvx.py`. Unlike [`Artifacts`], nothing -/// here reads the release's `SOURCE-MANIFEST.json`, which `nvx.py` does not check either. +/// `require_*` methods check them with the messages of `nvx.py`. Nothing here reads the release's +/// `SOURCE-MANIFEST.json`, which `nvx.py` does not check either. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ArtifactDir { /// The absolute directory. @@ -59,10 +59,12 @@ impl ArtifactDir { Self::new(root.as_ref(), Layout::Release) } - /// Uses a directory in the layout that its built artifacts reveal, as - /// [`Artifacts::from_dir`] tells them apart: a directory that contains `bin/openvmm[.exe]`, - /// `guest/vmlinux`, or `guest/initramfs.cpio.gz` is a release, and any other directory is a - /// repository checkout. + /// Uses a directory in the layout that its built artifacts reveal. + /// + /// A repository checkout also has a `guest/` directory, which holds the guest sources, so the + /// layouts are told apart by their built artifacts instead: a directory that contains + /// `bin/openvmm[.exe]`, `guest/vmlinux`, or `guest/initramfs.cpio.gz` is a release, and any + /// other directory is a repository checkout. /// /// Fails as [`repository`](Self::repository) does. pub fn from_dir(directory: impl AsRef) -> Result { @@ -141,16 +143,6 @@ impl ArtifactDir { .map(|name| self.artifact(&name)) } - /// Checks that the OpenVMM executable is a regular file and returns its path. - /// - /// Fails with [`ErrorCode::ArtifactMissing`] and the message `OpenVMM release binary not - /// found: PATH`. - /// - /// [`ErrorCode::ArtifactMissing`]: crate::ErrorCode::ArtifactMissing - pub fn require_openvmm(&self) -> Result { - required(self.openvmm(), "OpenVMM release binary") - } - /// Checks that the guest kernel is a regular file and returns its path. /// /// Fails with [`ErrorCode::ArtifactMissing`] and the message `Linux direct kernel not found: @@ -175,22 +167,6 @@ impl ArtifactDir { ) } - /// Returns the artifacts of a sandbox, as `nvx-rs sandbox run` and `nvx-rs sandbox start` - /// require them: the OpenVMM executable, the kernel, and the Alpine initramfs, in that order, - /// located in this directory. - /// - /// Fails with [`ErrorCode::ArtifactMissing`] for the first one that is not a regular file, - /// with the messages of [`require_openvmm`](Self::require_openvmm) and - /// [`require_kernel`](Self::require_kernel), or `initramfs not found: PATH`. - /// - /// [`ErrorCode::ArtifactMissing`]: crate::ErrorCode::ArtifactMissing - pub fn sandbox_artifacts(&self) -> Result { - self.require_openvmm()?; - self.require_kernel()?; - required(self.initramfs(Guest::Alpine), "initramfs")?; - Ok(Artifacts::located(self)) - } - /// Returns the paths of every artifact, for a caller to show or record. pub fn paths(&self) -> ArtifactPaths { ArtifactPaths { @@ -212,10 +188,9 @@ impl ArtifactDir { } } - /// Returns the identity of the sandbox runtime of this directory, the one that - /// [`sandbox_artifacts`](Self::sandbox_artifacts) and [`Artifacts::from_dir`] name: its - /// OpenVMM executable, its kernel, and its Alpine initramfs, as the directory's install - /// metadata names them, under the contracts that this crate speaks. + /// Returns the identity of the sandbox runtime of this directory: its OpenVMM executable, its + /// kernel, and its Alpine initramfs, as the directory's install metadata names them, under the + /// contracts that this crate speaks. /// /// The metadata names each artifact by the SHA-256 digest that its build or its package /// recorded, so nothing here reads or hashes an artifact: diff --git a/nvx-rs/src/host/artifacts/guest.rs b/nvx-rs/src/host/artifacts/guest.rs index 1bd29c610..502e13888 100644 --- a/nvx-rs/src/host/artifacts/guest.rs +++ b/nvx-rs/src/host/artifacts/guest.rs @@ -3,7 +3,6 @@ use std::fmt; use super::{DISTRO_MANIFEST_SUFFIX, PACKAGE_MANIFEST_SUFFIX}; -use crate::error::{Error, Result}; /// A Linux guest that `nvx.py` builds and runs, as `scripts/nvx_tools/guests.py` describes it. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] @@ -29,24 +28,6 @@ impl Guest { } } - /// Returns the guest named `name`. - /// - /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) and the message - /// `unsupported guest "NAME"; choose alpine, ubuntu, azurelinux`, which quotes the name with - /// Rust's debug formatting. - pub fn from_name(name: &str) -> Result { - Self::ALL - .into_iter() - .find(|guest| guest.name() == name) - .ok_or_else(|| { - let names: Vec<&str> = Self::ALL.iter().map(|guest| guest.name()).collect(); - Error::unsupported(format!( - "unsupported guest {name:?}; choose {}", - names.join(", ") - )) - }) - } - /// Returns the name of the guest's distribution, as messages show it: `Alpine Linux`, /// `Ubuntu Base`, or `Azure Linux`. pub const fn distribution(self) -> &'static str { diff --git a/nvx-rs/src/host/artifacts/mod.rs b/nvx-rs/src/host/artifacts/mod.rs index ecb757169..5f4cbe30e 100644 --- a/nvx-rs/src/host/artifacts/mod.rs +++ b/nvx-rs/src/host/artifacts/mod.rs @@ -1,9 +1,5 @@ //! Locating the OpenVMM executable and the NVX guest artifacts. //! -//! [`Artifacts`] names the three files that a VM runs. They come from an extracted NVX release -//! archive, an NVX repository checkout, or explicit paths. [`Artifacts::discover`] tries these -//! sources in a fixed order so consumers such as MXC need no NVX-specific configuration. -//! //! [`ArtifactDir`] names every artifact of the guests that `nvx.py` runs, which [`Guest`] //! describes, in a repository checkout or an extracted release: //! @@ -29,17 +25,9 @@ mod guest; mod identity; mod require; -use std::env; -use std::ffi::OsString; -use std::fs; use std::path::{Path, PathBuf}; -use serde_json::Value; - -use crate::contract; -use crate::error::{Error, ErrorCode, Result}; -use crate::host::platform; -use crate::host::runtime::RuntimeIdentity; +use crate::error::{Error, Result}; pub use self::dir::{ArtifactDir, ArtifactPaths, GuestPaths, Layout}; pub use self::guest::Guest; @@ -50,234 +38,6 @@ pub(crate) use self::require::require_existing; /// Name of the release manifest at the root of a release archive or repository checkout. pub const MANIFEST_NAME: &str = "SOURCE-MANIFEST.json"; -/// The OpenVMM executable and the NVX guest kernel and initramfs. -/// -/// Every path is used as given. Resolve relative paths with [`absolute`] before a VM runs them, -/// and check that the files exist, because nothing here does. -/// -/// Artifacts located in a release or a repository checkout keep its [`ArtifactDir`], whose -/// install metadata names their [`runtime_identity`](Self::runtime_identity). Artifacts named by -/// path, through [`new`](Self::new) or the explicit variables of [`discover`](Self::discover), -/// have none: their caller supplies the runtime identity of a launch. -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub struct Artifacts { - /// The `openvmm` executable. - pub openvmm: PathBuf, - /// The NVX guest kernel (`vmlinux`). - pub kernel: PathBuf, - /// The NVX Alpine initramfs (`initramfs.cpio.gz`): the guest userland and its control agent. - pub initrd: PathBuf, - /// The release or checkout in which the artifacts were located, if they were. - directory: Option, -} - -impl Artifacts { - /// Names the three artifacts that a VM needs. - pub fn new( - openvmm: impl Into, - kernel: impl Into, - initrd: impl Into, - ) -> Self { - Self { - openvmm: openvmm.into(), - kernel: kernel.into(), - initrd: initrd.into(), - directory: None, - } - } - - /// Returns the sandbox artifacts of `directory`, which they remember. - fn located(directory: &ArtifactDir) -> Self { - Self { - directory: Some(directory.clone()), - ..Self::new( - directory.openvmm(), - directory.kernel(), - directory.initramfs(Guest::Alpine), - ) - } - } - - /// Returns the release or repository checkout in which the artifacts were located, or `None` - /// for artifacts named by path. - pub fn artifact_dir(&self) -> Option<&ArtifactDir> { - self.directory.as_ref() - } - - /// Returns the runtime identity of these artifacts, which the install metadata of the release - /// or checkout in which they were located names, as [`ArtifactDir::runtime_identity`] reads - /// it, without reading or hashing an artifact. - /// - /// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) for artifacts named by - /// path, whose caller supplies their identity instead, for artifacts whose paths no longer - /// name the ones of their directory, and as [`ArtifactDir::runtime_identity`] fails. - pub fn runtime_identity(&self) -> Result { - let Some(directory) = &self.directory else { - return Err(Error::unavailable( - "the artifacts are named by path, so no install metadata identifies them: locate \ - them in an NVX release or repository checkout, or supply their runtime identity", - )); - }; - let located = Self::located(directory); - for (path, expected, description) in [ - (&self.openvmm, &located.openvmm, "OpenVMM executable"), - (&self.kernel, &located.kernel, "guest kernel"), - (&self.initrd, &located.initrd, "guest initramfs"), - ] { - if path != expected { - return Err(Error::unavailable(format!( - "the {description} {} is not the one of {}, whose install metadata \ - identifies the artifacts", - path.display(), - directory.root().display() - ))); - } - } - directory.runtime_identity() - } - - /// Checks that this host can run NVX VMs and that the three files exist. - /// - /// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) otherwise, with the - /// code [`ErrorCode::ArtifactMissing`] for a missing file. It does not check the hypervisor; - /// [`probe_hypervisor`](super::runtime::probe_hypervisor) does. - pub fn probe(&self) -> Result<()> { - if !platform::SUPPORTED { - return Err(Error::unavailable( - "the openvmm backend supports Linux and Windows hosts only", - )); - } - for (path, description) in [ - (&self.openvmm, "OpenVMM executable"), - (&self.kernel, "NVX guest kernel"), - (&self.initrd, "NVX guest initramfs"), - ] { - if !path.is_file() { - return Err(Error::new( - ErrorCode::ArtifactMissing, - format!("{description} not found: {}", path.display()), - )); - } - } - Ok(()) - } - - /// Uses an extracted NVX release archive: `bin/openvmm[.exe]`, `guest/vmlinux`, and - /// `guest/initramfs.cpio.gz`. - /// - /// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) when the release's - /// `SOURCE-MANIFEST.json` declares an incompatible control contract. - pub fn from_release_dir(release_dir: impl AsRef) -> Result { - Self::from_artifact_dir(&ArtifactDir::release(release_dir.as_ref())?) - } - - /// Uses an NVX repository checkout after `nvx-rs download` or a local build: - /// `openvmm/target/release/openvmm[.exe]`, `build/vmlinux`, and `build/initramfs.cpio.gz`. - pub fn from_repo_layout(repo_root: impl AsRef) -> Result { - Self::from_artifact_dir(&ArtifactDir::repository(repo_root.as_ref())?) - } - - /// Uses a directory in either the release or the repository layout. - /// - /// A repository checkout also has a `guest/` directory, which holds the guest sources, so the - /// layouts are told apart by their built artifacts instead: a directory that contains - /// `bin/openvmm[.exe]`, `guest/vmlinux`, or `guest/initramfs.cpio.gz` is a release, and any - /// other directory is a repository checkout. - pub fn from_dir(directory: impl AsRef) -> Result { - Self::from_artifact_dir(&ArtifactDir::from_dir(directory.as_ref())?) - } - - /// Uses the OpenVMM executable, guest kernel, and Alpine initramfs of `directory`, once its - /// `SOURCE-MANIFEST.json` declares the control contract that this crate speaks. - /// - /// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) as - /// [`check_manifest`] does. - fn from_artifact_dir(directory: &ArtifactDir) -> Result { - check_manifest(&directory.root().join(MANIFEST_NAME))?; - Ok(Self::located(directory)) - } - - /// Locates the artifacts on this host. - /// - /// Sources are tried in this order, and the first one present wins: - /// - /// 1. The [`ENV_OPENVMM`](Self::ENV_OPENVMM), [`ENV_KERNEL`](Self::ENV_KERNEL), and - /// [`ENV_INITRD`](Self::ENV_INITRD) variables, which must be set together. - /// 2. The [`ENV_DIR`](Self::ENV_DIR) variable, naming a release or repository directory. - /// 3. The [`BESIDE_EXE_DIR`](Self::BESIDE_EXE_DIR) directory next to the current executable, - /// in the release layout. Consumers ship a release there. - /// - /// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) when no source is - /// present or the chosen source is unusable. It does not check that the files exist. - pub fn discover() -> Result { - let exe_dir = env::current_exe() - .ok() - .and_then(|exe| exe.parent().map(Path::to_path_buf)); - discover_with(|name| env::var_os(name), exe_dir.as_deref()) - } - - /// Environment variable naming the `openvmm` executable. - pub const ENV_OPENVMM: &'static str = "NVX_OPENVMM"; - /// Environment variable naming the guest kernel. - pub const ENV_KERNEL: &'static str = "NVX_KERNEL"; - /// Environment variable naming the control initramfs. - pub const ENV_INITRD: &'static str = "NVX_INITRD"; - /// Environment variable naming a release or repository directory. - pub const ENV_DIR: &'static str = "NVX_ARTIFACTS_DIR"; - /// Name of the release-layout directory that [`Artifacts::discover`] looks for next to the - /// current executable. - pub const BESIDE_EXE_DIR: &'static str = "nvx"; -} - -/// Implements artifact discovery with an injectable environment and executable directory for -/// tests. -fn discover_with( - variable: impl Fn(&str) -> Option, - exe_dir: Option<&Path>, -) -> Result { - let explicit = [ - Artifacts::ENV_OPENVMM, - Artifacts::ENV_KERNEL, - Artifacts::ENV_INITRD, - ] - .map(|name| variable(name).filter(|value| !value.is_empty())); - match explicit { - [Some(openvmm), Some(kernel), Some(initrd)] => { - return Ok(Artifacts::new(openvmm, kernel, initrd)); - } - [None, None, None] => {} - _ => { - return Err(Error::unavailable(format!( - "set all or none of {}, {}, and {}", - Artifacts::ENV_OPENVMM, - Artifacts::ENV_KERNEL, - Artifacts::ENV_INITRD - ))); - } - } - if let Some(directory) = variable(Artifacts::ENV_DIR).filter(|value| !value.is_empty()) { - return Artifacts::from_dir(PathBuf::from(directory)); - } - if let Some(directory) = exe_dir.map(|directory| directory.join(Artifacts::BESIDE_EXE_DIR)) - && directory.join(MANIFEST_NAME).is_file() - { - return Artifacts::from_release_dir(directory); - } - Err(Error::new( - ErrorCode::ArtifactMissing, - format!( - "cannot locate the OpenVMM executable and NVX guest artifacts: set {} or {}, {}, and \ - {}, or place an NVX release in a {:?} directory next to the executable", - Artifacts::ENV_DIR, - Artifacts::ENV_OPENVMM, - Artifacts::ENV_KERNEL, - Artifacts::ENV_INITRD, - Artifacts::BESIDE_EXE_DIR, - ), - )) -} - /// Returns whether `directory` is in the release layout: whether it contains `bin/openvmm[.exe]`, /// `guest/vmlinux`, or `guest/initramfs.cpio.gz`. fn is_release_dir(directory: &Path) -> bool { @@ -308,38 +68,6 @@ pub fn absolute(path: &Path) -> Result { }) } -/// Reads and parses the JSON file at `path`, naming it with `description` in errors. -fn read_json(path: &Path, description: &str) -> Result { - let text = fs::read_to_string(path).map_err(|error| { - Error::unavailable(format!("cannot read {description} {}", path.display())) - .with_source(error) - })?; - serde_json::from_str(&text).map_err(|error| { - Error::unavailable(format!( - "{description} {} is not valid JSON", - path.display() - )) - .with_source(error) - }) -} - -/// Checks that an NVX `SOURCE-MANIFEST.json` declares the control contract this crate speaks, -/// failing with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) that names each -/// mismatching contract field. -pub fn check_manifest(path: &Path) -> Result<()> { - let manifest = read_json(path, "NVX manifest")?; - let mismatches = contract::manifest_mismatches(&manifest); - if mismatches.is_empty() { - Ok(()) - } else { - Err(Error::unavailable(format!( - "NVX manifest {} does not declare the compatible OpenVMM contracts ({})", - path.display(), - mismatches.join("; ") - ))) - } -} - /// File name of the guest kernel in both layouts. pub const KERNEL_NAME: &str = "vmlinux"; /// Suffix of the package manifest that lists the packages of an initramfs. diff --git a/nvx-rs/src/host/artifacts/tests.rs b/nvx-rs/src/host/artifacts/tests.rs index 33c1489e5..a0a83005f 100644 --- a/nvx-rs/src/host/artifacts/tests.rs +++ b/nvx-rs/src/host/artifacts/tests.rs @@ -1,11 +1,14 @@ -//! Tests of the discovery of artifacts, of the directories that hold them, and of their +//! Tests of the directories that hold the artifacts, of the guests' artifacts, and of their //! identities. -use std::collections::HashMap; +use std::fs; + +use serde_json::Value; use super::*; use crate::ErrorKind; -use crate::host::runtime::ArtifactIdentity; +use crate::contract; +use crate::host::runtime::{ArtifactIdentity, RuntimeIdentity}; const COMPATIBLE_MANIFEST: &str = r#"{"openvmm":{"microvm_abi_version":1, "time_abi_version":1,"restore_packet_version":1, @@ -33,203 +36,47 @@ fn checkout(root: &Path) { fs::write(root.join(MANIFEST_NAME), COMPATIBLE_MANIFEST).unwrap(); } -fn variables(pairs: &[(&str, &Path)]) -> impl Fn(&str) -> Option { - let map: HashMap = pairs - .iter() - .map(|(name, value)| ((*name).to_owned(), value.as_os_str().to_owned())) - .collect(); - move |name| map.get(name).cloned() -} - -#[test] -fn release_layout_names_the_guest_artifacts() { - let directory = tempfile::tempdir().unwrap(); - release(directory.path()); - let artifacts = Artifacts::from_dir(directory.path()).unwrap(); - let root = std::path::absolute(directory.path()).unwrap(); - assert_eq!( - artifacts.openvmm, - root.join("bin").join(openvmm_executable()) - ); - assert_eq!(artifacts.kernel, root.join("guest").join("vmlinux")); - assert_eq!( - artifacts.initrd, - root.join("guest").join("initramfs.cpio.gz") - ); - // A directory that the caller hands over, such as a `TempDir`, lives until the - // constructor returns, after it reads the manifest. - assert_eq!(Artifacts::from_release_dir(directory).unwrap(), artifacts); -} - #[test] fn repository_layout_is_detected_beside_the_guest_sources() { let directory = tempfile::tempdir().unwrap(); checkout(directory.path()); let root = std::path::absolute(directory.path()).unwrap(); - let expected = |artifacts: &Artifacts| { - assert_eq!(artifacts.kernel, root.join("build").join("vmlinux")); + let expected = |artifacts: &ArtifactDir| { + assert_eq!(artifacts.layout(), Layout::Repository); + assert_eq!(artifacts.kernel(), root.join("build").join("vmlinux")); assert_eq!( - artifacts.initrd, + artifacts.initramfs(Guest::Alpine), root.join("build").join("initramfs.cpio.gz") ); assert_eq!( - artifacts.openvmm, + artifacts.openvmm(), root.join("openvmm") .join("target") .join("release") .join(openvmm_executable()) ); }; - expected(&Artifacts::from_dir(directory.path()).unwrap()); - expected(&Artifacts::from_repo_layout(directory.path()).unwrap()); + expected(&ArtifactDir::from_dir(directory.path()).unwrap()); // Build outputs do not turn a checkout into a release. let build = directory.path().join("build"); fs::create_dir_all(&build).unwrap(); fs::write(build.join("vmlinux"), b"kernel").unwrap(); fs::write(build.join("initramfs.cpio.gz"), b"initrd").unwrap(); - expected(&Artifacts::from_dir(directory.path()).unwrap()); -} - -#[test] -fn the_directory_variable_accepts_a_repository_checkout() { - let directory = tempfile::tempdir().unwrap(); - checkout(directory.path()); - let artifacts = - discover_with(variables(&[(Artifacts::ENV_DIR, directory.path())]), None).unwrap(); - let build = std::path::absolute(directory.path()).unwrap().join("build"); - assert_eq!(artifacts.kernel, build.join("vmlinux")); + expected(&ArtifactDir::from_dir(directory.path()).unwrap()); } #[test] fn this_repository_uses_the_repository_layout() { let root = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); if root.join(MANIFEST_NAME).is_file() { - let artifacts = Artifacts::from_dir(root).unwrap(); - let kernel = std::path::absolute(root).unwrap().join("build"); - assert_eq!(artifacts.kernel, kernel.join("vmlinux")); let directory = ArtifactDir::from_dir(root).unwrap(); assert_eq!(directory.layout(), Layout::Repository); - assert_eq!(directory.kernel(), artifacts.kernel); + let build = std::path::absolute(root).unwrap().join("build"); + assert_eq!(directory.kernel(), build.join("vmlinux")); } } -#[test] -fn explicit_variables_win_and_must_be_complete() { - let directory = tempfile::tempdir().unwrap(); - release(directory.path()); - let (openvmm, kernel, initrd) = (Path::new("o"), Path::new("k"), Path::new("i")); - let artifacts = discover_with( - variables(&[ - (Artifacts::ENV_OPENVMM, openvmm), - (Artifacts::ENV_KERNEL, kernel), - (Artifacts::ENV_INITRD, initrd), - (Artifacts::ENV_DIR, directory.path()), - ]), - None, - ) - .unwrap(); - assert_eq!(artifacts, Artifacts::new(openvmm, kernel, initrd)); - - let error = discover_with(variables(&[(Artifacts::ENV_OPENVMM, openvmm)]), None).unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unavailable); -} - -#[test] -fn the_directory_variable_precedes_the_executable_directory() { - let variable_dir = tempfile::tempdir().unwrap(); - release(variable_dir.path()); - let exe_dir = tempfile::tempdir().unwrap(); - release(&exe_dir.path().join(Artifacts::BESIDE_EXE_DIR)); - - let artifacts = discover_with( - variables(&[(Artifacts::ENV_DIR, variable_dir.path())]), - Some(exe_dir.path()), - ) - .unwrap(); - assert!( - artifacts - .kernel - .starts_with(std::path::absolute(variable_dir.path()).unwrap()) - ); - - let artifacts = discover_with(variables(&[]), Some(exe_dir.path())).unwrap(); - assert!( - artifacts.kernel.starts_with( - std::path::absolute(exe_dir.path().join(Artifacts::BESIDE_EXE_DIR)).unwrap() - ) - ); -} - -#[test] -fn discovery_fails_when_no_source_is_present() { - let exe_dir = tempfile::tempdir().unwrap(); - let error = discover_with(variables(&[]), Some(exe_dir.path())).unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unavailable); - assert!(error.message().contains(Artifacts::ENV_DIR)); - // No build-time bundle remains to suggest. - assert!(!error.message().contains("bundled")); -} - -#[test] -fn incompatible_manifests_are_unavailable() { - let directory = tempfile::tempdir().unwrap(); - let manifest = directory.path().join(MANIFEST_NAME); - fs::write( - &manifest, - r#"{"openvmm":{"microvm_abi_version":1,"time_abi_version":1, - "restore_packet_version":1,"control_session_protocol_version":2, - "control_contract_revision":"nvx-microvm-v2-control-v2"}}"#, - ) - .unwrap(); - let error = check_manifest(&manifest).unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unavailable); - assert!( - error - .message() - .contains("(control_session_protocol_version: expected 1, found 2)"), - "{}", - error.message() - ); - // A release of an earlier OpenVMM declares the current control contracts, so the error names - // the microVM ABI, time ABI, and restore packet versions that its microVM had. - fs::write( - &manifest, - r#"{"openvmm":{"microvm_abi_version":3,"time_abi_version":2, - "restore_packet_version":5,"control_session_protocol_version":1, - "control_contract_revision":"nvx-microvm-v2-control-v2"}}"#, - ) - .unwrap(); - let error = check_manifest(&manifest).unwrap_err(); - assert!( - error.message().contains( - "(microvm_abi_version: expected 1, found 3; time_abi_version: expected 1, found 2; \ - restore_packet_version: expected 1, found 5)" - ), - "{}", - error.message() - ); - // A release from before time ABI 2 declares the current control revision, so the error names - // the microVM ABI, time ABI, and restore packet versions that differ. - fs::write( - &manifest, - r#"{"openvmm":{"microvm_abi_version":2,"control_session_protocol_version":1, - "control_contract_revision":"nvx-microvm-v2-control-v2"}}"#, - ) - .unwrap(); - let error = check_manifest(&manifest).unwrap_err(); - assert!( - error.message().contains( - "(microvm_abi_version: expected 1, found 2; time_abi_version: expected 1, found \ - none; restore_packet_version: expected 1, found none)" - ), - "{}", - error.message() - ); - let error = check_manifest(&directory.path().join("missing.json")).unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unavailable); -} - #[test] fn repository_manifest_declares_the_supported_contract() { let manifest = Path::new(env!("CARGO_MANIFEST_DIR")) @@ -237,7 +84,10 @@ fn repository_manifest_declares_the_supported_contract() { .unwrap() .join(MANIFEST_NAME); if manifest.is_file() { - check_manifest(&manifest).unwrap(); + let manifest: Value = + serde_json::from_str(&fs::read_to_string(&manifest).unwrap()).unwrap(); + let mismatches = contract::manifest_mismatches(&manifest); + assert!(mismatches.is_empty(), "{mismatches:?}"); } } @@ -248,15 +98,8 @@ fn guests_name_their_artifacts_as_nvx_py_does() { ["alpine", "ubuntu", "azurelinux"] ); for guest in Guest::ALL { - assert_eq!(Guest::from_name(guest.name()).unwrap(), guest); assert_eq!(guest.to_string(), guest.name()); } - let error = Guest::from_name("Ubuntu").unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unsupported); - assert_eq!( - error.message(), - "unsupported guest \"Ubuntu\"; choose alpine, ubuntu, azurelinux" - ); assert_eq!( Guest::ALL.map(Guest::distribution), ["Alpine Linux", "Ubuntu Base", "Azure Linux"] @@ -359,14 +202,10 @@ fn a_release_holds_the_guest_artifacts_in_guest() { artifacts.distro_layer(Guest::Ubuntu), Some(guest.join("ubuntu-distro.erofs")) ); - - // The release's artifacts are the ones that discovery finds for its directory. - let discovered = Artifacts::from_dir(directory.path()).unwrap(); - assert_eq!(artifacts.sandbox_artifacts().unwrap(), discovered); } #[test] -fn guest_artifacts_are_required_in_the_order_and_words_of_nvx_py() { +fn guest_artifacts_are_required_in_the_words_of_nvx_py() { let directory = tempfile::tempdir().unwrap(); checkout(directory.path()); let artifacts = ArtifactDir::repository(directory.path()).unwrap(); @@ -383,39 +222,11 @@ fn guest_artifacts_are_required_in_the_order_and_words_of_nvx_py() { fs::write(path, b"artifact").unwrap(); }; missing( - artifacts.sandbox_artifacts(), - "OpenVMM release binary", - artifacts.openvmm(), - ); - create(artifacts.openvmm()); - missing( - artifacts.sandbox_artifacts(), + artifacts.require_kernel(), "Linux direct kernel", artifacts.kernel(), ); create(artifacts.kernel()); - missing( - artifacts.sandbox_artifacts(), - "initramfs", - artifacts.initramfs(Guest::Alpine), - ); - create(artifacts.initramfs(Guest::Alpine)); - let located = artifacts.sandbox_artifacts().unwrap(); - assert_eq!( - [&located.openvmm, &located.kernel, &located.initrd], - [ - &artifacts.openvmm(), - &artifacts.kernel(), - &artifacts.initramfs(Guest::Alpine) - ] - ); - assert_eq!(located.artifact_dir(), Some(&artifacts)); - assert_eq!( - located, - Artifacts::from_repo_layout(directory.path()).unwrap() - ); - - assert_eq!(artifacts.require_openvmm().unwrap(), artifacts.openvmm()); assert_eq!(artifacts.require_kernel().unwrap(), artifacts.kernel()); missing( artifacts.require_initramfs(Guest::Ubuntu), @@ -683,49 +494,3 @@ fn metadata_that_is_not_a_regular_file_fails_at_once() { drop(writer); } } - -#[test] -fn located_artifacts_have_the_identity_of_their_directory() { - let directory = tempfile::tempdir().unwrap(); - release(directory.path()); - let digests = ["4", "5", "6"].map(|digit| digit.repeat(64)); - let text: String = [ - (&digests[0], format!("bin/{}", openvmm_executable())), - (&digests[1], "guest/vmlinux".to_owned()), - (&digests[2], "guest/initramfs.cpio.gz".to_owned()), - ] - .iter() - .map(|(digest, name)| format!("{digest} {name}\n")) - .collect(); - fs::write(directory.path().join(CHECKSUMS_NAME), text).unwrap(); - let located = Artifacts::from_dir(directory.path()).unwrap(); - assert_eq!( - located.artifact_dir(), - Some(&ArtifactDir::release(directory.path()).unwrap()) - ); - assert_eq!(located.runtime_identity().unwrap(), identity_of(&digests)); - - // Artifacts named by path have no install metadata. - let named = Artifacts::new(&located.openvmm, &located.kernel, &located.initrd); - assert_eq!(named.artifact_dir(), None); - let error = named.runtime_identity().unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unavailable); - assert!( - error.message().contains("supply their runtime identity"), - "{error}" - ); - // Nor does an artifact that replaced one of the directory's. - let mut replaced = located.clone(); - replaced.kernel = directory.path().join("vmlinux"); - let error = replaced.runtime_identity().unwrap_err(); - assert_eq!(error.kind(), ErrorKind::Unavailable); - assert_eq!( - error.message(), - format!( - "the guest kernel {} is not the one of {}, whose install metadata identifies the \ - artifacts", - replaced.kernel.display(), - located.artifact_dir().unwrap().root().display() - ) - ); -} diff --git a/nvx-rs/src/host/sandbox/distro.rs b/nvx-rs/src/host/sandbox/distro.rs index 1a2798914..b9df48e30 100644 --- a/nvx-rs/src/host/sandbox/distro.rs +++ b/nvx-rs/src/host/sandbox/distro.rs @@ -20,13 +20,11 @@ //! never reads, maps, or hashes either file. A missing or stale binding fails: no launch //! registers, verifies, or hashes a layer on its own. //! -//! A [`DistroBinding`] comes from explicit verification, or from a staging caller that verified -//! the layer itself and imports the binding with [`DistroBinding::import`], which does not hash -//! the layer again. It ties the [`FileSeal`] of the layer to the seal of its manifest, or to the -//! manifest's absence, to the revision and result of the distro policy, and to the layer's format -//! and the identity that the guest expects of it. Callers record bindings, as -//! `nvx-rs sandbox provision` records one in the sandbox's configuration, so their serde form is -//! part of the callers' state. +//! A [`DistroBinding`] comes from explicit verification. It ties the [`FileSeal`] of the layer to +//! the seal of its manifest, or to the manifest's absence, to the revision and result of the +//! distro policy, and to the layer's format and the identity that the guest expects of it. +//! Callers record bindings, as `nvx-rs sandbox provision` records one in the sandbox's +//! configuration, so their serde form is part of the callers' state. //! //! A seal detects accidental change, not deliberate tampering by the account that can write the //! sealed files, as [`seal`] explains. A caller therefore stages layers and their manifests @@ -42,10 +40,9 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use sha2::{Digest, Sha256}; -use super::geometry::BlockGeometry; use super::layer::SandboxLayer; use super::mount::os_error; -use super::{block_geometry, refused}; +use super::refused; use crate::error::{Error, Result}; use crate::host::artifacts::DISTRO_MANIFEST_SUFFIX; use crate::host::network::policy::MAX_JSON_DEPTH; @@ -80,8 +77,7 @@ impl Access<'static> { seal: &FileSeal::of, }; - /// The access of the staged check and of imports: [`seal::open_guarded`] and - /// [`FileSeal::of`]. + /// The access of the staged check: [`seal::open_guarded`] and [`FileSeal::of`]. pub(crate) const GUARDED: Self = Self { open: &seal::open_guarded, seal: &FileSeal::of, @@ -146,9 +142,8 @@ impl PolicyVerdict { /// The binding of a distro layer: the seals of the layer and of its manifest, or the manifest's /// absence, the policy verdict, and the layer's format and expected identity. /// -/// [`verify`](Self::verify) and [`import`](Self::import) make a binding, and -/// [`check`](Self::check) checks the layer against it before a launch. Its serde form, which -/// callers record, holds every field, each required: +/// [`verify`](Self::verify) makes a binding, and [`check`](Self::check) checks the layer against +/// it before a launch. Its serde form, which callers record, holds every field, each required: /// /// ```json /// {"version": 1, "format": "erofs", "identity": "11111111-1111-1111-1111-111111111111", @@ -184,53 +179,8 @@ pub struct DistroBinding { /// open of the layer shares it. Dropping it closes the file. #[derive(Debug)] pub struct CheckedDistro { - /// The checked layer. - file: File, - /// The path at which the check opened the layer. - path: PathBuf, - /// The layer's format, as the binding names it. - format: DistroFormat, -} - -impl CheckedDistro { - /// Returns the checked layer. - pub fn file(&self) -> &File { - &self.file - } - - /// Returns the path at which the check opened the layer. - pub fn path(&self) -> &Path { - &self.path - } - - /// Returns the block geometry that OpenVMM presents for the layer as the read-only `distro` - /// block device, read through the checked file, as OpenVMM reads it through its own open of - /// the layer: the layer's length, which must be a nonzero multiple of 512 bytes; logical - /// blocks of 512 bytes; and physical blocks of the I/O block size of the layer's filesystem, - /// 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 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. - /// - /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a length or a - /// physical block size that OpenVMM refuses, and with [`ErrorKind::Io`](crate::ErrorKind::Io) - /// if the metadata cannot be read. - pub fn geometry(&self) -> Result { - let description = match self.format { - DistroFormat::Erofs => "distro EROFS layer", - DistroFormat::Gpt => "distro GPT image", - }; - block_geometry(&self.file, &self.path, description) - } - - /// Returns the checked layer, which the caller then holds: an owned descriptor on Linux, and - /// an owned handle on Windows, which it converts into an `OwnedFd` or an `OwnedHandle`. - pub fn into_file(self) -> File { - self.file - } + /// The checked layer, which stays open until the caller drops it. + _file: File, } impl DistroBinding { @@ -256,22 +206,6 @@ impl DistroBinding { verify_with(layer, Access::READ) } - /// Returns the binding of the distro layer `layer`, staged and judged by its caller, which - /// verified the layer itself, without reading the layer or its manifest. - /// - /// The binding holds the seals of the layer and of its manifest, if one sits beside it, and - /// `verdict`. A caller that imports a binding vouches for the layer and its manifest as - /// [`verify`](Self::verify) would; it must therefore be authorized to stage layers, which - /// NVX leaves to it. Fails with - /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a path without a name, with - /// [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) if the layer or its manifest is - /// not a regular file, and with [`ErrorKind::Io`](crate::ErrorKind::Io) if either cannot be - /// opened or sealed, as for a manifest path whose lookup fails for another reason than a - /// missing file, which the import does not take for an absent manifest. - pub fn import(layer: &SandboxLayer, verdict: PolicyVerdict) -> Result { - import_with(layer, verdict, Access::GUARDED) - } - /// Checks the distro layer `layer` against this binding from metadata alone, and returns the /// layer, open, as the check found it. /// @@ -294,32 +228,6 @@ impl DistroBinding { self.check_with(layer, policy_revision, Access::GUARDED) } - /// Returns the layer's format. - pub fn format(&self) -> DistroFormat { - self.format - } - - /// Returns the identity that the guest expects of the layer: the UUID of its EROFS - /// filesystem, or the GUID of its GPT disk. - pub fn identity(&self) -> &str { - &self.identity - } - - /// Returns the seal of the layer. - pub fn image(&self) -> &FileSeal { - &self.image - } - - /// Returns the seal of the layer's manifest, or `None` for a layer without one. - pub fn manifest(&self) -> Option<&FileSeal> { - self.manifest.as_ref() - } - - /// Returns the policy verdict. - pub fn policy(&self) -> PolicyVerdict { - self.policy - } - /// Returns the binding of `layer`, with the seals of its image and manifest and `policy`. fn new( layer: &SandboxLayer, @@ -424,11 +332,7 @@ impl DistroBinding { } }, } - Ok(CheckedDistro { - file, - path: path.to_path_buf(), - format: self.format, - }) + Ok(CheckedDistro { _file: file }) } } @@ -509,19 +413,6 @@ pub(crate) fn verify_with(layer: &SandboxLayer, access: Access<'_>) -> Result, -) -> Result { - let path = layer.path(); - let manifest = manifest_path(path)?; - let image = seal_regular(path, access, &layer_description(layer))?; - let manifest = seal_if_present(&manifest, access, "sandbox distro manifest")?; - Ok(DistroBinding::new(layer, image, manifest, verdict)) -} - /// Returns how messages name the image of `layer`: `distro EROFS layer` or `distro GPT image`. fn layer_description(layer: &SandboxLayer) -> String { match layer.format() { diff --git a/nvx-rs/src/host/sandbox/distro/tests.rs b/nvx-rs/src/host/sandbox/distro/tests.rs index 15b4b88d6..0e53a079c 100644 --- a/nvx-rs/src/host/sandbox/distro/tests.rs +++ b/nvx-rs/src/host/sandbox/distro/tests.rs @@ -1,5 +1,4 @@ -//! Tests of distro bindings: explicit verification, imports, and the staged check, which reads no -//! content. +//! Tests of distro bindings: explicit verification, and the staged check, which reads no content. use std::cell::Cell; use std::fs::{self, OpenOptions}; @@ -9,8 +8,9 @@ use std::time::Duration; use super::*; use crate::ErrorKind; +use crate::host::config::BlockGeometry; use crate::host::paths; -use crate::host::sandbox::LayerRole; +use crate::host::sandbox::{LayerRole, block_geometry}; use crate::host::seal::open_read; use crate::host::seal::tests::open_metadata_only; @@ -158,18 +158,18 @@ fn verification_binds_the_layer_its_manifest_and_the_policy() { for with_manifest in [true, false] { let staged = Staged::new(with_manifest); let binding = DistroBinding::verify(&staged.layer()).unwrap(); - assert_eq!(binding.format(), DistroFormat::Erofs); - assert_eq!(binding.identity(), UUID); + assert_eq!(binding.format, DistroFormat::Erofs); + assert_eq!(binding.identity, UUID); assert_eq!( - binding.policy(), + binding.policy, PolicyVerdict { revision: DISTRO_POLICY_REVISION, result: PolicyResult::Accepted, } ); - assert_eq!(binding.image(), &seal_of(&staged.image())); + assert_eq!(binding.image, seal_of(&staged.image())); let manifest = with_manifest.then(|| seal_of(&staged.manifest())); - assert_eq!(binding.manifest(), manifest.as_ref()); + assert_eq!(binding.manifest, manifest); binding .check(&staged.layer(), DISTRO_POLICY_REVISION) .unwrap(); @@ -186,7 +186,7 @@ fn bindings_round_trip_through_their_serde_form() { assert_eq!(value["identity"], UUID); assert_eq!( value["image"], - serde_json::to_value(binding.image()).unwrap() + serde_json::to_value(&binding.image).unwrap() ); // The absence of a manifest is explicit. assert_eq!(value.get("manifest"), Some(&serde_json::Value::Null)); @@ -216,7 +216,7 @@ fn bindings_round_trip_through_their_serde_form() { assert_eq!( serde_json::from_value::(rejected) .unwrap() - .policy() + .policy .result, PolicyResult::Rejected ); @@ -227,16 +227,16 @@ fn the_staged_check_reads_no_content() { let staged = Staged::new(true); let layer = staged.layer(); let binding = DistroBinding::verify(&layer).unwrap(); - assert!(binding.manifest().is_some()); + assert!(binding.manifest.is_some()); // On a host that lets the check open the layer and its manifest and read their metadata, but // fails every read and mapping of their content, the check passes, and returns the layer. let checked = binding .check_with(&layer, DISTRO_POLICY_REVISION, METADATA_ONLY) .unwrap(); - assert_eq!(&FileSeal::of(checked.file()).unwrap(), binding.image()); + assert_eq!(FileSeal::of(&checked._file).unwrap(), binding.image); let mut byte = [0]; - assert!(checked.into_file().read(&mut byte).is_err()); + assert!((&checked._file).read(&mut byte).is_err()); // Verification, the positive control, reads both files there: the manifest first, which it // fails to read... @@ -301,27 +301,32 @@ fn checked_layers_present_the_geometry_that_openvmm_reads_through_them() { .unwrap(); }; let check = |layer: &SandboxLayer| { - import_with(layer, PolicyVerdict::ACCEPTED, METADATA_ONLY) + verify_with(layer, METADATA_ONLY) .unwrap() .check_with(layer, DISTRO_POLICY_REVISION, METADATA_ONLY) .unwrap() }; + // The geometry that OpenVMM presents for the layer, read through the checked file. + let geometry = |layer: &SandboxLayer, checked: &CheckedDistro| { + block_geometry(&checked._file, layer.path(), &layer_description(layer)) + }; resize(8192); - let checked = check(&staged.layer()); - let geometry = BlockGeometry { + let layer = staged.layer(); + let checked = check(&layer); + let expected = BlockGeometry { length: 8192, logical_block_size: 512, physical_block_size: physical_block_size(&image), }; // It reads the metadata of the checked file alone, which reads nothing. - assert_eq!(checked.geometry().unwrap(), geometry); + assert_eq!(geometry(&layer, &checked).unwrap(), expected); // The geometry is the checked file's, whatever later takes its path. #[cfg(target_os = "linux")] { let replacement = staged.other("replacement.erofs"); fs::write(&replacement, [0; 4096]).unwrap(); fs::rename(&replacement, &image).unwrap(); - assert_eq!(checked.geometry().unwrap(), geometry); + assert_eq!(geometry(&layer, &checked).unwrap(), expected); } drop(checked); // OpenVMM attaches block devices of nonzero multiples of 512 bytes alone. @@ -333,7 +338,7 @@ fn checked_layers_present_the_geometry_that_openvmm_reads_through_them() { for length in [0, 8191] { resize(length); assert_eq!( - failure(check(&layer).geometry()), + failure(geometry(&layer, &check(&layer))), ( ErrorKind::Unsupported, format!( @@ -379,12 +384,12 @@ fn layers_present_the_block_size_of_their_filesystem() { open: &open, seal: &FileSeal::of, }; - let checked = import_with(&staged.layer(), PolicyVerdict::ACCEPTED, access) + let checked = verify_with(&staged.layer(), access) .unwrap() .check_with(&staged.layer(), DISTRO_POLICY_REVISION, access) .unwrap(); assert_eq!( - checked.geometry().unwrap(), + block_geometry(&checked._file, &image, "distro EROFS layer").unwrap(), BlockGeometry { length: HUGE_PAGE, logical_block_size: 512, @@ -552,15 +557,13 @@ fn the_staged_check_fails_without_reading_for_another_policy_or_layer() { ); // A verdict that rejects the layer passes no check. - let rejected = import_with( - &layer, - PolicyVerdict { + let rejected = DistroBinding { + policy: PolicyVerdict { revision: DISTRO_POLICY_REVISION, result: PolicyResult::Rejected, }, - METADATA_ONLY, - ) - .unwrap(); + ..binding.clone() + }; assert_eq!( failure(rejected.check_with(&layer, DISTRO_POLICY_REVISION, METADATA_ONLY)), ( @@ -666,8 +669,8 @@ fn the_checked_file_is_the_object_that_the_check_compared() { // The caller owns the file, which is still the object that the check compared, with its // content, whatever has its path now, and which processes that the caller launches do not // inherit. On Linux, the replacement unlinked the object, which may move its change time. - let mut file = checked.into_file(); - let difference = binding.image().difference(&FileSeal::of(&file).unwrap()); + let mut file = checked._file; + let difference = binding.image.difference(&FileSeal::of(&file).unwrap()); assert!( difference.is_none() || (cfg!(target_os = "linux") && difference == Some("change time")), "{difference:?}" @@ -689,66 +692,18 @@ fn the_checked_file_is_the_object_that_the_check_compared() { ); } -#[test] -fn imports_bind_a_staged_layer_without_reading_it() { - let staged = Staged::new(true); - let layer = staged.layer(); - // The staging caller vouches for the layer and its manifest, which the import does not read: - // even a manifest that verification refuses. - fs::write(staged.manifest(), b"not JSON").unwrap(); - let binding = import_with(&layer, PolicyVerdict::ACCEPTED, METADATA_ONLY).unwrap(); - assert_eq!(binding.image(), &seal_of(&staged.image())); - assert_eq!(binding.manifest(), Some(&seal_of(&staged.manifest()))); - assert_eq!(binding.policy(), PolicyVerdict::ACCEPTED); - assert_eq!(binding.identity(), UUID); - binding.check(&layer, DISTRO_POLICY_REVISION).unwrap(); - - // A verdict of the caller's own policy passes the checks of that policy's revision alone. - let own = PolicyVerdict { - revision: 7, - result: PolicyResult::Accepted, - }; - let binding = DistroBinding::import(&layer, own).unwrap(); - binding.check(&layer, 7).unwrap(); - assert_eq!( - failure(binding.check(&layer, DISTRO_POLICY_REVISION)).0, - ErrorKind::Unavailable - ); - - // Without a manifest, the import binds its absence. - fs::remove_file(staged.manifest()).unwrap(); - let binding = DistroBinding::import(&layer, PolicyVerdict::ACCEPTED).unwrap(); - assert_eq!(binding.manifest(), None); - binding.check(&layer, DISTRO_POLICY_REVISION).unwrap(); - - // It binds regular files alone. - fs::remove_file(staged.image()).unwrap(); - fs::create_dir(staged.image()).unwrap(); - assert_eq!( - failure(DistroBinding::import(&layer, PolicyVerdict::ACCEPTED)), - ( - ErrorKind::Unavailable, - format!("distro EROFS layer not found: {}", shown(&staged.image())) - ) - ); -} - #[test] fn gpt_disks_bind_by_their_disk_guid() { let staged = Staged::new(false); let image = shown(&staged.image()); let gpt = SandboxLayer::gpt(LayerRole::Distro, staged.image().to_str().unwrap(), UUID); - // Verification of a disk without a manifest reads nothing, and binds what an import binds. + // Verification of a disk without a manifest reads nothing. let binding = verify_with(&gpt, METADATA_ONLY).unwrap(); assert_eq!( - (binding.format(), binding.identity()), + (binding.format, binding.identity.as_str()), (DistroFormat::Gpt, UUID) ); - assert_eq!( - import_with(&gpt, PolicyVerdict::ACCEPTED, METADATA_ONLY).unwrap(), - binding - ); let value = serde_json::to_value(&binding).unwrap(); assert_eq!(value["format"], "gpt"); assert_eq!( @@ -760,7 +715,7 @@ fn gpt_disks_bind_by_their_disk_guid() { let checked = binding .check_with(&gpt, DISTRO_POLICY_REVISION, METADATA_ONLY) .unwrap(); - assert_eq!(&FileSeal::of(checked.file()).unwrap(), binding.image()); + assert_eq!(FileSeal::of(&checked._file).unwrap(), binding.image); // A binding of one format checks no image of the other, even at its path and identity. let erofs = staged.layer(); @@ -773,7 +728,7 @@ fn gpt_disks_bind_by_their_disk_guid() { format!( "the distro binding of {image} names the {} layer with identity {UUID}, not \ the {} layer with identity {UUID}", - bound.format(), + bound.format, launched.format() ) ) @@ -783,7 +738,7 @@ fn gpt_disks_bind_by_their_disk_guid() { // Messages name a missing disk a GPT image. fs::remove_file(staged.image()).unwrap(); assert_eq!( - failure(DistroBinding::import(&gpt, PolicyVerdict::ACCEPTED)), + failure(DistroBinding::verify(&gpt)), ( ErrorKind::Unavailable, format!("distro GPT image not found: {image}") @@ -806,26 +761,23 @@ fn link_to_itself(path: &Path) -> bool { fn a_manifest_path_whose_lookup_fails_is_no_absent_manifest_for_a_binding() { // Verification finds a manifest as `Path.exists` does, which counts a path whose lookup fails // as none. The binding of the manifest's absence holds only while nothing is at its path, so - // the staged check refuses such a path, without reading anything, and so does an import. + // the staged check refuses such a path, without reading anything. let staged = Staged::new(false); let layer = staged.layer(); let binding = verify_with(&layer, METADATA_ONLY).unwrap(); - assert_eq!(binding.manifest(), None); + assert_eq!(binding.manifest, None); if !link_to_itself(&staged.manifest()) { eprintln!("skipped: this host does not let the test create a symbolic link"); return; } assert_eq!(verify_with(&layer, METADATA_ONLY).unwrap(), binding); - for (kind, message) in [ - failure(binding.check_with(&layer, DISTRO_POLICY_REVISION, METADATA_ONLY)), - failure(import_with(&layer, PolicyVerdict::ACCEPTED, METADATA_ONLY)), - ] { - assert_eq!(kind, ErrorKind::Io, "{message}"); - assert!( - message.starts_with(&format!("{:?}: ", shown(&staged.manifest()))), - "{message}" - ); - } + let (kind, message) = + failure(binding.check_with(&layer, DISTRO_POLICY_REVISION, METADATA_ONLY)); + assert_eq!(kind, ErrorKind::Io, "{message}"); + assert!( + message.starts_with(&format!("{:?}: ", shown(&staged.manifest()))), + "{message}" + ); // Once nothing is at the path, the binding holds again. fs::remove_file(staged.manifest()).unwrap(); binding diff --git a/nvx-rs/src/host/sandbox/mod.rs b/nvx-rs/src/host/sandbox/mod.rs index 28055573c..33e6b602a 100644 --- a/nvx-rs/src/host/sandbox/mod.rs +++ b/nvx-rs/src/host/sandbox/mod.rs @@ -17,10 +17,9 @@ //! layer and its manifest, and which a launch checks from metadata alone. //! - [`scratch`]: the binding of the scratch image, its identity, kind, and length, which a //! launch checks from metadata alone, and the claim of the image by one sandbox at a time. -//! - The block geometry that OpenVMM presents for each checked file, its length and its logical -//! and physical block sizes, which [`CheckedDistro::geometry`](distro::CheckedDistro::geometry) -//! and [`CheckedScratch::geometry`](scratch::CheckedScratch::geometry) read through the file, -//! as OpenVMM reads it. +//! - The block geometry that OpenVMM presents for a block device, its length and its logical and +//! physical block sizes, which the crate reads through an open file of the device's image, as +//! OpenVMM reads it. //! //! Workload IDs are ASCII decimal integers in their documented ranges. Layer UUIDs use their //! canonical lowercase hyphenated text. Host paths use the host's native flavor. @@ -131,8 +130,11 @@ const WINDOWS_PHYSICAL_BLOCK_SIZE: u32 = 4096; /// Returns the block geometry that OpenVMM presents for a sandbox block device whose file `file` /// holds open, which messages call `description` at `path`, read through `file` as OpenVMM reads -/// it through its own open of the file, as -/// [`CheckedDistro::geometry`](distro::CheckedDistro::geometry) describes. +/// it through its own open of the file: the file's length, which must be a nonzero multiple of +/// 512 bytes; logical blocks of 512 bytes; and physical blocks of the I/O block size of the file's +/// filesystem, 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. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a length or a physical /// block size that OpenVMM refuses, and with [`ErrorKind::Io`](crate::ErrorKind::Io) if the file's diff --git a/nvx-rs/src/host/sandbox/options/tests.rs b/nvx-rs/src/host/sandbox/options/tests.rs index e54f01680..829ea781a 100644 --- a/nvx-rs/src/host/sandbox/options/tests.rs +++ b/nvx-rs/src/host/sandbox/options/tests.rs @@ -204,9 +204,10 @@ fn distro_manifest_binds_the_layer() { }); layer.manifest(&valid.to_string()); let binding = layer.preflight("/bin/sh").unwrap(); - assert_eq!(binding.identity(), "11111111-1111-1111-1111-111111111111"); - assert_eq!(binding.format(), DistroFormat::Erofs); - assert!(binding.manifest().is_some()); + let recorded = serde_json::to_value(&binding).unwrap(); + assert_eq!(recorded["identity"], "11111111-1111-1111-1111-111111111111"); + assert_eq!(recorded["format"], DistroFormat::Erofs.as_str()); + assert!(recorded["manifest"].is_object()); binding .check(&layer.layer(), DISTRO_POLICY_REVISION) .unwrap(); @@ -378,7 +379,7 @@ fn the_entrypoint_check_needs_no_manifest_and_precedes_the_distro_verification() // A layer without a manifest passes the distro verification unread, and its binding records // the manifest's absence, but the entrypoint check still refuses systemd. let binding = layer.preflight("/sbin/init").unwrap(); - assert_eq!(binding.manifest(), None); + assert!(serde_json::to_value(&binding).unwrap()["manifest"].is_null()); assert_eq!( message(layer.preflight("/usr/lib/systemd/systemd")), "systemd entrypoints are unsupported by the sandbox security profile" @@ -417,7 +418,10 @@ fn sandbox_rejects_systemd_from_bound_distro_metadata() { &fs::File::open(layer.path("distro.erofs.manifest.json")).unwrap(), ) .unwrap(); - assert_eq!(binding.manifest(), Some(&sealed)); + assert_eq!( + serde_json::to_value(&binding).unwrap()["manifest"], + serde_json::to_value(&sealed).unwrap() + ); for (format, artifact, sha256) in [ ("\"1\"", "\"distro.erofs\"", sha256.as_str()), ("1.0", "\"distro.erofs\"", &sha256), diff --git a/nvx-rs/src/host/sandbox/scratch.rs b/nvx-rs/src/host/sandbox/scratch.rs index 0c182328b..e1099369a 100644 --- a/nvx-rs/src/host/sandbox/scratch.rs +++ b/nvx-rs/src/host/sandbox/scratch.rs @@ -54,9 +54,8 @@ use std::time::{Duration, Instant}; use serde::{Deserialize, Serialize}; -use super::geometry::BlockGeometry; use super::mount::os_error; -use super::{block_geometry, refused}; +use super::refused; use crate::error::{Error, ErrorCode, Result}; use crate::host::paths; use crate::host::seal::{FileId, FileKind, FileSeal, SealScheme}; @@ -145,43 +144,19 @@ pub struct CheckedScratch { ) )] claim: File, - /// The path at which the check opened the image. - path: PathBuf, } impl CheckedScratch { - /// Returns the checked image: on Linux, the image open for reading and writing, and on Windows - /// its guard. + /// Returns the checked image, open for reading and writing, whose open file description holds + /// the claim on Linux, which a VMM that inherits a copy of it keeps. + #[cfg(not(windows))] pub fn file(&self) -> &File { &self.file } - /// Returns the path at which the check opened the image. - pub fn path(&self) -> &Path { - &self.path - } - - /// Returns the block geometry that OpenVMM presents for the image as the `scratch` block - /// device, read through the checked file, as - /// [`CheckedDistro::geometry`](super::distro::CheckedDistro::geometry) reads that of a distro - /// layer: the image's length, logical blocks of 512 bytes, and physical blocks of the I/O - /// block size of the image's filesystem on Linux, of 4096 bytes on Windows, and of the logical - /// block size on other hosts. On Windows, the checked file is the image's guard, which may - /// read the image's metadata. - /// - /// It reads the file's metadata alone, never the image's content. The scratch image of a - /// clone must present the geometry of its template's scratch, its length included. - /// - /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a length or a - /// physical block size that OpenVMM refuses, and with [`ErrorKind::Io`](crate::ErrorKind::Io) - /// if the metadata cannot be read. - pub fn geometry(&self) -> Result { - block_geometry(&self.file, &self.path, DESCRIPTION) - } - - /// Returns the checked image, which the caller then holds with its claim: an owned descriptor - /// on Linux, and on Windows an owned handle, the image's guard, which then holds the claim + /// Returns the image's guard, an owned handle, which the caller then holds with the claim /// alone, since this closes the claim's handle. + #[cfg(windows)] pub fn into_file(self) -> File { self.file } @@ -228,31 +203,6 @@ impl ScratchBinding { self.check_with(&normalized(path)?, deadline, Access::WRITABLE) } - /// Returns the host that took the binding. - pub fn scheme(&self) -> SealScheme { - self.scheme - } - - /// Returns the device, or on Windows the volume serial number. - pub fn device(&self) -> u64 { - self.device - } - - /// Returns the inode, or on Windows the file ID. - pub fn file_id(&self) -> u128 { - self.file_id.0 - } - - /// Returns the kind of file. - pub fn kind(&self) -> FileKind { - self.kind - } - - /// Returns the length in bytes. - pub fn length(&self) -> u64 { - self.length - } - /// Returns the binding of the image that `seal` seals. fn of(seal: &FileSeal) -> Self { Self { @@ -303,7 +253,6 @@ impl ScratchBinding { file, #[cfg(windows)] claim, - path: path.to_path_buf(), }) } diff --git a/nvx-rs/src/host/sandbox/scratch/tests.rs b/nvx-rs/src/host/sandbox/scratch/tests.rs index 611110a12..83f18f513 100644 --- a/nvx-rs/src/host/sandbox/scratch/tests.rs +++ b/nvx-rs/src/host/sandbox/scratch/tests.rs @@ -8,6 +8,8 @@ use std::time::SystemTime; use super::*; use crate::ErrorKind; +use crate::host::config::BlockGeometry; +use crate::host::sandbox::block_geometry; /// The content of the staged scratch images. const CONTENT: &[u8] = b"scratch image\n"; @@ -151,13 +153,13 @@ fn binds_record_the_image_without_reading_it() { let staged = Staged::new(); let binding = staged.bind(); let seal = FileSeal::of(&File::open(staged.image()).unwrap()).unwrap(); - assert_eq!(binding.scheme(), seal.scheme()); + assert_eq!(binding.scheme, seal.scheme()); assert_eq!( - (binding.device(), binding.file_id()), + (binding.device, binding.file_id.0), (seal.device(), seal.file_id()) ); - assert_eq!(binding.kind(), FileKind::Regular); - assert_eq!(binding.length(), CONTENT.len() as u64); + assert_eq!(binding.kind, FileKind::Regular); + assert_eq!(binding.length, CONTENT.len() as u64); assert_eq!(ScratchBinding::bind(&staged.image()).unwrap(), binding); // The positive control: what the access of the test opens refuses every read. let mut byte = [0]; @@ -181,7 +183,7 @@ fn bindings_round_trip_through_their_serde_form() { value["scheme"], if cfg!(windows) { "windows" } else { "linux" } ); - assert_eq!(value["fileId"], format!("{:032x}", binding.file_id())); + assert_eq!(value["fileId"], format!("{:032x}", binding.file_id.0)); assert_eq!(value["kind"], "regular"); assert_eq!(value["length"], CONTENT.len()); assert_eq!( @@ -246,17 +248,16 @@ fn checks_claim_the_image_without_reading_it() { let staged = Staged::new(); let binding = staged.bind(); let checked = staged.check(&binding, Instant::now()).unwrap(); - assert_eq!(checked.path().to_str().unwrap(), shown(&staged.image())); - let seal = FileSeal::of(checked.file()).unwrap(); + let seal = FileSeal::of(&checked.file).unwrap(); assert_eq!( (seal.device(), seal.file_id()), - (binding.device(), binding.file_id()) + (binding.device, binding.file_id.0) ); // What the check holds reads nothing, and processes that this one launches do not inherit // it. let mut byte = [0]; - assert!(checked.file().read(&mut byte).is_err()); - assert!(!inheritable(checked.file())); + assert!((&checked.file).read(&mut byte).is_err()); + assert!(!inheritable(&checked.file)); #[cfg(windows)] { assert!((&checked.claim).read(&mut byte).is_err()); @@ -324,12 +325,15 @@ fn checked_images_present_the_geometry_that_openvmm_reads_through_them() { .set_len(length) .unwrap(); }; + // The geometry that OpenVMM presents for the image, read through what the check holds. + let geometry = + |checked: &CheckedScratch| block_geometry(&checked.file, &staged.normalized(), DESCRIPTION); resize(1 << 20); let binding = staged.bind(); let checked = staged.claim(&binding); // It reads the metadata of what the check holds, which reads nothing, while the claim lasts. assert_eq!( - checked.geometry().unwrap(), + geometry(&checked).unwrap(), BlockGeometry { length: 1 << 20, logical_block_size: 512, @@ -346,7 +350,7 @@ fn checked_images_present_the_geometry_that_openvmm_reads_through_them() { resize(length); let binding = staged.bind(); assert_eq!( - failure(staged.claim(&binding).geometry()), + failure(geometry(&staged.claim(&binding))), ( ErrorKind::Unsupported, format!( @@ -417,7 +421,7 @@ fn a_check_that_waited_for_a_claim_compares_the_image_again() { let staged = Staged::new(); let binding = staged.bind(); #[cfg(target_os = "linux")] - let holder = staged.check(&binding, Instant::now()).unwrap().into_file(); + let holder = staged.check(&binding, Instant::now()).unwrap().file; #[cfg(windows)] let holder = OpenOptions::new() .read(true) @@ -472,12 +476,15 @@ fn the_checked_image_is_the_object_that_the_check_compared() { // What the caller holds is still the object that the check compared, whatever has its path // now. - let seal = FileSeal::of(checked.file()).unwrap(); + let seal = FileSeal::of(&checked.file).unwrap(); assert_eq!( (seal.device(), seal.file_id(), seal.kind()), - (binding.device(), binding.file_id(), FileKind::Regular) + (binding.device, binding.file_id.0, FileKind::Regular) ); + #[cfg(windows)] let file = checked.into_file(); + #[cfg(not(windows))] + let file = checked.file; assert!(!inheritable(&file)); drop(file); diff --git a/nvx-rs/src/host/seal.rs b/nvx-rs/src/host/seal.rs index 3bc05e470..872899994 100644 --- a/nvx-rs/src/host/seal.rs +++ b/nvx-rs/src/host/seal.rs @@ -150,16 +150,6 @@ impl FileSeal { self.length } - /// Returns the modification time. - pub fn modified(&self) -> SealTime { - self.modified - } - - /// Returns the change time on Linux, and `None` on Windows. - pub fn changed(&self) -> Option { - self.changed - } - /// Returns what differs first between this seal and `other`, in the order version, scheme, /// identity, kind, length, modification time, and change time, or `None` if they are equal. pub(crate) fn difference(&self, other: &Self) -> Option<&'static str> { diff --git a/nvx-rs/src/host/seal/tests.rs b/nvx-rs/src/host/seal/tests.rs index f5a75ba2f..c882c541f 100644 --- a/nvx-rs/src/host/seal/tests.rs +++ b/nvx-rs/src/host/seal/tests.rs @@ -88,19 +88,19 @@ fn a_seal_holds_the_identity_kind_length_and_times_of_an_open_file() { assert_eq!(seal_of(&link), seal_of(&other)); assert_ne!(seal_of(&other).file_id(), seal.file_id()); let metadata = fs::metadata(&path).unwrap(); - assert_eq!(seal.modified(), seal_time(metadata.modified().unwrap())); + assert_eq!(seal.modified, seal_time(metadata.modified().unwrap())); #[cfg(target_os = "linux")] { use std::os::unix::fs::MetadataExt; - let changed = seal.changed().unwrap(); + let changed = seal.changed.unwrap(); assert_eq!( (changed.seconds, i64::from(changed.nanoseconds)), (metadata.ctime(), metadata.ctime_nsec()) ); } #[cfg(windows)] - assert_eq!(seal.changed(), None); + assert_eq!(seal.changed, None); // Every open of the unchanged file seals it alike, whatever its access. assert_eq!(FileSeal::of(&open_guarded(&path).unwrap()).unwrap(), seal); assert_eq!(FileSeal::of(&open_read(&path).unwrap()).unwrap(), seal); @@ -164,7 +164,7 @@ fn seals_differ_when_their_file_changes() { let touched = seal_of(&path); assert_eq!(appended.difference(&touched), Some("modification time")); assert_eq!( - touched.modified(), + touched.modified, seal_time(modified + Duration::from_secs(1)) ); assert_eq!(touched.difference(&touched.clone()), None); diff --git a/nvx-rs/src/lib.rs b/nvx-rs/src/lib.rs index dd1366e10..80e3f671b 100644 --- a/nvx-rs/src/lib.rs +++ b/nvx-rs/src/lib.rs @@ -27,9 +27,9 @@ //! NVX writes and reads, and in the layout in which GPT stores them. It has no dependencies, so //! the host and the guest share it. //! - [`Hypervisor`]: the hypervisors that run the VMs. -//! - [`host`] (feature `host`): artifact discovery, the OpenVMM command line, the lowering of -//! network and filesystem policies, the host's NVXS connection, process and platform -//! primitives, VMs that a caller creates, starts, reattaches to, and stops in private +//! - [`host`] (feature `host`): the layouts of NVX's artifacts, the OpenVMM command line, the +//! lowering of network and filesystem policies, the host's NVXS connection, process and +//! platform primitives, VMs that a caller creates, starts, reattaches to, and stops in private //! directories that it owns, and the manifests and compatibility keys of templates. //! - `guest` (feature `guest`, Linux): the programs that run in NVX guests, such as //! `nvx-console-pending`, the guest's NVXS connection, the libraries through which any guest @@ -60,20 +60,27 @@ //! use std::path::Path; //! use std::time::{Duration, Instant}; //! +//! use nvx::Hypervisor; //! use nvx::activation::{MachineIdMode, View}; //! use nvx::host::config::{Boot, ConsoleAttachment, RootConfig, VmConfig}; -//! use nvx::host::runtime::{LaunchOptions, Runtime}; +//! use nvx::host::runtime::{ArtifactIdentity, LaunchOptions, Runtime, RuntimeIdentity}; //! use nvx::host::vm::{self, VmDir}; -//! use nvx::{Artifacts, Hypervisor}; //! -//! // Locates OpenVMM, the guest kernel, and the Alpine initramfs, with the identity that the -//! // install metadata of their release or checkout gives them. -//! let artifacts = Artifacts::discover()?; +//! // The caller locates OpenVMM, the guest kernel, and the Alpine initramfs, and identifies each +//! // by the SHA-256 digest that its install metadata records, such as the `SHA256SUMS` of an NVX +//! // release: here, from the command line. +//! let arguments: Vec = std::env::args().skip(1).collect(); +//! let [openvmm, kernel, initramfs, openvmm_sha256, kernel_sha256, initramfs_sha256] = +//! <[String; 6]>::try_from(arguments).map_err(|_| "expected three paths and three digests")?; //! let runtime = Runtime { -//! identity: artifacts.runtime_identity()?, -//! openvmm: artifacts.openvmm, -//! kernel: artifacts.kernel, -//! initramfs: artifacts.initrd, +//! identity: RuntimeIdentity::new( +//! ArtifactIdentity::sha256(&openvmm_sha256)?, +//! ArtifactIdentity::sha256(&kernel_sha256)?, +//! ArtifactIdentity::sha256(&initramfs_sha256)?, +//! ), +//! openvmm: openvmm.into(), +//! kernel: kernel.into(), +//! initramfs: initramfs.into(), //! hypervisor: Hypervisor::from_env_or_default()?, //! cpu_profile: None, //! windows_breakaway: false, @@ -154,7 +161,5 @@ mod tests; pub use error::{Error, ErrorCode, ErrorKind, Result}; #[cfg(feature = "host")] -pub use host::artifacts::Artifacts; -#[cfg(feature = "host")] pub use host::filesystem::{guest_path, resolve_guest_path}; pub use hypervisor::Hypervisor; diff --git a/nvx-rs/tests/security_checklist.rs b/nvx-rs/tests/security_checklist.rs index 94ad4a916..f486d918a 100644 --- a/nvx-rs/tests/security_checklist.rs +++ b/nvx-rs/tests/security_checklist.rs @@ -22,7 +22,7 @@ use nvx::host::filesystem::Owner; use nvx::host::filesystem::{self, ExportRequest, Mapping, ReservedTargets, Target}; use nvx::host::process::{BootId, ProcessRef}; use nvx::host::runtime::{ArtifactIdentity, LaunchOptions, Runtime, RuntimeIdentity}; -use nvx::host::sandbox::distro::{DISTRO_POLICY_REVISION, DistroBinding, PolicyVerdict}; +use nvx::host::sandbox::distro::{DISTRO_POLICY_REVISION, DistroBinding}; use nvx::host::sandbox::layer::{LayerRole, SandboxLayer}; use nvx::host::sandbox::scratch::{CLAIM_RELEASE_WAIT, ScratchBinding}; use nvx::host::seal::FileSeal; @@ -155,7 +155,7 @@ fn a_staged_image_that_was_replaced_fails_its_check() { path.to_str().unwrap(), "01234567-89ab-cdef-0123-456789abcdef", ); - let binding = DistroBinding::import(&layer, PolicyVerdict::ACCEPTED).unwrap(); + let binding = DistroBinding::verify(&layer).unwrap(); drop(binding.check(&layer, DISTRO_POLICY_REVISION).unwrap()); fs::rename(&path, images.path().join("original.erofs")).unwrap(); fs::write(&path, b"layer").unwrap();