Skip to content

[DO NOT REVIEW] state_unit: fail a state change instead of panicking when a unit is lost - #5013

Merged
Pedro Henrique Penna (ppenna) merged 3 commits into
feature/microvm/devfrom
copilot/nvx-issue-1363-state-unit-lost-task
Nov 10, 2026
Merged

Pedro Henrique Penna (ppenna) merged 3 commits into
feature/microvm/devfrom
copilot/nvx-issue-1363-state-unit-lost-task

Conversation

@ppenna

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

Copy link
Copy Markdown
Contributor

Keeps a state unit whose task has ended from panicking the VM worker, for microsoft/nvx#1363. StateUnits::run_op, which stop, reset, save, restore, advance_time, and the rollback of a failed start use, panicked with UnitRecvError { .., source: Channel(Closed) } when a unit did not answer its request, as when its task has exited, by a stated policy that unit failure is not recoverable. OpenVMM's profiles abort on panic, so the process ended without reporting the failure or tearing the VM down. Only start already failed instead, leaving the unit in the terminal StartUncertain state.

Three commits on 6ddd6ca50, the head of feature/microvm/dev. microsoft/nvx#1448 pins this head.

Roll back a partial start without panicking on a lost unit (2fcd1573d)

  • Cause. The rollback of a failed start, which the chain's position 015 adds, stops the units that did start with run_op, so a started unit whose task then exits panics it. It also stops a started unit while a unit that depends on it may still run, such as a unit whose start is uncertain or, in a hot-add, a unit that already ran.
  • Change. As the later revisions of state_unit: roll back a partial start instead of panicking #4872 do, the rollback stops the started units in reverse dependency order: a started unit is not sent a stop request while a unit that depends on it is not stopped, unless that unit was removed, and a started unit whose stop is blocked or not answered is left in StartUncertain. Such a unit may still run, so the units then stay running, and a later StateUnits::stop stops it. A unit that failed to confirm its start is still stopped with the units that started, and can start again.
  • Tests. In tests/start.rs, a dependency of a unit whose start is uncertain, a started unit whose task exits before the rollback stops it, a new dependency of a running unit, and a unit that drops the rollback's stop request each fail the start and stay running, and a later stop stops them. At 6ddd6ca50, two of them panic in run_op with UnitRecvError, and the rollback stops the dependency in the other two.

Fail a stop instead of panicking when a unit is lost (5a750a862)

  • Cause. StateUnits::stop returned () and panicked in run_op on a unit that did not answer. A microVM stops its units for every snapshot quiesce, pause, and teardown.
  • Change. As state_unit: fail a stop instead of panicking when a unit is lost #4937 proposes upstream, the stop uses the rollback's ordered stop, which moves to a module that both share, and returns anyhow::Result<()>. A unit whose stop request is not answered is left in a new terminal state, StopUncertain, and the units that it depends on are not sent a stop request, since it may still use them; the units then stay running, so that a later stop can stop them. A unit removed during the stop, even while it waits behind a unit whose stop is not answered, is not a failure and no longer keeps its dependencies running. A stop also stops a unit in a terminal state, which keeps its marker.
  • Callers.
    • LoadedVm::pause returns the error, leaving the VM paused with state units running. A reset, a pulse save and restore, a VMRS dump, a pause, a worker restart, and PCI resource assignment report it, and the worker then tears the VM down, since those units can never start again: it does so whenever state units run while the VM does not, as a failed stop, or a start whose rollback did not stop every unit, leaves them.
    • VmRpc::Pause becomes failable, as this chain made VmRpc::Resume, so that the REPL, the ttrpc PauseVm handler, the VM controller's disk snapshot, and petri report a failed pause. During a microVM snapshot boundary it fails, as a resume does, rather than being dropped.
    • A microVM snapshot quiesce whose units do not all stop fails with the new SnapshotQuiesceError::Unrecoverable, and the VM controller terminates the source, which can neither roll back nor resume, with nothing published.
    • The teardown, a failed snapshot boundary, a failed restore readiness event, and an expired restore gate log a failed stop, since the VM ends anyway.
    • OpenHCL reports a failed stop to the requester of a pause, which UhVmRpc::Pause now allows, a diagnostic save, a worker restart, or a servicing request, and keeps serving, as after a failed start; state_unit: fail a stop instead of panicking when a unit is lost #4937 tears it down instead.
  • Tests. In the new tests/stop.rs, a lost unit fails the stop, keeps its dependency running, and fails a later start, reset, save, restore, and time advance, while the units run and after another stop; a unit whose stop is not answered stops when asked again and stays terminal; and a unit removed during a stop does not keep its dependency running, even while it waits behind a unit whose stop is not answered. At the previous commit, the first two, and the removal behind an unanswered stop, panic in run_op with UnitRecvError. The existing tests that stop units, among them [DO NOT REVIEW] state_unit: handle a unit in a terminal state instead of panicking #5012's in tests/terminal.rs, unwrap the result, and the snapshot filter's tests count Pause among the failable requests.

Fail a reset, save, restore, or time advance on a lost unit (a8a0e3517)

  • Cause. reset, save, restore, and advance_time reach the same panic in run_op. A microVM saves its state units for every snapshot, and restores them and advances their time for every restore.
  • Change. run_op fails the operation instead, naming the unit in the operation's StateTransitionError, and leaves the unit in the operation's terminal state, ResetUncertain, SaveUncertain, RestoreUncertain, or AdvanceTimeUncertain, since its state is unknown, so that a later start, reset, save, restore, or time advance fails too. A unit removed during the operation still does not fail it. run_op now serves only these failable requests.
  • Tests. In the new tests/lost.rs, a unit built with UnitBuilder::build whose receiver is dropped, as in the issue's reproduction, fails a save, a reset, a restore, and a time advance, naming the unit, while the other units complete it, and stays terminal; a unit removed during a save does not fail it. At the previous commit, the first four panic in run_op with UnitRecvError { name: "lost", source: Channel(Closed) }.

Decisions

  • Each operation leaves a unit that does not answer in a terminal state of its own, rather than in one shared state, so that the state names the request that went unanswered.
  • VmRpc::Pause becomes failable, like this chain's VmRpc::Resume, rather than dropping its reply or panicking on the failed stop, as state_unit: fail a stop instead of panicking when a unit is lost #4937's worker does.
  • A failed snapshot quiesce gets the new SnapshotQuiesceError::Unrecoverable, since neither RollbackSafe, after which the source resumes, nor Rejected, which refuses the request before it quiesces anything, describes a source that can never run again.
  • state_unit: roll back a partial start instead of panicking #4872's other later change, which keeps the units running after a start whose outcome is uncertain, and not only after an incomplete rollback, is not part of this pull request.

Validation

At this head, a8a0e3517, with Rust 1.97.1:

  • Windows: cargo nextest run --profile agent -p state_unit -p openvmm_core -p openvmm_defs -p openvmm_entry -p petri, 505 passed; cargo clippy --all-targets and cargo doc --no-deps (RUSTDOCFLAGS=-D warnings) for those packages, without warnings; cargo check -p vmm_tests --tests; and cargo xtask fmt --fix, clean. Each commit builds them, and passes the state_unit tests, 24, 28, and 33.
  • Linux (NVX's KVM test VM, Azure Linux 3.0): the same commands with underhill_core added, without warnings, 599 passed.
  • The issue's reproduction: at 6ddd6ca50, a stop, reset, save, restore, and time advance of a unit whose receiver is dropped panic with UnitRecvError { name: "lost", source: Channel(Closed) }, while its start fails; at this head all six fail with an error naming the unit, locally on Windows and on NVX's KVM and WHP test VMs.
  • End to end: NVX's microVM guest-boot, lifecycle, managed-lifecycle, sandbox-lifecycle, restore-downtime, snapshot-core, and snapshot-rollback scenarios passed with 1 and 2 vCPUs, with OpenVMM built from this head's tree on NVX's KVM, MSHV (musl), and WHP test VMs.

The Linux checks and the test VMs ran e9e626604, this head before two of its commit messages were reworded, with the same tree.

Land by fast-forwarding feature/microvm/dev to this head, a8a0e35178f61c0e82cbe465b7ebf70e0f0bb543, since this repository's merge button squashes and would leave NVX's pin off the branch.

When a start fails, its rollback stops the units that did start with
run_op, which panics when one of them does not answer, as when its task
exited after it started (microsoft/nvx#1363). The rollback also stops a
started unit while a unit that depends on it may still run, such as a
unit whose start is uncertain, or a unit that ran before a hot-add.

Stop the started units in reverse dependency order, as the later
revisions of #4872 do: a started unit is not sent a
stop request while a unit that depends on it is not stopped, unless
that unit was removed, and a started unit whose stop is blocked or not
answered is left in the terminal StartUncertain state. Such a unit may
still run, so the units then stay running, so that StateUnits::stop can
stop it. A unit that failed to confirm its start is still stopped with
the units that started, and can start again.

Tests: a dependency of a unit whose start is uncertain, a started unit
whose task exits before the rollback stops it, a new dependency of a
running unit, and a unit that drops the rollback's stop request each
fail the start and stay running, and a later stop stops them. Before
the change, two of them panic in run_op with UnitRecvError, and the
other two stop the dependency.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A unit whose task has exited, or that drops its stop request, makes
StateUnits::stop panic the VM worker with "UnitRecvError { name: ..,
source: Channel(Closed) }": run_op treats an unanswered request as an
unrecoverable unit failure (microsoft/nvx#1363). OpenVMM's profiles
abort on panic, so the process ends without reporting the failure or
tearing the VM down. A microVM stops its units when it quiesces for a
snapshot, and whenever it pauses or tears down.

Stop the units in reverse dependency order without panicking, with the
ordered stop of the start rollback, which moves to a module that both
share, as #4937 proposes upstream. A unit whose stop
request is not answered is left in a new terminal state, StopUncertain,
and the units that it depends on are not sent a stop request, since it
may still use them. StateUnits::stop then returns the failures, and the
units stay running, so that a later stop can stop them, as once that
unit is removed. A unit removed during a stop, even while it waits for a
unit that depends on it, is not a failure, and no longer keeps its
dependencies running. A stop also stops a unit in a terminal state,
which keeps its marker, and while a unit is in one, a later start,
reset, save, restore, or time advance fails.

The callers handle the error:

- LoadedVm::pause fails, leaving the VM paused and state units running.
  A reset, pulse save and restore, VMRS dump, or pause reports the
  failure, and the worker then tears the VM down, since those units can
  never start again: it does so whenever state units run while the VM
  does not, as a failed stop, or a start whose rollback did not stop
  every unit, leaves them.
- VmRpc::Pause becomes failable, as VmRpc::Resume is in this chain, so
  that the REPL, the ttrpc PauseVm handler, the VM controller's disk
  snapshot, and Petri report a failed pause. During a microVM snapshot
  boundary it fails, as a resume does, rather than being dropped.
- A microVM snapshot quiesce whose units do not all stop fails with the
  new SnapshotQuiesceError::Unrecoverable, and the controller
  terminates the source, which can neither roll back nor resume, with
  nothing published.
- A worker restart whose VM cannot stop reports the failure and tears
  the VM down, and PCI resource assignment returns the error.
- The teardown, a failed snapshot boundary, a failed restore readiness
  event, and an expired restore gate log a failed stop, since the VM
  ends anyway.
- OpenHCL reports a failed stop to the requester of a pause, which
  UhVmRpc::Pause now allows, a diagnostic save, a worker restart, or a
  servicing request, and keeps serving, as after a failed start.

Tests: a lost unit fails the stop, keeps its dependency running, and
fails a later start, reset, save, restore, or time advance, while the
units run and after another stop; a unit whose stop is unanswered stops
when asked again and stays terminal; a unit removed during a stop does
not keep its dependency running, even while it waits behind a unit whose
stop is unanswered. At the previous commit, the first two scenarios,
and the removal behind an unanswered stop, panic in run_op with
UnitRecvError.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
StateUnits::reset, save, restore, and advance_time panic the VM worker
when a unit does not answer their request, as when its task has exited
(microsoft/nvx#1363): run_op treats that as an unrecoverable unit
failure. A microVM saves its state units for a snapshot, and restores
them and advances their time when it restores one.

Have run_op fail the operation instead, naming the unit in the
operation's StateTransitionError, and leave the unit in the operation's
terminal state, ResetUncertain, SaveUncertain, RestoreUncertain, or
AdvanceTimeUncertain, since its state is unknown, so that a later start,
reset, save, restore, or time advance fails too. A unit removed during
the operation still does not fail it. run_op now serves only these
failable requests, so it returns each unit's result as an error.

Tests: a unit built with UnitBuilder::build whose receiver is dropped
fails a save, a reset, a restore, and a time advance, naming the unit,
while the other units complete it, and stays terminal; a unit removed
during a save does not fail it. At the previous commit, the first four
panic in run_op with UnitRecvError { name: "lost", source:
Channel(Closed) }.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions github-actions Bot added the release-nvx Targets a branch under feature/microvm/. label Nov 10, 2026
@ppenna
Pedro Henrique Penna (ppenna) merged commit a8a0e35 into feature/microvm/dev Nov 10, 2026
73 of 74 checks passed
@github-actions

Copy link
Copy Markdown

syeedali pushed a commit to syeedali/nvx that referenced this pull request Nov 11, 2026
A state unit whose task has exited makes the VM worker panic at its
next stop, save, reset, restore, or time advance, or in the rollback
of a failed start, with "UnitRecvError { .., source: Channel(Closed)
}" (microsoft#1363): StateUnits::run_op treats a request that a unit does not
answer as an unrecoverable unit failure. OpenVMM's profiles abort on
panic, so the process ends without reporting the failure or tearing
the VM down. A microVM stops its units for every snapshot quiesce,
pause, and teardown, saves them for every capture, and restores them
and advances their time for every restore. Only a start already failed
instead, leaving the unit in a terminal state.

Pin a8a0e35178f61c0e82cbe465b7ebf70e0f0bb543, the head of
microsoft/openvmm#5013, three commits on 6ddd6ca50, the head of
feature/microvm/dev:

- 2fcd1573d state_unit: roll back a partial start without panicking on
  a lost unit
- 5a750a862 state_unit: fail a stop instead of panicking when a unit
  is lost
- a8a0e3517 state_unit: fail a reset, save, restore, or time advance
  on a lost unit

A unit that does not answer a stop, a start's rollback, a reset, a
save, a restore, or a time advance now fails that request, named in
the error, and is left in a terminal state that fails every later
start, reset, save, restore, and time advance. Its dependencies keep
running when it does not stop, since it may still use them, and the
units then stay running for a later stop. The VM worker tears the VM
down when state units run while it is paused, VmRpc::Pause reports a
failed stop, and a snapshot quiesce whose units do not all stop fails
with SnapshotQuiesceError::Unrecoverable, on which the controller
terminates the source with nothing published.

Evidence: OpenVMM state_unit tests that follow the issue's
reproduction, a unit built with UnitBuilder::build whose receiver is
dropped, panic with "UnitRecvError { name: "lost", source:
Channel(Closed) }" in a stop, reset, save, restore, and time advance at
6ddd6ca50, and return an error naming the unit at the pin, on
nvx-kvm-test (Linux) and nvx-whp-test (Windows), both virtual machines,
and on a local Windows host. With the pin, the guest-boot, lifecycle,
managed-lifecycle, sandbox-lifecycle, restore-downtime, snapshot-core,
and snapshot-rollback scenarios pass with 1 and 2 vCPUs on
nvx-kvm-test (KVM), nvx-mshv-test (MSHV), and nvx-whp-test (WHP). The
OpenVMM tests of the changed packages pass, 505 on Windows and 599 on
Linux, where underhill_core is built and tested too.

The control-session vectors, the source excerpts, the contract
versions, and the CPU profiles that nvx-rs copies from OpenVMM match
the revision's.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
syeedali pushed a commit to syeedali/nvx that referenced this pull request Nov 11, 2026
Map positions 243-245, 2fcd1573d, 5a750a862, and a8a0e3517, which
microsoft/openvmm#5013 proposes for feature/microvm/dev on its head,
position 242, 6ddd6ca50, which microsoft/openvmm#5012 merged with
position 241, and move NVX's pin to position 245, 114 commits past the
mapped tip.

Position 243 has the rollback of a partial start, which PR09's position
015 adds, stop the units that started again in reverse dependency
order, without panicking on a unit whose task does not answer, as
microsoft/openvmm#4872's head does. It is therefore a PR09 fixup,
recorded as positions 241 and 242 are: the DAG's PR09 fixup step,
PR09's summary row, graph node, and mapping count it, and the fold
sentence after the DAG sends it into position 015, and its description
of the snapshot rollback into PR17's position 072.

Positions 244 and 245 have a stop, and a reset, save, restore, or time
advance, fail instead of panicking when a unit does not answer, which
no mapped commit adds, so they stay unassigned, with remarks on the
chain commits whose callers they change. microsoft/openvmm#4937
proposes position 244's change for upstream's code, so the paragraph on
it no longer says that it proposes no chain commit.

Replaying them on upstream main, ba105dd7f, conflicts in all three
files of position 243, 12 of the 20 of position 244, and two of the
three of position 245, since upstream main lacks the start rollback of
position 015 and the other chain commits that they build on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
syeedali pushed a commit to syeedali/nvx that referenced this pull request Nov 11, 2026
The NVX pin moves to cd5cd3814, position 246, 115 commits past the
mapped tip. Position 246, which microsoft/openvmm#5016 proposes for
feature/microvm/dev on its head, a8a0e3517, is unassigned. It adds the
PCI counterparts of two MMIO tests of the virtio restore: a restore
whose active queue lacks its progress fails before the device sees any
state, and a save before the first start returns the restored queue
progress and private state. It also corrects the module documentation
of the virtio-net saved-state tests, which still described the
deferred restore of device-private state. The Scope also says that
microsoft/openvmm#5013 merged positions 243-245.

Position 246 changes only the test modules of PR13's positions 039 and
048, the first of which positions 213 and 218 also extend. On upstream
main, both of its files conflict, since upstream main lacks both test
modules, so it follows PR13 upstream.

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

release-nvx Targets a branch under feature/microvm/.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant