diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 2eba81a5e..4f6a51f6b 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -115,13 +115,11 @@ changes only when the module's purpose does. | [`nvx::host::config`](#nvxhostconfig) | `VmConfig` and `CaptureConfig`: pure configuration, cold-boot kernel/OpenVMM arguments, portable length checks, and capture identity | | [`nvx::host::restore`](#nvxhostrestore) | A caller-driven restore to Ready without I/O or a clock, and its peer-verified restore-ready listener | | [`nvx::host::network`](#nvxhostnetwork) | The one lowering of a network posture into OpenVMM options, for the CLI and external orchestrators | -| [`nvx::host::network::policy`](#nvxhostnetworkpolicy) | Egress policy files and their compilation into OpenVMM's native rules | | [`nvx::host::filesystem`](#nvxhostfilesystem) | The planner of the host paths that every NVX caller exports to the guest through OpenVMM's virtio-fs device | | [`nvx::host::nvxs`](#nvxhostnvxs) | Compatibility paths for the host NVXS connection and its retained cause details | | [`nvx::host::transport`](#nvxhosttransport) | A verified VM endpoint connection and the blocking host NVXS driver, with the existing timed transport | | [`nvx::host::process`](#nvxhostprocess) | Verified references to OpenVMM processes, through which callers wait for them and end them | | [`nvx::host::report`](#nvxhostreport) | OpenVMM's outcome report | -| [`nvx::host::sandbox`](#nvxhostsandbox) | The launch profile of `nvx-rs sandbox`: its live shares and option checks; its layer and bindings are private to the CLI | | [`nvx::guest`](#nvxguest) | The programs that run in Linux guests; see [Guest binaries](#guest-binaries) | | [`nvx::guest::mappings`](#nvxguestmappings) | `Mapping` and `MappingTable`: host mappings, and the account of a host's mapping table, for any guest init or agent | | [`nvx::guest::nvxs`](#nvxguestnvxs) | `GuestConnection`: the guest's role of the NVXS session over a byte stream, such as the control console | @@ -936,12 +934,6 @@ lowering of egress policy files, with at most 256 rules for each action. the host, and fails with an `Unsupported` error for a malformed rule or for an action that needs more than 256 rules. -### `nvx::host::network::policy` - -Egress policy files: `compile_policy_file`, `compile_policy_bytes`, and -`compile_policy` compile one to a `CompiledEgressPolicy` of native rules; see -[Egress policy files](#egress-policy-files). - ### `nvx::host::filesystem` The planner of every NVX caller: `plan` turns an `ExportRequest`, the mapped @@ -1063,31 +1055,6 @@ unreadable or malformed report, retaining the underlying error. It keeps today's summary of the outcome category, status code and teardown steps, not free-form fields. -### `nvx::host::sandbox` - -The launch profile of `nvx-rs sandbox`, with native input validation: the -workload identity that `--workload-user` names, the live shares, and the checks -of the options. The image staging of the profile is private to the crate, for -the CLI (feature `cli`): `layer`'s `SandboxLayer`, the distro layer of a -sandbox, an EROFS layer or a GPT disk, the one layer that a sandbox takes; -`distro`'s `DistroBinding` of the layer, which explicit verification makes, and -its check from metadata alone; `scratch`'s `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 the seals of `nvx::host`'s private -`seal`, which they record. See [Distro bindings](#distro-bindings) and -[Images by path](#images-by-path). - -`mount`: `SandboxMount` requests of live shares of host directories and files, -their guest targets and the reserved ones, their access policies and owner, and -the checks of their host paths; and `plan_mounts`, which plans them through -`nvx::host::filesystem` for the container root. - -`options`: the checks that `nvx-rs sandbox` and `nvx-rs run` apply to their -options: the attribution of `--mount-deny`, `--mount-allow`, and `--mount-write` -to shares, the systemd entrypoint check and the explicit verification of the -distro layer, the targets of `nvx-rs run` shares, and the managed exec -environment; see [Sandbox input contract](#sandbox-input-contract). - ### `nvx::guest` The programs that run in Linux guests; see [Guest binaries](#guest-binaries). @@ -1342,9 +1309,11 @@ cloned, and formats none of them. `nvx-reseed` runs them. ## Egress policy files -`nvx::host::network::policy` compiles the egress policy files that -`nvx-rs run` and `nvx-rs sandbox` take with `--network-egress-policy-file` -([format](../doc/run.md#run-openvmm-directly)). It replaced the former +The crate's private `host::network::policy`, which only the CLI builds (feature +`cli`), compiles the egress policy files that `nvx-rs run` and `nvx-rs sandbox` +take with `--network-egress-policy-file` +([format](../doc/run.md#run-openvmm-directly)), and whose rules +[`egress compile`](#egress-compile-path) prints. It replaced the former `scripts/nvx_tools/egress_policy.py` when `nvx.py` delegated to `nvx-rs`, and compiles the policies that it compiled to the same rules, but reads policy files as strict JSON @@ -1537,6 +1506,22 @@ does, and holds the claim until its VM exits. A sandbox whose upper is in RAM, and its binding, and neither `start` nor `run` binds or claims one. ## Sandbox input contract +The crate's private `host::sandbox` is the launch profile of `nvx-rs sandbox`, +which only the CLI uses (feature `cli`), but for the block geometry that +`nvx::host::vm` and `nvx::host::snapshot` read and the tag of the shares' +virtio-fs device, which a snapshot's key names: the workload identity that +`--workload-user` names; the live shares of `--mount`, their guest targets and +the reserved ones, their access policies and owner, and the checks of their +host paths, which it plans through `nvx::host::filesystem` for the container +root; and the checks that `nvx-rs sandbox` and `nvx-rs run` apply to their +options: the attribution of `--mount-deny`, `--mount-allow`, and `--mount-write` +to shares, the systemd entrypoint check and the explicit verification of the +distro layer, the targets of `nvx-rs run` shares, and the managed exec +environment. Its image staging, the `layer`, the `distro` and `scratch` +bindings, and the seals of the private `host::seal` that they record, is +described in [Distro bindings](#distro-bindings) and +[Images by path](#images-by-path). + The sandbox launch profile specifies its own inputs rather than emulating Python. A UID or GID contains only ASCII decimal digits, without signs, whitespace, underscores, or Unicode digits, and is in `1..4294967294`. A layer UUID is @@ -2847,8 +2832,8 @@ own facilities replaced it: through their links, on Linux and Windows, and those of the scratch image and of the export planner that paths that are not valid Unicode are refused. -The sandbox specification tests, those of `nvx::host::sandbox`, specify the -documented option grammar. Properties cover canonical layer UUIDs, reserved +The sandbox specification tests, those of the private `host::sandbox`, specify +the documented option grammar. Properties cover canonical layer UUIDs, reserved targets, policy option sequences, and paths on the host's own filesystem. They check that a launch refuses both systemd entrypoints whatever the distro layer's manifest lists, and that verification refuses a manifest that lists `systemd` diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 2331d4fb9..78cada8a3 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -1079,16 +1079,6 @@ variant nvx::host::network::ProxyScheme::Https method nvx::host::network::ProxyScheme::as_str method nvx::host::network::ProxyScheme::from_name fn nvx::host::network::openvmm_arguments -mod nvx::host::network::policy -struct nvx::host::network::policy::CompiledEgressPolicy -field nvx::host::network::policy::CompiledEgressPolicy::allow -field nvx::host::network::policy::CompiledEgressPolicy::deny -const nvx::host::network::policy::MAX_JSON_DEPTH -const nvx::host::network::policy::MAX_POLICY_FILE_SIZE -const nvx::host::network::policy::MAX_RULES_PER_ACTION -fn nvx::host::network::policy::compile_policy -fn nvx::host::network::policy::compile_policy_bytes -fn nvx::host::network::policy::compile_policy_file mod nvx::host::nvxs struct nvx::host::nvxs::HostConnection method nvx::host::nvxs::HostConnection::attach @@ -1267,128 +1257,6 @@ field nvx::host::runtime::RuntimeIdentity::kernel method nvx::host::runtime::RuntimeIdentity::new field nvx::host::runtime::RuntimeIdentity::openvmm fn nvx::host::runtime::probe_hypervisor -mod nvx::host::sandbox -const nvx::host::sandbox::DEFAULT_ENTRYPOINT -const nvx::host::sandbox::DEFAULT_HOSTNAME -const nvx::host::sandbox::DEFAULT_WORKLOAD_IDENTITY -enum nvx::host::sandbox::MountAccess -variant nvx::host::sandbox::MountAccess::ReadOnly -variant nvx::host::sandbox::MountAccess::ReadWrite -method nvx::host::sandbox::MountAccess::as_str -enum nvx::host::sandbox::MountKind -variant nvx::host::sandbox::MountKind::Directory -variant nvx::host::sandbox::MountKind::File -method nvx::host::sandbox::MountKind::as_str -enum nvx::host::sandbox::MountOwner -variant nvx::host::sandbox::MountOwner::Caller -variant nvx::host::sandbox::MountOwner::Vmm -method nvx::host::sandbox::MountOwner::as_str -struct nvx::host::sandbox::MountPolicy -field nvx::host::sandbox::MountPolicy::allowed -field nvx::host::sandbox::MountPolicy::denied -method nvx::host::sandbox::MountPolicy::is_empty -method nvx::host::sandbox::MountPolicy::paths -field nvx::host::sandbox::MountPolicy::writable -enum nvx::host::sandbox::PolicyKind -variant nvx::host::sandbox::PolicyKind::Allowed -variant nvx::host::sandbox::PolicyKind::Denied -variant nvx::host::sandbox::PolicyKind::Writable -method nvx::host::sandbox::PolicyKind::as_str -method nvx::host::sandbox::PolicyKind::option -struct nvx::host::sandbox::SandboxMount -method nvx::host::sandbox::SandboxMount::host_path -method nvx::host::sandbox::SandboxMount::new -method nvx::host::sandbox::SandboxMount::parse -method nvx::host::sandbox::SandboxMount::validated -method nvx::host::sandbox::SandboxMount::with_kind -mod nvx::host::sandbox::mount -const nvx::host::sandbox::mount::AGGREGATE_MOUNT_TARGET -const nvx::host::sandbox::mount::MAX_MOUNT_POLICY_PATHS -const nvx::host::sandbox::mount::MAX_MOUNT_TARGET_BYTES -const nvx::host::sandbox::mount::MOUNT_TAG -enum nvx::host::sandbox::mount::MountAccess -variant nvx::host::sandbox::mount::MountAccess::ReadOnly -variant nvx::host::sandbox::mount::MountAccess::ReadWrite -method nvx::host::sandbox::mount::MountAccess::as_str -enum nvx::host::sandbox::mount::MountKind -variant nvx::host::sandbox::mount::MountKind::Directory -variant nvx::host::sandbox::mount::MountKind::File -method nvx::host::sandbox::mount::MountKind::as_str -enum nvx::host::sandbox::mount::MountOwner -variant nvx::host::sandbox::mount::MountOwner::Caller -variant nvx::host::sandbox::mount::MountOwner::Vmm -method nvx::host::sandbox::mount::MountOwner::as_str -struct nvx::host::sandbox::mount::MountPolicy -field nvx::host::sandbox::mount::MountPolicy::allowed -field nvx::host::sandbox::mount::MountPolicy::denied -method nvx::host::sandbox::mount::MountPolicy::is_empty -method nvx::host::sandbox::mount::MountPolicy::paths -field nvx::host::sandbox::mount::MountPolicy::writable -const nvx::host::sandbox::mount::POLICY_KINDS -enum nvx::host::sandbox::mount::PolicyKind -variant nvx::host::sandbox::mount::PolicyKind::Allowed -variant nvx::host::sandbox::mount::PolicyKind::Denied -variant nvx::host::sandbox::mount::PolicyKind::Writable -method nvx::host::sandbox::mount::PolicyKind::as_str -method nvx::host::sandbox::mount::PolicyKind::option -const nvx::host::sandbox::mount::RESERVED_EXACT_MOUNT_TARGETS -const nvx::host::sandbox::mount::RESERVED_MOUNT_TARGETS -struct nvx::host::sandbox::mount::SandboxMount -method nvx::host::sandbox::mount::SandboxMount::host_path -method nvx::host::sandbox::mount::SandboxMount::new -method nvx::host::sandbox::mount::SandboxMount::parse -method nvx::host::sandbox::mount::SandboxMount::validated -method nvx::host::sandbox::mount::SandboxMount::with_kind -fn nvx::host::sandbox::mount::mount_kind -fn nvx::host::sandbox::mount::parse_mount_value -fn nvx::host::sandbox::mount::plan_mounts -fn nvx::host::sandbox::mount::require_mount_owner_supported -fn nvx::host::sandbox::mount::validate_mount_host_paths -fn nvx::host::sandbox::mount::validate_mount_target -fn nvx::host::sandbox::mount::validate_mounts -mod nvx::host::sandbox::options -const nvx::host::sandbox::options::MAX_ENVIRONMENT_FILE_BYTES -const nvx::host::sandbox::options::MAX_EXEC_ENVIRONMENT -const nvx::host::sandbox::options::MAX_EXEC_ENVIRONMENT_BYTES -struct nvx::host::sandbox::options::PolicyPathOption -field nvx::host::sandbox::options::PolicyPathOption::kind -field nvx::host::sandbox::options::PolicyPathOption::path -field nvx::host::sandbox::options::PolicyPathOption::preceding_mount -const nvx::host::sandbox::options::SYSTEMD_ENTRYPOINTS -enum nvx::host::sandbox::options::SandboxOperation -variant nvx::host::sandbox::options::SandboxOperation::Deprovision -variant nvx::host::sandbox::options::SandboxOperation::Exec -variant nvx::host::sandbox::options::SandboxOperation::Provision -variant nvx::host::sandbox::options::SandboxOperation::Run -variant nvx::host::sandbox::options::SandboxOperation::Start -variant nvx::host::sandbox::options::SandboxOperation::Stop -method nvx::host::sandbox::options::SandboxOperation::as_str -struct nvx::host::sandbox::options::SandboxOptions -method nvx::host::sandbox::options::SandboxOptions::check -field nvx::host::sandbox::options::SandboxOptions::cwd -field nvx::host::sandbox::options::SandboxOptions::dry_run -field nvx::host::sandbox::options::SandboxOptions::entrypoint -field nvx::host::sandbox::options::SandboxOptions::environment -field nvx::host::sandbox::options::SandboxOptions::environment_file -field nvx::host::sandbox::options::SandboxOptions::inherit_default_environment -field nvx::host::sandbox::options::SandboxOptions::layers -field nvx::host::sandbox::options::SandboxOptions::mount_owner -field nvx::host::sandbox::options::SandboxOptions::mounts -field nvx::host::sandbox::options::SandboxOptions::net -field nvx::host::sandbox::options::SandboxOptions::network_options -field nvx::host::sandbox::options::SandboxOptions::network_profile -field nvx::host::sandbox::options::SandboxOptions::operation -field nvx::host::sandbox::options::SandboxOptions::outcome_report -field nvx::host::sandbox::options::SandboxOptions::policy_paths -field nvx::host::sandbox::options::SandboxOptions::scratch -field nvx::host::sandbox::options::SandboxOptions::upper -fn nvx::host::sandbox::options::exec_environment_from_file -fn nvx::host::sandbox::options::sandbox_mounts -fn nvx::host::sandbox::options::validate_exec_environment -fn nvx::host::sandbox::options::validate_run_share_targets -fn nvx::host::sandbox::options::validate_sandbox_entrypoint -fn nvx::host::sandbox::parse_workload_identity -fn nvx::host::sandbox::require_file mod nvx::host::snapshot struct nvx::host::snapshot::BlockGeometry field nvx::host::snapshot::BlockGeometry::length diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index 7f7a9be67..f2aff1fcb 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -537,16 +537,6 @@ variant nvx::host::network::ProxyScheme::Https method nvx::host::network::ProxyScheme::as_str method nvx::host::network::ProxyScheme::from_name fn nvx::host::network::openvmm_arguments -mod nvx::host::network::policy -struct nvx::host::network::policy::CompiledEgressPolicy -field nvx::host::network::policy::CompiledEgressPolicy::allow -field nvx::host::network::policy::CompiledEgressPolicy::deny -const nvx::host::network::policy::MAX_JSON_DEPTH -const nvx::host::network::policy::MAX_POLICY_FILE_SIZE -const nvx::host::network::policy::MAX_RULES_PER_ACTION -fn nvx::host::network::policy::compile_policy -fn nvx::host::network::policy::compile_policy_bytes -fn nvx::host::network::policy::compile_policy_file mod nvx::host::nvxs struct nvx::host::nvxs::HostConnection method nvx::host::nvxs::HostConnection::attach @@ -725,128 +715,6 @@ field nvx::host::runtime::RuntimeIdentity::kernel method nvx::host::runtime::RuntimeIdentity::new field nvx::host::runtime::RuntimeIdentity::openvmm fn nvx::host::runtime::probe_hypervisor -mod nvx::host::sandbox -const nvx::host::sandbox::DEFAULT_ENTRYPOINT -const nvx::host::sandbox::DEFAULT_HOSTNAME -const nvx::host::sandbox::DEFAULT_WORKLOAD_IDENTITY -enum nvx::host::sandbox::MountAccess -variant nvx::host::sandbox::MountAccess::ReadOnly -variant nvx::host::sandbox::MountAccess::ReadWrite -method nvx::host::sandbox::MountAccess::as_str -enum nvx::host::sandbox::MountKind -variant nvx::host::sandbox::MountKind::Directory -variant nvx::host::sandbox::MountKind::File -method nvx::host::sandbox::MountKind::as_str -enum nvx::host::sandbox::MountOwner -variant nvx::host::sandbox::MountOwner::Caller -variant nvx::host::sandbox::MountOwner::Vmm -method nvx::host::sandbox::MountOwner::as_str -struct nvx::host::sandbox::MountPolicy -field nvx::host::sandbox::MountPolicy::allowed -field nvx::host::sandbox::MountPolicy::denied -method nvx::host::sandbox::MountPolicy::is_empty -method nvx::host::sandbox::MountPolicy::paths -field nvx::host::sandbox::MountPolicy::writable -enum nvx::host::sandbox::PolicyKind -variant nvx::host::sandbox::PolicyKind::Allowed -variant nvx::host::sandbox::PolicyKind::Denied -variant nvx::host::sandbox::PolicyKind::Writable -method nvx::host::sandbox::PolicyKind::as_str -method nvx::host::sandbox::PolicyKind::option -struct nvx::host::sandbox::SandboxMount -method nvx::host::sandbox::SandboxMount::host_path -method nvx::host::sandbox::SandboxMount::new -method nvx::host::sandbox::SandboxMount::parse -method nvx::host::sandbox::SandboxMount::validated -method nvx::host::sandbox::SandboxMount::with_kind -mod nvx::host::sandbox::mount -const nvx::host::sandbox::mount::AGGREGATE_MOUNT_TARGET -const nvx::host::sandbox::mount::MAX_MOUNT_POLICY_PATHS -const nvx::host::sandbox::mount::MAX_MOUNT_TARGET_BYTES -const nvx::host::sandbox::mount::MOUNT_TAG -enum nvx::host::sandbox::mount::MountAccess -variant nvx::host::sandbox::mount::MountAccess::ReadOnly -variant nvx::host::sandbox::mount::MountAccess::ReadWrite -method nvx::host::sandbox::mount::MountAccess::as_str -enum nvx::host::sandbox::mount::MountKind -variant nvx::host::sandbox::mount::MountKind::Directory -variant nvx::host::sandbox::mount::MountKind::File -method nvx::host::sandbox::mount::MountKind::as_str -enum nvx::host::sandbox::mount::MountOwner -variant nvx::host::sandbox::mount::MountOwner::Caller -variant nvx::host::sandbox::mount::MountOwner::Vmm -method nvx::host::sandbox::mount::MountOwner::as_str -struct nvx::host::sandbox::mount::MountPolicy -field nvx::host::sandbox::mount::MountPolicy::allowed -field nvx::host::sandbox::mount::MountPolicy::denied -method nvx::host::sandbox::mount::MountPolicy::is_empty -method nvx::host::sandbox::mount::MountPolicy::paths -field nvx::host::sandbox::mount::MountPolicy::writable -const nvx::host::sandbox::mount::POLICY_KINDS -enum nvx::host::sandbox::mount::PolicyKind -variant nvx::host::sandbox::mount::PolicyKind::Allowed -variant nvx::host::sandbox::mount::PolicyKind::Denied -variant nvx::host::sandbox::mount::PolicyKind::Writable -method nvx::host::sandbox::mount::PolicyKind::as_str -method nvx::host::sandbox::mount::PolicyKind::option -const nvx::host::sandbox::mount::RESERVED_EXACT_MOUNT_TARGETS -const nvx::host::sandbox::mount::RESERVED_MOUNT_TARGETS -struct nvx::host::sandbox::mount::SandboxMount -method nvx::host::sandbox::mount::SandboxMount::host_path -method nvx::host::sandbox::mount::SandboxMount::new -method nvx::host::sandbox::mount::SandboxMount::parse -method nvx::host::sandbox::mount::SandboxMount::validated -method nvx::host::sandbox::mount::SandboxMount::with_kind -fn nvx::host::sandbox::mount::mount_kind -fn nvx::host::sandbox::mount::parse_mount_value -fn nvx::host::sandbox::mount::plan_mounts -fn nvx::host::sandbox::mount::require_mount_owner_supported -fn nvx::host::sandbox::mount::validate_mount_host_paths -fn nvx::host::sandbox::mount::validate_mount_target -fn nvx::host::sandbox::mount::validate_mounts -mod nvx::host::sandbox::options -const nvx::host::sandbox::options::MAX_ENVIRONMENT_FILE_BYTES -const nvx::host::sandbox::options::MAX_EXEC_ENVIRONMENT -const nvx::host::sandbox::options::MAX_EXEC_ENVIRONMENT_BYTES -struct nvx::host::sandbox::options::PolicyPathOption -field nvx::host::sandbox::options::PolicyPathOption::kind -field nvx::host::sandbox::options::PolicyPathOption::path -field nvx::host::sandbox::options::PolicyPathOption::preceding_mount -const nvx::host::sandbox::options::SYSTEMD_ENTRYPOINTS -enum nvx::host::sandbox::options::SandboxOperation -variant nvx::host::sandbox::options::SandboxOperation::Deprovision -variant nvx::host::sandbox::options::SandboxOperation::Exec -variant nvx::host::sandbox::options::SandboxOperation::Provision -variant nvx::host::sandbox::options::SandboxOperation::Run -variant nvx::host::sandbox::options::SandboxOperation::Start -variant nvx::host::sandbox::options::SandboxOperation::Stop -method nvx::host::sandbox::options::SandboxOperation::as_str -struct nvx::host::sandbox::options::SandboxOptions -method nvx::host::sandbox::options::SandboxOptions::check -field nvx::host::sandbox::options::SandboxOptions::cwd -field nvx::host::sandbox::options::SandboxOptions::dry_run -field nvx::host::sandbox::options::SandboxOptions::entrypoint -field nvx::host::sandbox::options::SandboxOptions::environment -field nvx::host::sandbox::options::SandboxOptions::environment_file -field nvx::host::sandbox::options::SandboxOptions::inherit_default_environment -field nvx::host::sandbox::options::SandboxOptions::layers -field nvx::host::sandbox::options::SandboxOptions::mount_owner -field nvx::host::sandbox::options::SandboxOptions::mounts -field nvx::host::sandbox::options::SandboxOptions::net -field nvx::host::sandbox::options::SandboxOptions::network_options -field nvx::host::sandbox::options::SandboxOptions::network_profile -field nvx::host::sandbox::options::SandboxOptions::operation -field nvx::host::sandbox::options::SandboxOptions::outcome_report -field nvx::host::sandbox::options::SandboxOptions::policy_paths -field nvx::host::sandbox::options::SandboxOptions::scratch -field nvx::host::sandbox::options::SandboxOptions::upper -fn nvx::host::sandbox::options::exec_environment_from_file -fn nvx::host::sandbox::options::sandbox_mounts -fn nvx::host::sandbox::options::validate_exec_environment -fn nvx::host::sandbox::options::validate_run_share_targets -fn nvx::host::sandbox::options::validate_sandbox_entrypoint -fn nvx::host::sandbox::parse_workload_identity -fn nvx::host::sandbox::require_file mod nvx::host::snapshot struct nvx::host::snapshot::BlockGeometry field nvx::host::snapshot::BlockGeometry::length diff --git a/nvx-rs/src/host/artifacts/mod.rs b/nvx-rs/src/host/artifacts/mod.rs index 551dfa0f0..722543bb0 100644 --- a/nvx-rs/src/host/artifacts/mod.rs +++ b/nvx-rs/src/host/artifacts/mod.rs @@ -26,6 +26,7 @@ mod dir; mod guest; #[cfg(feature = "cli")] mod identity; +#[cfg(feature = "cli")] mod require; #[cfg(feature = "cli")] @@ -39,8 +40,7 @@ pub(crate) use self::dir::{ArtifactDir, ArtifactPaths, Layout}; #[cfg(feature = "cli")] pub(crate) use self::guest::Guest; #[cfg(feature = "cli")] -pub(crate) use self::require::require_artifact; -pub(crate) use self::require::require_existing; +pub(crate) use self::require::{require_artifact, require_existing}; /// Name of the release manifest at the root of a release archive or repository checkout. #[cfg(feature = "cli")] diff --git a/nvx-rs/src/host/artifacts/require.rs b/nvx-rs/src/host/artifacts/require.rs index c5eaf07bf..a941a8b41 100644 --- a/nvx-rs/src/host/artifacts/require.rs +++ b/nvx-rs/src/host/artifacts/require.rs @@ -1,15 +1,12 @@ //! The checks that the artifacts that a launch runs are regular files. -use std::path::Path; -#[cfg(feature = "cli")] -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use crate::error::{Error, ErrorCode, Result}; use crate::host::paths; /// Checks that `path` is a regular file, as `require_file` of `nvx.py` does, and returns it, or /// fails with [`ErrorCode::ArtifactMissing`]. -#[cfg(feature = "cli")] pub(super) fn required(path: PathBuf, description: &str) -> Result { require_artifact(&path, description)?; Ok(path) @@ -17,7 +14,6 @@ pub(super) fn required(path: PathBuf, description: &str) -> Result { /// Checks that an artifact that a launch runs, such as the OpenVMM executable, is a regular /// file, as [`require_existing`] does, and fails with [`ErrorCode::ArtifactMissing`]. -#[cfg(feature = "cli")] pub(crate) fn require_artifact(path: &Path, description: &str) -> Result<()> { require_existing(path, description, ErrorCode::ArtifactMissing) } diff --git a/nvx-rs/src/host/mod.rs b/nvx-rs/src/host/mod.rs index 95d81b7ed..a08a7923b 100644 --- a/nvx-rs/src/host/mod.rs +++ b/nvx-rs/src/host/mod.rs @@ -12,9 +12,9 @@ //! that its workload has of it, its network, its export, its boot console, and what only its //! cold boot takes, with what a capture of it adds, a [`CaptureConfig`](config::CaptureConfig), //! whose identity names the capture's snapshots. -//! - [`network`] lowers egress rules, and the egress policy files that `nvx-rs run` and -//! `nvx-rs sandbox` accept, into OpenVMM's rules, and [`filesystem`] plans the export of host -//! paths through OpenVMM's aggregate virtio-fs device. +//! - [`network`] lowers egress rules into OpenVMM's rules, as its private `policy` (feature `cli`) +//! lowers the egress policy files that `nvx-rs run` and `nvx-rs sandbox` accept, and +//! [`filesystem`] plans the export of host paths through OpenVMM's aggregate virtio-fs device. //! - [`report`] reads OpenVMM's outcome report. //! - [`snapshot`] holds the compatibility key, [`SnapshotKey`](snapshot::SnapshotKey), derived //! from a capture's context and [`SnapshotManifest`](snapshot::SnapshotManifest), @@ -27,11 +27,13 @@ //! - [`process`] names processes by verified references: their ID, start time, and host boot, //! through which a pidfd or a process handle holds them, so that waits sleep until a process //! exits and termination reaches only it, whether or not the caller started it. -//! - [`sandbox`] is the launch profile of `nvx-rs sandbox`: a container over read-only EROFS -//! layers and a private ext4 scratch disk, with live shares, and the checks of its options. -//! Its private `distro` (feature `cli`) binds a distro layer to what explicit verification -//! found, so that the CLI's launches check the layer through the private `seal`, which seals open -//! files from their metadata alone, and its private `scratch` binds and claims a scratch image. +//! - The private `sandbox` is the launch profile of `nvx-rs sandbox`, for the CLI (feature +//! `cli`): a container over read-only EROFS layers and a private ext4 scratch disk, with live +//! shares, and the checks of its options. Its `distro` binds a distro layer to what explicit +//! verification found, so that the CLI's launches check the layer through the private `seal`, +//! which seals open files from their metadata alone, and its `scratch` binds and claims a +//! scratch image. Outside the CLI, [`vm`] and [`snapshot`] read the block geometry of an image +//! through it, and a snapshot's key names the tag of its shares' virtio-fs device. //! - [`vm`] holds a caller-owned VM directory, private to the current user, the fixed names of //! its files and endpoints, and a checked cold, capture or restore [`Prepared`](vm::Prepared) //! launch with its opened log and control credential, which the library never writes. A @@ -78,7 +80,7 @@ pub mod restore; #[cfg(feature = "cli")] mod restore_driver; pub mod runtime; -pub mod sandbox; +pub(crate) mod sandbox; pub(crate) mod seal; #[cfg(feature = "cli")] pub(crate) mod session; diff --git a/nvx-rs/src/host/network/arguments.rs b/nvx-rs/src/host/network/arguments.rs index 9789f0fa2..907cd28b7 100644 --- a/nvx-rs/src/host/network/arguments.rs +++ b/nvx-rs/src/host/network/arguments.rs @@ -20,8 +20,8 @@ use crate::net::{self, cidr}; /// proxy at the guest network's gateway (`--network-proxy GATEWAY:PORT`). Otherwise the portable /// profile applies the egress posture, the ingress default, the host-loopback access, which /// [`HostLoopback::Outbound`] leaves to OpenVMM's default, and the host-loopback forwards. Each -/// action's rules are lowered as an egress policy file's are ([`policy`]), and fail with its -/// messages, which name the rules after [`EgressRules::name`]. +/// action's rules are lowered as the CLI's egress policy files are, and fail with their messages, +/// which name the rules after [`EgressRules::name`]. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) when a rule is invalid, /// when the rules of one action need more than [`MAX_RULES`] OpenVMM rules, when host-loopback @@ -31,7 +31,6 @@ use crate::net::{self, cidr}; /// ([`NetworkPolicy::proxy`]), its proxy's port is one that [`NetworkProxy::port`] excludes, or /// its guest network has no gateway. /// -/// [`policy`]: crate::host::network::policy /// [`EgressRules::name`]: crate::host::network::EgressRules::name /// [`MAX_RULES`]: crate::host::network::MAX_RULES pub fn openvmm_arguments( diff --git a/nvx-rs/src/host/network/lowering.rs b/nvx-rs/src/host/network/lowering.rs index 29c7a97aa..32d472e4c 100644 --- a/nvx-rs/src/host/network/lowering.rs +++ b/nvx-rs/src/host/network/lowering.rs @@ -1,5 +1,5 @@ //! The lowering of egress rules into OpenVMM's native rules, which the egress policy compiler of -//! [`policy`](super::policy), [`openvmm_arguments`](super::openvmm_arguments), and +//! the private `policy` (feature `cli`), [`openvmm_arguments`](super::openvmm_arguments), and //! [`NetworkPolicy::native_egress_rules`](super::NetworkPolicy::native_egress_rules) share. //! //! It ports the former `scripts/nvx_tools/egress_policy.py` exactly. Rules of one action, allow diff --git a/nvx-rs/src/host/network/mod.rs b/nvx-rs/src/host/network/mod.rs index 162816188..a2e61a623 100644 --- a/nvx-rs/src/host/network/mod.rs +++ b/nvx-rs/src/host/network/mod.rs @@ -1,11 +1,11 @@ -//! Translation of a network posture into OpenVMM options, and the egress policy files of -//! `nvx.py`. +//! Translation of a network posture into OpenVMM options, and, in the private `policy` (feature +//! `cli`), the egress policy files of the CLI. //! //! OpenVMM's portable profile enforces an egress default plus IPv4 and IPv6 allow and deny rules //! that match a network of one family and, optionally, one protocol (TCP, UDP, or ICMP) and, for //! TCP or UDP, one port or an inclusive range of ports; deny rules take precedence. -//! [`EgressRule`]s are lowered into that form exactly, by the lowering that also compiles egress -//! policy files ([`policy`]): exceptions are subtracted from their networks, a rule without +//! [`EgressRule`]s are lowered into that form exactly, by the lowering that also compiles the CLI's +//! egress policy files: exceptions are subtracted from their networks, a rule without //! destinations matches `0.0.0.0/0` and `::/0`, `any` protocol with a port or a port range becomes //! a TCP and a UDP rule, destinations merge into the fewest networks that cover them, rules for //! every port of a protocol subsume narrower rules, and overlapping and adjacent port ranges @@ -36,7 +36,8 @@ mod address; mod arguments; mod loopback; mod lowering; -pub mod policy; +#[cfg(feature = "cli")] +pub(crate) mod policy; mod proxy; mod rule; diff --git a/nvx-rs/src/host/network/policy/mod.rs b/nvx-rs/src/host/network/policy/mod.rs index 94733a33a..699dd7689 100644 --- a/nvx-rs/src/host/network/policy/mod.rs +++ b/nvx-rs/src/host/network/policy/mod.rs @@ -63,14 +63,14 @@ use crate::error::{Error, Result}; /// Largest number of native rules that one action, allow or deny, compiles to: the number of /// rules of each action that OpenVMM accepts, [`MAX_RULES`](super::MAX_RULES). -pub const MAX_RULES_PER_ACTION: usize = super::MAX_RULES; +pub(crate) const MAX_RULES_PER_ACTION: usize = super::MAX_RULES; /// Largest egress policy file, in bytes: 1 MiB. -pub const MAX_POLICY_FILE_SIZE: usize = 1024 * 1024; +pub(crate) const MAX_POLICY_FILE_SIZE: usize = 1024 * 1024; /// Deepest nesting of arrays and objects in a policy file, the root included. A policy needs six: /// the root, an action, a rule, its `to`, a destination, and its `except`. -pub const MAX_JSON_DEPTH: usize = 32; +pub(crate) const MAX_JSON_DEPTH: usize = 32; /// The native OpenVMM rules of an egress policy. /// @@ -78,11 +78,11 @@ pub const MAX_JSON_DEPTH: usize = 32; /// `CIDR:PROTOCOL`, `CIDR:PROTOCOL:PORT`, or `CIDR:PROTOCOL:FIRST-LAST`. Its serde form is /// `{"allow": [...], "deny": [...]}`. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] -pub struct CompiledEgressPolicy { +pub(crate) struct CompiledEgressPolicy { /// Rules of traffic to permit, at most [`MAX_RULES_PER_ACTION`]. - pub allow: Vec, + pub(crate) allow: Vec, /// Rules of traffic to block, which OpenVMM applies first, at most [`MAX_RULES_PER_ACTION`]. - pub deny: Vec, + pub(crate) deny: Vec, } /// Compiles the egress policy file at `path`, as `compile_policy_file` of the former @@ -96,7 +96,7 @@ pub struct CompiledEgressPolicy { /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) when the file exceeds /// [`MAX_POLICY_FILE_SIZE`], is not UTF-8 JSON of the format, or holds an invalid policy; see /// [`compile_policy_bytes`]. -pub fn compile_policy_file(path: &Path) -> Result { +pub(crate) fn compile_policy_file(path: &Path) -> Result { let unreadable = |error: std::io::Error| { Error::io(format!( "failed to read egress policy file: {}", @@ -131,7 +131,7 @@ pub fn compile_policy_file(path: &Path) -> Result { /// - repeats a field in an object: `OBJECT has duplicate field "NAME"`, where `OBJECT` is the /// path of the object, or `egress policy` for the root; or /// - holds an invalid policy: see [`compile_policy`]. -pub fn compile_policy_bytes(data: &[u8], path: &Path) -> Result { +pub(crate) fn compile_policy_bytes(data: &[u8], path: &Path) -> Result { let path = path.display(); if data.len() > MAX_POLICY_FILE_SIZE { return Err(Error::unsupported(format!( @@ -169,7 +169,7 @@ pub fn compile_policy_bytes(data: &[u8], path: &Path) -> Result Result { +pub(crate) fn compile_policy(value: &Value) -> Result { let root = object(value, "egress policy")?; root.check_fields(ROOT_FIELDS, "egress policy")?; Ok(CompiledEgressPolicy { diff --git a/nvx-rs/src/host/network/rule.rs b/nvx-rs/src/host/network/rule.rs index ff8095210..199ef433e 100644 --- a/nvx-rs/src/host/network/rule.rs +++ b/nvx-rs/src/host/network/rule.rs @@ -43,10 +43,8 @@ pub enum Protocol { /// A destination network of an [`EgressRule`], without its exceptions. /// -/// Networks are read as egress policy files read their CIDRs ([`policy`]): host bits are cleared, +/// Networks are read as the CLI's egress policy files read their CIDRs: host bits are cleared, /// IPv4 netmasks such as `/255.0.0.0` are accepted, and IPv6 scopes are refused. -/// -/// [`policy`]: crate::host::network::policy #[derive(Debug, Clone, PartialEq, Eq)] pub struct Destination { /// Destination address or CIDR, such as `"10.0.0.0/8"` or `"192.0.2.1"`. diff --git a/nvx-rs/src/host/paths.rs b/nvx-rs/src/host/paths.rs index 791d4d992..4d3a85457 100644 --- a/nvx-rs/src/host/paths.rs +++ b/nvx-rs/src/host/paths.rs @@ -134,6 +134,7 @@ fn drive(path: &Path) -> Option { /// /// Fails as [`std::fs::canonicalize`] fails for a prefix for another reason than that it does not /// exist, and as [`text`] fails for a resolved path that is not valid Unicode. +#[cfg(feature = "cli")] pub(crate) fn resolve(path: &Path) -> io::Result { let absolute = absolute(path)?; let mut existing = absolute.as_path(); @@ -160,7 +161,7 @@ pub(crate) fn resolve(path: &Path) -> io::Result { /// Returns `path`, which [`std::fs::canonicalize`] gave, without its verbatim prefix, unless only /// the verbatim path names the object that it names. -#[cfg(windows)] +#[cfg(all(feature = "cli", windows))] fn without_verbatim_prefix(path: PathBuf) -> PathBuf { let Some(text) = path.to_str() else { return path; @@ -180,7 +181,7 @@ fn without_verbatim_prefix(path: PathBuf) -> PathBuf { } /// Returns `path` as it is: hosts other than Windows have no verbatim prefix. -#[cfg(not(windows))] +#[cfg(all(feature = "cli", not(windows)))] fn without_verbatim_prefix(path: PathBuf) -> PathBuf { path } diff --git a/nvx-rs/src/host/paths/tests.rs b/nvx-rs/src/host/paths/tests.rs index d7c39a4bf..e1ba773f0 100644 --- a/nvx-rs/src/host/paths/tests.rs +++ b/nvx-rs/src/host/paths/tests.rs @@ -1,3 +1,4 @@ +#[cfg(feature = "cli")] use std::fs; use super::*; @@ -131,6 +132,7 @@ fn verbatim_directories_keep_the_components_after_them() { } } +#[cfg(feature = "cli")] #[test] fn resolution_follows_links_and_keeps_missing_components() { let directory = tempfile::tempdir().unwrap(); @@ -158,7 +160,7 @@ fn resolution_follows_links_and_keeps_missing_components() { } } -#[cfg(windows)] +#[cfg(all(feature = "cli", windows))] #[test] fn resolution_keeps_the_verbatim_path_that_alone_names_the_object() { let directory = tempfile::tempdir().unwrap(); diff --git a/nvx-rs/src/host/sandbox/mod.rs b/nvx-rs/src/host/sandbox/mod.rs index 2ee1cc2c4..5abfafd13 100644 --- a/nvx-rs/src/host/sandbox/mod.rs +++ b/nvx-rs/src/host/sandbox/mod.rs @@ -1,19 +1,23 @@ //! The sandbox launch profile: a microVM that runs one workload in a container over a read-only //! distro image and a writable upper, with live virtio-fs shares. //! -//! This is the profile of `nvx-rs sandbox`: its documented limits and option semantics, with -//! native Rust input validation and diagnostics: +//! This is the profile of `nvx-rs sandbox`, private to the crate: its documented limits and option +//! semantics, with native Rust input validation and diagnostics. Only the CLI (feature `cli`) +//! uses them, but for the block geometry, which [`host::vm`](crate::host::vm) and +//! [`host::snapshot`](crate::host::snapshot) read too, and the tag of the shares' virtio-fs +//! device, which a snapshot's key names: //! //! - The private `layer` (feature `cli`): `SandboxLayer`, the distro layer of a sandbox, the one //! `LayerRole` that a sandbox takes: an EROFS filesystem, as `nvx-rs sandbox` names it with //! `ROLE,PATH,EROFS_UUID`, or a GPT disk. -//! - [`SandboxMount`]: a requested live share of a host directory or regular file at a guest -//! target, with its access mode, owner, and access-policy paths; [`mount`] holds the rules of -//! guest targets and the checks of host paths, and plans the shares of a sandbox through the -//! planner of every NVX caller, [`host::filesystem`](crate::host::filesystem). -//! - [`options`]: the checks that `nvx-rs sandbox` and `nvx-rs run` apply to their options: the -//! attribution of access-policy paths to shares, the systemd entrypoint check, the targets of -//! `nvx-rs run` shares, and the managed exec environment. +//! - The private `mount`: `SandboxMount` (feature `cli`), a requested live share of a host +//! directory or regular file at a guest target, with its access mode, owner, and access-policy +//! paths, which `--mount` names; `mount` holds the rules of guest targets and the checks of host +//! paths, and plans the shares of a sandbox through the planner of every NVX caller, +//! [`host::filesystem`](crate::host::filesystem). +//! - The private `options` (feature `cli`): the checks that `nvx-rs sandbox` and `nvx-rs run` +//! apply to their options: the attribution of access-policy paths to shares, the systemd +//! entrypoint check, the targets of `nvx-rs run` shares, and the managed exec environment. //! - The private `distro` (feature `cli`): the binding of the distro layer, which explicit //! verification makes from the layer and its manifest, and which a launch checks from metadata //! alone. @@ -40,8 +44,9 @@ pub(crate) mod distro; pub(crate) mod geometry; #[cfg(feature = "cli")] pub(crate) mod layer; -pub mod mount; -pub mod options; +pub(crate) mod mount; +#[cfg(feature = "cli")] +pub(crate) mod options; #[cfg(feature = "cli")] pub(crate) mod scratch; @@ -50,17 +55,23 @@ use std::path::Path; use self::geometry::BlockGeometry; use self::mount::os_error; -pub use self::mount::{MountAccess, MountKind, MountOwner, MountPolicy, PolicyKind, SandboxMount}; -use crate::error::{Error, ErrorCode, Result}; +#[cfg(feature = "cli")] +use crate::error::ErrorCode; +use crate::error::{Error, Result}; +#[cfg(feature = "cli")] use crate::host::artifacts::require_existing; +#[cfg(feature = "cli")] use crate::protocol::cmdline; /// The workload identity by default, `65534:65534`. -pub const DEFAULT_WORKLOAD_IDENTITY: (u32, u32) = (65534, 65534); +#[cfg(feature = "cli")] +pub(crate) const DEFAULT_WORKLOAD_IDENTITY: (u32, u32) = (65534, 65534); /// The entrypoint by default. -pub const DEFAULT_ENTRYPOINT: &str = "/bin/sh"; +#[cfg(feature = "cli")] +pub(crate) const DEFAULT_ENTRYPOINT: &str = "/bin/sh"; /// The hostname by default. -pub const DEFAULT_HOSTNAME: &str = "nvx-sandbox"; +#[cfg(feature = "cli")] +pub(crate) const DEFAULT_HOSTNAME: &str = "nvx-sandbox"; /// Returns the error of a request that the profile refuses. pub(crate) fn refused(message: impl Into) -> Error { @@ -74,7 +85,8 @@ pub(crate) fn refused(message: impl Into) -> Error { /// /// Each ID contains only ASCII decimal digits, without a sign or whitespace. Fails with /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). -pub fn parse_workload_identity(value: &str) -> Result<(u32, u32)> { +#[cfg(feature = "cli")] +pub(crate) fn parse_workload_identity(value: &str) -> Result<(u32, u32)> { let fields: Vec<&str> = value.split(':').collect(); let [uid, gid] = fields[..] else { return Err(refused("--workload-user must be UID:GID")); @@ -98,6 +110,7 @@ pub fn parse_workload_identity(value: &str) -> Result<(u32, u32)> { } /// Parses nonempty ASCII decimal digits into a native integer, refusing signs and whitespace. +#[cfg(feature = "cli")] pub(crate) fn parse_decimal(value: &str) -> Option { if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) { return None; @@ -107,6 +120,7 @@ pub(crate) fn parse_decimal(value: &str) -> Option { /// Returns whether a value cannot stay one unquoted kernel token: whitespace, a control /// character, a quote, or a UTF-8 byte that Linux treats as whitespace. +#[cfg(feature = "cli")] pub(crate) fn has_kernel_separator(value: &str) -> bool { value .chars() @@ -121,7 +135,8 @@ pub(crate) fn has_kernel_separator(value: &str) -> bool { /// Fails with [`ErrorKind::Unavailable`](crate::ErrorKind::Unavailable) and the message /// `{description} not found: {path}`, with `path` spelled with its components alone, as /// [`Path::components`] reads them. -pub fn require_file(path: &Path, description: &str) -> Result<()> { +#[cfg(feature = "cli")] +pub(crate) fn require_file(path: &Path, description: &str) -> Result<()> { require_existing(path, description, ErrorCode::Unavailable) } @@ -186,5 +201,5 @@ pub(crate) fn block_geometry(file: &File, path: &Path, description: &str) -> Res } } -#[cfg(test)] +#[cfg(all(test, feature = "cli"))] mod tests; diff --git a/nvx-rs/src/host/sandbox/mount/aggregate.rs b/nvx-rs/src/host/sandbox/mount/aggregate.rs index fd06b3b15..9f1d584ba 100644 --- a/nvx-rs/src/host/sandbox/mount/aggregate.rs +++ b/nvx-rs/src/host/sandbox/mount/aggregate.rs @@ -29,7 +29,7 @@ use crate::host::sandbox::refused; /// Fails as those checks and [`filesystem::plan`] do, and with /// [`ErrorKind::Io`](crate::ErrorKind::Io) if a relative host path meets an unreadable current /// directory, or Windows cannot qualify a drive-relative path. -pub fn plan_mounts(mounts: &[SandboxMount]) -> Result> { +pub(crate) fn plan_mounts(mounts: &[SandboxMount]) -> Result> { validate_mounts(mounts)?; for mount in mounts { mount.validated()?; diff --git a/nvx-rs/src/host/sandbox/mount/host_paths.rs b/nvx-rs/src/host/sandbox/mount/host_paths.rs index d98c32209..9493fee0e 100644 --- a/nvx-rs/src/host/sandbox/mount/host_paths.rs +++ b/nvx-rs/src/host/sandbox/mount/host_paths.rs @@ -44,7 +44,7 @@ impl HostQueries<'_> { /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), or /// [`ErrorKind::Io`](crate::ErrorKind::Io) if a path cannot be resolved or examined. -pub fn validate_mount_host_paths(mounts: &[SandboxMount]) -> Result<()> { +pub(crate) fn validate_mount_host_paths(mounts: &[SandboxMount]) -> Result<()> { validate_mount_host_paths_with(mounts, &HostQueries::native()) } @@ -102,13 +102,6 @@ pub(crate) fn validate_mount_host_paths_with( /// [`ErrorKind::Io`](crate::ErrorKind::Io) if a path cannot be examined. /// /// [`plan_mounts`]: crate::host::sandbox::mount::plan_mounts -#[cfg_attr( - not(feature = "cli"), - allow( - dead_code, - reason = "only the CLI's sandboxes check their shares again" - ) -)] pub(crate) fn check_distinct_export_roots( export: &ExportPlan, host: &HostQueries<'_>, diff --git a/nvx-rs/src/host/sandbox/mount/mod.rs b/nvx-rs/src/host/sandbox/mount/mod.rs index 213438df7..bb3553663 100644 --- a/nvx-rs/src/host/sandbox/mount/mod.rs +++ b/nvx-rs/src/host/sandbox/mount/mod.rs @@ -7,7 +7,9 @@ //! sandbox records the resulting [`ExportPlan`]. //! //! OpenVMM attaches one virtio-fs device, with the tag [`MOUNT_TAG`], which exports the plan at -//! [`GUEST_EXPORT`], from which the guest maps it as the plan's mapping table orders. +//! [`GUEST_EXPORT`], from which the guest maps it as the plan's mapping table orders. The tag, +//! which a snapshot's key names, is the one part of the shares that the crate uses outside the +//! CLI; the rest is the CLI's (feature `cli`). //! //! A share's access-policy paths are host paths: `--mount-deny` hides a path, `--mount-allow` //! exposes a path inside a denied path again, and `--mount-write` makes a path one of the only @@ -17,55 +19,59 @@ //! [`filesystem::plan`]: crate::host::filesystem::plan //! [`ExportPlan`]: crate::host::filesystem::ExportPlan //! [`GUEST_EXPORT`]: crate::host::filesystem::GUEST_EXPORT +//! [`RESERVED_MOUNT_TARGETS`]: crate::protocol::share::RESERVED_MOUNT_TARGETS +//! [`RESERVED_EXACT_MOUNT_TARGETS`]: crate::protocol::share::RESERVED_EXACT_MOUNT_TARGETS +#[cfg(feature = "cli")] mod aggregate; +#[cfg(feature = "cli")] mod boot; +#[cfg(feature = "cli")] mod host_paths; mod request; +#[cfg(feature = "cli")] use std::fmt; -pub use crate::protocol::share::{RESERVED_EXACT_MOUNT_TARGETS, RESERVED_MOUNT_TARGETS}; - -pub use self::aggregate::plan_mounts; -pub use self::host_paths::validate_mount_host_paths; -#[cfg_attr( - not(feature = "cli"), - allow( - unused_imports, - reason = "only the CLI's sandboxes check their shares again" - ) -)] -pub(crate) use self::host_paths::{HostQueries, check_distinct_export_roots}; -pub use self::request::{ - SandboxMount, mount_kind, parse_mount_value, require_mount_owner_supported, - validate_mount_target, validate_mounts, +#[cfg(feature = "cli")] +pub(crate) use self::aggregate::plan_mounts; +#[cfg(feature = "cli")] +pub(crate) use self::host_paths::{ + HostQueries, check_distinct_export_roots, validate_mount_host_paths, }; +pub(crate) use self::request::os_error; +#[cfg(feature = "cli")] pub(crate) use self::request::{ - has_forbidden_character, has_irregular_component, os_error, targets_overlap, + SandboxMount, has_forbidden_character, has_irregular_component, mount_kind, parse_mount_value, + require_mount_owner_supported, targets_overlap, validate_mounts, }; /// The tag of OpenVMM's virtio-fs device. -pub const MOUNT_TAG: &str = "microvm"; +pub(crate) const MOUNT_TAG: &str = "microvm"; /// The guest directory at which the guest mounts the aggregate of several shares. -pub const AGGREGATE_MOUNT_TARGET: &str = "/run/nvx/shares"; +#[cfg(feature = "cli")] +pub(crate) const AGGREGATE_MOUNT_TARGET: &str = "/run/nvx/shares"; /// The most denied, allowed, and writable paths each that OpenVMM accepts for a share. -pub const MAX_MOUNT_POLICY_PATHS: usize = 128; +#[cfg(feature = "cli")] +pub(crate) const MAX_MOUNT_POLICY_PATHS: usize = 128; /// The longest guest target, in UTF-8 bytes. -pub const MAX_MOUNT_TARGET_BYTES: usize = 4096; +#[cfg(feature = "cli")] +pub(crate) const MAX_MOUNT_TARGET_BYTES: usize = 4096; /// The access mode of a share. +#[cfg(feature = "cli")] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum MountAccess { +pub(crate) enum MountAccess { /// `ro`, the default. ReadOnly, /// `rw`. ReadWrite, } +#[cfg(feature = "cli")] impl MountAccess { /// Returns the mode's name: `ro` or `rw`. - pub const fn as_str(self) -> &'static str { + pub(crate) const fn as_str(self) -> &'static str { match self { Self::ReadOnly => "ro", Self::ReadWrite => "rw", @@ -73,6 +79,7 @@ impl MountAccess { } } +#[cfg(feature = "cli")] impl fmt::Display for MountAccess { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(self.as_str()) @@ -80,8 +87,9 @@ impl fmt::Display for MountAccess { } /// The host identity of a share's file operations. +#[cfg(feature = "cli")] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum MountOwner { +pub(crate) enum MountOwner { /// `vmm`, the default: every operation runs as OpenVMM. Vmm, /// `caller`: each operation runs as the guest caller's identity, with guest root squashed to @@ -89,9 +97,10 @@ pub enum MountOwner { Caller, } +#[cfg(feature = "cli")] impl MountOwner { /// Returns the owner's name: `vmm` or `caller`. - pub const fn as_str(self) -> &'static str { + pub(crate) const fn as_str(self) -> &'static str { match self { Self::Vmm => "vmm", Self::Caller => "caller", @@ -99,6 +108,7 @@ impl MountOwner { } } +#[cfg(feature = "cli")] impl fmt::Display for MountOwner { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(self.as_str()) @@ -107,17 +117,19 @@ impl fmt::Display for MountOwner { /// What a share binds: a host directory, or one regular host file without its directory, which /// only the child of an aggregate can share. +#[cfg(feature = "cli")] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum MountKind { +pub(crate) enum MountKind { /// `directory`, the default. Directory, /// `file`. File, } +#[cfg(feature = "cli")] impl MountKind { /// Returns the kind's name: `directory` or `file`. - pub const fn as_str(self) -> &'static str { + pub(crate) const fn as_str(self) -> &'static str { match self { Self::Directory => "directory", Self::File => "file", @@ -125,6 +137,7 @@ impl MountKind { } } +#[cfg(feature = "cli")] impl fmt::Display for MountKind { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(self.as_str()) @@ -132,8 +145,9 @@ impl fmt::Display for MountKind { } /// A kind of access-policy path. +#[cfg(feature = "cli")] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum PolicyKind { +pub(crate) enum PolicyKind { /// A path that the share hides: `--mount-deny`. Denied, /// A path inside a denied path that the share exposes again: `--mount-allow`. @@ -143,15 +157,17 @@ pub enum PolicyKind { } /// The kinds of access-policy paths, in the order in which OpenVMM's options carry them. -pub const POLICY_KINDS: [PolicyKind; 3] = [ +#[cfg(feature = "cli")] +pub(crate) const POLICY_KINDS: [PolicyKind; 3] = [ PolicyKind::Denied, PolicyKind::Allowed, PolicyKind::Writable, ]; +#[cfg(feature = "cli")] impl PolicyKind { /// Returns the kind's name in messages: `denied`, `allowed`, or `writable`. - pub const fn as_str(self) -> &'static str { + pub(crate) const fn as_str(self) -> &'static str { match self { Self::Denied => "denied", Self::Allowed => "allowed", @@ -161,7 +177,7 @@ impl PolicyKind { /// Returns the option that requests a path of this kind: `--mount-deny`, `--mount-allow`, or /// `--mount-write`. - pub const fn option(self) -> &'static str { + pub(crate) const fn option(self) -> &'static str { match self { Self::Denied => "--mount-deny", Self::Allowed => "--mount-allow", @@ -170,6 +186,7 @@ impl PolicyKind { } } +#[cfg(feature = "cli")] impl fmt::Display for PolicyKind { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(self.as_str()) @@ -177,19 +194,21 @@ impl fmt::Display for PolicyKind { } /// The access-policy paths of a share, as given: host paths, relative to the share or absolute. +#[cfg(feature = "cli")] #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct MountPolicy { +pub(crate) struct MountPolicy { /// Paths that the share hides. - pub denied: Vec, + pub(crate) denied: Vec, /// Paths inside denied paths that the share exposes again. - pub allowed: Vec, + pub(crate) allowed: Vec, /// The only paths of a read-write share that the workload can modify, if any. - pub writable: Vec, + pub(crate) writable: Vec, } +#[cfg(feature = "cli")] impl MountPolicy { /// Returns the paths of `kind`. - pub fn paths(&self, kind: PolicyKind) -> &[String] { + pub(crate) fn paths(&self, kind: PolicyKind) -> &[String] { match kind { PolicyKind::Denied => &self.denied, PolicyKind::Allowed => &self.allowed, @@ -198,10 +217,10 @@ impl MountPolicy { } /// Returns whether the policy has no paths. - pub fn is_empty(&self) -> bool { + pub(crate) fn is_empty(&self) -> bool { POLICY_KINDS.iter().all(|&kind| self.paths(kind).is_empty()) } } -#[cfg(test)] +#[cfg(all(test, feature = "cli"))] pub(super) mod tests; diff --git a/nvx-rs/src/host/sandbox/mount/request.rs b/nvx-rs/src/host/sandbox/mount/request.rs index 258aa82d1..dcf1ac731 100644 --- a/nvx-rs/src/host/sandbox/mount/request.rs +++ b/nvx-rs/src/host/sandbox/mount/request.rs @@ -2,16 +2,25 @@ //! [`SandboxMount`], one share with its access, owner, kind, and access-policy paths. use std::io; -use std::path::{Component, Path, PathBuf}; +use std::path::Path; +#[cfg(feature = "cli")] +use std::path::{Component, PathBuf}; +#[cfg(feature = "cli")] use super::{ MAX_MOUNT_POLICY_PATHS, MAX_MOUNT_TARGET_BYTES, MountAccess, MountKind, MountOwner, MountPolicy, POLICY_KINDS, PolicyKind, }; -use crate::error::{Error, Result}; +use crate::error::Error; +#[cfg(feature = "cli")] +use crate::error::Result; +#[cfg(feature = "cli")] use crate::host::filesystem::within; +#[cfg(feature = "cli")] use crate::host::paths; +#[cfg(feature = "cli")] use crate::host::sandbox::{has_kernel_separator, refused}; +#[cfg(feature = "cli")] use crate::protocol::share::{self, RESERVED_TARGET_CHARACTERS, Reservation}; /// Splits a `--mount` value, `GUEST_TARGET,HOST_PATH[,ro|rw]`, into its guest target, host path, @@ -21,7 +30,8 @@ use crate::protocol::share::{self, RESERVED_TARGET_CHARACTERS, Reservation}; /// one, so a host path may contain commas when the mode is given. OpenVMM reads its `--mount` /// and `--mount-child` values the same way. Fails with /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). -pub fn parse_mount_value(value: &str) -> Result<(String, String, MountAccess)> { +#[cfg(feature = "cli")] +pub(crate) fn parse_mount_value(value: &str) -> Result<(String, String, MountAccess)> { let Some((target, rest)) = value.split_once(',') else { return Err(refused("--mount must be GUEST_TARGET,HOST_PATH[,ro|rw]")); }; @@ -44,6 +54,7 @@ pub fn parse_mount_value(value: &str) -> Result<(String, String, MountAccess)> { /// Returns whether `target` cannot stay one kernel token or holds one of the /// [`RESERVED_TARGET_CHARACTERS`], NUL, `\`, `=`, `,`, and `"`. +#[cfg(feature = "cli")] pub(crate) fn has_forbidden_character(target: &str) -> bool { has_kernel_separator(target) || target @@ -52,6 +63,7 @@ pub(crate) fn has_forbidden_character(target: &str) -> bool { } /// Returns whether a guest target has an empty, `.`, or `..` component after its leading slash. +#[cfg(feature = "cli")] pub(crate) fn has_irregular_component(target: &str) -> bool { target .split('/') @@ -67,9 +79,10 @@ pub(crate) fn has_irregular_component(target: &str) -> bool { /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). /// -/// [`RESERVED_MOUNT_TARGETS`]: crate::host::sandbox::mount::RESERVED_MOUNT_TARGETS -/// [`RESERVED_EXACT_MOUNT_TARGETS`]: crate::host::sandbox::mount::RESERVED_EXACT_MOUNT_TARGETS -pub fn validate_mount_target(target: &str) -> Result<()> { +/// [`RESERVED_MOUNT_TARGETS`]: crate::protocol::share::RESERVED_MOUNT_TARGETS +/// [`RESERVED_EXACT_MOUNT_TARGETS`]: crate::protocol::share::RESERVED_EXACT_MOUNT_TARGETS +#[cfg(feature = "cli")] +pub(crate) fn validate_mount_target(target: &str) -> Result<()> { if !target.starts_with('/') || target == "/" || target.len() > MAX_MOUNT_TARGET_BYTES @@ -96,11 +109,13 @@ pub fn validate_mount_target(target: &str) -> Result<()> { /// per-request POSIX identity for OpenVMM to switch to. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). -pub fn require_mount_owner_supported(owner: MountOwner) -> Result<()> { +#[cfg(feature = "cli")] +pub(crate) fn require_mount_owner_supported(owner: MountOwner) -> Result<()> { require_mount_owner_supported_on(owner, cfg!(windows)) } /// Implements [`require_mount_owner_supported`] for a Windows host or another. +#[cfg(feature = "cli")] pub(crate) fn require_mount_owner_supported_on(owner: MountOwner, windows: bool) -> Result<()> { if owner == MountOwner::Caller && windows { return Err(refused( @@ -112,13 +127,15 @@ pub(crate) fn require_mount_owner_supported_on(owner: MountOwner, windows: bool) } /// Returns whether two guest targets overlap: whether they are equal, or one contains the other. +#[cfg(feature = "cli")] pub(crate) fn targets_overlap(left: &str, right: &str) -> bool { within(left, right) || within(right, left) } /// Returns the kind of share that `host_path` requests: a file for a regular file, and /// otherwise a directory, which [`SandboxMount::validated`] then requires. -pub fn mount_kind(host_path: &Path) -> MountKind { +#[cfg(feature = "cli")] +pub(crate) fn mount_kind(host_path: &Path) -> MountKind { if host_path.is_file() { MountKind::File } else { @@ -140,8 +157,9 @@ pub(crate) fn os_error(error: &io::Error, path: Option<&Path>) -> Error { /// again, and its writable paths, when present, are the only paths of a read-write share that /// the workload can modify. A share of a file binds one regular host file, whose mode applies to /// the whole file, so it has none of them. +#[cfg(feature = "cli")] #[derive(Clone, Debug, PartialEq, Eq)] -pub struct SandboxMount { +pub(crate) struct SandboxMount { /// The guest directory or file at which the share appears. pub(super) guest_target: String, /// The host directory or file, with its components normalized. @@ -156,6 +174,7 @@ pub struct SandboxMount { pub(super) kind: MountKind, } +#[cfg(feature = "cli")] impl SandboxMount { /// Returns the share of `host_path` at `guest_target`, with `host_path` spelled with its /// components alone, as [`Path::components`] reads them. @@ -165,7 +184,7 @@ impl SandboxMount { /// component, policy paths for a file, more than [`MAX_MOUNT_POLICY_PATHS`] policy paths of a /// kind, empty or repeated ones, allowed paths without denied paths, and writable paths of a /// read-only share. - pub fn new( + pub(crate) fn new( guest_target: &str, host_path: &str, access: MountAccess, @@ -188,7 +207,7 @@ impl SandboxMount { /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), as /// [`parse_mount_value`] and [`new`](Self::new) do. - pub fn parse( + pub(crate) fn parse( value: &str, policy: MountPolicy, owner: MountOwner, @@ -265,7 +284,7 @@ impl SandboxMount { /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) for a file share /// with policy paths. - pub fn with_kind(&self, kind: MountKind) -> Result { + pub(crate) fn with_kind(&self, kind: MountKind) -> Result { Self::checked(Self { kind, ..self.clone() @@ -273,7 +292,7 @@ impl SandboxMount { } /// Returns the host directory or file. - pub fn host_path(&self) -> &Path { + pub(crate) fn host_path(&self) -> &Path { &self.host_path } @@ -287,7 +306,7 @@ impl SandboxMount { /// file is not shared in its place. Fails with /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), or /// [`ErrorKind::Io`](crate::ErrorKind::Io) if the host path cannot be examined. - pub fn validated(&self) -> Result<()> { + pub(crate) fn validated(&self) -> Result<()> { require_mount_owner_supported(self.owner)?; let host_path = self.host_path.as_path(); let kind_matches = match self.kind { @@ -382,6 +401,7 @@ impl SandboxMount { /// Returns `path` joined to `directory` without resolving links, as [`paths::absolute_in`] joins /// them. +#[cfg(feature = "cli")] fn absolute_host_path(directory: &Path, path: &Path) -> Result { paths::absolute_in(directory, path).map_err(|error| os_error(&error, Some(path))) } @@ -390,7 +410,8 @@ fn absolute_host_path(directory: &Path, path: &Path) -> Result { /// another, so that no share hides another, and every share has the same owner. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). -pub fn validate_mounts(mounts: &[SandboxMount]) -> Result<()> { +#[cfg(feature = "cli")] +pub(crate) fn validate_mounts(mounts: &[SandboxMount]) -> Result<()> { for (index, mount) in mounts.iter().enumerate() { for other in &mounts[..index] { if targets_overlap(&other.guest_target, &mount.guest_target) { diff --git a/nvx-rs/src/host/sandbox/mount/tests.rs b/nvx-rs/src/host/sandbox/mount/tests.rs index ac11002fd..3d409baf7 100644 --- a/nvx-rs/src/host/sandbox/mount/tests.rs +++ b/nvx-rs/src/host/sandbox/mount/tests.rs @@ -6,6 +6,7 @@ use std::path::{Path, PathBuf}; use super::aggregate::plan_mounts_with; pub(crate) use super::host_paths::validate_mount_host_paths_with; use super::request::require_mount_owner_supported_on; +pub(crate) use super::request::validate_mount_target; use super::*; use crate::ErrorKind; use crate::error::Result; diff --git a/nvx-rs/src/host/sandbox/options.rs b/nvx-rs/src/host/sandbox/options.rs index cfe39c6a2..9f27742a7 100644 --- a/nvx-rs/src/host/sandbox/options.rs +++ b/nvx-rs/src/host/sandbox/options.rs @@ -25,17 +25,18 @@ use crate::error::Result; use crate::protocol::root::RootUpper; /// The entrypoints of systemd, which the sandbox security profile does not run. -pub const SYSTEMD_ENTRYPOINTS: [&str; 2] = ["/usr/lib/systemd/systemd", "/lib/systemd/systemd"]; +pub(crate) const SYSTEMD_ENTRYPOINTS: [&str; 2] = + ["/usr/lib/systemd/systemd", "/lib/systemd/systemd"]; /// The largest environment file of a managed exec, in bytes. -pub const MAX_ENVIRONMENT_FILE_BYTES: usize = 1024 * 1024; +pub(crate) const MAX_ENVIRONMENT_FILE_BYTES: usize = 1024 * 1024; /// The most entries of a managed exec environment. -pub const MAX_EXEC_ENVIRONMENT: usize = 256; +pub(crate) const MAX_EXEC_ENVIRONMENT: usize = 256; /// The longest entry of a managed exec environment, in UTF-8 bytes. -pub const MAX_EXEC_ENVIRONMENT_BYTES: usize = 4096; +pub(crate) const MAX_EXEC_ENVIRONMENT_BYTES: usize = 4096; /// An operation of `nvx-rs sandbox`. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum SandboxOperation { +pub(crate) enum SandboxOperation { /// `run`: a sandbox that runs one managed execution and stops, without a state directory, the /// default. Run, @@ -53,7 +54,7 @@ pub enum SandboxOperation { impl SandboxOperation { /// Returns the operation's name. - pub const fn as_str(self) -> &'static str { + pub(crate) const fn as_str(self) -> &'static str { match self { Self::Run => "run", Self::Provision => "provision", @@ -84,55 +85,55 @@ impl fmt::Display for SandboxOperation { /// A `--mount-deny`, `--mount-allow`, or `--mount-write` option, with the position among the /// `--mount` options of the one before it. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct PolicyPathOption { +pub(crate) struct PolicyPathOption { /// The kind of path, which the option gives. - pub kind: PolicyKind, + pub(crate) kind: PolicyKind, /// The index of the last `--mount` option before this one, if any. - pub preceding_mount: Option, + pub(crate) preceding_mount: Option, /// The host path, as given. - pub path: String, + pub(crate) path: String, } /// The options of `nvx-rs sandbox` that [`SandboxOptions::check`] examines, as given. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct SandboxOptions { +pub(crate) struct SandboxOptions { /// The operation. - pub operation: SandboxOperation, + pub(crate) operation: SandboxOperation, /// `--dry-run`. - pub dry_run: bool, + pub(crate) dry_run: bool, /// `--cwd`. - pub cwd: Option, + pub(crate) cwd: Option, /// The `--environment` options. - pub environment: Vec, + pub(crate) environment: Vec, /// `--environment-file`. - pub environment_file: Option, + pub(crate) environment_file: Option, /// `--inherit-default-environment`. - pub inherit_default_environment: bool, + pub(crate) inherit_default_environment: bool, /// Whether any network policy option was given: `--net`, `--network-profile`, /// `--network-egress`, `--network-ingress`, `--network-egress-policy-file`, /// `--host-loopback`, `--network-proxy`, `--network-egress-allow`, /// `--network-egress-deny`, or `--host-loopback-forward`. - pub network_options: bool, + pub(crate) network_options: bool, /// Whether `--net` was given. - pub net: bool, + pub(crate) net: bool, /// Whether `--network-profile` was given. - pub network_profile: bool, + pub(crate) network_profile: bool, /// `--entrypoint`. - pub entrypoint: String, + pub(crate) entrypoint: String, /// Whether `--outcome-report` was given. - pub outcome_report: bool, + pub(crate) outcome_report: bool, /// The `--mount` options. - pub mounts: Vec, + pub(crate) mounts: Vec, /// The `--mount-deny`, `--mount-allow`, and `--mount-write` options, in order. - pub policy_paths: Vec, + pub(crate) policy_paths: Vec, /// `--mount-owner`. - pub mount_owner: Option, + pub(crate) mount_owner: Option, /// Whether any `--layer` was given. - pub layers: bool, + pub(crate) layers: bool, /// Whether `--scratch` was given. - pub scratch: bool, + pub(crate) scratch: bool, /// `--upper`, if given: a launch keeps its upper on scratch without it. - pub upper: Option, + pub(crate) upper: Option, } impl SandboxOptions { @@ -150,7 +151,7 @@ impl SandboxOptions { /// is in RAM. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). - pub fn check(&self) -> Result>> { + pub(crate) fn check(&self) -> Result>> { let operation = self.operation; if self.dry_run && operation != SandboxOperation::Run { return Err(refused("--dry-run is only valid for sandbox run")); @@ -248,7 +249,7 @@ impl SandboxOptions { /// the `--mount` before it. Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) /// for a policy path without a `--mount` before it among several, and as /// [`SandboxMount::parse`] and [`SandboxMount::with_kind`] do. -pub fn sandbox_mounts( +pub(crate) fn sandbox_mounts( mounts: &[String], policy_paths: &[PolicyPathOption], owner: Option, @@ -310,7 +311,7 @@ pub(crate) fn sandbox_mounts_with( /// the aggregate itself. `nvx-rs run` checks them when it attaches several shares, or a file. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). -pub fn validate_run_share_targets(targets: &[&str]) -> Result<()> { +pub(crate) fn validate_run_share_targets(targets: &[&str]) -> Result<()> { for (index, target) in targets.iter().enumerate() { if !target.starts_with('/') || *target == "/" @@ -339,7 +340,7 @@ pub fn validate_run_share_targets(targets: &[&str]) -> Result<()> { /// This is the cheap check of every launch, which reads nothing: it applies whether or not the /// distro layer has a manifest, and never reads the manifest's package list. Fails with /// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). -pub fn validate_sandbox_entrypoint(entrypoint: &str) -> Result<()> { +pub(crate) fn validate_sandbox_entrypoint(entrypoint: &str) -> Result<()> { if SYSTEMD_ENTRYPOINTS.contains(&entrypoint) { return Err(refused( "systemd entrypoints are unsupported by the sandbox security profile", @@ -353,7 +354,7 @@ pub fn validate_sandbox_entrypoint(entrypoint: &str) -> Result<()> { /// of at most [`MAX_EXEC_ENVIRONMENT_BYTES`] bytes, and no key twice. /// /// Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported). -pub fn validate_exec_environment(entries: &[String]) -> Result<()> { +pub(crate) fn validate_exec_environment(entries: &[String]) -> Result<()> { if entries.len() > MAX_EXEC_ENVIRONMENT { return Err(refused(format!( "managed exec environment exceeds {MAX_EXEC_ENVIRONMENT} entries" @@ -391,7 +392,7 @@ pub fn validate_exec_environment(entries: &[String]) -> Result<()> { /// The file must be strict UTF-8 JSON; non-finite numbers and unpaired surrogate escapes are /// refused. Fails with [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), naming the /// file and the native I/O or JSON error when it cannot be read or decoded. -pub fn exec_environment_from_file(path: &str) -> Result> { +pub(crate) fn exec_environment_from_file(path: &str) -> Result> { let path = Path::new(path); let mut data = Vec::new(); File::open(path) diff --git a/nvx-rs/src/host/sandbox/tests.rs b/nvx-rs/src/host/sandbox/tests.rs index 4e756fd23..9d911d9e7 100644 --- a/nvx-rs/src/host/sandbox/tests.rs +++ b/nvx-rs/src/host/sandbox/tests.rs @@ -4,6 +4,7 @@ use std::fs; use std::path::PathBuf; +use super::mount::{MountAccess, MountKind, MountOwner, MountPolicy, SandboxMount}; use super::*; use crate::ErrorKind; use crate::host::filesystem::ExportPlan; @@ -186,12 +187,12 @@ fn file_shares_take_no_policy_paths_or_reserved_targets() { mount("/etc/app,app", &[]); for target in ["/etc", "/run", "/run/nvx"] { assert_eq!( - message(mount::validate_mount_target(target)), + message(mount::tests::validate_mount_target(target)), format!("sandbox mount target {target} is reserved") ); } assert_eq!( - message(mount::validate_mount_target("/run/nvx/time/state")), + message(mount::tests::validate_mount_target("/run/nvx/time/state")), "sandbox mount target /run/nvx/time/state overlaps the reserved /run/nvx/time tree" ); for target in [ @@ -200,7 +201,7 @@ fn file_shares_take_no_policy_paths_or_reserved_targets() { "/run/nvxtime", "/run/nvx/timer", ] { - mount::validate_mount_target(target).unwrap(); + mount::tests::validate_mount_target(target).unwrap(); } } diff --git a/nvx-rs/src/host/sandbox/tests/specification.rs b/nvx-rs/src/host/sandbox/tests/specification.rs index 8e990c8f5..83d30577f 100644 --- a/nvx-rs/src/host/sandbox/tests/specification.rs +++ b/nvx-rs/src/host/sandbox/tests/specification.rs @@ -11,7 +11,10 @@ use proptest::prelude::*; use super::distro::DistroFormat; #[cfg(feature = "cli")] use super::layer::{LAYER_ROLES, LayerRole, SandboxLayer}; -use super::mount::{parse_mount_value, validate_mount_target}; +use super::mount::tests::validate_mount_target; +use super::mount::{ + MountAccess, MountKind, MountOwner, MountPolicy, SandboxMount, parse_mount_value, +}; use super::*; use crate::ErrorKind; use crate::protocol::cmdline::tests::model; diff --git a/nvx-rs/src/protocol/cmdline.rs b/nvx-rs/src/protocol/cmdline.rs index bd7a76b78..79bf0affc 100644 --- a/nvx-rs/src/protocol/cmdline.rs +++ b/nvx-rs/src/protocol/cmdline.rs @@ -26,9 +26,9 @@ //! [`HostExport::parse`](super::export::HostExport::parse), //! [`RootLayout::parse`](super::root::RootLayout::parse), and //! [`parse_host_mapping_count`](super::parse_host_mapping_count) read their tokens with it, and -//! so do the time ABI's check of report-only mode, in `nvx::guest::time`, and the sandbox plans' -//! check of the user's kernel parameters, in `nvx::host::sandbox`, which splits them both as the -//! kernel and as the init does, and compares their names as the kernel does. +//! so do the time ABI's check of report-only mode, in `nvx::guest::time`, and the CLI's check of +//! the user's kernel parameters of a sandbox, which splits them both as the kernel and as the +//! init does, and compares their names as the kernel does. //! //! The kernel leaves OpenVMM's `virtnet_dns=` and `virtnet_gw=` tokens to the guest, which reads //! them from the whole command line, as OpenVMM checks them: diff --git a/nvx-rs/tests/cli/egress.rs b/nvx-rs/tests/cli/egress.rs index 73c6a160e..fa9392206 100644 --- a/nvx-rs/tests/cli/egress.rs +++ b/nvx-rs/tests/cli/egress.rs @@ -2,9 +2,6 @@ //! which the library lowers an egress policy file, each list in the order in which OpenVMM receives //! them, or fails with the library's refusal of the file. -use std::path::Path; - -use nvx::host::network::policy::compile_policy_file; use serde_json::{Value, json}; use crate::harness::Root; @@ -103,8 +100,6 @@ fn egress_compile_prints_the_library_lowering() { root.write("policy.json", policy); } let outcome = root.nvx_rs(&["egress", "compile", case.path]); - let path = root.expand(case.path); - let lowered = compile_policy_file(Path::new(&path)); eprintln!("case: {}", case.name); match case.expected { Ok((allow, deny)) => { @@ -116,13 +111,6 @@ fn egress_compile_prints_the_library_lowering() { "{}", case.name ); - let lowered = lowered.unwrap(); - assert_eq!( - printed, - json!({"allow": lowered.allow, "deny": lowered.deny}), - "{}", - case.name - ); } Err((code, fragments)) => { let fragments: Vec = fragments @@ -130,10 +118,7 @@ fn egress_compile_prints_the_library_lowering() { .map(|fragment| root.expand(fragment)) .collect(); let fragments: Vec<&str> = fragments.iter().map(String::as_str).collect(); - let message = outcome.assert_error(code, &fragments); - let refusal = lowered.unwrap_err(); - assert_eq!(refusal.code().as_str(), code, "{}", case.name); - assert_eq!(message, refusal.message(), "{}", case.name); + outcome.assert_error(code, &fragments); } } } diff --git a/nvx-rs/tests/cli/openvmm.rs b/nvx-rs/tests/cli/openvmm.rs index f46b8e3ce..a10f46639 100644 --- a/nvx-rs/tests/cli/openvmm.rs +++ b/nvx-rs/tests/cli/openvmm.rs @@ -14,17 +14,29 @@ use crate::harness::{HYPERVISOR, OPENVMM, Root, is_hex}; /// Caller bytes left by the two-directory fixture, its native bootstrap, and a terminating NUL. pub(crate) fn two_share_command_line_room() -> usize { use nvx::host::filesystem::GUEST_EXPORT; - use nvx::host::sandbox::mount::MOUNT_TAG; let kernel = "nvx_hostname=6e7678 nvx_machine_id=image nvx_maps=2 nvx_mode=one-shot \ nvx_share=0,/a,ro nvx_share=1,/b,rw"; - // OpenVMM appends the tokens of the aggregate, read-write since one share is. - let fragment = format!( - " virtfs_dir={GUEST_EXPORT} virtfs_tag={MOUNT_TAG} virtfs_mode=rw virtfs_aggregate=1" - ); + // OpenVMM appends the tokens of the aggregate, read-write since one share is, which name its + // virtio-fs device by its tag, `microvm`. + let fragment = + format!(" virtfs_dir={GUEST_EXPORT} virtfs_tag=microvm virtfs_mode=rw virtfs_aggregate=1"); nvx::host::config::COMMAND_LINE_BUDGET - kernel.len() - fragment.len() - 2 } +/// Splits an OpenVMM `--mount` or `--mount-child` value of a fixture, `NAME,HOST_PATH[,ro|rw]`, +/// as OpenVMM reads it: the name, a guest target or a child's, ends at the first comma, and with +/// another comma the mode follows the last one. Returns the name, the host path, and whether the +/// share is read-only, as it is by default. +fn mount_value(value: &str) -> (String, String, bool) { + let (name, rest) = value + .split_once(',') + .unwrap_or_else(|| panic!("{value:?} names no host path")); + let (host, mode) = rest.rsplit_once(',').unwrap_or((rest, "ro")); + assert!(matches!(mode, "ro" | "rw"), "{value:?} has no mode"); + (name.to_owned(), host.to_owned(), mode == "ro") +} + /// The options of OpenVMM that take no value. const FLAGS: [&str; 3] = [ "--single-process", @@ -257,7 +269,6 @@ impl Options { NetworkProxy, }; use nvx::host::runtime::{ArtifactIdentity, LaunchOptions, Runtime, RuntimeIdentity}; - use nvx::host::sandbox::mount::{MountAccess, parse_mount_value}; use nvx::host::vm::{self, VmDir}; use nvx::host::network::{ForwardProtocol, Protocol}; @@ -267,7 +278,7 @@ impl Options { let mut shares = Vec::new(); let mut files = BTreeSet::new(); for value in old.values("--mount") { - let (target, host, access) = parse_mount_value(value)?; + let (target, host, read_only) = mount_value(value); let host = PathBuf::from(host); let host = if host.is_absolute() { host @@ -277,13 +288,13 @@ impl Options { if host.is_file() { files.insert(target.clone()); } - shares.push((target, host, access)); + shares.push((target, host, read_only)); } let tokens: Vec<_> = caller.split_ascii_whitespace().map(str::to_owned).collect(); let mut generated = Vec::new(); for value in old.values("--mount-child") { let child = value.strip_suffix(",file").unwrap_or(value); - let (name, host, access) = parse_mount_value(child)?; + let (name, host, read_only) = mount_value(child); let token = tokens .iter() .find(|token| { @@ -307,7 +318,7 @@ impl Options { if value.ends_with(",file") { files.insert(target.clone()); } - shares.push((target, host, access)); + shares.push((target, host, read_only)); generated.push(token.clone()); } for token in generated.iter().rev() { @@ -342,11 +353,11 @@ impl Options { } let mappings: Vec<_> = shares .iter() - .map(|(target, host, access)| Mapping { + .map(|(target, host, read_only)| Mapping { name: "--mount", path: host, target: Target::Guest(target), - read_only: *access == MountAccess::ReadOnly, + read_only: *read_only, }) .collect(); let export = filesystem::plan(&ExportRequest {