From 5364d0aa1e2ee250a63311d80362e26c0b0def29 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Mon, 19 Oct 2026 12:24:41 -0300 Subject: [PATCH] nvx-rs: compute the outermost export paths with one function filesystem::plan reduces three lists of canonical host paths to the paths that no other path in the list contains, and did it three ways. export_roots sorted, deduplicated, and filtered the mapped paths inline. denied_paths sorted and deduplicated the denied paths, collected the nested ones, and removed them with retain. The paths to which OpenVMM limits a read-only child's writes went through outermost, which filtered Mapped entries by their canonical paths, and plan then mapped and sorted the survivors. outermost now takes the paths themselves: it sorts them, drops duplicates, and returns those that no other contains. export_roots, denied_paths, and the write list of plan call it, and the two inline copies are gone. plan collects a child's read-write mappings as their canonical paths, which is all that the writable flag and the write list need. Each list keeps its contents and order. export_roots and denied_paths ran the same sort, deduplication, and containment test that outermost runs now. plan keeps one mapped entry per canonical path, so deduplicating the read-write paths removes nothing, and sorting them before the containment test instead of after it gives the same sorted list. The plan tests cover the three lists, and a new unit test pins the order, deduplication, and containment by whole components that they share. A throwaway differential test compared the three former computations with outermost on 7.3 million lists of paths, every list of up to three paths and random lists of up to twelve, in Windows and Linux path forms: all results were identical. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nvx-rs/src/host/filesystem.rs | 64 ++++++++++++++--------------------- 1 file changed, 25 insertions(+), 39 deletions(-) diff --git a/nvx-rs/src/host/filesystem.rs b/nvx-rs/src/host/filesystem.rs index a0e59cefa..a8df69f37 100644 --- a/nvx-rs/src/host/filesystem.rs +++ b/nvx-rs/src/host/filesystem.rs @@ -347,22 +347,18 @@ pub fn plan(request: &ExportRequest<'_>) -> Result> { root.display() ))); }; - let read_write: Vec<&Mapped> = inside + let read_write: Vec = inside .iter() - .copied() .filter(|entry| !entry.read_only) + .map(|entry| entry.canonical.clone()) .collect(); let writable = !read_write.is_empty(); // Unless the read-write mappings cover the whole child, OpenVMM limits writes to them. - let mut write: Vec = if writable && own.read_only { - outermost(&read_write) - .iter() - .map(|entry| entry.canonical.clone()) - .collect() + let write = if writable && own.read_only { + outermost(read_write) } else { Vec::new() }; - write.sort(); let child_denied: Vec = denied .iter() .filter(|path| path.starts_with(&root)) @@ -448,18 +444,7 @@ fn volume_root_refusal(canonical: &Path) -> Option<&'static str> { /// Returns the paths to export, in order: the outermost mapped directories and the mapped files /// outside them, each exported alone. Distinct paths must not name one directory or file. fn export_roots(mapped: &[Mapped]) -> Result> { - let mut candidates: Vec = mapped.iter().map(|entry| entry.canonical.clone()).collect(); - candidates.sort(); - candidates.dedup(); - let roots: Vec = candidates - .iter() - .filter(|candidate| { - !candidates - .iter() - .any(|other| other != *candidate && candidate.starts_with(other)) - }) - .cloned() - .collect(); + let roots = outermost(mapped.iter().map(|entry| entry.canonical.clone()).collect()); // OpenVMM refuses one object exported twice, which bind mounts and hard links make // possible. let mut identities: Vec<(FileIdentity, &Path)> = Vec::with_capacity(roots.len()); @@ -517,31 +502,21 @@ fn denied_paths(list: PathList<'_>, mapped: &[Mapped]) -> Result> { } denied.push(canonical); } - denied.sort(); - denied.dedup(); - let nested: Vec = denied + Ok(outermost(denied)) +} + +/// Sorts `paths`, drops duplicates, and returns those that no other of them contains. +fn outermost(mut paths: Vec) -> Vec { + paths.sort(); + paths.dedup(); + paths .iter() .filter(|path| { - denied + !paths .iter() .any(|other| other != *path && path.starts_with(other)) }) .cloned() - .collect(); - denied.retain(|path| !nested.contains(path)); - Ok(denied) -} - -/// Returns the entries that no other entry contains. -fn outermost<'a>(entries: &[&'a Mapped]) -> Vec<&'a Mapped> { - entries - .iter() - .copied() - .filter(|entry| { - !entries.iter().any(|other| { - other.canonical != entry.canonical && entry.canonical.starts_with(&other.canonical) - }) - }) .collect() } @@ -1359,6 +1334,17 @@ mod tests { ); } + /// Checks the order, duplicates, and containment by whole components that the roots, denied + /// paths, and writable paths share, which needs no host paths. + #[test] + fn outermost_paths_are_sorted_once_without_the_paths_inside_others() { + let paths = ["/b/c", "/a/bc", "/b", "/a/b", "/b/c/d", "/a/b"].map(PathBuf::from); + assert_eq!( + outermost(paths.to_vec()), + ["/a/b", "/a/bc", "/b"].map(PathBuf::from) + ); + } + #[test] fn unenforceable_policies_are_rejected() { let directory = root();