Repository navigation
nvx-rs: terminate VMs of another runtime identity on stop - #751
Merged
Pedro Henrique Penna (ppenna) merged 4 commits intoOct 23, 2026
Merged
Conversation
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
Cross-platform forced termination and runtime-record cleanup require human sign-off and confirmation of the remaining backend CI gates.
No open issues 🎉
What changed in this PR
This PR extends the shared NVX supervisor so owners can stop sandboxes started under a different runtime identity without contacting an incompatible guest protocol. It builds on #721’s store integration and implements the runtime-identity termination policy in #622.
Changes:
- Adds identity-verified forced termination to the supervisor, instance API, CLI, and ACI backend while preserving ordinary same-identity stop behavior.
- Separates boot-contract versioning from the microVM ABI and records boot contract version 3.
- Expands termination, recovery, and identity-safety tests, with corresponding documentation and fixture updates.
| File | Description |
|---|---|
| scripts/test_microvm_tests.py | Extends lifecycle fakes and regression checks. |
| scripts/nvx_tools/sandbox_lifecycle_tests.py | Tests cross-identity termination on real VMs. |
| nvx-rs/tests/supervisor.rs | Covers termination bounds, permissions, and process identity. |
| nvx-rs/tests/store.rs | Checks mismatches with boot contract 2. |
| nvx-rs/tests/fixtures/store/runtime.json | Records boot contract 3. |
| nvx-rs/tests/fixtures/store/launch-spawned.json | Updates spawned-launch boot contract. |
| nvx-rs/tests/fixtures/store/launch-prepared.json | Updates prepared-launch boot contract. |
| nvx-rs/tests/fixtures/store/invalid/runtime-unknown-field.json | Updates the invalid runtime fixture’s contract. |
| nvx-rs/tests/fixtures/store/invalid/launch-without-log.json | Updates the invalid launch fixture’s contract. |
| nvx-rs/tests/fake_openvmm.rs | Tests instance termination without control sessions. |
| nvx-rs/tests/cli.rs | Tests mismatched stops, stale identities, and recovery. |
| nvx-rs/src/testing/fake_openvmm.rs | Records accepted control connections for tests. |
| nvx-rs/src/protocol/root/tests.rs | Pins root tokens to the boot-contract version. |
| nvx-rs/src/protocol/root.rs | Documents boot-token versioning. |
| nvx-rs/src/host/supervisor/tests.rs | Tests forced-only termination and error paths. |
| nvx-rs/src/host/supervisor/mod.rs | Adds bounded termination and shared stop helpers. |
| nvx-rs/src/host/store/tests.rs | Checks the dedicated boot-contract constant. |
| nvx-rs/src/host/store/identity.rs | Uses boot-contract versioning in runtime identities. |
| nvx-rs/src/host/instance.rs | Exposes termination for mismatched instances. |
| nvx-rs/src/host/cli/sandbox/lifecycle.rs | Terminates mismatched VMs and emits warnings. |
| nvx-rs/src/contract.rs | Defines boot contract 3 and versioning rules. |
| nvx-rs/README.md | Documents APIs and cross-identity lifecycle behavior. |
| doc/usage.md | Describes backend identity acceptance tests. |
| doc/run.md | Documents mismatched stops and lifecycle coverage. |
| aci_edge_sandboxes/tests/openvmm_fake.rs | Checks forced stops after identity changes. |
| aci_edge_sandboxes/tests/openvmm_e2e.rs | Tests reattachment and cross-identity termination. |
| aci_edge_sandboxes/src/openvmm/mod.rs | Routes mismatched stops through termination. |
| aci_edge_sandboxes/src/openvmm/config.rs | Documents the updated mismatch policy. |
| aci_edge_sandboxes/README.md | Explains termination behavior and test coverage. |
Review effort: Balanced
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a1-host-h4-instance-20261021
branch
from
October 22, 2026 21:07
a73868d to
55a3e8f
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a4-pr6-runtime-identity-20261021
branch
from
October 22, 2026 21:38
9961615 to
b763908
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a1-host-h4-instance-20261021
branch
from
October 22, 2026 21:44
55a3e8f to
d73f87d
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a1-host-h4-instance-20261021
branch
from
October 22, 2026 22:19
d73f87d to
3e5a32d
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a4-pr6-runtime-identity-20261021
branch
from
October 22, 2026 22:39
b763908 to
3bed036
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a1-host-h4-instance-20261021
branch
2 times, most recently
from
October 22, 2026 23:51
445bbe6 to
a869a3e
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a4-pr6-runtime-identity-20261021
branch
from
October 23, 2026 00:22
3bed036 to
0ca7932
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a4-pr6-runtime-identity-20261021
branch
from
October 23, 2026 02:57
0ca7932 to
0240a37
Compare
Enrique Saurez (esaurez)
marked this pull request as ready for review
October 23, 2026 03:35
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a4-pr6-runtime-identity-20261021
branch
from
October 23, 2026 04:02
0240a37 to
327267c
Compare
Copilot stopped reviewing on behalf of
Enrique Saurez (esaurez) due to an error
October 23, 2026 04:03
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a4-pr6-runtime-identity-20261021
branch
from
October 23, 2026 04:39
327267c to
6fd5543
Compare
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a4-pr6-runtime-identity-20261021
branch
from
October 23, 2026 05:38
6fd5543 to
d18ea38
Compare
A VM that another runtime identity started may speak other boot or control contracts, so nothing drives it through a control session. Its owner could stop it only with the artifacts that started it. Now the owner stops it by identity-verified forced termination: - supervisor: Vmm::terminate takes the forced step alone, through the VMM's verified process identity, bounded by the caller. It clears the runtime record once the VMM has exited, and keeps it when the exit is not confirmed, so that a later call retries. Records whose process has exited, as after a reuse of its ID or an earlier boot, end nothing. - instance: LockedInstance::terminate ends a Mismatched VM, and the Mismatched error points at it. - CLI: stop terminates a VM of another runtime identity within --timeout and warns on standard error that it did. start and exec refuse it, naming the parts that differ, and point at stop. The stop of a VM of the same identity is unchanged. - aci_edge_sandboxes: stop terminates such a VM and reports it as forced. Every other operation still fails with backend_error. - fake OpenVMM: count the control connections that it accepts, so that tests prove that no session reached the VM. Part of #622 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 84d06582-33fd-47b2-954e-904c4ea1a084
The scenario now restarts the CLI under an artifact root whose build provenance names another initramfs, as after an upgrade, while the sandbox runs. That CLI must refuse to exec and start, and deprovision must find the sandbox running, while the CLI of the checkout still drives the VM. Its stop must then terminate OpenVMM without a guest shutdown, warn that it did, and leave no runtime record, capability, or control endpoint, after which the sandbox starts again. Part of #622 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 84d06582-33fd-47b2-954e-904c4ea1a084
The runtime identity recorded the microVM ABI version as its boot contract, so the version stayed 2 when the boot tokens of a layered root began to name the image's format and could keep the root's upper in RAM (#676). A VM booted with the earlier tokens kept the runtime identity of one booted with the new ones whenever the identities of its artifacts were unchanged. nvx::contract::BOOT_CONTRACT_VERSION now versions the boot contract: the microVM ABI, the kernel command-line tokens that configure the guest, and the root that the guest assembles from them. It is 3, for the image format token and the RAM upper. The module documents when each contract version must change, and a test pins the version beside every token form of a layered root, whose exhaustive matches stop compiling when a format or upper is added. The store's fixtures and the CLI's goldens record boot contract 3. Part of #622 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 84d06582-33fd-47b2-954e-904c4ea1a084
The CLI's stop of a VM of another runtime identity waited inside Vmm::terminate for up to --timeout seconds, a wait that does not see Ctrl-C. An OpenVMM that outlived its termination, as one in uninterruptible I/O, therefore held the stop until the timeout passed. The termination now waits one retry interval at most, and the stop's own wait, which Ctrl-C ends, waits for the rest. Once OpenVMM has exited, a second termination finds it exited, signals nothing, and clears its runtime record, and the stop reports the forced termination as before. A stop that Ctrl-C ends keeps the record, so that the next stop retries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 84d06582-33fd-47b2-954e-904c4ea1a084
Enrique Saurez (esaurez)
force-pushed
the
esaurez/a4-pr6-runtime-identity-20261021
branch
from
October 23, 2026 11:12
143ba20 to
b8fbce6
Compare
Pedro Henrique Penna (ppenna)
deleted the
esaurez/a4-pr6-runtime-identity-20261021
branch
October 23, 2026 12:32
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.

A sandbox that another runtime started (another OpenVMM, kernel, initramfs, or boot or control contract) is never driven through a guest protocol that it might not speak. Until now its owner could not stop it either: the CLI and
host::instancereported it and asked for the artifacts that had started it. This PR lets the owner stop it by identity-verified forced termination, and versions the boot contract, so that the runtime identity changes with the boot tokens.Termination
Vmm::terminate(locked, bound)takes the forced step alone, through the VMM's verified process identity (a pidfd on Linux, a process handle on Windows), with no graceful step, and waits up to the caller's bound. It returnsForced,Gracefulif the VMM had already exited, orTimedOut. It clears the runtime record once the VMM has exited. A VMM that still runs, or whose state cannot be determined, keeps its record, so that a later call retries. Bounds of zero or aboveMAX_TIMEOUTare refused.Vmm::stopkeeps its behavior and now shares its helpers withterminate.host::instance:LockedInstance::terminate(mismatched, timeout)ends aFound::MismatchedVM and returnsStopped { forced, report: None }: it reads no outcome report, which only the other runtime's OpenVMM could have written. It fails, naming the process, if OpenVMM did not exit in time.Mismatched::errorpoints at it.stopof a sandbox whose VM runs under another runtime identity terminates its OpenVMM within--timeout, exits 0, and warns on standard error. The termination itself waits one retry interval at most, and the stop's own wait, which Ctrl-C ends, waits for the rest; a stop that Ctrl-C ends before OpenVMM's exit is confirmed keeps the runtime record for the next stop.startandexecrefuse such a sandbox, naming the parts that differ, and point atstop, anddeprovisionrefuses it as running.stopof a sandbox of the same identity is unchanged: graceful, and it reports a timeout without forced termination.aci_edge_sandboxes:stopterminates such a VM and reports it asforced. Every other operation still fails withbackend_error.A record whose process reference names a process that has exited, as after a reuse of its ID or on a later host boot, is found exited before any identity comparison, so nothing is signaled. An interrupted launch is recovered as before, whatever the identity.
Boot contract version
The runtime identity recorded
MICROVM_ABI_VERSION, 2, as its boot contract, so it did not change when #676 made the layered root's boot token name the image format and added the RAM upper.nvx::contract::BOOT_CONTRACT_VERSION, now 3, versions the boot contract: the microVM ABI, the kernel command-line tokens that configure the guest, and the root that the guest assembles from them.nvx::contractdocuments when each ofMICROVM_ABI_VERSION,BOOT_CONTRACT_VERSION,CONTROL_SESSION_PROTOCOL_VERSION, andCONTROL_CONTRACT_REVISIONmust change. A test inprotocol::rootpins the version beside every token form of a layered root, and its exhaustive matches stop compiling when a format or upper is added.MICROVM_ABI_VERSIONitself is unchanged, since releases declare it in theirSOURCE-MANIFEST.json.The artifact identities still come from install or staging metadata, a release's
SHA256SUMSor a checkout's build provenance, and nothing is hashed at launch.Intended golden and message changes
nvx-rs/tests/fixtures/cli.json: the 26runtime.jsonsnapshots, in 13 cases, record"bootContract": 3instead of 2. No other golden changes.nvx-rs/tests/fixtures/store/*.json:"bootContract": 3.startandexecof a mismatched sandbox:sandbox runs under another runtime identity than these artifacts, which differ in PARTS, so it is not driven: stop it to terminate its OpenVMM process PID without a guest shutdown, which replaces... which differ in PARTS: stop it with the artifacts that started it (OpenVMM process PID).stopof a mismatched sandbox: status 0 andwarning: sandbox ran under another runtime identity than these artifacts, which differ in PARTS: its OpenVMM process PID was terminated without a guest shutdown, instead of the error above.Mismatched::error, whichaci_edge_sandboxesreports asbackend_error:sandbox NAME runs under another runtime identity, which differs in PARTS, so it is not driven: stop it to terminate its OpenVMM process PID without a guest shutdown, which replaces... stop it with the NVX build and artifacts that started it (OpenVMM process PID).Tests
fake-openvmm-TOKEN-sessions.logbeside its kernel, so the instance and CLI tests prove that a termination opened no session.host::instance: a restarted orchestrator of another identity terminates the VM without a session; an outcome report beside such a VM is never read, whether the stop terminates OpenVMM or OpenVMM exited after it was found; records that name another process, by start time or boot, end nothing.aci_edge_sandboxes: the fake test follows the newstop, whose metadata holds onlyforcedeven beside an outcome report; the new real-hypervisor testa_vm_of_another_runtime_identity_is_only_terminatedchecks that a restarted backend reattaches, and that one whose initramfs differs refusesexecand terminates the VM onstop, after which the sandbox starts again.sandbox-lifecycle(real VMs): a CLI under an artifact root whose build provenance names another initramfs refusesexec,start, anddeprovisionwhile the checkout's CLI still drives the VM, and itsstopterminates OpenVMM and leaves onlyconfig.json,sandbox.lock, andopenvmm.log; the sandbox then starts again.The test adapter's restart scenarios (#718) cover the same-identity restart on the host side, so this PR reuses them, unchanged, rather than adding another: a later process of the adapter's host program reattaches to the VM, which is not its child, in a later session epoch, and a third one stops it gracefully. Their other scenario checks that a host program that ships another build of the guest agent finds the VM mismatched, in its initramfs, and signals nothing. The adapter's
stopstill signals nothing to a mismatched VM, which is its own policy; having it terminate one throughVmm::terminateis left to the test adapter's end-to-end work in #622.Part of #622
Refs #620, #535
Validation
On this head, with the artifacts of the development releases
v0.1.0-dev.872ef6a1bbdfon Windows andv0.1.0-dev.03d945bc2d3aon Linux, whose OpenVMM matches this branch's gitlink:Windows 11 x64, WHP:
nvx-rs,aci_edge_sandboxes, andtests/runtime-adapter(the protocol, and thehostfeature), ascheck-nvx-rsandcheck-aci-edge-sandboxesrun them:cargo fmt --all --check; Clippy with-D warningsfor all targets and features, and for the library without default features;cargo test --lockedwith all features and with the defaults, and the library's build and tests without default features;cargo docwith-D warnings;cargo +1.89 check; the artifact-bundling test against a staged synthetic release; andpython scripts/nvx.py test-agent-bundling --toolchain 1.95.0. All pass.validate-nvx's Windows steps, except itspip install:compileall, theunittestsuites,test_adversarial.py,test_hosts.py, and the--helpchecks, with Git'sbashfirst onPATHfor the setup suite's Linux cases. All pass.ruff check,ruff format --check, andscripts/check_markdown_links.py.python scripts/nvx.py test-microvm --backend whp --scenario lifecycle --scenario managed-lifecycle --scenario sandbox-lifecycle --scenario managed-exec-config --scenario sandbox-proxy-only: pass.python scripts/nvx.py test-aci-edge-sandboxes --backend whp: 19 passed.cargo test --locked --features host -- --ignoredintests/runtime-adapter, withNVX_HYPERVISOR=whp, the checkout asNVX_ARTIFACTS_DIR, and a guest agent built on Linux: 14 passed, the two restart scenarios and the twelve layered ones.Linux x86_64 with the MSHV kernel, as a non-root user:
check-nvx-rsandcheck-aci-edge-sandboxes, with Rust 1.95.0 and each crate's minimum version: the checks above,cargo check --target aarch64-apple-darwinof both crates, the artifact-bundling test,test-agent-bundling,check-guest-binaries,build-guest-binariesandtest-guest-binaries, and the runtime adapter's protocol,host, andguestconfigurations, the last forx86_64-unknown-linux-musl. All pass.validate-nvx's Linux steps under Python 3.12:compileall, theunittestsuites,test_adversarial.py,test_hosts.py, and the--helpchecks. All pass.check-quality's Python checks with the pinned ruff 0.15.22 and pyright 1.1.411:ruff check,ruff format --check,pyright --pythonplatform Linuxand--pythonplatform Windows,scripts/check_markdown_links.py, and Specula'scompilealland tests. All pass.Linux x86_64 with the MSHV kernel, as root, because
/dev/mshvis root-only on that host:test-microvmscenarios with--backend mshv: pass.test-aci-edge-sandboxes --backend mshv: 14 passed, the new test included. The five tests that write through host mappings fail withPermission denied, as they do as root without this change.NVX_HYPERVISOR=mshvand the guest agent built there: 14 passed.Linux x86_64, KVM, as a non-root user, with these commits on an earlier head of #721, which pinned the previous OpenVMM and preceded #718, and the artifacts of #676's merge:
check-nvx-rsandcheck-aci-edge-sandboxesbut the bundling and guest-binary steps,check-quality's Python checks, andcompileall,scripts/test_performance.py, andscripts/test_microvm_tests.pyunder Python 3.10. All pass.test-microvmscenarios butsandbox-proxy-only, which did not exist yet, with--backend kvm: pass.test-aci-edge-sandboxes --backend kvm: 19 passed.On KVM and MSHV, OpenVMM ran with
--cpu-profile hostthrough a wrapper, because those hosts cannot present their CPUs' built-in profiles.Left to CI: the backend matrix on the self-hosted runners (OpenVMM unit and VMM tests, the microVM scenarios,
test-aci-edge-sandboxes, the test adapter's VM scenarios, and benchmarks), KVM on this head included, andcheck-quality's shell and PowerShell checks, whose inputs this PR doesn't change. CI also covers this head merged with the OpenVMM thatdevpins since #788.