Repository navigation
restore: report the attribution of a restore that fails in its guest (SL5.1d) - #1515
Merged
Pedro Henrique Penna (ppenna) merged 3 commits intoNov 12, 2026
Conversation
Contributor
This pull request changes existing testsIt modifies or removes 6 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
End-to-end tests
Relocated unchanged: 0; added: 5. Compared the merge base |
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues remain for the restore-status propagation and CLI attribution changes.
No open issues 🎉
What changed in this PR
This PR closes the in-guest restore-attribution gap in #1118’s acceptance coverage. nvx-init reports existing failure-class statuses, and the CLI preserves restore evidence and attribution without changing public APIs or error codes.
Changes:
- Classify failed restored activations and propagate their statuses through nvx-init.
- Preserve restore attribution in CLI errors, including guest exits and unreadable outcome reports.
- Add regression coverage and update documentation and the acceptance case matrix.
| File | Description |
|---|---|
scripts/test_microvm_tests.py |
Adds fake-CLI regressions for missing and incorrect attribution. |
scripts/nvx_tools/microvm_tests.py |
Requires status 54 and request attribution for zeroed-scratch restores. |
nvx-rs/src/host/restore/tests/live_init.rs |
Updates deadline and changed-request status expectations. |
nvx-rs/src/host/cli/sandbox/lifecycle/start/tests/restore.rs |
Tests attribution for guest failures and unreadable reports. |
nvx-rs/src/host/cli/sandbox/lifecycle/start.rs |
Retains restore errors and includes attribution in CLI diagnostics. |
nvx-rs/src/guest/managed_agent/session/tests.rs |
Tests failure-status propagation and requests during activation. |
nvx-rs/src/guest/managed_agent/session.rs |
Exposes activation failure status and rejects requests during application. |
nvx-rs/src/guest/managed_agent/dispatch.rs |
Returns the failed activation’s status to PID 1. |
nvx-rs/src/guest/init/boot/tests.rs |
Covers activation failure classes and status selection. |
nvx-rs/src/guest/init/boot.rs |
Maps activation failures and refused requests to existing statuses. |
nvx-rs/src/guest/init.rs |
Updates rustdoc for restored-activation exit statuses. |
nvx-rs/README.md |
Documents nvx-init’s class-specific failure statuses. |
doc/usage.md |
Describes attributed restore errors shown by the CLI. |
doc/run.md |
Documents the strengthened zeroed-scratch acceptance check. |
doc/guest-agent-guidelines.md |
Describes failure-class statuses after activation failures. |
doc/acceptance-case-matrix.md |
Records replacement coverage and removes the attribution-gap note. |
Review effort: Balanced
A restored activation that failed in nvx-init, such as one whose root does not assemble over a zeroed scratch image, ended the VM with the generic status 125, as any other failure of PID 1. The host's restore then saw only the guest's exit: RestoreEvidence::reported classifies the statuses of nvx::protocol::status, and found none in 125, so it attributed the failure to nothing, as the old adapter's worker, whose failure_status picked the class, had not. nvx-init now ends the VM with the status of the failure's class, from the statuses that nvx::protocol::status already defines: once the control session has failed, and no helper ended, the session asks the restored activation for its failure, and Restored::failure_status returns its class: - the activation's own failure, which a deadline that has passed by then also is, by its cause, ACTIVATION_DEADLINE to ACTIVATION_ORDER. A failed application takes the class of the first cause that names one: the ApplyError of the record's application, or the step of nvx-init's own space that it marks, the workload's account and hostname and the announcer, or else the root's, 54, whose runtime file systems and switch the other steps are; - an activate request that Restored refuses before its activation takes it: ACTIVATION_PROTOCOL for one that changes the accepted request, before or after Ready, as the status's docs say, and ACTIVATION_RECORD for one that does not decode. A request while the record applies now fails the activation, as its state machine's table says, where it ended the session without it. An expiry that only the last check finds is reported on the console too; every other failure was reported where it happened. A cold boot, a ready activation and every other failure keep their statuses, 125 among them. Tests: a_failed_activation_ends_the_vm_with_the_status_of_its_class drives an activation to each cause, each class of application failure and each refused request, and a_failed_restored_activation_gives_init_the_status_of_its_class checks the session's wiring, with a request while the record applies. The live restore fixture's deadline and changed cases, which pinned 125, now expect ACTIVATION_DEADLINE and ACTIVATION_PROTOCOL. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A restore whose guest failed before it was ready printed only how its VM ended, such as `OpenVMM exited during startup: guest-exit status 125`: once the VM had exited by itself, launch_locked replaced the restore driver's RestoreError, whose evidence attributes the failure, with the outcome report's category and status, or with the error of a report that it could not read, and a restore that failed otherwise printed the error's phase and evidence without its attribution. The start of a restored sandbox, which `snapshot restore`, a later `sandbox start` and `run --restore-snapshot` share, now keeps the restore driver's error and prints what its evidence attributes the failure to: after the VM's outcome when the VM exited by itself, as `OpenVMM exited during startup: guest-exit status 54; restore failed during Activate: UnusableRoot, attributed to the request`, after the error of an outcome report that it cannot read, and in the `snapshot restore failed` error otherwise. A cold start's messages and every error's code are unchanged. Tests: a_restore_that_fails_before_its_guest_is_ready_reports_its_attribution restores through the fake VMM, whose guest ends its VM with ACTIVATION_ROOT's status after an outcome report, or after one that cannot be parsed, or answers the Activate with a malformed Ready. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
snapshot-core's restore over a zeroed scratch image, which the guest cannot mount as its upper, required only that the CLI report the guest's exit with status 125, and the case matrix recorded the missing attribution as a known gap. The scenario now requires nvx-init's status of a root that does not assemble, 54, and that the CLI attribute the failure to the request from the restore's evidence, UnusableRoot, which gives back the coverage of the old adapter test a_clone_whose_activation_fails_ends_before_it_is_ready. Its fake CLI fails the scenario for a guest that ends with the generic status and for a failure attributed to another cause. The case matrix names the new tests in that test's row, and nvx-init's in the row of the adapter worker's test of each class's status; its Snapshots table describes the attributed failure in place of the known gap, which goes; and Run describes the scenario's new check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pedro Henrique Penna (ppenna)
force-pushed
the
agents/slim-sl5-1d-restore-attribution
branch
from
November 12, 2026 11:26
746f1f9 to
c185074
Compare
Pedro Henrique Penna (ppenna)
marked this pull request as ready for review
November 12, 2026 11:26
Pedro Henrique Penna (ppenna)
marked this pull request as draft
November 12, 2026 11:27
Pedro Henrique Penna (ppenna)
marked this pull request as ready for review
November 12, 2026 11:27
Pedro Henrique Penna (ppenna)
merged commit Nov 12, 2026
00c55b9
into
feature/nvx-refactor
110 of 130 checks passed
Pedro Henrique Penna (ppenna)
deleted the
agents/slim-sl5-1d-restore-attribution
branch
November 12, 2026 11:47
This was referenced Nov 12, 2026
Closed
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 #1118 (SL5.1d, "a restore that fails in its guest reports its attribution"), follow-up A2 of SL5.1c. Parent #1086, umbrella #1076.
This branch is three commits on
feature/nvx-refactor671a192d. It is not stacked.What and why
A restore that failed in its guest printed only how its VM ended:
snapshot-core's restore over a zeroed scratch image, which the guest cannot mount as its upper, reportederror[exited]: OpenVMM exited during startup: guest-exit status 125. The case matrix recorded this as a known gap. Two things lost the attribution:nvx::protocol::status(RestoreEvidence::reported), and 125 is none of them, so the evidence wasExitedand the attributionUnknown. The old adapter's worker picked the class's status (failure_status), which is the coverage thata_clone_whose_activation_fails_ends_before_it_is_readygave.RestoreErrorwith the outcome report's category and status, and a restore that failed otherwise printed the error's phase and evidence without its attribution.nvx-init (
guest/init/boot.rs,guest/managed_agent/{session,dispatch}.rs) now ends a VM whose restored activation failed with the status of the failure's class, from the statuses thatnvx::protocol::statusalready defines. Once the control session has failed, and no helper ended,serve_initasks the session for the restored activation's failure, andRestored::failure_statusreturns its class:ACTIVATION_DEADLINE, also for a deadline that only this last check finds,ACTIVATION_PROTOCOL,ACTIVATION_RECORD,ACTIVATION_RESOURCESorACTIVATION_ORDER;ApplyErrorof the record's application (its GPT disk and root's assembly,ACTIVATION_ROOT; its machine ID and view,ACTIVATION_WORKLOAD; its export,ACTIVATION_EXPORT; its network settings,ACTIVATION_NETWORK), or the step of nvx-init's own space that a private marker classes (the workload's account and hostname,ACTIVATION_WORKLOAD; the announcer,ACTIVATION_NETWORK), or else the root's, 54, whose runtime file systems and switch the other steps are;ACTIVATION_PROTOCOLfor one that changes the accepted request, before or after Ready, asACTIVATION_PROTOCOL's docs say, andACTIVATION_RECORDfor one that does not decode.A request while the record applies now fails the activation with
ActivationInput::Request, as the state machine's table says, where it ended the session without failing it. An expiry that only the last check finds is reported on the console withNVX-INIT-ACTIVATION:, as every other failure already was where it happened. A cold boot, a ready activation, a helper's end and every other control-session failure keep their statuses, 125 among them:sandbox-lifecycle's cold-boot refusal of a workload identity still reportsguest-exit status 125.Decision, approved for the workstream on Pedro Henrique Penna (@ppenna)'s behalf: nvx-init also classes the activate requests that it refuses before its activation sees them. A changed activate request, even after Ready, gets 51,
ACTIVATION_PROTOCOL, as that status's documented "before or after" says; one that doesn't decode gets 52,ACTIVATION_RECORD; and another request while the record applies gets 51. This stays within the existing failure classes and changes neither contract nor the public API. The ignoredlive_init.rsfixture'schangedanddeadlinecases expect 51 and 50 accordingly.The CLI (
host/cli/sandbox/lifecycle/start.rs): the start of a restored sandbox, whichsnapshot restore, a latersandbox startandrun --restore-snapshotshare, keeps the restore driver'sRestoreErrorand prints what its evidence attributes the failure to (attributed to the request,the host,the snapshotorthe guest's repair, orof unknown attribution):OpenVMM exited during startup: guest-exit status 54; restore failed during Activate: UnusableRoot, attributed to the request;cannot parse outcome report …; restore failed during Activate: ReportUnreadable, attributed to the host;snapshot restore failed: …error otherwise.Every error's code is unchanged, and so are a cold start's messages.
Real-VM coverage.
snapshot-core's restore over a zeroed scratch image now requiresguest-exit status 54; restore failed during …: UnusableRoot, attributed to the request. That is the attribution of an in-guest restore failure thata_clone_whose_activation_fails_ends_before_it_is_readychecked, in the existing scenario. Its fake CLI injects two more regressions that the scenario must catch: a guest that ends with the generic 125, and a failure attributed to another cause.Docs. The case matrix drops the known-gap paragraph. Its Snapshots table describes the attributed failure, and the rows of
a_clone_whose_activation_fails_ends_before_it_is_readyand of the adapter worker'seach_class_of_failure_ends_the_clone_with_its_own_statusname the new tests.doc/run.mddescribes the scenario's new check,doc/usage.mda failed restore's output,doc/guest-agent-guidelines.mdnvx-init's status after a failed activation, andnvx-rs/README.mdnvx-init's statuses.init.rs's rustdoc lists them too.Contracts.
BOOT_CONTRACT_VERSION,CONTROL_CONTRACT_REVISIONand the public API are unchanged (python3 scripts/public_api.pypasses). No failure class is new: the statuses are those thatnvx::protocol::statusdefines, and the host already classified them.Size
Measured against
feature/nvx-refactorccea8757, and unchanged by the rebase onto671a192d(the counted files'git diff --numstatis identical): 152 core lines (estimate 120, stop 250) in 5 counted files (estimate 4):guest/init/boot.rs77: the class mapping, the step marker and the refused requests;host/cli/sandbox/lifecycle/start.rs34;guest/managed_agent/session.rs22: the accessor, and the request during the application, mostly the reindented drain loop;scripts/nvx_tools/microvm_tests.py14, plus 7 lines of docstrings;guest/managed_agent/dispatch.rs5.guest/init.rschanges rustdoc only. The tests and docs don't count.Commits
guest: end a failed restored activation with its class's statuscli: report the attribution of a restore that fails in its guesttests: check the attribution of a restore that fails in its guestNot in this PR
BOOT_CONTRACT_VERSIONorCONTROL_CONTRACT_REVISION.Validation
After the rebase onto
671a192dThe head is
c1850747. The rebase conflicted only indoc/acceptance-case-matrix.md, where #1502 (SL3.7c) rewrote the NIC sentence of the known-gap paragraph that this PR drops: the matrix keeps #1502's rows and NIC sentence, and gains this PR's attribution.git range-diffshows the guest commit unchanged and the CLI commit changed in context only.untiered-network-policy, which CI's step now includes. It used this head's Alpine initramfs, built there withnvx.py build-initramfs --guest alpine(5e607036…, which holds this nvx-init and SL6.3's/init), the static musl CLI, and run 43149037772's kernel, Ubuntu layer, scratch template and OpenVMM, whose pin14dc4116is unchangedsnapshot-corepassed in 15.4 s, with the zeroed scratch'serror[exited]: OpenVMM exited during startup: guest-exit status 54; restore failed during Activate: UnusableRoot, attributed to the requestcargo fmt --all --check;cargo clippy --locked -- -D warningswith--all-targets --all-features,--lib,--lib --no-default-featuresand--lib --bins --features cli;cargo test --locked --all-features;cargo test --locked --lib --no-default-features;cargo docwith-D warnings;cargo +1.89 check --locked --all-targets --all-features;public_api.py;check-guest-binaries --toolchain 1.95.0cargo fmt --all --checkandcargo clippy --locked --all-targets --all-features -- -D warningspython -m unittest scripts/test_microvm_tests.pyruff check,ruff format --check,pyright --pythonplatform Linuxand--pythonplatform Windows,check_test_layout.py,check_markdown_links.py99d2022085fBefore the rebase, on
ccea8757Real VMs, at the head
746f1f96. The runs used the kernel, the Ubuntu EROFS layer, the scratch template and the OpenVMM (14dc4116) of CI run 43149037772, the push of5b3fab2c, checked by their SHA-256 digests. The Alpine initramfs is this head's, built on nvx-kvm-test withnvx.py build-initramfs --guest alpine(efa21836…), since it holds nvx-init. On Linux, the CLI is the static musl release build, run as CI runs it; on WHP, the MSVC release build. Each run is CI's public sandbox step:managed-exec-config,sandbox-lifecycle,sandbox-proxy-only,sandbox-ram-upper,sandbox-gpt-layer,vm-lifecycle,snapshot-coreandsnapshot-tiers.snapshot-coreerror[exited]: OpenVMM exited during startup: guest-exit status 54; restore failed during Activate: UnusableRoot, attributed to the requestBefore the review fixes (the refused requests, the request during the application and the unreadable report), the branch at
6b7957d3, on5b3fab2c, passed the same runs on all three backends. KVM also passedsnapshot-corewith a glibc CLI.The fix makes the difference.
4bd05851…, nvx-init with 125) and this CLI,snapshot-corefails on KVM at the zeroed scratch:guest-exit status 125; restore failed during Activate: Exited, of unknown attribution.a_restore_that_fails_before_its_guest_is_ready_reports_its_attributionfails against the base'sstart.rs:unusable-root: OpenVMM exited during startup: guest-exit status 54.Restored::failure_statusandSession::activation_status, which the base doesn't have.Rust, every step of
check-nvx-rs, on nvx-mshv-test at746f1f96, all passed:cargo fmt --all --checkcargo clippy --locked -- -D warningswith--all-targets --all-features;--lib;--lib --no-default-features;--lib --no-default-features --features serde;--lib --bins --features cli;--lib --bins --no-default-features --features testingcargo test --locked --all-features,cargo test --locked, andcargo build/test --locked --lib --no-default-featuresRUSTDOCFLAGS="-D warnings" cargo doc --locked --no-deps --all-featurescargo +1.89 check --locked --all-targets --all-features(MSRV)python3 scripts/public_api.pypython3 scripts/nvx.py check-guest-binaries --toolchain 1.95.0On Windows (local),
cargo clippy --locked --all-targets --all-features -- -D warningsandcargo test --locked --all-featurespassed (1484 library tests).Gates, from this branch's
check-qualityandvalidate-nvxactions, aslocal_gates.py --filesselects them, on Windows (local):ruff checkandruff format --checkofscripts .github/speculapyright --pythonplatform Linuxand--pythonplatform Windowspython -m unittest scripts/test_microvm_tests.pycheck_markdown_links.py,test_markdown_links.py,check_test_layout.pyLeft to CI: the macOS check, the guest artifacts' Docker builds, the default microVM shards, shellcheck and shfmt (no shell script changes), and Specula's tests.
Changes to existing tests
python3 scripts/check_test_changes.py --base origin/feature/nvx-refactorlists 6 changes to existing tests and 5 added items, so this PR has thetests-modifiedlabel:host::cli::sandbox::lifecycle::start::tests::restore,Fixture::spawn: the fake VMM's recipe names the VM's outcome report, which the new behaviors write.serve, the fake guest: two new behaviors that the new test uses. Withunusable-root, it answers the Activate with an outcome report of status 54 and exits with 54. Withunreadable-report, it writes a truncated report.host::restore::tests::live_init::run, the#[ignore]live restore fixture: itsdeadlinecase now expectsACTIVATION_DEADLINE, 50, and itschangedcaseACTIVATION_PROTOCOL, 51, where both pinned 125. It needs explicit host admission and template-manifest inputs, and didn't run here._FakeSnapshotCli._startinscripts/test_microvm_tests.py: the zeroed scratch's error is the new one. The faultsunclassified-guest-failureandmisattributed-guest-failurereturn the old 125 message and a failure attributed to the host.PublicSnapshotTests.test_snapshot_core_detects_regressions_of_snapshots_and_their_restoresrequires the scenario to catch those two faults.SnapshotCoreAcceptance.check_failed_restores, the scenario itself, as above.Added:
guest::init::boot::tests::a_failed_activation_ends_the_vm_with_the_status_of_its_class,guest::managed_agent::session::tests::a_failed_restored_activation_gives_init_the_status_of_its_class,host::cli::sandbox::lifecycle::start::tests::restore::a_restore_that_fails_before_its_guest_is_ready_reports_its_attribution, and the scenario's constants.