Repository navigation
sandbox: refuse shares of a sandbox's own files (SL3.5c) - #1485
Merged
Pedro Henrique Penna (ppenna) merged 1 commit intoNov 11, 2026
Merged
Pedro Henrique Penna (ppenna) merged 1 commit into
Pedro Henrique Penna (ppenna) merged 1 commit into
Conversation
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>
Contributor
This pull request changes existing testsIt 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
Relocated unchanged: 0; added: 11. Compared the merge base |
Contributor
There was a problem hiding this comment.
🟢 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
Pedro Henrique Penna (ppenna)
merged commit Nov 11, 2026
7bc238e
into
feature/nvx-refactor
47 checks passed
Pedro Henrique Penna (ppenna)
deleted the
agents/slim-sl3-5c-own-files
branch
November 11, 2026 19:28
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
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
--mountwhose 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).provisionrefuses such a share before it records the sandbox, and before it creates anything. The check runs insandbox::runbeforelifecycle::provisioncreates or locks the state directory, so a refused provision leaves the host as it was, including a state directory that it would have created.startrefuses it again, from the recorded configuration, right afterConfig::launchand before it checks or claims the images. A share whose path came to resolve into one of these files sinceprovision, such as through a link, is refused too.host::paths::resolvehas 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 thatresolvekeeps for a path that only it names, such as a directory whose name ends in a period.provisionthat finds no artifact root, which it does not need, leaves the artifacts tostart. Both protect the artifact root's OpenVMM, which a managedstartruns, whateverNVX_OPENVMMnames.doc/run.mdsays so.error[unsupported]: sandbox mount host path X overlaps the sandbox's NAME Y; a share cannot expose the sandbox's own files.sandbox runis 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) andnvx-rs/README.md(Other differences from nvx.py). The public API doesn't change, since the check is private tohost::cli::sandbox.BOOT_CONTRACT_VERSIONandCONTROL_CONTRACT_REVISIONdon't change either.Size
71 core lines (estimate 120, stop 240) in 2 counted files:
nvx-rs/src/host/cli/sandbox/mod.rs(68) andnvx-rs/src/host/cli/sandbox/lifecycle/start.rs(3), measured againstfeature/nvx-refactor3340bbd7. The tests and docs don't count.Tests of the area
check_test_changes.py --base origin/feature/nvx-refactorreports one change to an existing test, so this PR hastests-modified:download::provision_and_deprovision_need_no_artifact_rootnow 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 ofprovisionandstart. 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 helpersown_files_root,own_files_provisionandshare_host, and theOWN_FILEStable.provision_refuses_shares_of_its_own_filesandstart_refuses_recorded_shares_of_its_own_filesrun 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, andstartlaunches 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 afterprovision.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(head9ccd5f7b, checkpoint on #1110). This PR replays its two commits for this slice,b1f1446c543and3a3a5a7084e, 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-refactorhas noscripts/local_gates.py, sodev'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 onnvx-kvm-test(KVM, virtual machine, Azure Linux 3). Everything below ran on the head,09a64b6a, based on3340bbd7.The rebase onto
3340bbd7conflicted only instart.rs's imports, next to theRETRY_INTERVALandExecLockimports that SL3.9 (#1469) added. The resolution keeps both sides:git range-diffshows only that context change, andrebase_check.py verifypassed. Earlier rebases, ontoa53a2626(after #1466), applied the same patch without conflicts and passed the same checks.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 themcargo clippy -p nvx --lib --no-default-features;cargo doc --all-featureswith-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 configurationscargo test --all-features --test cli -- its_own_files that_it_would_create only_a_verbatim_path roots_openvmm need_no_artifact_rootcargo test --all-features --no-fail-fast;cargo testcli100 passed (2 ignored), and every other suite and the doctests; default features: lib 1222 passednvx-clisuite: validate-nvx's Windows step, without itspip installtest_adversarial.py84,test_hosts.py4, and eachnvx.py ... --helpnvx.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 configurationscargo test --all-features --no-fail-fastcli108 passed, and every other suite passes except 28guest::lib tests (2275 passed). Those fail in their isolated processes on this host (EOPNOTSUPPfrom a mount,EINVALforESRCH). The same 28 fail at the base on the same host, and no guest code changed between the basesnvx.py check-guest-binariesguest::musl lib tests fail (1116 passed), the same set as at the base on the same hostnvx-clisuite: validate-nvx's Linux step, without itspip installtest_adversarial.py84,test_hosts.py4, and eachnvx.py ... --helpnvx-kvm-testis shared, and other work loaded it at times (load average 4 to 10 on 8 CPUs). Under that load, 4 of 9 runs of theclisuite at the head had one or two one-shotnvx-rs runtests lose the fake OpenVMM's console output:one_shot::one_shot_runs_exit_with_the_workload_or_openvmm_statusorone_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 runnever reaches the new check.connect_before_exitinhost/cli/run.rsneeds 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
--mounttoprovision, and the share scenarios pass--mountto OpenVMM directly. They are left to CI.