Skip to content

nvx-rs: plan every caller's host exports through one planner - #765

Open
Enrique Saurez (esaurez) wants to merge 8 commits into
devfrom
esaurez/a4-pr4-export-composition-20261021
Open

Enrique Saurez (esaurez) wants to merge 8 commits into
devfrom
esaurez/a4-pr4-export-composition-20261021

Conversation

@esaurez

@esaurez Enrique Saurez (esaurez) commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

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 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 (nvx-rs: enforce read-only mappings nested in read-write mappings on the host #552, from nvx-rs: refuse read-only paths inside read-write directories on every host #672), and OpenVMM's limits: 256 children and the policy-path bounds.
  • 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.

Part of #622 (scope item 4)

Validation

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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 dev October 22, 2026 21:21
@esaurez
Enrique Saurez (esaurez) force-pushed the esaurez/a4-pr4-export-composition-20261021 branch from 3492e39 to 49eb502 Compare October 22, 2026 21:46
Copilot AI balanced review requested due to automatic review settings October 22, 2026 21:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Export rendering can discard a read-only bind, and policy composition and recorded-plan validation still have correctness gaps.

4 open issues

Review effort: Balanced

Comment thread nvx-rs/src/host/sandbox/mount.rs
Comment thread nvx-rs/src/host/filesystem.rs
Comment thread nvx-rs/src/host/filesystem.rs
Comment thread nvx-rs/src/host/filesystem.rs
Copilot AI balanced review requested due to automatic review settings October 22, 2026 22:14
@esaurez
Enrique Saurez (esaurez) force-pushed the esaurez/a4-pr4-export-composition-20261021 branch from 49eb502 to 3f024cf Compare October 22, 2026 22:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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.

2 open issues
4 resolved since last review

Review effort: Balanced

Comment thread nvx-rs/src/host/sandbox/plan.rs
Comment thread nvx-rs/src/host/sandbox/plan.rs
Copilot AI balanced review requested due to automatic review settings October 22, 2026 22:51
@esaurez
Enrique Saurez (esaurez) force-pushed the esaurez/a4-pr4-export-composition-20261021 branch from 3f024cf to 314558c Compare October 22, 2026 22:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Security-sensitive export enforcement and incompatible persisted-state changes across platforms warrant final human review.

No open issues 🎉

2 resolved since last review

Review effort: Balanced

@esaurez
Enrique Saurez (esaurez) marked this pull request as ready for review October 22, 2026 22:59
Copilot AI balanced review requested due to automatic review settings October 22, 2026 23:56
@esaurez
Enrique Saurez (esaurez) force-pushed the esaurez/a4-pr4-export-composition-20261021 branch from 314558c to eacd531 Compare October 22, 2026 23:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Security-sensitive cross-platform export handling and incompatible persistence changes warrant final human review with the remaining CI results.

No open issues 🎉

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 23, 2026 01:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The cross-platform filesystem access-control refactor and incompatible persistence changes warrant final human review.

No open issues 🎉

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 23, 2026 02:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Medium severity 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.

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 23, 2026 02:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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.

No open issues 🎉

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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.

No open issues 🎉

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 23, 2026 05:39
@esaurez
Enrique Saurez (esaurez) force-pushed the esaurez/a4-pr4-export-composition-20261021 branch from 9d89947 to 5a7476f Compare October 23, 2026 05:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Cross-platform filesystem isolation and persisted-format changes need human review, and the one-shot overlap recheck remains unresolved.

1 open issue

Review effort: Balanced

Comment thread nvx-rs/src/host/cli/sandbox/mod.rs Outdated
Copilot AI balanced review requested due to automatic review settings October 23, 2026 06:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Cross-platform filesystem isolation and persisted-format changes warrant final human review alongside the remaining KVM and full-matrix validation.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings October 23, 2026 07:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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.

No open issues 🎉

Review effort: Balanced

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Cross-platform filesystem-policy enforcement and incompatible persisted-state changes warrant final human review alongside CI results.

No open issues 🎉

Review effort: Balanced

Enrique Saurez and others added 8 commits October 23, 2026 10:36
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
Copilot AI balanced review requested due to automatic review settings October 23, 2026 13:36
@ppenna
Pedro Henrique Penna (ppenna) force-pushed the esaurez/a4-pr4-export-composition-20261021 branch from 1f60357 to cda9371 Compare October 23, 2026 13:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants