From e48c68c6daa6f8d76a68805a78420ede593ecee9 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Thu, 12 Nov 2026 04:44:19 -0800 Subject: [PATCH 1/2] tests: check the policy reader and the mount syntax through the CLI SL6.7b (#1124) makes host::network::policy and host::sandbox private to the CLI. Two of the CLI's scenario tests reached them through their public paths, which they cannot once those are private, so they drive the nvx-rs binary alone, with the same expectations: - egress_compile_prints_the_library_lowering compared what `nvx-rs egress compile` printed with the rules of compile_policy_file, and its refusals with the reader's error. It keeps every case's native rules, error code and message fragments, which the binary must print, and the grammar, model and property tests of host::network::policy check the reader's own rules and refusals. - two_share_command_line_room spells the tag of OpenVMM's virtio-fs device, `microvm`, as the lifecycle tests do, in place of MOUNT_TAG, and cold_run_expected splits a fixture's --mount and --mount-child values as OpenVMM reads them, with a helper of its own, in place of parse_mount_value: independently of the CLI's parser, as its documentation says. Part of #1124 (SL6.7b). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nvx-rs/tests/cli/egress.rs | 17 +---------------- nvx-rs/tests/cli/openvmm.rs | 35 +++++++++++++++++++++++------------ 2 files changed, 24 insertions(+), 28 deletions(-) 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 { From 484e0c7ff038264ca1e43f26487c102bd56fd570 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Thu, 12 Nov 2026 04:49:10 -0800 Subject: [PATCH 2/2] host: make the policy-file reader and the mount syntax private to the CLI host::network::policy and host::sandbox were public API of nvx, although only NVX's CLI reads egress policy files and the options of nvx-rs sandbox and nvx-rs run: the policy-file reader and its limits, the rules that it compiles, the --mount syntax, the shares that it requests and their plan, the checks of the options, and the sandbox's defaults. They become crate-private where they are, gated by the features of their users, with no change of behavior: - host::network::policy becomes a pub(crate) module that only the cli feature builds, with its JSON reader, compile_policy_file, compile_policy_bytes, compile_policy and CompiledEgressPolicy, and its limits, MAX_RULES_PER_ACTION, MAX_POLICY_FILE_SIZE and MAX_JSON_DEPTH. The lowering already takes MAX_RULES directly, and the shared normalization stays public: EgressRule, Destination, PortSelector and NetworkPolicy::native_egress_rules. - host::sandbox becomes pub(crate). Its options module, with SandboxOptions, PolicyPathOption, SandboxOperation and the checks of the options, takes cli, and so do, in its mount module, the --mount syntax, SandboxMount, the mount types and limits, plan_mounts and plan_mounts_with, the checks of host paths and path_text, and, at its root, the defaults, parse_workload_identity, parse_decimal, has_kernel_separator and require_file. What the crate uses outside the CLI stays ungated: block_geometry and BlockGeometry, which host::vm and host::snapshot read, MOUNT_TAG, which a snapshot's key names, refused and os_error. - host::sandbox no longer re-exports the mount types, nor its mount module the reserved targets of protocol::share or validate_mount_target, which nothing reads at those paths once they are private. The sandbox's tests import the mount types from the mount module, and validate_mount_target through the mount module's tests, as they do validate_mount_host_paths_with. - What these users leave to the CLI takes cli too: host::artifacts::require_existing, which only require_file and the CLI's require_artifact call, and host::paths::resolve, with without_verbatim_prefix and its two tests, which only the CLI and the checks of shares call. The unit tests of host::network::policy and host::sandbox, and the two host::paths tests of resolve, take cli with their modules, so the all-features tests run them as before. The public API snapshots lose 132 lines on each platform, every path under nvx::host::sandbox and nvx::host::network::policy, and gain none. The docs of host, host::network and host::sandbox, and the doc links of openvmm_arguments, Destination and the lowering, name the reader and the profile as the CLI's; the README drops the two modules' rows and sections and holds their details in Egress policy files and Sandbox input contract. Part of #1124 (SL6.7b). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nvx-rs/README.md | 61 +++----- nvx-rs/public-api/linux.txt | 132 ------------------ nvx-rs/public-api/windows.txt | 132 ------------------ nvx-rs/src/host/artifacts/mod.rs | 4 +- nvx-rs/src/host/artifacts/require.rs | 6 +- nvx-rs/src/host/mod.rs | 20 +-- nvx-rs/src/host/network/arguments.rs | 5 +- nvx-rs/src/host/network/lowering.rs | 2 +- nvx-rs/src/host/network/mod.rs | 11 +- nvx-rs/src/host/network/policy/mod.rs | 18 +-- nvx-rs/src/host/network/rule.rs | 4 +- nvx-rs/src/host/paths.rs | 5 +- nvx-rs/src/host/paths/tests.rs | 4 +- nvx-rs/src/host/sandbox/mod.rs | 53 ++++--- nvx-rs/src/host/sandbox/mount/aggregate.rs | 2 +- nvx-rs/src/host/sandbox/mount/host_paths.rs | 9 +- nvx-rs/src/host/sandbox/mount/mod.rs | 95 ++++++++----- nvx-rs/src/host/sandbox/mount/request.rs | 51 +++++-- nvx-rs/src/host/sandbox/mount/tests.rs | 1 + nvx-rs/src/host/sandbox/options.rs | 69 ++++----- nvx-rs/src/host/sandbox/tests.rs | 7 +- .../src/host/sandbox/tests/specification.rs | 5 +- nvx-rs/src/protocol/cmdline.rs | 6 +- 23 files changed, 238 insertions(+), 464 deletions(-) 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: