Skip to content

restore: report the attribution of a restore that fails in its guest (SL5.1d) - #1515

Merged
Pedro Henrique Penna (ppenna) merged 3 commits into
feature/nvx-refactorfrom
agents/slim-sl5-1d-restore-attribution
Nov 12, 2026
Merged

Pedro Henrique Penna (ppenna) merged 3 commits into
feature/nvx-refactorfrom
agents/slim-sl5-1d-restore-attribution

Conversation

@ppenna

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

Copy link
Copy Markdown
Contributor

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-refactor 671a192d. 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, reported error[exited]: OpenVMM exited during startup: guest-exit status 125. The case matrix recorded this as a known gap. Two things lost the attribution:

  • nvx-init ended the VM with the generic status 125 after any failure, a failed restored activation included. The host's restore classifies the statuses of nvx::protocol::status (RestoreEvidence::reported), and 125 is none of them, so the evidence was Exited and the attribution Unknown. The old adapter's worker picked the class's status (failure_status), which is the coverage that a_clone_whose_activation_fails_ends_before_it_is_ready gave.
  • The CLI, once OpenVMM had exited by itself, replaced the restore driver's RestoreError with 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 that nvx::protocol::status already defines. Once the control session has failed, and no helper ended, serve_init asks the session for the restored activation's failure, and Restored::failure_status returns its class:

  • the activation's own failure, by its cause: ACTIVATION_DEADLINE, also for a deadline that only this last check finds, ACTIVATION_PROTOCOL, ACTIVATION_RECORD, ACTIVATION_RESOURCES or ACTIVATION_ORDER;
  • for a failed application of the record, the class of the first cause that names one: the ApplyError of 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;
  • an activate request that nvx-init refuses before its activation takes it: ACTIVATION_PROTOCOL for one that changes the accepted request, before or after Ready, as ACTIVATION_PROTOCOL's docs say, and ACTIVATION_RECORD for 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 with NVX-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 reports guest-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 ignored live_init.rs fixture's changed and deadline cases expect 51 and 50 accordingly.

The CLI (host/cli/sandbox/lifecycle/start.rs): the start of a restored sandbox, which snapshot restore, a later sandbox start and run --restore-snapshot share, keeps the restore driver's RestoreError and prints what its evidence attributes the failure to (attributed to the request, the host, the snapshot or the guest's repair, or of unknown attribution):

  • after the VM's outcome, when the VM exited by itself: 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: cannot parse outcome report …; restore failed during Activate: ReportUnreadable, attributed to the host;
  • in the 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 requires guest-exit status 54; restore failed during …: UnusableRoot, attributed to the request. That is the attribution of an in-guest restore failure that a_clone_whose_activation_fails_ends_before_it_is_ready checked, 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_ready and of the adapter worker's each_class_of_failure_ends_the_clone_with_its_own_status name the new tests. doc/run.md describes the scenario's new check, doc/usage.md a failed restore's output, doc/guest-agent-guidelines.md nvx-init's status after a failed activation, and nvx-rs/README.md nvx-init's statuses. init.rs's rustdoc lists them too.

Contracts. BOOT_CONTRACT_VERSION, CONTROL_CONTRACT_REVISION and the public API are unchanged (python3 scripts/public_api.py passes). No failure class is new: the statuses are those that nvx::protocol::status defines, and the host already classified them.

Size

Measured against feature/nvx-refactor ccea8757, and unchanged by the rebase onto 671a192d (the counted files' git diff --numstat is identical): 152 core lines (estimate 120, stop 250) in 5 counted files (estimate 4):

  • guest/init/boot.rs 77: the class mapping, the step marker and the refused requests;
  • host/cli/sandbox/lifecycle/start.rs 34;
  • guest/managed_agent/session.rs 22: the accessor, and the request during the application, mostly the reindented drain loop;
  • scripts/nvx_tools/microvm_tests.py 14, plus 7 lines of docstrings;
  • guest/managed_agent/dispatch.rs 5.

guest/init.rs changes rustdoc only. The tests and docs don't count.

Commits

  1. guest: end a failed restored activation with its class's status
  2. cli: report the attribution of a restore that fails in its guest
  3. tests: check the attribution of a restore that fails in its guest

Not in this PR

  • New failure classes, and any change to BOOT_CONTRACT_VERSION or CONTROL_CONTRACT_REVISION.
  • The attribution of cold-boot failures, which keep 125.
  • A class for a control session's other failures during an activation, such as a framing error or a closed console, and for an announcer that ends after Ready, which ends the VM as a helper does: they keep 125.

Validation

After the rebase onto 671a192d

The head is c1850747. The rebase conflicted only in doc/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-diff shows the guest commit unchanged and the CLI commit changed in context only.

Check Result
CI run 43209354829 passed: 33 jobs succeeded, among them every microVM shard on KVM, MSHV and WHP; 9 skipped as on every pull request (publication, performance history, the debug kernels of KVM and WHP, and the unchanged pin's OpenVMM CI)
CI's public sandbox step on Linux/MSHV, nvx-mshv-test (kernel 6.18), virtual machine: the 8 scenarios below and untiered-network-policy, which CI's step now includes. It used this head's Alpine initramfs, built there with nvx.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 pin 14dc4116 is unchanged 9 passed; snapshot-core passed in 15.4 s, with the zeroed scratch's error[exited]: OpenVMM exited during startup: guest-exit status 54; restore failed during Activate: UnusableRoot, attributed to the request
On nvx-mshv-test: cargo fmt --all --check; cargo clippy --locked -- -D warnings with --all-targets --all-features, --lib, --lib --no-default-features and --lib --bins --features cli; cargo test --locked --all-features; cargo test --locked --lib --no-default-features; cargo doc with -D warnings; cargo +1.89 check --locked --all-targets --all-features; public_api.py; check-guest-binaries --toolchain 1.95.0 passed
On Windows: cargo fmt --all --check and cargo clippy --locked --all-targets --all-features -- -D warnings passed
On Windows: python -m unittest scripts/test_microvm_tests.py 363 tests, OK (9 skipped)
On Windows: ruff check, ruff format --check, pyright --pythonplatform Linux and --pythonplatform Windows, check_test_layout.py, check_markdown_links.py passed, 0 errors
The workstream's case-matrix check, against 99d2022085f passed

Before the rebase, on ccea8757

Real 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 of 5b3fab2c, checked by their SHA-256 digests. The Alpine initramfs is this head's, built on nvx-kvm-test with nvx.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-core and snapshot-tiers.

Backend Host Public step snapshot-core Zeroed scratch's error
Linux/KVM nvx-kvm-test (kernel 6.6), virtual machine 8 passed passed, 16.2 s error[exited]: OpenVMM exited during startup: guest-exit status 54; restore failed during Activate: UnusableRoot, attributed to the request
Linux/MSHV nvx-mshv-test (kernel 6.18), virtual machine 8 passed passed, 15.4 s the same
Windows/WHP local, Windows 11 8 passed passed, 22.8 s the same

Before the review fixes (the refused requests, the request during the application and the unreadable report), the branch at 6b7957d3, on 5b3fab2c, passed the same runs on all three backends. KVM also passed snapshot-core with a glibc CLI.

The fix makes the difference.

  • With the base's initramfs (CI's 4bd05851…, nvx-init with 125) and this CLI, snapshot-core fails 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_attribution fails against the base's start.rs: unusable-root: OpenVMM exited during startup: guest-exit status 54.
  • The guest tests exercise Restored::failure_status and Session::activation_status, which the base doesn't have.

Rust, every step of check-nvx-rs, on nvx-mshv-test at 746f1f96, all passed:

Check Result
cargo fmt --all --check passed
cargo clippy --locked -- -D warnings with --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 testing passed
cargo test --locked --all-features, cargo test --locked, and cargo build/test --locked --lib --no-default-features passed, the new tests among them
RUSTDOCFLAGS="-D warnings" cargo doc --locked --no-deps --all-features passed
cargo +1.89 check --locked --all-targets --all-features (MSRV) passed
python3 scripts/public_api.py passed, unchanged
python3 scripts/nvx.py check-guest-binaries --toolchain 1.95.0 passed

On Windows (local), cargo clippy --locked --all-targets --all-features -- -D warnings and cargo test --locked --all-features passed (1484 library tests).

Gates, from this branch's check-quality and validate-nvx actions, as local_gates.py --files selects them, on Windows (local):

Check Result
ruff check and ruff format --check of scripts .github/specula passed
pyright --pythonplatform Linux and --pythonplatform Windows passed, 0 errors
python -m unittest scripts/test_microvm_tests.py 353 tests, OK (9 skipped)
check_markdown_links.py, test_markdown_links.py, check_test_layout.py passed

Left 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-refactor lists 6 changes to existing tests and 5 added items, so this PR has the tests-modified label:

  • 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.
  • The same module's serve, the fake guest: two new behaviors that the new test uses. With unusable-root, it answers the Activate with an outcome report of status 54 and exits with 54. With unreadable-report, it writes a truncated report.
  • host::restore::tests::live_init::run, the #[ignore] live restore fixture: its deadline case now expects ACTIVATION_DEADLINE, 50, and its changed case ACTIVATION_PROTOCOL, 51, where both pinned 125. It needs explicit host admission and template-manifest inputs, and didn't run here.
  • _FakeSnapshotCli._start in scripts/test_microvm_tests.py: the zeroed scratch's error is the new one. The faults unclassified-guest-failure and misattributed-guest-failure return the old 125 message and a failure attributed to the host.
  • PublicSnapshotTests.test_snapshot_core_detects_regressions_of_snapshots_and_their_restores requires 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.

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

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

This pull request changes existing tests

It 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

Change What Before After
modified helper fn spawn in host::cli::sandbox::lifecycle::start::tests::restore, impl Fixture`` nvx-rs/src/host/cli/sandbox/lifecycle/start/tests/restore.rs:144 nvx-rs/src/host/cli/sandbox/lifecycle/start/tests/restore.rs:145
modified helper fn serve in host::cli::sandbox::lifecycle::start::tests::restore nvx-rs/src/host/cli/sandbox/lifecycle/start/tests/restore.rs:919 nvx-rs/src/host/cli/sandbox/lifecycle/start/tests/restore.rs:961
modified helper fn run in host::restore::tests::live_init nvx-rs/src/host/restore/tests/live_init.rs:160 nvx-rs/src/host/restore/tests/live_init.rs:160
modified helper def _FakeSnapshotCli._start in scripts/test_microvm_tests.py scripts/test_microvm_tests.py:3668 scripts/test_microvm_tests.py:3668
modified test def PublicSnapshotTests.test_snapshot_core_detects_regressions_of_snapshots_and_their_restores in scripts/test_microvm_tests.py scripts/test_microvm_tests.py:4166 scripts/test_microvm_tests.py:4179

End-to-end tests

Change What Before After
modified helper def SnapshotCoreAcceptance.check_failed_restores in scripts/nvx_tools/microvm_tests.py scripts/nvx_tools/microvm_tests.py:2823 scripts/nvx_tools/microvm_tests.py:2830

Relocated unchanged: 0; added: 5. Compared the merge base 671a192d0a57 with c1850747280f.

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 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>

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 block the restore-failure attribution changes.

No open issues 🎉

Review effort: Balanced

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants