Skip to content

Wait for virtio device workers to complete resets - #532

Open
Lencerf wants to merge 8 commits into
google:mainfrom
Lencerf:fix/virtio-reset
Open

Lencerf wants to merge 8 commits into
google:mainfrom
Lencerf:fix/virtio-reset

Conversation

@Lencerf

@Lencerf Lencerf commented Nov 9, 2026

Copy link
Copy Markdown
Collaborator

A virtio transport reports a device reset as completed right away, while the
device worker might still be using the queues, e.g. writing into ring memory
that the driver has already reused. This series makes workers acknowledge each
reset, and the transports wait for that acknowledgement:

  • The PCI transport keeps the old device_status until the worker acknowledges
    a reset requested by the driver. The driver polls device_status until it
    reads 0 (VirtIO spec 1.4, Sec 4.1.4.3.2), so vCPUs never block. Writes to the
    common configuration are ignored in the meantime.
  • Pci::reset, which runs on reboot, waits up to 10 seconds for the worker, and
    fails the reboot with an error otherwise.
  • The vhost-user frontend stops rings with GET_VRING_BASE instead of only
    disabling them, and the vhost-user backend waits for its worker before
    replying to SET_STATUS or GET_VRING_BASE.
  • A device reset no longer clears the MSI-X table, which belongs to the PCI
    function.
  • Workers survive errors, and a device whose worker failed sets
    DEVICE_NEEDS_RESET and sends a configuration change notification.

Every commit passes cargo +nightly fmt --check, cargo clippy --all-targets -D warnings on x86_64-unknown-linux-gnu/musl, aarch64-unknown-linux-gnu and
aarch64-apple-darwin, and cargo test --workspace locally.

The PCI transport and the vhost-user backend each carried a copy of the
code that sends a wake event to a device worker and kicks its notifier.

Move the event sender and the notifier from VirtioDevice into a new
WorkerCtl, which the device shares with its transport, and keep the only
copy of that code there. The following changes extend WorkerCtl to track
device resets.

VuChannel events from the vhost-user backend now also kick the notifier,
so that a running worker picks them up right away.

Signed-off-by: Changyuan Lyu <changyuanl@google.com>
A transport asks the worker of a device to reset it with a Reset event,
but has no way to learn when the worker has stopped using the queues.
The transport therefore reports a reset as completed right away, while
the worker might still write to the rings, e.g. into memory the driver
has already reused.

Number each Reset event, and have the worker send the number back once
it has left its event loop, drained in-flight requests, cleaned up the
backend and released the guest memory layout. A worker that has not
started acknowledges a reset right away. The worker is the only holder
of the sending end, so its exit also completes all pending resets.
WorkerCtl offers poll_reset() to transports that must not block, and
wait_reset() to those that can.

WorkerCtl takes a sequence number and sends the event under one lock,
so that the worker sees resets in the order of their numbers. It
compares the numbers in serial number arithmetic, which stays correct
even if the counter wraps around.

So that a reset can still complete after an error, the worker no longer
exits when its event loop fails. It cleans up the backend, which also
covers a partially activated device, and waits for the next start. For
the same reason, vhost-vsock now logs errors during reset instead of
panicking.

The transports still report resets as completed right away. The
following changes make them wait for the acknowledgement.

Signed-off-by: Changyuan Lyu <changyuanl@google.com>
When the driver writes 0 to device_status, the transport clears the
registers and reports the reset as completed right away, while the
worker might still be using the queues. The driver then reuses the ring
memory, which the worker can still write into. Pci::reset, which runs
when the VM reboots, has the same problem.

For a reset requested by the driver, keep all registers, including the
non-zero device_status, until the worker acknowledges the reset. The
driver must poll device_status until it reads 0 before reinitializing
the device (VirtIO spec 1.4, Sec 4.1.4.3.2), so the vCPU does not wait.
The transport completes the reset once a later register access finds
the acknowledgement, and ignores all writes in the meantime. A device
that has not started completes its reset right away.

Pci::reset waits for the worker instead, for at most 10 seconds. If the
worker does not complete the reset in time, the reboot fails with an
error rather than hanging on a stuck worker.

Queue notifications do not touch the registers, so they skip the new
lock.

Signed-off-by: Changyuan Lyu <changyuanl@google.com>
To reset a device, the vhost-user frontend disables the rings with
SET_VRING_ENABLE and then sends SET_STATUS(0). Per the vhost-user spec,
the backend still processes a disabled ring, only without side effects.
For example, it still consumes transmitted packets and writes the used
ring. The worker could therefore report the reset as completed while
the backend still writes guest memory.

Stop each ring with GET_VRING_BASE first. Once all rings are stopped,
the device is suspended and the backend no longer writes guest memory
or sends notifications. Each message waits for its reply, so the rings
have stopped by the time the worker acknowledges the reset.

Signed-off-by: Changyuan Lyu <changyuanl@google.com>
The vhost-user backend replies to SET_STATUS without DRIVER_OK right
after asking the worker to reset, and replies to GET_VRING_BASE without
stopping anything. A frontend takes either reply to mean that the rings
have stopped, and might then hand the ring memory back to the guest
while the worker still uses it.

Wait for the worker to complete the reset before replying. Blocking is
fine here: in the vhost-user protocol the reply is the only completion
signal, and the backend thread serves no one else. GET_VRING_BASE stops
the whole device, since one worker serves all queues.

Track whether the worker runs separately from the device status, so
that a frontend can restart the device after stopping it with
GET_VRING_BASE, and can change the memory layout while it is stopped.

Signed-off-by: Changyuan Lyu <changyuanl@google.com>
A device reset clears the whole MSI-X table, which masks all vectors and
zeroes their addresses and data. The table belongs to the PCI function,
though, and the virtio spec only requires a device reset to unmap the
configuration and queue events from the vectors. A driver that resets
the device and then reuses the vectors it has programmed would receive
no more interrupts. Linux reprograms the table after each reset, which
hides the problem.

Keep what the guest has programmed into the table on a device reset,
and only release the irqfds that workers set up. Clear the table on a
PCI reset instead.

Signed-off-by: Changyuan Lyu <changyuanl@google.com>
A worker that fails while its device is running cleans up and waits for
the next start, but the transport cannot tell, and keeps presenting a
working device to the driver.

Turn the reset acknowledgement channel into a channel for all worker
reports, and have the worker report failures there. A failure stands
until a reset completes. Reports arrive in order, so a failure reported
before the acknowledgement of a reset is gone with that reset, while a
later one is not. The following change lets the transports tell the
driver.

Signed-off-by: Changyuan Lyu <changyuanl@google.com>
When the worker of a running device fails, e.g. because the driver has
set up a queue that the device cannot use, the device silently stops
serving the queues.

Set DEVICE_NEEDS_RESET and send a configuration change notification,
which the virtio spec asks of a device that enters an error state that
needs a reset (VirtIO spec 1.4, Sec 2.1.2). The PCI transport sets the
bit when the driver reads device_status, and the vhost-user backend
when the frontend reads the status. Resetting the device clears it.

Signed-off-by: Changyuan Lyu <changyuanl@google.com>
@coveralls

Copy link
Copy Markdown

Coverage Report for CI Build 42698488512

Coverage increased (+1.4%) to 40.834%

Details

  • Coverage increased (+1.4%) from the base build.
  • Patch coverage: 29 uncovered changes across 5 files (233 of 262 lines covered, 88.93%).
  • 2 coverage regressions across 2 files.

Uncovered Changes

File Changed Covered %
alioth/src/virtio/dev/vsock/vhost_vsock.rs 14 0 0.0%
alioth/src/virtio/dev/dev.rs 116 109 93.97%
alioth/src/virtio/vu/backend.rs 29 23 79.31%
alioth/src/virtio/pci.rs 82 81 98.78%
alioth/src/virtio/vu/frontend.rs 7 6 85.71%
Total (6 files) 262 233 88.93%

Coverage Regressions

2 previously-covered lines in 2 files lost coverage.

File Lines Losing Coverage Coverage
alioth/src/virtio/dev/vsock/vhost_vsock.rs 1 0.0%
alioth/src/virtio/vu/backend.rs 1 32.07%

Coverage Stats

Coverage Status
Relevant Lines: 16392
Covered Lines: 6488
Line Coverage: 39.58%
Relevant Branches: 1708
Covered Branches: 903
Branch Coverage: 52.87%
Branches in Coverage %: Yes
Coverage Strength: 892.92 hits per line

💛 - Coveralls

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants