Skip to content

refactor(vhost-user-block): delegate to the generic vhost-user frontend - #6146

Draft
1stvamp wants to merge 3 commits into
firecracker-microvm:mainfrom
triggerdotdev:refactor/vhost-user-block-delegate
Draft

refactor(vhost-user-block): delegate to the generic vhost-user frontend#6146
1stvamp wants to merge 3 commits into
firecracker-microvm:mainfrom
triggerdotdev:refactor/vhost-user-block-delegate

Conversation

@1stvamp

@1stvamp 1stvamp commented Aug 22, 2026

Copy link
Copy Markdown

Changes

Moves vhost-user-block onto the shared VhostUserDevice frontend, so it no longer carries its own copy of the feature negotiation, config space fetch, queue allocation and vring setup. It keeps the fields that are specific to it: the drive id, partuuid, cache type, and the root and readonly flags.

One behaviour does change. Fetching the config space now goes through the shared frontend, which requires the backend to return as many bytes as were asked for, so a backend that answers a 60 byte request with fewer now fails to attach where before it was accepted and left the rest of the config space short.

read_only is now read from the features the backend acked rather than from a local that a later shadowing narrowed, so the ordering trap fixed in #6083 cannot come back.

Reason

De-duplicates the vhost-user frontend between block and the generic device that follows, so there is one implementation of the protocol handling rather than two.

This is step 2 of the 3 @ShadowCurse asked for in #6072.

License Acceptance

By submitting this pull request, I confirm that my contribution is made under
the terms of the Apache 2.0 license. For more information on following Developer
Certificate of Origin and signing off your commits, please check
CONTRIBUTING.md.

PR Checklist

  • I have read and understand CONTRIBUTING.md.
  • I have run tools/devtool checkbuild --all to verify that the PR passes
    build checks on all supported architectures.
  • I have run tools/devtool checkstyle to verify that the PR passes the
    automated style checks.
  • I have described what is done in these changes, why they are needed, and
    how they are solving the problem in a clear and encompassing way.
  • I have updated any relevant documentation (both in code and in the docs)
    in the PR.
  • I have mentioned all user-facing changes in CHANGELOG.md.
  • If a specific issue led to this PR, this PR closes the issue.
  • When making API changes, I have followed the
    Runbook for Firecracker API changes.
  • I have tested all new and changed functionalities in unit tests and/or
    integration tests.
  • I have linked an issue to every new TODO.

  • This functionality cannot be added in rust-vmm.

This is part 3 of 4 in a stack made with GitButler:

The VIRTIO_BLK_F_RO check read acked_features after it had been
reassigned to acked_features & PROTOCOL_FEATURES, so it tested bit 5
against 0x4000_0000 and never matched. read_only was therefore always
false, even though the device does ask the backend for VIRTIO_BLK_F_RO,
and docs/api_requests/block-vhost-user.md documents the backend as the
place a readonly vhost-user drive is configured.

read_only feeds Block::read_only(), which decides whether a root device
gets ro or rw on the guest kernel cmdline, so a readonly vhost-user root
device was given rw. The guest still sees VIRTIO_BLK_F_RO and retries
the root mount readonly, so it boots, but /proc/cmdline is wrong and a
later remount,rw fails. There is no way to correct this from config:
VhostUserBlockConfig::try_from rejects is_read_only, which leaves the
backend as the only source of the flag.

Compute read_only before acked_features is narrowed, so the shadowing
cannot swallow the bit again. test_new_all_features already advertises
VIRTIO_BLK_F_RO from its mock backend and asserted the broken result, so
it now asserts read_only is set, and test_vhost_user_block asserts ro
reaches the guest cmdline.

Signed-off-by: Wes Mason <wes@1stvamp.org>
The vhost-user frontends duplicate everything that is not specific to
the virtio device type they implement: connecting to the backend socket,
negotiating virtio and protocol features, fetching the config space,
allocating queues and eventfds, and setting up the backend's vrings on
activation.

Add VhostUserDevice to hold that shared state, built from a
VhostUserDeviceSpec carrying the parts that do vary: socket path, queue
count and size, the features to offer, the config space size, whether
CONFIG is mandatory, and the metrics name. Device types embed it and
keep their own state alongside.

Two details worth noting for the callers that follow. Fetching the
config space is skipped when the backend does not ack CONFIG, leaving an
empty config space, since only a frontend with no device-specific
fallback needs to insist on it. And activate() sets up the queues the
guest marked ready rather than every allocated queue, because the number
a driver initialises is dictated by the backend-owned config space, so
initializing an unconfigured queue returns NotReady and aborts
activation. Real vring indices are preserved: queues 0 and 2 being ready
maps to vrings 0 and 2, not 0 and 1. Activating with no ready queues is
rejected rather than quietly setting up nothing.

The spec is validated before the socket is touched, since a frontend
built from it may be configured at runtime rather than from constants:
the queue count is bounded by what the PCI notification region can
address, the queue size has to be a power of two, and the config space
size has to fit the protocol's own limit.

The config space size is a size rather than an upper bound, in that the
backend has to return exactly that many bytes. The vhost crate checks
the size the reply declares but not the length of the payload behind it,
so the length is checked here too. Otherwise a backend could declare the
size that was asked for, send fewer bytes, and leave the guest reading
past the end of what it sent.

No device type uses this yet.

Signed-off-by: Wes Mason <wes@1stvamp.org>
The block device carried its own copy of the feature negotiation, config
space fetch, queue allocation and vring setup, none of which is specific
to block. Move that state into VhostUserDevice and keep only the
block-specific fields here: the drive id, partuuid, cache type and the
root and readonly flags.

Most of the behaviour is unchanged. Block still offers the same feature
set, including FLUSH when the cache type is Writeback, still treats
CONFIG as optional, and still reports metrics under block_{drive_id}.
Activating without a ready queue reports QueueError::NotReady as before,
though it now comes from the generic readiness check rather than from
initializing queue 0. The double-activation assert moves into the
generic device, which owns the state it guards.

read_only is now read from the features the backend acked rather than
from a local that a later shadowing narrowed, so the ordering trap fixed
in "fix(vhost-user-block): honour a readonly backend" cannot come back.

VhostUserBlockError keeps its existing variants and their messages,
which surface through the API, and gains one for the generic errors
block itself cannot produce.

One behaviour does change. Fetching the config space now goes through
the shared frontend, which requires the backend to return as many bytes
as were asked for, so a backend that answers a 60 byte request with
fewer now fails to attach where before it was accepted and left the rest
of the config space short.

Signed-off-by: Wes Mason <wes@1stvamp.org>
@1stvamp
1stvamp force-pushed the refactor/vhost-user-block-delegate branch from 9db1d56 to 9a9a8f3 Compare August 25, 2026 01:17
@zulinx86 zulinx86 added the Status: Awaiting assignee Indicates that an issue or pull request is awaiting action from its assignee. label Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Status: Awaiting assignee Indicates that an issue or pull request is awaiting action from its assignee.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants