Repository navigation
cli: restore managed sandboxes from named snapshots (SL3.8a2) - #1487
Merged
Pedro Henrique Penna (ppenna) merged 1 commit intoNov 12, 2026
Merged
Pedro Henrique Penna (ppenna) merged 1 commit into
Pedro Henrique Penna (ppenna) merged 1 commit into
Conversation
Contributor
This pull request changes existing testsIt modifies or removes 2 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: 6. Compared the merge base |
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The tests do not exercise snapshot provisioning-to-restored-start wiring, so omitting the source during provisioning could silently cause a cold start without failing tests.
1 open issue
What changed in this PR
Adds snapshot restore to create managed sandboxes from named snapshots, using the restored-start lifecycle introduced in #1486.
Changes:
- Dispatches snapshot capture and restore through a shared command parser.
- Persists the snapshot source and workload during provisioning, then starts the sandbox from that snapshot.
- Adds parser and refusal tests, and documents restore behavior and state files.
| File | Description |
|---|---|
nvx-rs/src/host/cli/snapshots/tests.rs |
Tests command selection, restore options, and state-directory refusal. |
nvx-rs/src/host/cli/snapshots/capture/tests.rs |
Adapts capture tests to the relocated subcommand parser. |
nvx-rs/src/host/cli/snapshots/capture.rs |
Parses capture arguments after shared command dispatch. |
nvx-rs/src/host/cli/snapshots.rs |
Adds restore parsing, snapshot checks, and command orchestration. |
nvx-rs/src/host/cli/sandbox/mod.rs |
Connects source-aware provisioning to sandbox startup. |
nvx-rs/src/host/cli/sandbox/lifecycle/mod.rs |
Records the optional snapshot source during provisioning. |
nvx-rs/src/host/cli/mod.rs |
Routes snapshot commands through the shared dispatcher. |
nvx-rs/README.md |
Documents restored sandboxes and their state files. |
doc/usage.md |
Explains restore usage, constraints, locking, and recovery. |
Review effort: Balanced
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/slim-sl3-8a1-restored-start
branch
from
November 11, 2026 21:31
8c9847b to
8011acd
Compare
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/slim-sl3-8a2-snapshot-restore
branch
from
November 11, 2026 21:31
bf63b6a to
12f0f38
Compare
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/slim-sl3-8a1-restored-start
branch
2 times, most recently
from
November 11, 2026 23:05
830970e to
97a932d
Compare
Base automatically changed from
agents/slim-sl3-8a1-restored-start
to
feature/nvx-refactor
November 11, 2026 23:21
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/slim-sl3-8a2-snapshot-restore
branch
from
November 11, 2026 23:23
12f0f38 to
8315693
Compare
Pedro Henrique Penna (ppenna)
marked this pull request as ready for review
November 11, 2026 23:23
`snapshot restore NAME --store-dir STORE --state-dir STATE` provisions a managed sandbox with the options of `sandbox provision` that a restore takes, and starts it from the named snapshot that `snapshot capture` published. The sandbox's configuration records the snapshot, its store, and its workload, the entrypoint and its arguments, so each later `sandbox start` restores the snapshot again, as SL3.8a1's start does. The command takes provision's `--layer-format` and `--ram-upper-percent`, so it restores an EROFS or a GPT image, with a scratch upper or a RAM upper and its cap, but none of the options that only an execution or a cold boot takes, which it leaves unrecognized. It opens the snapshot under its lock first, so a missing snapshot provisions nothing, and refuses a state directory in the snapshot store, whose files a start or deprovision could take for a view. A sandbox whose start fails stays provisioned, as after `sandbox provision`. No index of a snapshot's sandboxes or record of a restore in progress exists. Each restore runs the workload as an exec request, as `sandbox run` runs its own, not from boot tokens, so the command checks it as `exec` does, before it provisions anything, and the sandbox's launch takes the default workload, as a run's does. Its arguments may thus contain whitespace and quotes, which `sandbox provision` refuses. `lifecycle::provision` takes the snapshot that it records, which only `snapshot restore` passes. The `snapshot` subcommand parser moves from the capture module to the snapshots module, which dispatches both commands. This is the second half of SL3.8a, from the first implementation on esaurez/slim-3.2.2-sl3.8a-private-20261108 (e7d77fc). Tests: the snapshot parser, which selects capture or a restore with the options of a provision, provision's image format and RAM upper cap among them, requires the snapshot, its store and the state directory, and leaves the options of an execution or a cold boot unrecognized; the capture parser, which now parses only the options after `capture`; the refusal of a state directory in the store, and of a workload that is not a valid exec request, a relative entrypoint or 65 arguments, before anything is provisioned; and the command, with a store that holds a strict snapshot, an artifact root of fakes, and a workload whose argument holds whitespace and quotes: it records the snapshot, the resolved store and the workload, and its start restores that snapshot, as a memory size that the snapshot refuses shows, and fails before it launches or links anything, which leaves the sandbox provisioned. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/slim-sl3-8a2-snapshot-restore
branch
from
November 11, 2026 23:49
8315693 to
fc50a29
Compare
Pedro Henrique Penna (ppenna)
merged commit Nov 12, 2026
64ad98a
into
feature/nvx-refactor
48 checks passed
Pedro Henrique Penna (ppenna)
deleted the
agents/slim-sl3-8a2-snapshot-restore
branch
November 12, 2026 00:05
This was referenced Nov 12, 2026
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 #1114 (SL3.8a2, the second half of SL3.8a, "
snapshot restorestarts a sandbox from a named snapshot"). Parent #1084, umbrella #1076.Follows #1486 (SL3.8a1, merged as
99855f75e0e), which added the restored start that this command uses. This PR was stacked on it and now targetsfeature/nvx-refactor.What and why
snapshot restore NAME --store-dir STORE --state-dir STATEcreates a managed sandbox from a named snapshot thatsnapshot capturepublished:It provisions the sandbox with the options of
sandbox provision, such as the EROFS or GPT image, a scratch or RAM upper and its cap, the hostname and the workload user.The sandbox's configuration records the snapshot, its store and the workload (
--entrypointand its--argvalues).It then starts the sandbox through SL3.8a1's restored start, which runs the workload until it exits with status 0. Each later
sandbox startrestores the snapshot again.Refusals. The command does not take the options that only an execution or a cold boot takes, and leaves them unrecognized:
--cwd,--environment,--environment-file,--inherit-default-environment,--exec-timeout-ms,--wait,--outcome-report,--cmdlineand--dry-run.sandbox provision.Workload. Each restore runs the workload as an exec request over the control protocol, as
sandbox runruns its own, not from boot tokens. So the command checks it asexecdoes, before it provisions anything, and the sandbox's launch takes the default workload, as a run's does. The workload's arguments may thus contain whitespace and quotes, whichsandbox provisionrefuses.Wiring.
lifecycle::provisiontakes the source that it records; onlysnapshot restorepasses one. Thesnapshotsubcommand parser moves from the capture module tosnapshots.rs, which dispatches both commands.Docs.
doc/usage.mdgains asnapshot restoresection. Thenvx-rs/README.mdstate table gainsopenvmm/(the view) andrestore-ready.sock.Not in this PR, per #1114:
run --restore-snapshot(SL3.8b), and everything that #1486 lists.Takeover. This replays Enrique Saurez (@esaurez)'s first implementation (
esaurez/slim-3.2.2-sl3.8a-private-20261108,e7d77fcdda), with his authorship.Split and size. 202 core lines in 5 counted files:
snapshots.rs172,sandbox/mod.rs18,cli/mod.rs6,lifecycle/mod.rs3 andsnapshots/capture.rs3. SL3.8a measures 550 core lines across its two PRs (#1486 348, this PR 202; 549 in their combined diff), against its 500-line stop. Continuing past the stop was approved on Pedro Henrique Penna (@ppenna)'s behalf, up to the PR rules' hard limit of 600 core lines, because the command and the restored start are one coupled change. Its 8 counted files were not, since #1076 allows at most 7, so SL3.8a is split into SL3.8a1 and this PR, at 5 counted files each; #1486 explains why no single PR reaches 7.BOOT_CONTRACT_VERSION,CONTROL_CONTRACT_REVISIONand the public API are unchanged.Validation
The gates are those that
local_gates.pyselects for these files, run from this branch's action definitions atbf63b6aaf7f, which contains SL3.8a1, on66fd43145ce.Rebases. #1486 was rebased onto
d4edeeb9ab9(SL3.4b, #1490), and this PR onto it, as12f0f38578f. This PR's change applied unchanged;git range-diffshowed only context insandbox/mod.rs, whoserunnow also serves SL3.4b'ssandbox run. SL3.4b changed only the help text of the sandbox options, sosnapshot restoretakes the same options. This PR then followed #1486 onto584c2ddde55andf834bcb9672, cleanly but for one resolution:start/tests/restore.rskeeps both #1486's new test of its review and this PR's command test (below). Once #1486 merged as99855f75e0e, whose tree equals its last head's,git rebase --ontoreplayed this PR onto it unchanged, as8315693905c; the second review fix below then made its headfc50a29e9f5. At that head, on Windows, all passed:cargo fmt --check, the five Clippy variants with-D warnings, the lib tests ofhost::cli::sandbox,host::sandbox,host::cli::snapshotsandhost::restore*, the wholecliintegration suite, andcheck_test_layout.py. On Linux (nvx-kvm-test), the same checks and the macOS check passed at7ebdb1778ba, which has the command test, before the last rebases and the second review fix; CI runs them on Linux at the head. The table and the real-VM results below are from before the rebases, but for the restores of the second review fix.Review fixes. Copilot's reviews found two gaps:
No test of the command's wiring from the provision to the restored start.
snapshot_restore_records_its_source_and_starts_a_restore_of_it, instart/tests/restore.rs, runssnapshot restorethroughsnapshots::parseandsnapshots::run, with a store that holds a strict snapshot and an artifact root of fakes. It checks:Config::source()returns the name, the store aspaths::resolveresolves it, and the workload;--memory-mib 2176, which the snapshot refuses, it fails withUnsupported("snapshot memory must be from boot"), a check that only a restore makes;sandbox deprovisionremoves it.The test fails if
sandbox::restoreprovisions without the snapshot, or if the start ignores the recorded source.A workload with whitespace was refused. Provision's launch checked the workload as kernel-command-line tokens, which refuse whitespace and quotes, although each restore sends it as an exec request over NVXC. The launch of a restored sandbox now takes the default workload, as a run's does, and
snapshot restorechecks the workload withSession::exec_payload, asexecandruncheck theirs, before it provisions anything. The command test now restores/bin/echo 'a "restored" sandbox'; without the launch's default workload, it fails with "sandbox arguments must be nonempty and contain no whitespace, control characters, or quotes".a_restore_refuses_a_workload_that_is_not_a_valid_exec_request_before_it_provisionsrefuses a relative entrypoint and 65 arguments before anything is created, and fails without the check.Real VMs at
fc50a29e9f5, on KVM (nvx-kvm-test) and MSHV (nvx-mshv-test), aftersnapshot capture:snapshot restoreof an EROFS image with a RAM upper and/bin/echo 'a "restored" sandbox'printsa "restored" sandboxand records that workload. A latersandbox startrestores again and prints it again./bin/sh -c 'printf "%s|%s\n" "restored sandbox" "$(id -u)"; test -d /run/nvx', which printsrestored sandbox|65534, and asandbox execworks afterwards.sandbox provisionstill refuses an argument with whitespace.cargo fmt --check; the five Clippy variants with-D warnings;cargo test --all-features,cargo test, and--lib --no-default-featuresbuild and test; bundling;cargo docwith-D warnings; MSRV 1.89; runtime adapter (protocol-only and host-only);public_api.py;check_test_layout.py;check_test_changes.py;check_markdown_links.pyNVX_RSset to this branch's releasenvx-rs(test_usage_documents_every_option_of_nvx_rsamong them),test_adversarial.py,test_hosts.py, and thenvx.py--helpcheckscargo test, the--lib --no-default-featuresbuild and test, doc, MSRV, bundling, macOS check,public_api.py, guest-binary build and test, runtime adapter (protocol, host and guest),check_test_layout.py,check_markdown_links.pycargo test --all-features;check-guest-binariesOperation not supported): 28guest::tests of the lib, and 26 of the guest build on musl. Both sets are exactly those that fail at the base66fd43145ceon that host.snapshot capture, thensnapshot restore, on KVM (nvx-kvm-test, Azure Linux 3, kernel 6.6), MSHV (nvx-mshv-test, kernel 6.18 mshv3) and WHP (Windows 11 developer machine)test-microvm --scenario sandbox-lifecycle --scenario sandbox-ram-upper --scenario managed-exec-config --scenario sandbox-proxy-only, KVM and MSHVThe restore matrix on each backend:
snapshot capture basepublishes exactlykey.bin,snapshot.jsonandopenvmm/.snapshot restorepasses with each image format (EROFS, GPT) and upper (scratch; RAM with caps 25 and 90).lowerdir=/run/nvx/layers/distro, or/run/nvx/layers/p2for GPT, and the scratch or tmpfs upper.--timeout 2fails with "another command still holds the lock of the snapshot base" and leaves the view. With 60 s, it ends right after that restore (KVM 8.26 s vs 8.25 s, MSHV 8.24 vs 8.21, WHP 8.56 vs 8.50).The real-VM runs used OpenVMM
14dc4116: from the release on Linux, and built at that pin for WHP. They used the release's kernel, an initramfs built from this branch, the release's Ubuntu EROFS layer, and a GPT image made from it. The public scenarios used an Ubuntu layer built from this branch's guest sources (build-distro-layer --guest ubuntu), since the release's layer predates #1460's probe.--workload-user: a pre-existing limitation. A restore with--workload-user 1000:1000fails in the guest (NVX-MANAGED-ERROR: stage=data status=71, guest exit 125) on KVM and MSHV. This is the layered root's existing rule, not this change:protocol::features::WORKLOAD_ACCOUNT).sandbox startwith that image and identity fails the same way: at66fd43145ce, this branch's base before the rebase, and at its head then,bf63b6aaf7f.1:1(daemon) and65534:65534(nobody), both cold and restored starts work, and the restored workload runs as that user.Left to CI: validate-nvx's Linux CLI job; the guest tests that need working isolated child processes, which fail on nvx-kvm-test at the base too; and MSHV's and WHP's own CI runs.
Changes to existing tests (
tests-modified)check_test_changes.py --base origin/feature/nvx-refactor, with #1486 merged, lists 2 changes and 6 added tests, none relocated. The added ones are thesnapshotparser's test and itscommandhelper, the refusals of a state directory in the snapshot store and of a workload that is not a valid exec request, and the command test above and itsartifact_roothelper. The changes:optionshelper ofhost::cli::snapshots::capture::testsnow passes the arguments aftercapture, since the capture parser no longer sees the subcommand: thesnapshotparser insnapshots.rsselects it.capture_parser_refuses_restore_bad_names_and_invalid_optionsis renamedcapture_parser_refuses_bad_names_and_invalid_options.restoreis now a command, which thesnapshotparser's tests cover. The capture parser keeps refusing an escaping name, a missing store, a zero timeout and an invalid processor count, and its--help.