From c6d9d6042b8fa0415812b4724850dcf8c09ebce4 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Thu, 12 Nov 2026 06:32:40 -0800 Subject: [PATCH] host: delete the planner items that only tests use SL6.5a2 (#1125) makes the export planner crate-private and gates it with cli. Five of its items have no caller in any NVX program, only in the public API and in tests, so the warning-denied build of the cli feature would find them dead once they are private. This deletes them first, so that SL6.5a2 stays visibility-only: - resolve_guest_path, with its re-exports at nvx:: and nvx::host::filesystem::, which gave the guest path of a host path once resolved, or as spelled when it did not resolve. No NVX program translates a host path that way. guest_path, the convention itself, stays public at both paths. - Target::Convention, with its branch of resolve_mapping, which derived a mapping's target from its resolved host path, and Mapping::convention, which mapped a list's paths there. NVX's callers, nvx-rs run and the sandbox's shares, give every target. resolve_mapping takes the target that the mapping gives, and checks it as before. - ExportRequest::new, and PathList::EMPTY, which only it used. The CLI builds its requests field by field. The docs of host::filesystem and of plan say that the caller gives each target, which guest_path may derive, and the README's filesystem section follows. The tests keep what they checked of the code that stays: - host::filesystem's tests place each path of their plan helper at the guest path that guest_path derives from the resolved host path, as the convention did, with a convention helper, and build their requests directly: plain_request holds what ExportRequest::new did. - resolved_guest_paths_canonicalize_temp_directory_case_aliases retires with resolve_guest_path. On Windows, spellings_of_one_object_share_its_guest_path now takes its case, the object's whole path in capitals, that of the temporary directory included, whose guest path as spelled is not the resolved path's: it plans to the object's one child, at the resolved path's guest path. - planned_boot_shares_keep_table_sources_targets_access_and_file_kind and the security checklist build their requests directly, the checklist through a helper that takes the owner, which the_caller_owner_is_refused_on_windows sets. The public API snapshots lose 6 lines on each platform, the five items with resolve_guest_path at both of its paths, and gain none. Part of #1122 (SL6.6d). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nvx-rs/README.md | 14 +- nvx-rs/public-api/linux.txt | 6 - nvx-rs/public-api/windows.txt | 6 - nvx-rs/src/host/cli/run/guest_tokens/tests.rs | 15 +- nvx-rs/src/host/filesystem/guest_path.rs | 14 -- nvx-rs/src/host/filesystem/mod.rs | 103 +++----------- nvx-rs/src/host/filesystem/tests.rs | 133 +++++++++++------- nvx-rs/src/lib.rs | 2 +- nvx-rs/tests/security_checklist.rs | 34 +++-- 9 files changed, 145 insertions(+), 182 deletions(-) diff --git a/nvx-rs/README.md b/nvx-rs/README.md index 4f6a51f6b..37cbc960f 100644 --- a/nvx-rs/README.md +++ b/nvx-rs/README.md @@ -937,13 +937,13 @@ action that needs more than 256 rules. ### `nvx::host::filesystem` The planner of every NVX caller: `plan` turns an `ExportRequest`, the mapped -host paths at explicit guest targets or at those of the host-path convention, -their denied, allowed, and write paths, and the owner, into the `ExportPlan` of -host paths through OpenVMM's aggregate virtio-fs device, for a root's -`ReservedTargets`, the blockless root's or the container root's: its children, -its ordered binds, which give the host-mapping table, and the identities that -each start checks again; and `guest_path` and `resolve_guest_path` (also at -`nvx::`). +host paths at their guest targets, their denied, allowed, and write paths, and +the owner, into the `ExportPlan` of host paths through OpenVMM's aggregate +virtio-fs device, for a root's `ReservedTargets`, the blockless root's or the +container root's: its children, its ordered binds, which give the host-mapping +table, and the identities that each start checks again; and `guest_path` (also +at `nvx::`), the guest path at which the host-path convention places a resolved +host path. ### `nvx::host::nvxs` diff --git a/nvx-rs/public-api/linux.txt b/nvx-rs/public-api/linux.txt index 78cada8a3..0003feaca 100644 --- a/nvx-rs/public-api/linux.txt +++ b/nvx-rs/public-api/linux.txt @@ -968,7 +968,6 @@ struct nvx::host::filesystem::ExportRequest field nvx::host::filesystem::ExportRequest::allowed field nvx::host::filesystem::ExportRequest::denied field nvx::host::filesystem::ExportRequest::mappings -method nvx::host::filesystem::ExportRequest::new field nvx::host::filesystem::ExportRequest::owner field nvx::host::filesystem::ExportRequest::reserved field nvx::host::filesystem::ExportRequest::write @@ -978,7 +977,6 @@ field nvx::host::filesystem::FileIdentity::index const nvx::host::filesystem::GUEST_EXPORT const nvx::host::filesystem::MAX_CHILDREN struct nvx::host::filesystem::Mapping -method nvx::host::filesystem::Mapping::convention field nvx::host::filesystem::Mapping::name field nvx::host::filesystem::Mapping::path field nvx::host::filesystem::Mapping::read_only @@ -987,7 +985,6 @@ enum nvx::host::filesystem::Owner variant nvx::host::filesystem::Owner::Caller variant nvx::host::filesystem::Owner::Vmm struct nvx::host::filesystem::PathList -const nvx::host::filesystem::PathList::EMPTY field nvx::host::filesystem::PathList::name field nvx::host::filesystem::PathList::paths const nvx::host::filesystem::RESERVED_TARGETS @@ -998,12 +995,10 @@ field nvx::host::filesystem::ReservedTargets::exact method nvx::host::filesystem::ReservedTargets::reservation field nvx::host::filesystem::ReservedTargets::trees enum nvx::host::filesystem::Target -variant nvx::host::filesystem::Target::Convention variant nvx::host::filesystem::Target::Guest field nvx::host::filesystem::Target::Guest::0 fn nvx::host::filesystem::guest_path fn nvx::host::filesystem::plan -fn nvx::host::filesystem::resolve_guest_path mod nvx::host::network enum nvx::host::network::Access variant nvx::host::network::Access::Allow @@ -1799,7 +1794,6 @@ const nvx::protocol::view::HOSTNAME_TOKEN const nvx::protocol::view::MACHINE_ID_TOKEN fn nvx::protocol::view::parse fn nvx::protocol::view::tokens -fn nvx::resolve_guest_path mod nvx::testing (hidden) mod nvx::testing::fake_openvmm (hidden) fn nvx::testing::fake_openvmm::main (hidden) diff --git a/nvx-rs/public-api/windows.txt b/nvx-rs/public-api/windows.txt index f2aff1fcb..f2a80bec3 100644 --- a/nvx-rs/public-api/windows.txt +++ b/nvx-rs/public-api/windows.txt @@ -426,7 +426,6 @@ struct nvx::host::filesystem::ExportRequest field nvx::host::filesystem::ExportRequest::allowed field nvx::host::filesystem::ExportRequest::denied field nvx::host::filesystem::ExportRequest::mappings -method nvx::host::filesystem::ExportRequest::new field nvx::host::filesystem::ExportRequest::owner field nvx::host::filesystem::ExportRequest::reserved field nvx::host::filesystem::ExportRequest::write @@ -436,7 +435,6 @@ field nvx::host::filesystem::FileIdentity::index const nvx::host::filesystem::GUEST_EXPORT const nvx::host::filesystem::MAX_CHILDREN struct nvx::host::filesystem::Mapping -method nvx::host::filesystem::Mapping::convention field nvx::host::filesystem::Mapping::name field nvx::host::filesystem::Mapping::path field nvx::host::filesystem::Mapping::read_only @@ -445,7 +443,6 @@ enum nvx::host::filesystem::Owner variant nvx::host::filesystem::Owner::Caller variant nvx::host::filesystem::Owner::Vmm struct nvx::host::filesystem::PathList -const nvx::host::filesystem::PathList::EMPTY field nvx::host::filesystem::PathList::name field nvx::host::filesystem::PathList::paths const nvx::host::filesystem::RESERVED_TARGETS @@ -456,12 +453,10 @@ field nvx::host::filesystem::ReservedTargets::exact method nvx::host::filesystem::ReservedTargets::reservation field nvx::host::filesystem::ReservedTargets::trees enum nvx::host::filesystem::Target -variant nvx::host::filesystem::Target::Convention variant nvx::host::filesystem::Target::Guest field nvx::host::filesystem::Target::Guest::0 fn nvx::host::filesystem::guest_path fn nvx::host::filesystem::plan -fn nvx::host::filesystem::resolve_guest_path mod nvx::host::network enum nvx::host::network::Access variant nvx::host::network::Access::Allow @@ -1257,7 +1252,6 @@ const nvx::protocol::view::HOSTNAME_TOKEN const nvx::protocol::view::MACHINE_ID_TOKEN fn nvx::protocol::view::parse fn nvx::protocol::view::tokens -fn nvx::resolve_guest_path mod nvx::testing (hidden) mod nvx::testing::fake_openvmm (hidden) fn nvx::testing::fake_openvmm::main (hidden) diff --git a/nvx-rs/src/host/cli/run/guest_tokens/tests.rs b/nvx-rs/src/host/cli/run/guest_tokens/tests.rs index ed46ea5f1..0f41932e6 100644 --- a/nvx-rs/src/host/cli/run/guest_tokens/tests.rs +++ b/nvx-rs/src/host/cli/run/guest_tokens/tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::host::filesystem::{ExportRequest, Mapping, ReservedTargets, Target}; +use crate::host::filesystem::{ExportRequest, Mapping, Owner, PathList, ReservedTargets, Target}; #[test] fn blockless_modes_and_workloads_use_cli_owned_names() { @@ -64,10 +64,15 @@ fn planned_boot_shares_keep_table_sources_targets_access_and_file_kind() { read_only: true, }, ]; - let plan = crate::host::filesystem::plan(&ExportRequest::new( - &mappings, - ReservedTargets::BLOCKLESS_ROOT, - )) + let none = |name| PathList { name, paths: &[] }; + let plan = crate::host::filesystem::plan(&ExportRequest { + mappings: &mappings, + denied: none("--mount-deny"), + allowed: none("--mount-allow"), + write: none("--mount-write"), + owner: Owner::Vmm, + reserved: ReservedTargets::BLOCKLESS_ROOT, + }) .unwrap() .unwrap(); let table = plan.guest_table(); diff --git a/nvx-rs/src/host/filesystem/guest_path.rs b/nvx-rs/src/host/filesystem/guest_path.rs index 6f92b54f0..187abeb76 100644 --- a/nvx-rs/src/host/filesystem/guest_path.rs +++ b/nvx-rs/src/host/filesystem/guest_path.rs @@ -2,8 +2,6 @@ use std::path::{Component, Path, Prefix}; -use super::host_path::canonicalize; - /// Joins an aggregate child index and child-relative source into a guest export path. pub(super) fn guest_source(child: usize, source: &str) -> String { if source.is_empty() { @@ -54,18 +52,6 @@ pub fn guest_path(path: &Path) -> Option { Some(guest) } -/// Returns the guest path of an existing host path the way mappings derive it: from the -/// resolved path, so links, letter case, and short names do not matter. A path that does not -/// exist is translated as written. -/// -/// Use it to turn a host working directory into a `process.cwd` inside a mapped path. -pub fn resolve_guest_path(path: &Path) -> Option { - match canonicalize(path) { - Ok(canonical) => guest_path(&canonical), - Err(_) => guest_path(path), - } -} - /// Returns whether the guest path `path` is `directory` or one of its descendants, comparing /// whole components: `/run/nvx` is within `/run`, but `/runner` is not, and every absolute path is /// within the root, `/`. diff --git a/nvx-rs/src/host/filesystem/mod.rs b/nvx-rs/src/host/filesystem/mod.rs index 4fdc6094f..3dced3d5f 100644 --- a/nvx-rs/src/host/filesystem/mod.rs +++ b/nvx-rs/src/host/filesystem/mod.rs @@ -14,9 +14,9 @@ //! visible; the paths to which the writes of a read-write directory are narrowed; the host //! identity of the file operations; and the guest targets that the guest's root keeps for itself. //! -//! - A caller gives each target, or lets MXC's convention for Linux guests derive it from the -//! resolved host path: a Windows path `C:\work\src` appears at `/mnt/c/work/src`, and a Linux -//! path at the same path ([`fn@guest_path`]). +//! - A caller gives each target, such as the guest path that MXC's convention for Linux guests +//! derives from a resolved host path ([`fn@guest_path`]): a Windows path `C:\work\src` appears +//! at `/mnt/c/work/src`, and a Linux path at the same path. //! - The reserved targets depend on the root, and the caller supplies them ([`ReservedTargets`]): //! the blockless root keeps the guest's system trees, and the container root of a layered //! sandbox keeps the trees of its runtime. @@ -51,9 +51,9 @@ use crate::protocol::MAX_HOST_MAPPINGS; use crate::protocol::export::EXPORT_DIRECTORY; use crate::protocol::share::{RESERVED_EXACT_MOUNT_TARGETS, RESERVED_MOUNT_TARGETS}; +pub use self::guest_path::guest_path; pub(crate) use self::guest_path::within; use self::guest_path::{depth, guest_source}; -pub use self::guest_path::{guest_path, resolve_guest_path}; pub(crate) use self::host_path::{canonicalize, is_reparse_point}; use self::host_path::{identity, relative_text}; use self::limits::{check_policy_paths, check_root}; @@ -240,8 +240,6 @@ impl Bind { /// Where the guest sees a mapped host path. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Target<'a> { - /// At the guest path that [`fn@guest_path`] derives from the resolved host path. - Convention, /// At this absolute guest path: a path other than the root, of at most 4,095 bytes, without /// NUL characters or empty, `.`, or `..` components. Guest(&'a str), @@ -261,19 +259,6 @@ pub struct Mapping<'a> { pub read_only: bool, } -impl<'a> Mapping<'a> { - /// Returns the mappings of the paths of `list`, under its name, at the guest paths of the - /// convention, read-only or read-write. - pub fn convention(list: PathList<'a>, read_only: bool) -> impl Iterator + 'a { - list.paths.iter().map(move |path| Self { - name: list.name, - path, - target: Target::Convention, - read_only, - }) - } -} - /// A list of host paths, and the name by which messages refer to its entries, such as /// `readOnly`. #[derive(Debug, Clone, Copy)] @@ -284,14 +269,6 @@ pub struct PathList<'a> { pub paths: &'a [PathBuf], } -impl PathList<'static> { - /// A list without paths. - pub const EMPTY: Self = Self { - name: "", - paths: &[], - }; -} - /// The host paths that a sandbox maps, those that it hides inside them and shows again inside /// those, the paths to which it narrows writes, the host identity of the file operations, and the /// guest targets that the guest's root keeps for itself. @@ -315,27 +292,11 @@ pub struct ExportRequest<'a> { pub reserved: ReservedTargets<'a>, } -impl<'a> ExportRequest<'a> { - /// Returns the request that maps `mappings` into a root that reserves `reserved`, without - /// denied, allowed, or write paths, as the VMM. - pub fn new(mappings: &'a [Mapping<'a>], reserved: ReservedTargets<'a>) -> Self { - Self { - mappings, - denied: PathList::EMPTY, - allowed: PathList::EMPTY, - write: PathList::EMPTY, - owner: Owner::Vmm, - reserved, - } - } -} - /// Plans the export of `request`, or returns `None` if it maps nothing. /// -/// Mapped paths must exist, and resolve to paths that are valid UTF-8. A mapping's target is the -/// one that the request gives, or the guest path that the convention derives from the resolved -/// host path, so two spellings of one object (links, letter case, or short names) map to one guest -/// path; either must be free in the request's [`ReservedTargets`]. An object mapped twice at one +/// Mapped paths must exist, and resolve to paths that are valid UTF-8, so two spellings of one +/// object (links, letter case, or short names) name one object. A mapping's target, which the +/// request gives, must be free in the request's [`ReservedTargets`]. An object mapped twice at one /// target is mapped once, read-only if either mapping is. Two objects never share a target, and a /// target inside another mapping's must show the very object that the other mapping shows there. /// A whole volume cannot be mapped, and a policy that would need more children than OpenVMM @@ -556,7 +517,7 @@ fn resolve_mapping<'a>(mapping: &Mapping<'a>, reserved: ReservedTargets<'_>) -> let Mapping { name, path, - target, + target: Target::Guest(target), read_only, } = *mapping; let canonical = canonicalize(path).map_err(|error| { @@ -584,44 +545,24 @@ fn resolve_mapping<'a>(mapping: &Mapping<'a>, reserved: ReservedTargets<'_>) -> { return unsupported(format!("{name} entry {} {reason}", path.display())); } - let target = match target { - Target::Convention => { - let Some(target) = guest_path(&canonical) else { - return unsupported(format!( - "{name} entry {} has no guest path; use an absolute local path", - path.display() - )); - }; - if let Some(reserved) = reserved.reservation(&target) { - return unsupported(format!( - "{name} entry {} would cover the guest's {reserved}", - path.display() - )); - } - target - } - Target::Guest(target) => { - if let Some(problem) = target_problem(target) { - return unsupported(format!( - "{name} entry {} has the guest target {target:?}, which {problem}", - path.display() - )); - } - if let Some(reserved) = reserved.reservation(target) { - return unsupported(format!( - "{name} entry {} has the guest target {target}, which would cover the \ - guest's {reserved}", - path.display() - )); - } - target.to_owned() - } - }; + if let Some(problem) = target_problem(target) { + return unsupported(format!( + "{name} entry {} has the guest target {target:?}, which {problem}", + path.display() + )); + } + if let Some(reserved) = reserved.reservation(target) { + return unsupported(format!( + "{name} entry {} has the guest target {target}, which would cover the guest's \ + {reserved}", + path.display() + )); + } Ok(Mapped { name, given: path.to_path_buf(), canonical, - target, + target: target.to_owned(), read_only, directory: metadata.is_dir(), }) diff --git a/nvx-rs/src/host/filesystem/tests.rs b/nvx-rs/src/host/filesystem/tests.rs index 9cf6c9c72..37bd115ac 100644 --- a/nvx-rs/src/host/filesystem/tests.rs +++ b/nvx-rs/src/host/filesystem/tests.rs @@ -12,31 +12,69 @@ struct FilesystemPolicy { denied_paths: Vec, } +/// Plans `policy` into the blockless root, each path at the guest path at which MXC's convention +/// for Linux guests places it, [`convention`]. fn plan(policy: &FilesystemPolicy) -> Result> { - let mappings: Vec> = Mapping::convention( - PathList { - name: "filesystem.readonlyPaths", - paths: &policy.readonly_paths, - }, - true, - ) - .chain(Mapping::convention( - PathList { - name: "filesystem.readwritePaths", - paths: &policy.readwrite_paths, - }, - false, - )) + let entries: Vec<(&str, &Path, String, bool)> = [ + ("filesystem.readonlyPaths", &policy.readonly_paths, true), + ("filesystem.readwritePaths", &policy.readwrite_paths, false), + ] + .into_iter() + .flat_map(|(name, paths, read_only)| { + paths + .iter() + .map(move |path| (name, path.as_path(), convention(path), read_only)) + }) .collect(); + let mappings: Vec> = entries + .iter() + .map(|(name, path, target, read_only)| Mapping { + name, + path, + target: Target::Guest(target), + read_only: *read_only, + }) + .collect(); super::plan(&ExportRequest { denied: PathList { name: "filesystem.deniedPaths", paths: &policy.denied_paths, }, - ..ExportRequest::new(&mappings, ReservedTargets::BLOCKLESS_ROOT) + ..plain_request(&mappings, ReservedTargets::BLOCKLESS_ROOT) }) } +/// Returns the guest path at which MXC's convention for Linux guests places `path`: the one that +/// [`guest_path`] derives from the resolved host path, so that every spelling of one object +/// (links, letter case, or short names) takes one target. A path that does not resolve takes an +/// empty target, which the planner never reads, since it refuses the path first. +fn convention(path: &Path) -> String { + canonicalize(path) + .ok() + .and_then(|resolved| guest_path(&resolved)) + .unwrap_or_default() +} + +/// Returns the request that maps `mappings` into a root that reserves `reserved`, as the VMM, +/// without denied, allowed, or write paths. +fn plain_request<'a>( + mappings: &'a [Mapping<'a>], + reserved: ReservedTargets<'a>, +) -> ExportRequest<'a> { + let none = PathList { + name: "", + paths: &[], + }; + ExportRequest { + mappings, + denied: none, + allowed: none, + write: none, + owner: Owner::Vmm, + reserved, + } +} + fn root() -> tempfile::TempDir { let directory = tempfile::tempdir().unwrap(); for name in [ @@ -687,37 +725,34 @@ fn whole_volumes_are_refused() { fn spellings_of_one_object_share_its_guest_path() { let directory = root(); let base = directory.path(); - let upper = if cfg!(windows) { - base.join("WORK").join("SRC") + let src = base.join("work").join("src"); + // On Windows, its names in other letter case, below the temporary directory and throughout + // its path, that of the temporary directory included, whose guest path as spelled is not the + // resolved path's; elsewhere, its path with a `.` component. + let spellings = if cfg!(windows) { + let whole = PathBuf::from(src.to_str().unwrap().to_uppercase()); + assert_ne!(guest_path(&whole), guest_path(&canonical(&src))); + vec![base.join("WORK").join("SRC"), whole] } else { - base.join("work").join(".").join("src") + vec![base.join("work").join(".").join("src")] }; let policy = FilesystemPolicy { - readonly_paths: vec![upper], - readwrite_paths: vec![base.join("work").join("src")], + readonly_paths: spellings, + readwrite_paths: vec![src.clone()], ..FilesystemPolicy::default() }; let mapping = plan(&policy).unwrap().unwrap(); assert_eq!(mapping.binds.len(), 1); assert!(mapping.binds[0].read_only); + assert_eq!(mapping.children.len(), 1); + assert_eq!(mapping.children[0].root, canonical(&src)); assert!(!mapping.children[0].writable); assert_eq!( mapping.binds[0].target, - guest_path(&canonical(&base.join("work").join("src"))).unwrap() + guest_path(&canonical(&src)).unwrap() ); } -#[cfg(windows)] -#[test] -fn resolved_guest_paths_canonicalize_temp_directory_case_aliases() { - let directory = root(); - let path = directory.path().join("work").join("out"); - let upper = PathBuf::from(path.to_str().unwrap().to_uppercase()); - let expected = guest_path(&canonical(&path)).unwrap(); - assert_ne!(guest_path(&upper), Some(expected.clone())); - assert_eq!(resolve_guest_path(&upper), Some(expected)); -} - #[cfg(windows)] #[test] fn paths_that_only_a_verbatim_path_names_are_refused() { @@ -1100,7 +1135,7 @@ fn explicit_targets_place_mappings_at_any_free_guest_path() { // One object may appear at several targets. at(&work, "/mirror", false), ]; - let request = ExportRequest::new(&mappings, ReservedTargets::CONTAINER_ROOT); + let request = plain_request(&mappings, ReservedTargets::CONTAINER_ROOT); let mapping = super::plan(&request).unwrap().unwrap(); let roots: Vec<&Path> = mapping .children @@ -1129,7 +1164,7 @@ fn explicit_targets_place_mappings_at_any_free_guest_path() { exact: &[], }; for error in [ - super::plan(&ExportRequest::new(&mappings, everything)).unwrap_err(), + super::plan(&plain_request(&mappings, everything)).unwrap_err(), mapping.check(everything).unwrap_err(), ] { assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); @@ -1149,7 +1184,7 @@ fn explicit_targets_place_mappings_at_any_free_guest_path() { convention.check(ReservedTargets::BLOCKLESS_ROOT).unwrap(); let refused = |mappings: &[Mapping<'_>], expected: &str| { - let request = ExportRequest::new(mappings, ReservedTargets::CONTAINER_ROOT); + let request = plain_request(mappings, ReservedTargets::CONTAINER_ROOT); let error = super::plan(&request).unwrap_err(); assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); assert!( @@ -1314,7 +1349,7 @@ fn policy_paths_hide_show_and_narrow_writes() { name: "write", paths: write, }, - ..ExportRequest::new(mappings, ReservedTargets::CONTAINER_ROOT) + ..plain_request(mappings, ReservedTargets::CONTAINER_ROOT) }; super::plan(&request) }; @@ -1477,7 +1512,7 @@ fn narrowed_writes_pin_only_what_lies_in_writable_paths() { name: "write", paths: &write, }, - ..ExportRequest::new(&mappings, ReservedTargets::CONTAINER_ROOT) + ..plain_request(&mappings, ReservedTargets::CONTAINER_ROOT) }; let mapping = super::plan(&request).unwrap().unwrap(); let child = &mapping.children[0]; @@ -1537,7 +1572,7 @@ fn the_caller_owner_reaches_openvmm_on_linux_hosts() { let mappings = [at(&work, "/workspace", false)]; let request = ExportRequest { owner: Owner::Caller, - ..ExportRequest::new(&mappings, ReservedTargets::CONTAINER_ROOT) + ..plain_request(&mappings, ReservedTargets::CONTAINER_ROOT) }; if cfg!(windows) { let error = super::plan(&request).unwrap_err(); @@ -1571,7 +1606,7 @@ fn the_caller_owner_reaches_openvmm_on_linux_hosts() { let mappings = [at(system, "/srv/system", true)]; let request = ExportRequest { owner: Owner::Caller, - ..ExportRequest::new(&mappings, ReservedTargets::CONTAINER_ROOT) + ..plain_request(&mappings, ReservedTargets::CONTAINER_ROOT) }; let error = super::plan(&request).unwrap_err(); assert!( @@ -1583,12 +1618,9 @@ fn the_caller_owner_reaches_openvmm_on_linux_hosts() { } } // The default owner is left out of the record. - let vmm = super::plan(&ExportRequest::new( - &mappings, - ReservedTargets::CONTAINER_ROOT, - )) - .unwrap() - .unwrap(); + let vmm = super::plan(&plain_request(&mappings, ReservedTargets::CONTAINER_ROOT)) + .unwrap() + .unwrap(); assert!(serde_json::to_value(&vmm).unwrap().get("owner").is_none()); } @@ -1615,7 +1647,7 @@ fn read_write_mappings_to_which_writes_are_narrowed_stay_visible() { name: "write", paths: write, }, - ..ExportRequest::new(mappings, ReservedTargets::CONTAINER_ROOT) + ..plain_request(mappings, ReservedTargets::CONTAINER_ROOT) }) }; // OpenVMM narrows the writes of the tree to the read-write private, which it cannot hide: @@ -1671,7 +1703,7 @@ fn denied_mapped_directories_inside_denied_ones_show_their_allowed_paths() { name: "allowed", paths: allowed, }, - ..ExportRequest::new(mappings, ReservedTargets::CONTAINER_ROOT) + ..plain_request(mappings, ReservedTargets::CONTAINER_ROOT) }) }; let both = [at(&tree, "/tree", false), at(&private, "/private", false)]; @@ -1767,11 +1799,8 @@ fn mapped_paths_resolve_to_utf8_paths() { std::os::unix::fs::symlink(&share, &link).unwrap(); for path in [&share, &link] { let mappings = [at(path, "/workspace", false)]; - let error = super::plan(&ExportRequest::new( - &mappings, - ReservedTargets::CONTAINER_ROOT, - )) - .unwrap_err(); + let error = + super::plan(&plain_request(&mappings, ReservedTargets::CONTAINER_ROOT)).unwrap_err(); assert_eq!(error.kind(), ErrorKind::Unsupported, "{error}"); assert!( error diff --git a/nvx-rs/src/lib.rs b/nvx-rs/src/lib.rs index 9c91c39fd..468d1a576 100644 --- a/nvx-rs/src/lib.rs +++ b/nvx-rs/src/lib.rs @@ -157,5 +157,5 @@ mod tests; pub use error::{Error, ErrorCode, ErrorKind, Result}; #[cfg(feature = "host")] -pub use host::filesystem::{guest_path, resolve_guest_path}; +pub use host::filesystem::guest_path; pub use hypervisor::Hypervisor; diff --git a/nvx-rs/tests/security_checklist.rs b/nvx-rs/tests/security_checklist.rs index 519134092..437f0c7cf 100644 --- a/nvx-rs/tests/security_checklist.rs +++ b/nvx-rs/tests/security_checklist.rs @@ -19,19 +19,33 @@ use std::process::{Command, Stdio}; use nvx::activation::{MachineIdMode, View}; use nvx::host::config::{Boot, ConsoleAttachment, RootConfig, VmConfig}; -#[cfg(windows)] -use nvx::host::filesystem::Owner; -use nvx::host::filesystem::{self, ExportRequest, Mapping, ReservedTargets, Target}; +use nvx::host::filesystem::{ + self, ExportRequest, Mapping, Owner, PathList, ReservedTargets, Target, +}; use nvx::host::runtime::{ArtifactIdentity, LaunchOptions, Runtime, RuntimeIdentity}; use nvx::host::{snapshot, vm}; use nvx::{ErrorKind, Hypervisor}; +/// Returns the request that maps `mappings` into the blockless root as `owner`, without denied, +/// allowed, or write paths. +fn request<'a>(mappings: &'a [Mapping<'a>], owner: Owner) -> ExportRequest<'a> { + let none = PathList { + name: "", + paths: &[], + }; + ExportRequest { + mappings, + denied: none, + allowed: none, + write: none, + owner, + reserved: ReservedTargets::BLOCKLESS_ROOT, + } +} + /// Plans the export of `mappings` into the blockless root. fn plan(mappings: &[Mapping<'_>]) -> nvx::Result> { - filesystem::plan(&ExportRequest::new( - mappings, - ReservedTargets::BLOCKLESS_ROOT, - )) + filesystem::plan(&request(mappings, Owner::Vmm)) } #[test] @@ -94,10 +108,10 @@ fn an_export_refuses_a_read_only_path_inside_a_writable_directory() { #[cfg(windows)] #[test] fn the_caller_owner_is_refused_on_windows() { - let mut request = ExportRequest::new(&[], ReservedTargets::BLOCKLESS_ROOT); - request.owner = Owner::Caller; assert_eq!( - filesystem::plan(&request).unwrap_err().kind(), + filesystem::plan(&request(&[], Owner::Caller)) + .unwrap_err() + .kind(), ErrorKind::Unsupported ); }