Skip to content

sandbox: refuse shares of a sandbox's own files (SL3.5c) - #1485

Merged
Pedro Henrique Penna (ppenna) merged 1 commit into
feature/nvx-refactorfrom
agents/slim-sl3-5c-own-files
Nov 11, 2026
Merged

Pedro Henrique Penna (ppenna) merged 1 commit into
feature/nvx-refactorfrom
agents/slim-sl3-5c-own-files

Conversation

@ppenna

Copy link
Copy Markdown
Contributor

Part of #1110 (SL3.5c, "A sandbox can't share its own files"). Parent #1083, umbrella #1076.

What and why

A managed sandbox could share its own files with its guest: a --mount whose host path is, holds, or lies inside the sandbox's state directory, image, or scratch image, or the OpenVMM, kernel, or initramfs that its VM runs. Through such a share, the workload could read or change the sandbox's records and storage, or what the host runs at the next start (#818).

  • provision refuses such a share before it records the sandbox, and before it creates anything. The check runs in sandbox::run before lifecycle::provision creates or locks the state directory, so a refused provision leaves the host as it was, including a state directory that it would have created.
  • start refuses it again, from the recorded configuration, right after Config::launch and before it checks or claims the images. A share whose path came to resolve into one of these files since provision, such as through a link, is refused too.
  • Both compare each export child's host path with one bounded list of protected paths once host::paths::resolve has resolved them. The comparison goes component by component (Path::starts_with, in both directions). The list holds the state directory, the image, the scratch image (scratch upper only), and the artifact root's OpenVMM, kernel and Alpine initramfs. The comparison drops the Windows verbatim prefix that resolve keeps for a path that only it names, such as a directory whose name ends in a period.
  • A provision that finds no artifact root, which it does not need, leaves the artifacts to start. Both protect the artifact root's OpenVMM, which a managed start runs, whatever NVX_OPENVMM names.
  • There is no alias matrix, as the Scope says: hard links, bind mounts, and a link inside a shared directory through which the host reaches one of these files are not detected. doc/run.md says so.
  • The refusal reads: error[unsupported]: sandbox mount host path X overlaps the sandbox's NAME Y; a share cannot expose the sandbox's own files.
  • sandbox run is not checked here: SL3.4b checks the protected paths that apply to its stateless launch, as the Scope's "Not in this PR" says.

The docs change in doc/run.md (Live host shares), doc/usage.md (sandbox --mount) and nvx-rs/README.md (Other differences from nvx.py). The public API doesn't change, since the check is private to host::cli::sandbox. BOOT_CONTRACT_VERSION and CONTROL_CONTRACT_REVISION don't change either.

Size

71 core lines (estimate 120, stop 240) in 2 counted files: nvx-rs/src/host/cli/sandbox/mod.rs (68) and nvx-rs/src/host/cli/sandbox/lifecycle/start.rs (3), measured against feature/nvx-refactor 3340bbd7. The tests and docs don't count.

Tests of the area

check_test_changes.py --base origin/feature/nvx-refactor reports one change to an existing test, so this PR has tests-modified:

  • download::provision_and_deprovision_need_no_artifact_root now provisions a share too, so it also checks that a provision without an artifact root still checks its shares, against the sandbox's own files, and succeeds. Its assertions don't change, and its comment says why the share is there.

Kept unchanged: lifecycles_of_every_configuration, lifecycle_of_a_sandbox_whose_upper_is_in_ram, configurations_of_every_format_start_and_exec, refusals_of_lifecycle_operations, and the other CLI tests of provision and start. Their shares lie outside the protected paths, so they still provision and start.

Added in nvx-rs/tests/cli/lifecycle.rs. The script counts 11 added items: these 7 tests, their helpers own_files_root, own_files_provision and share_host, and the OWN_FILES table.

  • provision_refuses_shares_of_its_own_files and start_refuses_recorded_shares_of_its_own_files run over one table: equal to, containing and inside the state directory; the image and its directory; the scratch image and its directory; the kernel, the initramfs and their directory; and the OpenVMM. Each refusal leaves the root unchanged, and start launches nothing.
  • provision_refuses_a_share_that_holds_the_state_directory_that_it_would_create.
  • provision_refuses_a_share_that_holds_a_state_directory_that_only_a_verbatim_path_names (Windows).
  • provision_refuses_a_share_of_the_roots_openvmm_whatever_the_variable_names.
  • start_refuses_a_share_that_came_to_reach_its_own_files, through a link made after provision.
  • shares_beside_its_own_files_provision_and_start: shares whose names only start as the sandbox's files do still provision, start, stop and deprovision.

The refusal tests fail without the check. The verbatim-path test fails without the prefix's removal, and the last test fails with a textual prefix comparison.

History: the first implementation

Enrique Saurez (@esaurez)'s first implementation is on esaurez/slim-3.1.5-sl3.5c-private-20261108 (head 9ccd5f7b, checkpoint on #1110). This PR replays its two commits for this slice, b1f1446c543 and 3a3a5a7084e, as one commit with Enrique Saurez (@esaurez) as its author, adapted to the typed record (SL3.5a, #1466) and the CLI-owned lifecycle (SL3.5b, #1460). The branch's other commits (guest exec isolation experiments, console CRCRLF parsing, runtime-adapter fixture names) are outside this slice's Scope and are not included.

Validation

feature/nvx-refactor has no scripts/local_gates.py, so dev's copy selected the gates. They ran with this branch's own action commands and pinned toolchain (1.95.0, MSRV 1.89), on Windows locally and on Linux on nvx-kvm-test (KVM, virtual machine, Azure Linux 3). Everything below ran on the head, 09a64b6a, based on 3340bbd7.

The rebase onto 3340bbd7 conflicted only in start.rs's imports, next to the RETRY_INTERVAL and ExecLock imports that SL3.9 (#1469) added. The resolution keeps both sides: git range-diff shows only that context change, and rebase_check.py verify passed. Earlier rebases, onto a53a2626 (after #1466), applied the same patch without conflicts and passed the same checks.

Host Checks Result
Windows nvx.py verify; check_markdown_links.py; check_test_layout.py; cargo fmt --check; the five Clippy runs of the Lint step with -D warnings, among them cargo clippy -p nvx --lib --no-default-features; cargo doc --all-features with -D warnings; MSRV 1.89 check; the artifact bundling test (21 passed); the lib build and tests without default features; the runtime adapter's protocol-only and host-only configurations pass
Windows the refusal tests: cargo test --all-features --test cli -- its_own_files that_it_would_create only_a_verbatim_path roots_openvmm need_no_artifact_root 8 passed. The link test created its link and ran in full
Windows cargo test --all-features --no-fail-fast; cargo test pass: lib 1463 passed (17 ignored), cli 100 passed (2 ignored), and every other suite and the doctests; default features: lib 1222 passed
Windows Python nvx-cli suite: validate-nvx's Windows step, without its pip install pass: 1834 tests (73 skipped), test_adversarial.py 84, test_hosts.py 4, and each nvx.py ... --help
nvx-kvm-test nvx.py verify; fmt; the five Clippy runs; cargo test (lib 1197 passed); the lib build and tests without default features; bundling (21 passed); doc; MSRV 1.89; the macOS check; the public API snapshots; the guest binaries' build and tests (33 passed); the runtime adapter's protocol-only, host-only and guest-only configurations pass
nvx-kvm-test the refusal tests 7 passed (the verbatim-path test is Windows-only)
nvx-kvm-test cargo test --all-features --no-fail-fast cli 108 passed, and every other suite passes except 28 guest:: lib tests (2275 passed). Those fail in their isolated processes on this host (EOPNOTSUPP from a mount, EINVAL for ESRCH). The same 28 fail at the base on the same host, and no guest code changed between the bases
nvx-kvm-test nvx.py check-guest-binaries 26 guest:: musl lib tests fail (1116 passed), the same set as at the base on the same host
nvx-kvm-test Python nvx-cli suite: validate-nvx's Linux step, without its pip install pass: 1834 tests (39 skipped), test_adversarial.py 84, test_hosts.py 4, and each nvx.py ... --help

nvx-kvm-test is shared, and other work loaded it at times (load average 4 to 10 on 8 CPUs). Under that load, 4 of 9 runs of the cli suite at the head had one or two one-shot nvx-rs run tests lose the fake OpenVMM's console output: one_shot::one_shot_runs_exit_with_the_workload_or_openvmm_status or one_shot::cold_console_output_preserves_non_utf8_bytes. Both pass alone (5 of 5 runs), and the whole suite passes at low load. This change doesn't cause it:

  • nvx-rs run never reaches the new check.
  • One of the failures came in a run that filtered out every test that this PR adds or changes.
  • The likely cause is a race in the one-shot console attach, which this PR doesn't touch. connect_before_exit in host/cli/run.rs needs the console's peer to be alive (check_original_peer), and it gives up the console when OpenVMM has exited. The fake OpenVMM accepts the console, writes its output and exits at once, so a client that the load delays between its connect and its peer check finds it gone and drops what it wrote.

The base's 4 runs passed, but at low load.

Not run: the microVM scenarios. The check refuses before anything launches and returns at once for a sandbox without shares. No scenario provisions a sandbox share: the sandbox scenarios pass no --mount to provision, and the share scenarios pass --mount to OpenVMM directly. They are left to CI.

A managed sandbox could share its own files with its guest: a --mount
whose host path is, holds, or lies inside the sandbox's state
directory, image, or scratch image, or the OpenVMM, kernel, or
initramfs that its VM runs. Through such a share, the workload could
read or change the sandbox's records and storage, or what the host
runs at the next start (#818).

provision now refuses such a share before it records the sandbox, and
before it creates anything, and start refuses it again, from the
recorded configuration, before it claims or launches anything. Both
compare each share's host path with the sandbox's own files once
host::paths::resolve has resolved them, component by component, so
start also refuses a share that came to reach one of them through a
link since provision. The comparison drops the Windows verbatim prefix
that resolve keeps for a path that only it names, such as that of a
directory whose name ends in a period. A provision that finds no
artifact root, which it does not need, leaves the artifacts to start.
Both protect the artifact root's OpenVMM, which start runs, whatever
NVX_OPENVMM names. Hard links and bind mounts of these files are not
detected, as the documentation says.

The new CLI tests cover each refusal at provision and at start, a
state directory that provision would create, a state directory that
only a verbatim path names on Windows, the root's OpenVMM with
NVX_OPENVMM set, a share that a link redirected after provision, and
shares beside the sandbox's files whose names only start as theirs do.
The refusals fail without the check, the verbatim case without the
prefix's removal, and the last test with a textual prefix comparison.
provision_and_deprovision_need_no_artifact_root now provisions a share
too.

This replays esaurez's first implementation, b1f1446 and
3a3a5a7, adapted to the typed record and the CLI-owned lifecycle.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

This pull request changes existing tests

It modifies or removes 1 existing test asset(s), listed below. Review each with care: a change to a test can hide a regression that the test caught. See doc/contribute.md#changes-to-existing-tests.

Test code

Change What Before After
modified test fn provision_and_deprovision_need_no_artifact_root in cli/download (tests) nvx-rs/tests/cli/download.rs:203 nvx-rs/tests/cli/download.rs:203

Relocated unchanged: 0; added: 11. Compared the merge base 3340bbd7ee4a with 09a64b6ab09c.

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.

🟢 Approval recommended

No active review issues are reported for the managed sandbox protected-path checks.

No open issues 🎉

What changed in this PR

This PR adds protected-path checks to the managed sandbox CLI, preventing guest shares from exposing the sandbox’s own state, images, or runtime artifacts. It implements SL3.5c within the CLI-owned lifecycle work without changing the public API.

Changes:

  • Reject overlapping resolved paths before provisioning creates state, and recheck recorded shares at start before checking or claiming images.
  • Add regression coverage for protected paths, redirected links, Windows verbatim paths, neighboring paths, and provisioning without an artifact root.
  • Document the protection and its limits, including undetected hard-link and bind-mount aliases.
File Description
nvx-rs/​tests/​cli/​lifecycle.rs Adds refusal, path-resolution, and allowed-neighbor regression tests.
nvx-rs/​tests/​cli/​download.rs Extends artifact-free provisioning coverage to include a share.
nvx-rs/​src/​host/​cli/​sandbox/​mod.rs Adds the shared protected-path check and invokes it before provisioning.
nvx-rs/​src/​host/​cli/​sandbox/​lifecycle/​start.rs Rechecks recorded shares before image checks and claims.
nvx-rs/​README.md Describes the managed sandbox protection among CLI differences.
doc/​usage.md Updates --mount documentation with protected-path restrictions.
doc/​run.md Explains check timing, path resolution, and alias limitations.

Review effort: Balanced

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

Labels

feat-nvx-refactor tests-modified Changes existing tests, test data, or test wiring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants