You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
nvx-rs: plan every caller's host exports through one planner - #765
One planner now turns host shares into an export plan for the CLI's sandbox shares and for the external orchestrators that use nvx::host::filesystem, for the blockless root and for the container root of a layered sandbox. This is the first part of A4's shared export composition; delivery does not change yet.
What changed
nvx::host::filesystem is the one planner.
An ExportRequest holds Mappings, each at an explicit guest target (Target::Guest) or at the target that the host-path convention derives (Target::Convention, Mapping::convention).
The caller supplies the guest targets that its root keeps (ReservedTargets): BLOCKLESS_ROOT (RESERVED_TARGETS), or CONTAINER_ROOT, built from protocol::share's trees and exact paths. Ancestors of a reserved tree are refused too.
The request carries denied, allowed, and write paths and the owner (Owner::Vmm or Owner::Caller), which are the CLI's --mount-deny, --mount-allow, --mount-write, and --mount-owner.
The planner also refuses mapped paths that do not resolve to UTF-8 and read-write mappings that OpenVMM would have to make writable while hiding them.
ExportPlan::check validates a recorded plan without reading the host: host paths in the form in which the planner resolves them, OpenVMM's rules for the denied, allowed, and write paths of each exported directory, write paths that match its read-write mappings as the planner derives them, and the identity of every denied or mapped object that a workload could move.
Plans no longer record hidden children, which only the reading of earlier records kept.
The typed plan holds the export plan.
SandboxPlan holds the ExportPlan instead of SandboxMount shares (SandboxPlan::with_export). Its serde form is version 3, and earlier versions are not read.
SandboxMount stays as the CLI's parsed --mount request. plan_mounts plans the requests through the planner for the container root, and boot_shares renders a plan as today's OpenVMM arguments and nvx_share= boot tokens. It refuses a bind that the tokens cannot express: a path below a share's root.
The CLI plans its shares once.
run and provision plan the shares from their resolved host paths. provision records the plan in config.json format 7, and start checks it again, including that its exported directories and files still do not overlap, by path or through a hard link or bind mount.
Formats 2 to 6, which recorded the requests, are no longer read; such a sandbox is deprovisioned and provisioned again.
A one-shot run checks its plan again after the distro layer's explicit verification, as start does, including that its exported directories and files still do not overlap.
nvx-rs run, the plain VM command that nvx-rs: make the nvx-rs CLI the backend of nvx.py run #704 ported from nvx.py run, still renders its shares from its options, as the Python implementation did, through mount::aggregate_share_tokens, which this PR keeps for it. Moving it onto the planner is left for a follow-up.
aci_edge_sandboxes keeps using the planner through its LaunchSpec; only its call site changes. Its sandbox records move to format 4.
Behavior changes
The shares and their policy paths are resolved once, when they are planned. The aggregate's children follow their resolved host paths, and sandbox follows a symbolic link in a policy path to a path inside the same share. OpenVMM receives the resolved paths, so a dry run prints them.
A denied path that does not exist inside a share is refused, and so is a whole-volume share.
A policy path outside the host directory of a sandbox's only share fails before OpenVMM starts (status 1 instead of OpenVMM's status 2).
On Windows, the planner refuses a host path that only a verbatim \\?\ path names, such as that of a directory whose name ends in a period or a space, for every caller. It used to drop the prefix, and the path without it names another object, or none.
The CLI goldens change only there and in the recorded config.json of the share lifecycles. The microVM workflow reads the recorded shares from the export plan.
At this head, on dev at 03d945bc, on Windows 11 (WHP), and on Linux with MSHV on a VM host, where builds and checks ran as a non-root user and VMs as root, which alone may open the hypervisor device there:
python scripts/nvx.py verify
In nvx-rs, with NVX_BUNDLE=skip: cargo fmt --all --check; cargo clippy --locked --all-targets --all-features -- -D warnings; cargo clippy --locked --lib --no-default-features -- -D warnings; cargo test --locked --all-features; cargo test --locked; cargo build --locked --lib --no-default-features and cargo test --locked --lib --no-default-features; cargo doc --locked --no-deps --all-features with RUSTDOCFLAGS=-D warnings; cargo +1.89 check --locked --all-targets --all-features; on Linux also the aarch64-apple-darwin check, the agent bundling test, and the guest binaries' check and build.
aci_edge_sandboxes: the steps of check-aci-edge-sandboxes (fmt, both clippy configurations, both test configurations, docs, MSRV; macOS check on Linux).
tests/runtime-adapter: the protocol-only and host-only configurations on both, and the guest-only (musl) configuration on Linux, whose MSRV check used the GNU target there.
check-quality's Python and shell checks: ruff check, ruff format --check, pyright --pythonplatform Linux and Windows with the pinned tools of requirements-dev.txt, and shellcheck and shfmt with the pinned images.
validate-nvx's compileall, unittest list, and help commands on both, with NVX_RS naming a release build of nvx-rs with the cli feature, as CI's CLI jobs run them, so that the tests that run the real nvx-rs run too; on Windows, the test_setup_azure tests of the Linux setup scripts fail on this host with and without this change.
Real VMs on WHP, and on MSHV (OpenVMM with --cpu-profile host on this VM host), with the artifacts of v0.1.0-dev.03d945bc2d3a, the development release of this base, whose OpenVMM is 7ff31ed7, the base's gitlink, and on both the Alpine initramfs of this tree, built on Linux:
the microVM workflow's Ubuntu sandbox share steps: one-shot and managed shares, concurrent read-write, read-only and file shares, and the access policy;
nvx.py test-microvm with filesystem-shares, filesystem-policy, filesystem-comma-paths, filesystem-owner, denied-filesystem-paths, filesystem-snapshot, managed-lifecycle, managed-exec-config, and sandbox-lifecycle;
nvx.py test-aci-edge-sandboxes: all pass on WHP; on MSHV, as root, the five tests that write through read-write mappings fail with Permission denied with and without this change, and the other 13 pass;
the runtime adapter's hypervisor tests: cargo test --locked --features host -- --ignored in tests/runtime-adapter;
the virtio restore probe with a virtio-fs share (nvx.py benchmark --suite device-restore-profile --restore-devices virtiofs --warmups 0 --runs 1), in both activation modes.
Left to CI: KVM, the caller-owned share step (it needs a non-root user on Linux), the full microVM matrix, and benchmarks.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Cross-platform host-access enforcement and incompatible persisted-state changes warrant final human review alongside the remaining CI validation.
No open issues 🎉
What changed in this PR
Unifies host-share planning for the CLI and external orchestrators through nvx::host::filesystem, advancing A4’s shared export composition while retaining the existing delivery mechanisms.
Changes:
Adds explicit or convention-derived guest targets, root-specific reservations, and shared access-policy and ownership validation.
Stores export plans in typed and CLI sandbox state, with new record versions and host checks before launch.
Updates adapter integration, tests, workflow assertions, and documentation for the planned exports.
File
Description
nvx-rs/tests/cli.rs
Normalizes export identity fields in CLI recordings.
nvx-rs/src/host/sandbox/tests.rs
Adapts launch tests to export plans.
nvx-rs/src/host/sandbox/plan/tests.rs
Tests export validation and versioned persistence.
nvx-rs/src/host/sandbox/plan/form.rs
Serializes export plans instead of mount requests.
nvx-rs/src/host/sandbox/plan.rs
Holds, checks, and renders planned exports.
nvx-rs/src/host/sandbox/options/tests.rs
Updates share-option expectations for resolved paths.
nvx-rs/src/host/sandbox/mount/tests.rs
Tests shared planning and boot-token limitations.
nvx-rs/src/host/sandbox/mount.rs
Routes mount requests through the planner and renders boot shares.
nvx-rs/src/host/sandbox/mod.rs
Separates export plans from launch settings.
nvx-rs/src/host/sandbox/golden.rs
Adapts golden rendering tests to export plans.
nvx-rs/src/host/launch.rs
Updates export fixtures for the revised structure.
nvx-rs/src/host/instance.rs
Clarifies ownership of persisted record versions.
nvx-rs/src/host/filesystem.rs
Implements shared target, policy, ownership, and plan validation.
nvx-rs/src/host/cli/sandbox/state.rs
Records shares in format 7 and rejects retired formats.
nvx-rs/src/host/cli/sandbox/mod.rs
Plans shares once and rechecks one-shot exports.
nvx-rs/src/host/cli/sandbox/lifecycle.rs
Documents persisted export plans and restart checks.
nvx-rs/README.md
Describes the shared planner and updated state format.
doc/run.md
Documents path resolution and reprovisioning requirements.
aci_edge_sandboxes/tests/openvmm_fake.rs
Separates instance-version expectations from adapter records.
aci_edge_sandboxes/src/openvmm/state.rs
Introduces adapter record format 4.
aci_edge_sandboxes/src/openvmm/mod.rs
Uses the unified mapping request API.
.github/workflows/run-nvx-microvm-tests.yml
Reads persisted shares and policies from export plans.
Review effort: Balanced
Base automatically changed from
esaurez/a4-552-nested-read-only-20261020 to
devOctober 22, 2026 21:21
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Security-sensitive export validation and incompatible state changes require human review, with unresolved recorded-policy and start-time overlap checks.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Security-sensitive cross-platform export checks and incompatible record formats warrant human review, with a recorded-policy validation defect still unresolved.
No open issues 🎉
1 previously missed issue
In code that hasn't changed since last review
Reject NUL bytes in policy paths during plan validation
nvx-rs/src/host/sandbox/plan.rs:262
A recorded plan can pass this check with a NUL-containing policy path. For example, a read-only directory child with denied = [root/secret\0suffix], deniedIdentities = [null], and a read-only root bind passes ExportPlan::check: the shared policy_limits validator does not reject NULs. With an existing directory root, verify also succeeds because it skips denied paths whose identity is null. Deserialization therefore accepts a plan whose rendered OpenVMM arguments cannot be passed to a process. SandboxMount previously rejected these policy paths. Reject NULs in the shared policy validator and add a recorded-plan regression test.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Cross-platform access-policy enforcement and incompatible persisted-format changes warrant human sign-off and completion of the remaining backend CI validation.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Security-sensitive cross-platform filesystem enforcement and incompatible persisted-state changes warrant final human review with cross-platform CI results.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Security-sensitive cross-platform export validation and incompatible persisted-format changes warrant final human review alongside the remaining CI gates.
host::filesystem becomes the one planner of NVX's virtio-fs exports, for
the blockless root and for the container root of a layered sandbox. An
ExportRequest gives each mapping an explicit guest target or lets the
host-path convention derive it, names the guest targets that the root
keeps for itself (ReservedTargets::BLOCKLESS_ROOT, or CONTAINER_ROOT
from protocol::share), and carries the denied, allowed, and write paths
and the owner. The plan keeps the identity records that each start
checks again, the refusal of a read-only path inside a part that the
guest may write, and OpenVMM's limits, and ExportPlan::check validates a
recorded plan without reading the host.
Plans no longer record hidden children, which only the reading of
earlier records kept: a denied path that would hide a mapped path is
refused instead. aci_edge_sandboxes keeps planning through its
LaunchSpec, and only its call site changes; its sandbox records move to
format 4.
Part of #622
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e4de0d1f-0989-4804-ab6b-3c2891554406
The CLI's --mount shares, with their --mount-deny, --mount-allow, and
--mount-write paths and --mount-owner, now go through host::filesystem's
planner for the container root, and SandboxPlan holds the ExportPlan
that it returns instead of the SandboxMount requests, in a version 3
form without a reader for earlier versions. Delivery does not change
yet: boot_shares renders the same OpenVMM arguments and nvx_share= boot
tokens from the plan's binds for every lifecycle, and refuses a bind
that the tokens cannot express. A prepared launch renders them the same
way, without the working directory that the requests' relative paths
needed.
A managed sandbox records the plan in configuration format 7 and checks
it again at each start, so an object that a workload could move must
still be the one that it was at provision, and a one-shot run checks it
again after the distro layer's explicit verification. Formats 2 to 6,
which recorded the requests, are no longer read. The shares follow their
resolved host paths, a denied path that does not exist inside a share
and a policy path outside a sandbox's only share now fail before OpenVMM
starts, and the microVM workflow reads the recorded shares from the
plan.
Part of #622
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e4de0d1f-0989-4804-ab6b-3c2891554406
The test that runs the real nvx-rs, which CI's NVX CLI jobs run and
local runs skip without NVX_RS, expected a dry run of `sandbox` to pass
a relative --mount-deny path through to OpenVMM. Shares are now planned
through the export planner, which resolves each policy path once, in
the share's host directory, and gives OpenVMM the path to which it
resolves. The test now expects that path, quoted as nvx-rs prints it.
Part of #622
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e4de0d1f-0989-4804-ab6b-3c2891554406
ExportPlan::check accepted recorded plans that plan never makes. A
child whose root only a read-only bind maps could list any write paths,
or none, and so let the guest write paths that no read-write bind maps,
while a write path that skipped a read-write bind left it read-only. And
a writable child could leave a denied path or a mapped object that a
workload could move without its identity, so that verify would not
notice the workload swapping it before the next start.
check now requires a child's write paths to be the outermost paths of
its read-write binds, unless one maps the whole child, when they may
narrow its writes only to paths that keep its other read-write binds
writable. It also requires an identity for each denied path and bind
that lies strictly inside a part that the guest may write, as plan pins
them. The deny test helper pins the paths that plan pins, the example
of a persisted plan pins its denied path, and regression cases cover
both rules, on hand-written plans and on one that plan makes.
Part of #622
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e4de0d1f-0989-4804-ab6b-3c2891554406
The planner checked that a target inside another mapping's shows the
object that the other mapping shows there by pushing the guest names
onto the other's host path. A Windows host reads a backslash in a
guest name as a separator, and a name with a root or a drive as
another path, so the planner accepted nested targets whose bind
sources ExportPlan::check then refused, and whose names the guest
could not resolve. It now compares the guest names, as text, with the
host's names below the other's object, as check compares bind
sources, and check refuses a recorded source that the host would read
as other names.
A reservation of the guest's root, /, reserved no target below it,
since a guest path was within a directory only if what followed the
directory began with a separator. Every absolute guest path is now
within the root, so such a reservation refuses every mapping, planned
or recorded.
Debug builds now check each plan that the planner makes, so tests fail
wherever the planner and check disagree.
Part of #622
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e4de0d1f-0989-4804-ab6b-3c2891554406
ExportPlan::check accepted a recorded child root or policy path with a
NUL character, which no name holds and no process argument carries:
OpenVMM could then not be started, and verify skipped a denied path
without an identity. The share requests refuse NUL characters, but a
recorded plan bypasses them. check also accepted a `..` component,
through which a path that lies inside a child by its text leaves it,
and, on Windows, a network path or a verbatim one, which the planner
never records.
check now requires every host path of a child, its root and its
denied, allowed, and write paths, to be in the form in which the
planner resolves paths: without a NUL character or a `..` component,
and, on Windows, on a local drive without the verbatim prefix.
Part of #622
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e4de0d1f-0989-4804-ab6b-3c2891554406
The explicit verification of the distro layer reads all of it after the
shares were planned, and a one-shot run then checked only the host
objects that the export plan recorded. A hard link that replaced one
shared file with another passes those checks, since a file share's root
is not pinned, so the run could export one file under two access modes.
A run now checks its launch again as provision checks one before
recording it and start checks a recorded one, which also refuses shares
whose host paths came to overlap, by path or through a hard link or a
bind mount.
Part of #622
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e4de0d1f-0989-4804-ab6b-3c2891554406
The planner resolved each host path and dropped the Windows verbatim
prefix of the result, which a recorded plan and OpenVMM's options leave
out. Windows reads a path without the prefix differently: it drops a
trailing period or space of a name, among other things. A share of
\\?\D:\shares\work., a directory that only a verbatim path names, was
therefore planned as D:\shares\work., which names D:\shares\work, and
that directory was exported in its place, with the share's access. A
denied path could hide another path in the same way. The planner now
drops the prefix only from a path that resolves to the same path again,
as Python's realpath does, and refuses the others, for every caller.
Part of #622
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e4de0d1f-0989-4804-ab6b-3c2891554406
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Cross-platform filesystem isolation and incompatible persisted-state changes warrant final human review despite the regression coverage and reported validation.
No open issues 🎉
Review effort: Balanced
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
One planner now turns host shares into an export plan for the CLI's
sandboxshares and for the external orchestrators that usenvx::host::filesystem, for the blockless root and for the container root of a layered sandbox. This is the first part of A4's shared export composition; delivery does not change yet.What changed
nvx::host::filesystemis the one planner.ExportRequestholdsMappings, each at an explicit guest target (Target::Guest) or at the target that the host-path convention derives (Target::Convention,Mapping::convention).ReservedTargets):BLOCKLESS_ROOT(RESERVED_TARGETS), orCONTAINER_ROOT, built fromprotocol::share's trees and exact paths. Ancestors of a reserved tree are refused too.Owner::VmmorOwner::Caller), which are the CLI's--mount-deny,--mount-allow,--mount-write, and--mount-owner.ExportPlan::checkvalidates a recorded plan without reading the host: host paths in the form in which the planner resolves them, OpenVMM's rules for the denied, allowed, and write paths of each exported directory, write paths that match its read-write mappings as the planner derives them, and the identity of every denied or mapped object that a workload could move.The typed plan holds the export plan.
SandboxPlanholds theExportPlaninstead ofSandboxMountshares (SandboxPlan::with_export). Its serde form is version 3, and earlier versions are not read.SandboxMountstays as the CLI's parsed--mountrequest.plan_mountsplans the requests through the planner for the container root, andboot_sharesrenders a plan as today's OpenVMM arguments andnvx_share=boot tokens. It refuses a bind that the tokens cannot express: a path below a share's root.The CLI plans its shares once.
runandprovisionplan the shares from their resolved host paths.provisionrecords the plan inconfig.jsonformat 7, andstartchecks it again, including that its exported directories and files still do not overlap, by path or through a hard link or bind mount.startdoes, including that its exported directories and files still do not overlap.nvx-rs run, the plain VM command that nvx-rs: make the nvx-rs CLI the backend of nvx.py run #704 ported fromnvx.py run, still renders its shares from its options, as the Python implementation did, throughmount::aggregate_share_tokens, which this PR keeps for it. Moving it onto the planner is left for a follow-up.aci_edge_sandboxeskeeps using the planner through itsLaunchSpec; only its call site changes. Its sandbox records move to format 4.Behavior changes
sandboxfollows a symbolic link in a policy path to a path inside the same share. OpenVMM receives the resolved paths, so a dry run prints them.\\?\path names, such as that of a directory whose name ends in a period or a space, for every caller. It used to drop the prefix, and the path without it names another object, or none.config.jsonof the share lifecycles. The microVM workflow reads the recorded shares from the export plan.Part of #622 (scope item 4)
Validation
At this head, on
devat03d945bc, on Windows 11 (WHP), and on Linux with MSHV on a VM host, where builds and checks ran as a non-root user and VMs as root, which alone may open the hypervisor device there:python scripts/nvx.py verifynvx-rs, withNVX_BUNDLE=skip:cargo fmt --all --check;cargo clippy --locked --all-targets --all-features -- -D warnings;cargo clippy --locked --lib --no-default-features -- -D warnings;cargo test --locked --all-features;cargo test --locked;cargo build --locked --lib --no-default-featuresandcargo test --locked --lib --no-default-features;cargo doc --locked --no-deps --all-featureswithRUSTDOCFLAGS=-D warnings;cargo +1.89 check --locked --all-targets --all-features; on Linux also theaarch64-apple-darwincheck, the agent bundling test, and the guest binaries' check and build.aci_edge_sandboxes: the steps ofcheck-aci-edge-sandboxes(fmt, both clippy configurations, both test configurations, docs, MSRV; macOS check on Linux).tests/runtime-adapter: the protocol-only and host-only configurations on both, and the guest-only (musl) configuration on Linux, whose MSRV check used the GNU target there.check-quality's Python and shell checks:ruff check,ruff format --check,pyright --pythonplatform LinuxandWindowswith the pinned tools ofrequirements-dev.txt, and shellcheck and shfmt with the pinned images.validate-nvx'scompileall, unittest list, and help commands on both, withNVX_RSnaming a release build ofnvx-rswith theclifeature, as CI's CLI jobs run them, so that the tests that run the realnvx-rsrun too; on Windows, thetest_setup_azuretests of the Linux setup scripts fail on this host with and without this change.--cpu-profile hoston this VM host), with the artifacts ofv0.1.0-dev.03d945bc2d3a, the development release of this base, whose OpenVMM is7ff31ed7, the base's gitlink, and on both the Alpine initramfs of this tree, built on Linux:nvx.py test-microvmwithfilesystem-shares,filesystem-policy,filesystem-comma-paths,filesystem-owner,denied-filesystem-paths,filesystem-snapshot,managed-lifecycle,managed-exec-config, andsandbox-lifecycle;nvx.py test-aci-edge-sandboxes: all pass on WHP; on MSHV, as root, the five tests that write through read-write mappings fail withPermission deniedwith and without this change, and the other 13 pass;cargo test --locked --features host -- --ignoredintests/runtime-adapter;nvx.py benchmark --suite device-restore-profile --restore-devices virtiofs --warmups 0 --runs 1), in both activation modes.