Skip to content

sandbox: run a workload once through the managed VM API (SL3.4b) - #1490

Merged
Pedro Henrique Penna (ppenna) merged 2 commits into
feature/nvx-refactorfrom
agents/slim-sl3-4b-sandbox-run
Nov 11, 2026
Merged

Pedro Henrique Penna (ppenna) merged 2 commits into
feature/nvx-refactorfrom
agents/slim-sl3-4b-sandbox-run

Conversation

@ppenna

@ppenna Pedro Henrique Penna (ppenna) commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Part of #1111 (SL3.4b, "sandbox run boots a layered root through the new APIs: a cold boot, one exec and a stop"). Parent #1083, umbrella #1076.

Base: feature/nvx-refactor 7bc238eb, which includes SL3.5c (#1485).

What and why

sandbox run still booted a one-shot VM. It passed its shares and its workload as boot tokens and ran OpenVMM on the terminal with --microvm-lifecycle one-shot, so the shell /init booted the layered root through the old one-shot init. It then exited with OpenVMM's status. After this PR, nvx-init boots every layered root that the CLI starts.

A run now launches the VM that a provision and a start of the same options would launch, without a state directory:

  • It serializes the configuration that provision records and reads it back as start does, then launches it through host::vm::{create, spawn} in a temporary VM directory, nvx-sandbox-run-<random> in the system's temporary directory. The command line holds nvx_sandbox=1 and no OpenVMM lifecycle option.
  • Once nvx-init is ready and has mapped the shares, it runs its workload once over NVXC. It takes exec's options: --cwd, --environment, --environment-file, --inherit-default-environment and --exec-timeout-ms.
  • It then sends STOP and waits up to --timeout for OpenVMM to exit. A VM that hasn't exited by then is forced down, as sandbox stop forces down only the verified process. The run warns that it did, and still exits with the workload's status.
  • It writes the workload's standard output and then its standard error, and exits with the workload's status. A guest that exits during startup fails the run as it fails start. Ctrl-C forces the VM down and exits with 130.
  • It takes provision's --layer-format {erofs,gpt}, and --upper ram with --ram-upper-percent, 50 by default.
  • It refuses --state-dir. Before it downloads or launches anything, it checks its kernel command line and its exec request, through Session::exec_payload, which the first commit splits out of Session::exec. A dry run prints the command and launches nothing. The VM directory is removed once OpenVMM's exit is confirmed, and kept only if it isn't.
  • --outcome-report forwards OpenVMM's report of the run (operation run), as nvx-rs run does. The report keeps its owner's permissions whatever the umask.
  • Protected paths (nvx-rs: GPT images, the RAM upper and protected paths in nvx-rs sandbox #818). A run refuses the shares that start refuses: of its image, its scratch image when it has one, and the artifact root's OpenVMM, kernel and Alpine initramfs. In place of a state directory, it also refuses a share of its temporary VM directory, through which the workload could change what OpenVMM writes there. It also refuses a share of the OpenVMM that it runs, which NVX_OPENVMM may name instead of the root's. provision and start share the check, so they refuse a share of that OpenVMM too.
  • The one-shot code that this leaves without callers is deleted: SandboxPlan::one_shot_openvmm_arguments, SandboxPlan::one_shot_kernel_command_line, Delivery::BootTokens, PreparedLaunch::one_shot, and the CLI's run_plan and prepare_run. All of them were crate-private.
  • In run-nvx-microvm-tests.yml, the sandbox run steps pass --arg init, the smoke probe's profile for an execution of nvx-init. The caller-owned share step now expects the run to fail while it maps its share (could not mount a mapped host path (error 1)), before its workload, as a start fails.
  • The docs change in doc/run.md, doc/usage.md, doc/ci.md and nvx-rs/README.md.

The public API doesn't change: everything that this PR deletes was crate-private, and scripts/public_api.py passes. BOOT_CONTRACT_VERSION and CONTROL_CONTRACT_REVISION don't change either.

Readings of the Scope

Size

365 core lines, 16 of them help text, against the estimate of 300 (stop 600), measured against feature/nvx-refactor 7bc238eb. The fake OpenVMM, nvx-rs/src/testing/fake_openvmm.rs, also gains 27 lines for the test of a review fix (fake_drop_stop=1). It is test infrastructure behind the testing feature, counted with the tests.

7 counted files:

  • nvx-rs/src/host/cli/sandbox/lifecycle/start.rs (260)
  • nvx-rs/src/host/cli/sandbox/mod.rs (44)
  • .github/workflows/run-nvx-microvm-tests.yml (20)
  • nvx-rs/src/host/cli/sandbox/options.rs (16, help text)
  • nvx-rs/src/host/cli/session.rs (16)
  • nvx-rs/src/host/sandbox/options.rs (8)
  • nvx-rs/src/host/cli/sandbox/lifecycle/mod.rs (1)

That is within #1076's "6 or 7, where a split would be harder to follow". nvx-rs/src/host/sandbox/plan.rs and nvx-rs/src/host/sandbox/prepared.rs only delete the one-shot code that this change leaves dead. The PR rules require this PR to delete it ("deletes consumers before their providers, and with them the crate-private code that the deletion leaves unused"), so they are leaving code and don't count. The tests and docs don't count.

The two commits stay in one PR. The first only prepares the second: sandbox run is the one caller that needs Session::exec_payload apart from Session::exec. A PR of its own would need an expect(dead_code) hook and would be harder to follow.

History: the first implementation

Enrique Saurez (@esaurez)'s first implementation is on esaurez/slim-3.1.6-sl3.4b-private-20261109 (head d7b739fc). The main commit replays its a734c60c659 with Enrique Saurez (@esaurez) as its author, adapted to:

The run itself is rewritten on start's launch, and the outcome report is OpenVMM's. The branch's other commits are outside this slice's Scope.

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 its pinned toolchain (1.95.0, MSRV 1.89).

The full validation ran at 1008a3e0, this patch on #1485's earlier head 5caac3f9. The rebase onto #1485's head 09a64b6a, which includes SL3.9 (#1469), conflicted in three places, all next to SL3.9's --wait:

  • doc/usage.md: this PR's rows, with SL3.9's --wait row kept after --timeout;
  • the imports of lifecycle/start.rs: both sides kept;
  • exec_workload's call in sandbox/mod.rs: it now takes this PR's shared ExecRequest and SL3.9's options.wait.

git range-diff shows only those changes. At the head, 279679d1, the narrow checks passed again:

  • cargo fmt --check;
  • Clippy --all-targets --all-features, --lib --no-default-features and --lib --bins --features cli, with -D warnings;
  • the sandbox, session and exec-lock lib tests (194 passed);
  • the cli suite (109 passed, 2 ignored);
  • scripts/test_microvm_tests.py (343 tests);
  • Ruff, check_test_layout.py and check_markdown_links.py;
  • on nvx-kvm-test, the VM qualification below (165/165) and the six test-microvm scenarios below.

Once #1485 merged, the branch was rebased onto feature/nvx-refactor 7bc238eb (git rebase --onto origin/feature/nvx-refactor 09a64b6a). git range-diff shows both commits unchanged, and the head's tree, b09ec616, is identical to 279679d1's, so the checks above stand. rebase_check.py verify reports only that SL3.5c's 09a64b6a was dropped: it merged as 7bc238eb with the same patch.

Review fixes. Copilot's review of the draft found three gaps, fixed in 5d0f26d9:

  • A STOP request that fails with EndpointClosed now gets a stop's grace period before the run forces the VM down.
  • The launch's check_command_line() now runs with the checks that precede the download, so a balanced double quote in --cmdline downloads nothing.
  • remove_vm_directory also removes a report's staging file (StateFile::owns_temporary).

Each fix has a test that fails without it: sandbox_run::a_run_waits_for_a_vm_whose_endpoint_closes_as_it_stops, which uses the fake's new fake_drop_stop=1, a new case in download::commands_download_nothing_for_options_that_they_refuse, and the unit test lifecycle::start::tests::a_run_removes_its_vm_directory_with_a_report_s_staging_file.

At 5d0f26d9 these checks pass:

  • on Windows: fmt; the five Clippy runs; cargo test --all-features for the lib (1464 passed), cli (110 passed, 2 ignored), fake_openvmm, fake_openvmm_behavior and managed_lifecycle;
  • on nvx-kvm-test: Clippy --all-targets --all-features and --features testing, the fake's suites, and cli. cli had 118 passed and 1 failed: one_shot::one_shot_runs_exit_with_the_workload_or_openvmm_status lost the fake's console output under load (load average 5). This is the nvx-rs run console race that sandbox: refuse shares of a sandbox's own files (SL3.5c) #1485 documents. It passed 3 of 3 runs alone, and this PR doesn't touch that path.
Host Checks Result
Windows nvx.py verify; 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 test --all-features --no-fail-fast and cargo test; the lib build and tests without default features; the artifact bundling test; cargo doc --all-features with -D warnings; MSRV 1.89; the runtime adapter's protocol-only and host-only configurations pass. The lib suite once hit the intermittent host::restore_driver::tests::ready_has_sixty_seconds_and_start_thirty_from_ready_for_each_pending_accept_and_read (fixed since by the SL4.3 follow-up), and passed on rerun
Windows Python nvx-cli suite (validate-nvx's Windows step, without its pip install), test_adversarial.py, each nvx.py ... --help, Ruff lint and format, Pyright (Linux and Windows), check_test_layout.py, check_markdown_links.py pass: 1834 tests (73 skipped), 84 adversarial
nvx-mshv-test (MSHV, virtual machine) fmt; the five Clippy runs; cargo test --all-features (cli 113 passed) and cargo test; the lib without default features; bundling; doc; MSRV 1.89; the macOS check; the public API snapshots; the guest binaries' lint, build and tests; the runtime adapter's protocol-only, host-only and guest-only configurations pass. The suite ran with default signal dispositions: a first attempt started in the background inherited an ignored SIGINT, which failed every interrupt test, and was discarded
nvx-kvm-test, nvx-mshv-test, and WHP on a local workstation VM qualification of sandbox run, below KVM 165/165, MSHV 165/165, WHP 152/152
nvx-kvm-test, nvx-mshv-test, WHP the workflow's sandbox steps, extracted verbatim (6 on Linux, 5 on Windows): smoke, live-share, managed live-share, caller-owned share (Linux), concurrent read-write and read-only shares, share access policy pass
nvx-kvm-test, nvx-mshv-test, WHP nvx.py test-microvm --scenario managed-exec-config --scenario sandbox-lifecycle --scenario sandbox-proxy-only --scenario sandbox-ram-upper --scenario sandbox-blocks --scenario managed-lifecycle pass

The VM qualification ran sandbox run with the Ubuntu distro layer: EROFS, and a GPT disk whose second partition holds an ext4 copy of it, each with a scratch upper, a RAM upper at the default 50% and one capped at 37%. For each of the six, it checked:

  • the smoke probe's profile in the single exec;
  • nvx_sandbox=1 and no --microvm-lifecycle on the command line, and the image's identity and the upper's tokens;
  • a graceful stop, by the forwarded outcome report and no warning;
  • no VM or state directory left behind;
  • --cwd;
  • exact --environment and --environment-file environments, without PATH;
  • --inherit-default-environment, and the default environment;
  • streams and status 7, with an argument that holds spaces;
  • the upper's size in the guest: 120748 KiB at 50% and 89356 KiB at 37% of 256 MiB, and the 64 MiB scratch;
  • a missing program, which ends with status 125 and stops the VM;
  • refusals of shares of the image, its directory, the scratch image, its directory, the OpenVMM directory, the kernel, the initramfs, and a link or junction to an ancestor;
  • a share elsewhere, which maps;
  • a dry run.

Once each, it also checked:

  • the refusal of --state-dir;
  • the refusal of a share of the temporary directory;
  • the refusal of a share of the OpenVMM that NVX_OPENVMM names, and a run of that OpenVMM;
  • on Linux, outcome reports that stay readable by their owner under umasks 077, 377 and 777.

The hosts can't reach the Ubuntu archives, so the distro layer came from the base's CI run 42994995631. This PR doesn't change its inputs. The kernel and initramfs were built from the branch. WHP ran an OpenVMM built locally from the pinned 14dc4116.

Left to CI: the full default test-microvm suite and its shards. The Linux unit gates ran on MSHV rather than on nvx-kvm-test, whose kernel fails 28 guest:: lib tests at the base too.

Changes to existing tests

check_test_changes.py --base origin/feature/nvx-refactor reports 76 changes to existing tests and 25 additions, so this PR has tests-modified. By area:

  • Shares of the library plan, rewritten. These tests checked the one-shot rendering, which this PR deletes: a directory as --mount, an aggregate at /run/nvx/shares with digest-named children, and nvx_share= boot tokens. They now check the managed rendering that remains, with their refusals and budgets: the aggregate at /run/nvx/hostfs/root with indexed children, nvx_maps=N, and the export's command-line fragment. The script also lists the helpers Shares::run and renderings and these tests' imports.
    • In host::sandbox::tests: launch_contract_rejects_duplicates_and_reserved_tokens, launch_contract_attaches_mount_and_reserves_bootstrap_tokens, launch_contract_attaches_several_shares_as_an_aggregate, launch_bounds_shares_by_the_kernel_command_line and launch_contract_attaches_a_file_as_an_aggregate_child.
    • In host::sandbox::tests::specification: launch_arguments_follow_the_openvmm_contract, accepted_launch_shapes_keep_their_arguments_and_kernel_tokens, shares_follow_the_openvmm_and_guest_contracts, caller_parameters_follow_linux_and_guest_rules, the_reproduction_of_microsoft_nvx_723_is_refused, and the proptests kernel_command_line_stays_within_1024_bytes, reserved_kernel_tokens_are_always_refused and accepted_values_stay_one_kernel_parameter.
    • In host::sandbox::options::tests: sandbox_command_forwards_mount_to_openvmm, sandbox_command_shares_a_file, sandbox_command_shares_host_paths_with_commas, sandbox_command_forwards_mount_owner, sandbox_command_hides_a_share_root_behind_its_allowed_paths and sandbox_command_attributes_mount_deny_to_the_preceding_mount.
    • In host::sandbox::plan::tests: the_managed_lifecycle_exports_the_shares_and_announces_their_mapping_table.
  • Options.
    • sandbox_exec_rejects_ambiguous_or_one_shot_environment is renamed sandbox_run_and_exec_alone_take_the_execution_options: run now takes the execution options, and every other operation refuses them with the new message.
    • managed_non_launch_operations_reject_network_policy_options takes --outcome-report's new message.
    • In host::cli::sandbox::tests, provision_options_do_not_change_other_commands is renamed image_options_are_only_valid_for_run_and_provision, since run takes --layer-format and --ram-upper-percent now.
    • run_checks_again_that_its_shares_do_not_overlap moves to lifecycle/start/tests.rs, beside the record step that it now checks.
  • CLI tests of the run, rewritten for the managed run.
    • dry_run::dry_runs_print_the_openvmm_command_and_launch_nothing and its helper sandbox_dry_runs expect the managed command of a start. A --cmdline that holds double quotes is now refused, as start refuses one.
    • validation's sandbox_refusals moves the execution-option refusals off run, and adds a relative --cwd and a nameless --environment for run.
    • one_shot::one_shot_runs_exit_with_the_workload_or_openvmm_status keeps nvx-rs run only. The sandbox runs that it covered move to tests/cli/sandbox_run.rs, with the helper sandbox_run, and their helpers sandbox_launches and workload are retired.
    • In tests/cli.rs:
      • a_distro_manifest_path_whose_lookup_fails_refuses_provision_and_start expects the run's dry run to fail as provision does;
      • tcp_forwards_take_any_egress_policy and network_policy_files_compile_into_the_typed_policy anchor the network rendering on --single-process, since the run has no lifecycle option;
      • a_running_sandbox_claims_its_scratch_image, a_run_passes_openvmm_no_handle_of_its_caller and interrupting_a_run_kills_openvmm_and_exits_with_130 (with sleeping_run) run workloads that the fake OpenVMM executes over NVXC, and the last also checks that the VM directory is gone;
      • a_run_refuses_a_batch_file_as_openvmm (with batch_openvmm) expects host::vm's spawn error.
    • download::sandbox_run_reads_a_packaged_layer_once_no_download_replaces_it runs /bin/sh -c "exit 7" over NVXC.
  • Retired with the one-shot code.
    • openvmm.rs's assert_checked_blocks, assert_checked_distro, checked_distro and descriptor: the checked one-shot handoff is gone, and the run's blocks are checked as a start's are.
    • dry_run.rs's workload.
    • The five test hooks of host/sandbox/plan.rs, with their allowlist entry.
  • Python.
    • scripts/test_microvm_tests.py's fakes _FakeSandboxCli._run and _FakeRamUpperSandboxCli model the managed run. PublicSandboxLifecycleTests.test_acceptance_detects_lifecycle_regressions and PublicSandboxRamUpperTests.test_acceptance_detects_ram_upper_regressions add faults for a dropped fixture, a lifecycle option and another cap.
    • The scenarios' LifecycleAcceptance.check_identity_admission runs the probe with --arg=init, and expects the run of an identity that the image lacks to fail as a managed start fails, with status 1 and the guest's status from the outcome report, without running its workload.
    • RamUpperAcceptance.run, check_run_report and check_one_shot, now check_run, check the managed run's command line, the default cap, the forwarded report, and that nothing remains.
  • CI wiring. The smoke steps pass --arg init, and the caller-owned step expects the run to fail before its workload, as above.

Added: tests/cli/sandbox_run.rs, with these tests:

  • each format and upper;
  • exec's options;
  • shares;
  • refusals of the sandbox's own files, its VM directory, and the OpenVMM that NVX_OPENVMM names;
  • a guest that exits during startup;
  • a workload that the guest refuses;
  • a VM that doesn't stop;
  • the outcome report under restrictive umasks (Linux).

openvmm.rs adds the managed_run fixture and normalized_run_outputs. The tests of the two refusals that the review added fail without the fix.

@ppenna Pedro Henrique Penna (ppenna) added tests-modified Changes existing tests, test data, or test wiring feat-nvx-refactor labels Nov 11, 2026
@github-actions

github-actions Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

This pull request changes existing tests

It modifies or removes 84 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 helper fn run in host::sandbox::options::tests, impl Shares`` nvx-rs/src/host/sandbox/options/tests.rs:925 nvx-rs/src/host/sandbox/options/tests.rs:939
modified helper fn renderings in host::sandbox::plan::tests nvx-rs/src/host/sandbox/plan/tests.rs:117 nvx-rs/src/host/sandbox/plan/tests.rs:116
modified helper fn batch_openvmm in cli (tests) nvx-rs/tests/cli.rs:2266 nvx-rs/tests/cli.rs:2272
modified helper fn sleeping_run in cli/interrupts (tests) nvx-rs/tests/cli.rs:2803 nvx-rs/tests/cli.rs:2809
modified helper fn sandbox_dry_runs in cli/dry_run (tests) nvx-rs/tests/cli/dry_run.rs:138 nvx-rs/tests/cli/dry_run.rs:131
modified helper fn sandbox_run in cli/one_shot (tests) (moved) nvx-rs/tests/cli/one_shot.rs:130 nvx-rs/tests/cli/sandbox_run.rs:22
modified helper fn sandbox_refusals in cli/validation (tests) nvx-rs/tests/cli/validation.rs:135 nvx-rs/tests/cli/validation.rs:135
modified helper def _FakeSandboxCli._run in scripts/test_microvm_tests.py scripts/test_microvm_tests.py:1112 scripts/test_microvm_tests.py:1112
modified helper class _FakeRamUpperSandboxCli in scripts/test_microvm_tests.py scripts/test_microvm_tests.py:2668 scripts/test_microvm_tests.py:2674
modified helper def _FakeRamUpperSandboxCli._run in scripts/test_microvm_tests.py scripts/test_microvm_tests.py:2690 scripts/test_microvm_tests.py:2697
modified imports the imports of host::cli::sandbox::tests nvx-rs/src/host/cli/sandbox/tests.rs:3 nvx-rs/src/host/cli/sandbox/tests.rs:3
modified imports the imports of host::sandbox::options::tests nvx-rs/src/host/sandbox/options/tests.rs:3 nvx-rs/src/host/sandbox/options/tests.rs:3
modified imports the imports of host::sandbox::plan::tests nvx-rs/src/host/sandbox/plan/tests.rs:4 nvx-rs/src/host/sandbox/plan/tests.rs:4
modified imports the imports of cli/dry_run (tests) nvx-rs/tests/cli/dry_run.rs:5 nvx-rs/tests/cli/dry_run.rs:5
modified imports the imports of cli/one_shot (tests) nvx-rs/tests/cli/one_shot.rs:6 nvx-rs/tests/cli/one_shot.rs:6
modified test fn provision_options_do_not_change_other_commands in host::cli::sandbox::tests (renamed to image_options_are_only_valid_for_run_and_provision) nvx-rs/src/host/cli/sandbox/tests.rs:59 nvx-rs/src/host/cli/sandbox/tests.rs:55
modified test fn run_checks_again_that_its_shares_do_not_overlap in host::cli::sandbox::tests (moved) nvx-rs/src/host/cli/sandbox/tests.rs:132 nvx-rs/src/host/cli/sandbox/lifecycle/start/tests.rs:13
modified test fn sandbox_exec_rejects_ambiguous_or_one_shot_environment in host::sandbox::options::tests (renamed to sandbox_run_and_exec_alone_take_the_execution_options) nvx-rs/src/host/sandbox/options/tests.rs:609 nvx-rs/src/host/sandbox/options/tests.rs:609
modified test fn managed_non_launch_operations_reject_network_policy_options in host::sandbox::options::tests nvx-rs/src/host/sandbox/options/tests.rs:737 nvx-rs/src/host/sandbox/options/tests.rs:750
modified test fn sandbox_command_forwards_mount_to_openvmm in host::sandbox::options::tests nvx-rs/src/host/sandbox/options/tests.rs:980 nvx-rs/src/host/sandbox/options/tests.rs:993
modified test fn sandbox_command_shares_a_file in host::sandbox::options::tests nvx-rs/src/host/sandbox/options/tests.rs:1015 nvx-rs/src/host/sandbox/options/tests.rs:1028
modified test fn sandbox_command_shares_host_paths_with_commas in host::sandbox::options::tests nvx-rs/src/host/sandbox/options/tests.rs:1046 nvx-rs/src/host/sandbox/options/tests.rs:1059
modified test fn sandbox_command_forwards_mount_owner in host::sandbox::options::tests nvx-rs/src/host/sandbox/options/tests.rs:1106 nvx-rs/src/host/sandbox/options/tests.rs:1119
modified test fn sandbox_command_hides_a_share_root_behind_its_allowed_paths in host::sandbox::options::tests nvx-rs/src/host/sandbox/options/tests.rs:1236 nvx-rs/src/host/sandbox/options/tests.rs:1249
modified test fn sandbox_command_attributes_mount_deny_to_the_preceding_mount in host::sandbox::options::tests nvx-rs/src/host/sandbox/options/tests.rs:1271 nvx-rs/src/host/sandbox/options/tests.rs:1281
modified test fn the_managed_lifecycle_exports_the_shares_and_announces_their_mapping_table in host::sandbox::plan::tests nvx-rs/src/host/sandbox/plan/tests.rs:330 nvx-rs/src/host/sandbox/plan/tests.rs:321
modified test fn launch_contract_rejects_duplicates_and_reserved_tokens in host::sandbox::tests nvx-rs/src/host/sandbox/tests.rs:199 nvx-rs/src/host/sandbox/tests.rs:199
modified test fn launch_contract_attaches_mount_and_reserves_bootstrap_tokens in host::sandbox::tests nvx-rs/src/host/sandbox/tests.rs:393 nvx-rs/src/host/sandbox/tests.rs:385
modified test fn launch_contract_attaches_several_shares_as_an_aggregate in host::sandbox::tests nvx-rs/src/host/sandbox/tests.rs:461 nvx-rs/src/host/sandbox/tests.rs:441
modified test fn launch_bounds_shares_by_the_kernel_command_line in host::sandbox::tests nvx-rs/src/host/sandbox/tests.rs:587 nvx-rs/src/host/sandbox/tests.rs:556
modified test fn launch_contract_attaches_a_file_as_an_aggregate_child in host::sandbox::tests nvx-rs/src/host/sandbox/tests.rs:658 nvx-rs/src/host/sandbox/tests.rs:620
modified test fn launch_arguments_follow_the_openvmm_contract in host::sandbox::tests::specification nvx-rs/src/host/sandbox/tests/specification.rs:211 nvx-rs/src/host/sandbox/tests/specification.rs:211
modified test fn accepted_launch_shapes_keep_their_arguments_and_kernel_tokens in host::sandbox::tests::specification nvx-rs/src/host/sandbox/tests/specification.rs:254 nvx-rs/src/host/sandbox/tests/specification.rs:250
modified test fn shares_follow_the_openvmm_and_guest_contracts in host::sandbox::tests::specification nvx-rs/src/host/sandbox/tests/specification.rs:366 nvx-rs/src/host/sandbox/tests/specification.rs:364
modified test fn caller_parameters_follow_linux_and_guest_rules in host::sandbox::tests::specification nvx-rs/src/host/sandbox/tests/specification.rs:564 nvx-rs/src/host/sandbox/tests/specification.rs:557
modified test fn the_reproduction_of_microsoft_nvx_723_is_refused in host::sandbox::tests::specification nvx-rs/src/host/sandbox/tests/specification.rs:620 nvx-rs/src/host/sandbox/tests/specification.rs:610
modified test fn kernel_command_line_stays_within_1024_bytes in host::sandbox::tests::specification, proptest!`` nvx-rs/src/host/sandbox/tests/specification.rs:742 nvx-rs/src/host/sandbox/tests/specification.rs:731
modified test fn reserved_kernel_tokens_are_always_refused in host::sandbox::tests::specification, proptest!`` nvx-rs/src/host/sandbox/tests/specification.rs:812 nvx-rs/src/host/sandbox/tests/specification.rs:787
modified test fn accepted_values_stay_one_kernel_parameter in host::sandbox::tests::specification, proptest!`` nvx-rs/src/host/sandbox/tests/specification.rs:845 nvx-rs/src/host/sandbox/tests/specification.rs:819
modified test fn a_distro_manifest_path_whose_lookup_fails_refuses_provision_and_start in cli (tests) nvx-rs/tests/cli.rs:565 nvx-rs/tests/cli.rs:569
modified test fn tcp_forwards_take_any_egress_policy in cli (tests) nvx-rs/tests/cli.rs:1524 nvx-rs/tests/cli.rs:1527
modified test fn network_policy_files_compile_into_the_typed_policy in cli (tests) nvx-rs/tests/cli.rs:1588 nvx-rs/tests/cli.rs:1591
modified test fn a_running_sandbox_claims_its_scratch_image in cli (tests) nvx-rs/tests/cli.rs:1834 nvx-rs/tests/cli.rs:1837
modified test fn a_run_passes_openvmm_no_handle_of_its_caller in cli (tests) nvx-rs/tests/cli.rs:2204 nvx-rs/tests/cli.rs:2207
modified test fn a_run_refuses_a_batch_file_as_openvmm in cli (tests) nvx-rs/tests/cli.rs:2284 nvx-rs/tests/cli.rs:2290
modified test fn interrupting_a_run_kills_openvmm_and_exits_with_130 in cli/interrupts (tests) nvx-rs/tests/cli.rs:2830 nvx-rs/tests/cli.rs:2835
modified test fn commands_download_nothing_for_options_that_they_refuse in cli/download (tests) nvx-rs/tests/cli/download.rs:144 nvx-rs/tests/cli/download.rs:144
modified test fn sandbox_run_reads_a_packaged_layer_once_no_download_replaces_it in cli/download (tests) nvx-rs/tests/cli/download.rs:286 nvx-rs/tests/cli/download.rs:297
modified test fn dry_runs_print_the_openvmm_command_and_launch_nothing in cli/dry_run (tests) nvx-rs/tests/cli/dry_run.rs:1311 nvx-rs/tests/cli/dry_run.rs:1271
modified test fn one_shot_runs_exit_with_the_workload_or_openvmm_status in cli/one_shot (tests) nvx-rs/tests/cli/one_shot.rs:681 nvx-rs/tests/cli/one_shot.rs:498

... and 14 more, which the job's summary lists.

End-to-end tests

Change What Before After
modified helper def LifecycleAcceptance.check_identity_admission in scripts/nvx_tools/sandbox_lifecycle_tests.py scripts/nvx_tools/sandbox_lifecycle_tests.py:617 scripts/nvx_tools/sandbox_lifecycle_tests.py:617
modified helper def check_run_report in scripts/nvx_tools/sandbox_ram_upper_tests.py scripts/nvx_tools/sandbox_ram_upper_tests.py:70 scripts/nvx_tools/sandbox_ram_upper_tests.py:78
modified helper def RamUpperAcceptance.run in scripts/nvx_tools/sandbox_ram_upper_tests.py scripts/nvx_tools/sandbox_ram_upper_tests.py:114 scripts/nvx_tools/sandbox_ram_upper_tests.py:120
modified helper def RamUpperAcceptance.check_one_shot in scripts/nvx_tools/sandbox_ram_upper_tests.py (renamed to check_run) scripts/nvx_tools/sandbox_ram_upper_tests.py:144 scripts/nvx_tools/sandbox_ram_upper_tests.py:150

Test doubles

Change What Before After
modified helper struct Options in testing::fake_openvmm nvx-rs/src/testing/fake_openvmm.rs:350 nvx-rs/src/testing/fake_openvmm.rs:352
modified helper fn parse in testing::fake_openvmm nvx-rs/src/testing/fake_openvmm.rs:1335 nvx-rs/src/testing/fake_openvmm.rs:1339
modified helper enum Flow in testing::fake_openvmm nvx-rs/src/testing/fake_openvmm.rs:2539 nvx-rs/src/testing/fake_openvmm.rs:2544
modified helper fn run in testing::fake_openvmm, impl < S : Read + Write + Pending > Session < '_ , S >`` nvx-rs/src/testing/fake_openvmm.rs:2674 nvx-rs/src/testing/fake_openvmm.rs:2682
modified helper fn serve_client in testing::fake_openvmm nvx-rs/src/testing/fake_openvmm.rs:2963 nvx-rs/src/testing/fake_openvmm.rs:2972
modified helper fn serve in testing::fake_openvmm nvx-rs/src/testing/fake_openvmm.rs:3162 nvx-rs/src/testing/fake_openvmm.rs:3178
modified helper fn serve in testing::fake_openvmm nvx-rs/src/testing/fake_openvmm.rs:3254 nvx-rs/src/testing/fake_openvmm.rs:3277

CI test wiring

Change What Before After
added a line that may skip line if nvx-rs/target/release/nvx-rs sandbox \ .github/workflows/run-nvx-microvm-tests.yml:524
added a line that may skip line --memory-mib 256 2>"${errors}"; then .github/workflows/run-nvx-microvm-tests.yml:535
added a line that may skip line fi .github/workflows/run-nvx-microvm-tests.yml:538
modified line nvx-rs/target/release/nvx-rs sandbox \ .github/workflows/run-nvx-microvm-tests.yml:515 .github/workflows/run-nvx-microvm-tests.yml:523
modified line --memory-mib 256 .github/workflows/run-nvx-microvm-tests.yml:525 .github/workflows/run-nvx-microvm-tests.yml:535
modified line "${smoke_arguments[@​]}" \ .github/workflows/run-nvx-microvm-tests.yml:648 .github/workflows/run-nvx-microvm-tests.yml:664
modified line "${smoke_arguments[@​]}" \ .github/workflows/run-nvx-microvm-tests.yml:773 .github/workflows/run-nvx-microvm-tests.yml:789
modified line Invoke-Nvx sandbox --hypervisor "${{ inputs.backend }}" --layer $layer --scratch $scratch @​shareOptions @​smokeArgumen... .github/workflows/run-nvx-microvm-tests.yml:1104 .github/workflows/run-nvx-microvm-tests.yml:1124
modified line Invoke-Nvx sandbox --hypervisor "${{ inputs.backend }}" --layer $layer --scratch $scratch @​shareOptions @​smokeArgumen... .github/workflows/run-nvx-microvm-tests.yml:1205 .github/workflows/run-nvx-microvm-tests.yml:1225

Relocated unchanged: 0; added: 28. Compared the merge base 7bc238eb3d0c with 5d0f26d9aee3.

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

A STOP endpoint closure can bypass the configured shutdown grace period, forcing OpenVMM down before shutdown or outcome reporting completes.

3 open issues
What changed in this PR

Migrates sandbox run to the managed VM API as part of the CLI refactor: nvx-init boots the layered root, executes one workload over NVXC, and stops the VM without persistent sandbox state.

Changes:

  • Adds managed execution options, EROFS/GPT images, capped RAM uppers, protected-path checks, and temporary VM cleanup.
  • Removes unused one-shot launch code while preserving the public API.
  • Updates regression tests, platform smoke checks, and documentation for the managed run behavior.
File Description
scripts/​test-layout-allowlist.json Removes retired plan test-hook exemptions.
scripts/​test_microvm_tests.py Updates acceptance fakes and regression expectations.
scripts/​nvx_tools/​sandbox_ram_upper_tests.py Checks managed runs, default RAM caps, and reports.
scripts/​nvx_tools/​sandbox_lifecycle_tests.py Expects identity rejection during managed startup.
nvx-rs/​tests/​cli/​validation.rs Updates execution-option validation cases.
nvx-rs/​tests/​cli/​sandbox_run.rs Adds managed-run integration coverage.
nvx-rs/​tests/​cli/​openvmm.rs Adds managed-run expectations and cleanup assertions.
nvx-rs/​tests/​cli/​one_shot.rs Removes obsolete sandbox one-shot cases.
nvx-rs/​tests/​cli/​lifecycle.rs Shares lifecycle fixtures with run tests.
nvx-rs/​tests/​cli/​dry_run.rs Expects managed launch commands.
nvx-rs/​tests/​cli/​download.rs Exercises workload status through NVXC.
nvx-rs/​tests/​cli.rs Registers run tests and updates launch checks.
nvx-rs/​src/​host/​sandbox/​tests/​specification.rs Tests managed share rendering instead of boot tokens.
nvx-rs/​src/​host/​sandbox/​tests.rs Updates share and command-line contract tests.
nvx-rs/​src/​host/​sandbox/​prepared.rs Removes the private one-shot preparation path.
nvx-rs/​src/​host/​sandbox/​plan/​tests.rs Removes one-shot rendering assertions.
nvx-rs/​src/​host/​sandbox/​plan.rs Removes one-shot renderers and boot-token delivery.
nvx-rs/​src/​host/​sandbox/​options/​tests.rs Tests run execution options and managed shares.
nvx-rs/​src/​host/​sandbox/​options.rs Allows execution options for run and exec.
nvx-rs/​src/​host/​cli/​session.rs Extracts exec payload validation for preflight use.
nvx-rs/​src/​host/​cli/​sandbox/​tests.rs Updates image-option eligibility tests.
nvx-rs/​src/​host/​cli/​sandbox/​options.rs Documents expanded run options in CLI help.
nvx-rs/​src/​host/​cli/​sandbox/​mod.rs Routes runs through the managed lifecycle.
nvx-rs/​src/​host/​cli/​sandbox/​lifecycle/​start/​tests.rs Checks share overlap during configuration reconstruction.
nvx-rs/​src/​host/​cli/​sandbox/​lifecycle/​start.rs Implements temporary launch, exec, stop, and cleanup.
nvx-rs/​src/​host/​cli/​sandbox/​lifecycle/​mod.rs Exposes the managed run implementation internally.
nvx-rs/​README.md Describes managed run behavior and share delivery.
doc/​usage.md Updates sandbox options and execution semantics.
doc/​ci.md Documents nvx-init smoke checks and mapping refusals.
.github/​workflows/​run-nvx-microvm-tests.yml Switches sandbox probes to the nvx-init profile.

Review effort: Balanced

Base automatically changed from agents/slim-sl3-5c-own-files to feature/nvx-refactor November 11, 2026 19:28
Session::exec checked a workload's arguments, working directory, and
environment, and encoded its request, once it had attached to the VM.
Those checks and the encoding move to Session::exec_payload, which exec
still calls first, so that a caller can refuse a request that exec
would refuse before it starts a VM. exec keeps the check of its
response timeout, which concerns the exchange alone.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ppenna
Pedro Henrique Penna (ppenna) marked this pull request as ready for review November 11, 2026 19:31
sandbox run still booted a one-shot VM: it passed its shares and its
workload as boot tokens, and ran OpenVMM on the terminal with its
lifecycle option, so that the shell /init booted the layered root
through the old one-shot init, and it exited with OpenVMM's status.

A run now launches the VM that a provision and a start of the same
options would launch, without a state directory. It serializes the
configuration that provision records, reads it back as start does, and
launches it through host::vm in a temporary VM directory, with
nvx_sandbox=1 and no OpenVMM lifecycle option, so that nvx-init boots
its root. Once nvx-init is ready and has mapped the shares, the run
executes its workload once over NVXC, with exec's --cwd, --environment,
--environment-file, and --inherit-default-environment, asks nvx-init to
stop the VM, waits up to --timeout for it to exit before it forces it
down, as a stop does, also when its control endpoint closes as it is
asked to stop, then writes the workload's output and exits with its
status. Once the VM has exited, it removes the VM directory, with the
staging file of a report that OpenVMM did not finish writing.

A run takes provision's --layer-format, for an EROFS or a GPT image, and
--ram-upper-percent, for a RAM upper's cap, 50 percent by default. It
refuses --state-dir, and checks its command line, as the launch checks
it too, and its request before it downloads or launches anything. It
refuses the shares that start refuses, with its temporary VM directory
in place of a state directory, through which the workload could change
what OpenVMM writes there, and those of the OpenVMM that it runs, which
NVX_OPENVMM may name instead of the artifact root's; provision and start
refuse a share of that one too (#818). --outcome-report forwards
OpenVMM's report of the run, which keeps its owner's permissions
whatever the umask. The one-shot plan, its boot tokens, and its prepared
launch, which nothing else used, are gone.

The CLI tests of the run cover each format and upper, exec's options,
shares, the refusal of the sandbox's own files, of its VM directory, and
of the OpenVMM that NVX_OPENVMM names, a guest that exits during
startup, a workload that the guest refuses, a VM that does not stop, a
VM whose endpoint closes as it stops, which the fake OpenVMM's new
fake_drop_stop=1 emulates, a command line that the launch refuses, which
downloads nothing, and the outcome report under restrictive umasks; a
unit test covers the removal of a report's staging file; the dry-run,
validation, and one-shot tests take the managed run's command and
refusals. The public sandbox scenarios and the CI smoke steps run the
smoke probe with the init profile, which a managed execution has, and
expect a caller-owned share to fail the run before its workload, as it
fails a start.

This replays esaurez's first implementation, a734c60, adapted to the
typed record, the CLI-owned lifecycle, and SL3.4a's cold boot.

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

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 were identified for the migration of sandbox run to managed VM execution.

No open issues 🎉

3 resolved since last review

Review effort: Balanced

@ppenna
Pedro Henrique Penna (ppenna) merged commit d4edeeb into feature/nvx-refactor Nov 11, 2026
48 checks passed
@ppenna
Pedro Henrique Penna (ppenna) deleted the agents/slim-sl3-4b-sandbox-run branch November 11, 2026 20:13
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Nov 11, 2026
The first draft of the guidelines predates the nvx-init that
feature/nvx-refactor now holds: #1453 routes the new sandbox boots
to it, #1431 serves capture requests, #1447 activates repaired
sessions, #1439 keeps a restore's announcer as a helper, and #1490
runs sandbox run through it too. Check each statement against that
code, and correct those that did not hold:

- an execution takes the VM's one workload identity, which nvx-init
  resolves, or creates, once before its switch, not an account mode
  of its own request, which nothing selects yet;
- the child also drops its capability bounding set, and nvx-init
  kills an execution's process group when its leader ends;
- nvx-init captures only the Fresh boot of a platform capture,
  refuses a session with a root with ENOTSUP, and shares one
  30-second deadline among its checks, its sweep and the capture;
- the restored broker restarts at its first epoch with a reset
  queued, and nvx-init fails on anything but that reset;
- the host sends no MAPS for an empty mapping table, which no
  nvx_maps token announces;
- no launch of NVX's CLI carries OpenVMM's older lifecycle token,
  which still routes a layered launch to the older init agent;
- the crate still keeps older calls that start processes, which
  nvx-init does not use.

Quote, and compile, the space's steps in the order of nvx-init's
cold boot and restore, and replace the capture excerpt with one that
handles each outcome and disposition. The steps compile in SL5.1a's
unit test of nvx-init's boot, the_reference_space_steps_compile,
which now holds them as the guidelines quote them, and the
guidelines link SL5.1a's guest-workflows example. Organize the page
by ownership, the space, the executions, the capture, the transport,
the restore, failures and replay. Trim design 4.2 and 4.3 to the
architecture and boundaries of nvx-init and of the older layered and
blockless roots, which still run. Link the guidelines from the
rustdoc of guest::activation too, list the page in the project
structure, and give the case matrix the doc tests that the
guidelines quote, and the lines to which the links move two others.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Nov 11, 2026
The first draft of the guidelines predates the nvx-init that
feature/nvx-refactor now holds: #1453 routes the new sandbox boots
to it, #1431 serves capture requests, #1447 activates repaired
sessions, #1439 keeps a restore's announcer as a helper, and #1490
runs sandbox run through it too. Check each statement against that
code, and correct those that did not hold:

- an execution takes the VM's one workload identity, which nvx-init
  resolves, or creates, once before its switch, not an account mode
  of its own request, which nothing selects yet;
- the child also drops its capability bounding set, and nvx-init
  kills an execution's process group when its leader ends;
- nvx-init captures only the Fresh boot of a platform capture,
  refuses a session with a root with ENOTSUP, and shares one
  30-second deadline among its checks, its sweep and the capture;
- the restored broker restarts at its first epoch with a reset
  queued, and nvx-init fails on anything but that reset;
- the host sends no MAPS for an empty mapping table, which no
  nvx_maps token announces;
- no launch of NVX's CLI carries OpenVMM's older lifecycle token,
  which still routes a layered launch to the older init agent;
- since #1494, no call of nvx starts the announcer: nvx-init forks
  it and runs /sbin/nvx-ingress itself, and nvx-ingress start runs
  it for an agent that cannot hold it;
- the crate still keeps older calls that start processes, which
  nvx-init does not use.

Quote, and compile, the space's steps in the order of nvx-init's
cold boot and restore, and replace the capture excerpt with one that
handles each outcome and disposition. The steps compile in SL5.1a's
unit test of nvx-init's boot, the_reference_space_steps_compile,
which now holds them as the guidelines quote them, and the
guidelines link SL5.1a's guest-workflows example. Organize the page
by ownership, the space, the executions, the capture, the transport,
the restore, failures and replay. Trim design 4.2 and 4.3 to the
architecture and boundaries of nvx-init and of the older layered and
blockless roots, which still run. Link the guidelines from the
rustdoc of guest::activation too, list the page in the project
structure, and give the case matrix the doc tests that the
guidelines quote, and the lines to which the links move two others.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna) added a commit that referenced this pull request Nov 11, 2026
The first draft of the guidelines predates the nvx-init that
feature/nvx-refactor now holds: #1453 routes the new sandbox boots
to it, #1431 serves capture requests, #1447 activates repaired
sessions, #1439 keeps a restore's announcer as a helper, and #1490
runs sandbox run through it too. Check each statement against that
code, and correct those that did not hold:

- an execution takes the VM's one workload identity, which nvx-init
  resolves, or creates, once before its switch, not an account mode
  of its own request, which nothing selects yet;
- the child also drops its capability bounding set, and nvx-init
  kills an execution's process group when its leader ends;
- nvx-init captures only the Fresh boot of a platform capture,
  refuses a session with a root with ENOTSUP, and shares one
  30-second deadline among its checks, its sweep and the capture;
- the restored broker restarts at its first epoch with a reset
  queued, and nvx-init fails on anything but that reset;
- the host sends no MAPS for an empty mapping table, which no
  nvx_maps token announces;
- no launch of NVX's CLI carries OpenVMM's older lifecycle token,
  which still routes a layered launch to the older init agent;
- since #1494, no call of nvx starts the announcer: nvx-init forks
  it and runs /sbin/nvx-ingress itself, and nvx-ingress start runs
  it for an agent that cannot hold it;
- the crate still keeps older calls that start processes, which
  nvx-init does not use.

Quote, and compile, the space's steps in the order of nvx-init's
cold boot and restore, and replace the capture excerpt with one that
handles each outcome and disposition. The steps compile in SL5.1a's
unit test of nvx-init's boot, the_reference_space_steps_compile,
which now holds them as the guidelines quote them, and the
guidelines link SL5.1a's guest-workflows example. Organize the page
by ownership, the space, the executions, the capture, the transport,
the restore, failures and replay. Trim design 4.2 and 4.3 to the
architecture and boundaries of nvx-init and of the older layered and
blockless roots, which still run. Link the guidelines from the
rustdoc of guest::activation too, list the page in the project
structure, and give the case matrix the doc tests that the
guidelines quote, and the lines to which the links move two others.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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