Skip to content

fix(vmm): reject vhost-user hot-add when memory cannot be shared - #6204

Open
Manciukic wants to merge 4 commits into
firecracker-microvm:mainfrom
Manciukic:vhost-user-hotplug
Open

Manciukic wants to merge 4 commits into
firecracker-microvm:mainfrom
Manciukic:vhost-user-hotplug

Conversation

@Manciukic

@Manciukic Manciukic commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Changes

Hot-adding a vhost-user block device (PUT /drives/{id} with socket) now fails with 400 when the backend cannot map guest memory. That is a microVM booted without any vhost-user drive, whose memory is anonymous and has no descriptor to hand over, or one restored from a snapshot memory file, whose mapping is private, so the backend would map its own copy of the file.

The check runs after the transport check, so an MMIO VM still gets PciNotEnabled. update_mem_table applies the same condition, so the vhost-user handshake refuses private file mappings too; both use one helper. Integration tests cover the accepted case, a VM booted with a vhost-user drive gaining a second one and writing through it, and both rejected cases. The CHANGELOG, the hotplug docs and the vhost-user block docs record the restriction.

Reason

Until now the request succeeded with 204 and the device failed later at DRIVER_OK, with nothing in the API response to connect the two. In the snapshot case nothing failed at all: guest writes never reached the backend, and the backend's writes landed in the snapshot file.

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.

@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.71429% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.09%. Comparing base (68698ad) to head (19af4ff).

Files with missing lines Patch % Lines
src/vmm/src/device_manager/mod.rs 33.33% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6204      +/-   ##
==========================================
+ Coverage   83.05%   83.09%   +0.03%     
==========================================
  Files         277      277              
  Lines       31481    31506      +25     
==========================================
+ Hits        26147    26180      +33     
+ Misses       5334     5326       -8     
Flag Coverage Δ
5.10-m5n.metal 83.33% <85.71%> (+0.04%) ⬆️
5.10-m6a.metal 82.71% <85.71%> (+0.04%) ⬆️
5.10-m6g.metal 80.18% <85.71%> (+0.04%) ⬆️
5.10-m6i.metal 83.33% <85.71%> (+0.04%) ⬆️
5.10-m7a.metal-48xl 82.70% <85.71%> (+0.04%) ⬆️
5.10-m7g.metal 80.18% <85.71%> (+0.04%) ⬆️
5.10-m7i.metal-24xl 83.30% <85.71%> (+0.04%) ⬆️
5.10-m7i.metal-48xl 83.30% <85.71%> (+0.03%) ⬆️
5.10-m8g.metal-24xl 80.18% <85.71%> (+0.04%) ⬆️
5.10-m8g.metal-48xl 80.18% <85.71%> (+0.04%) ⬆️
5.10-m8i.metal-48xl 83.30% <85.71%> (+0.03%) ⬆️
5.10-m8i.metal-96xl 83.30% <85.71%> (+0.03%) ⬆️
5.10-m9g.metal-48xl 80.18% <85.71%> (+0.04%) ⬆️
6.1-m5n.metal 83.35% <85.71%> (+0.03%) ⬆️
6.1-m6a.metal 82.73% <85.71%> (+0.04%) ⬆️
6.1-m6g.metal 80.18% <85.71%> (+0.04%) ⬆️
6.1-m6i.metal 83.35% <85.71%> (+0.03%) ⬆️
6.1-m7a.metal-48xl 82.72% <85.71%> (+0.04%) ⬆️
6.1-m7g.metal 80.18% <85.71%> (+0.04%) ⬆️
6.1-m7i.metal-24xl 83.37% <85.71%> (+0.04%) ⬆️
6.1-m7i.metal-48xl 83.36% <85.71%> (+0.04%) ⬆️
6.1-m8g.metal-24xl 80.18% <85.71%> (+0.04%) ⬆️
6.1-m8g.metal-48xl 80.18% <85.71%> (+0.04%) ⬆️
6.1-m8i.metal-48xl 83.37% <85.71%> (+0.04%) ⬆️
6.1-m8i.metal-96xl 83.36% <85.71%> (+0.03%) ⬆️
6.1-m9g.metal-48xl 80.18% <85.71%> (+0.04%) ⬆️
6.18-m5n.metal 83.35% <85.71%> (+0.03%) ⬆️
6.18-m6a.metal 82.73% <85.71%> (+0.04%) ⬆️
6.18-m6g.metal 80.29% <85.71%> (+0.04%) ⬆️
6.18-m6i.metal 83.35% <85.71%> (+0.03%) ⬆️
6.18-m7a.metal-48xl 82.72% <85.71%> (+0.04%) ⬆️
6.18-m7g.metal 80.28% <85.71%> (+0.03%) ⬆️
6.18-m7i.metal-24xl 83.37% <85.71%> (+0.04%) ⬆️
6.18-m7i.metal-48xl 83.37% <85.71%> (+0.04%) ⬆️
6.18-m8g.metal-24xl 80.29% <85.71%> (+0.04%) ⬆️
6.18-m8g.metal-48xl 80.29% <85.71%> (+0.04%) ⬆️
6.18-m8i.metal-48xl 83.36% <85.71%> (+0.03%) ⬆️
6.18-m8i.metal-96xl 83.36% <85.71%> (+0.03%) ⬆️
6.18-m9g.metal-48xl 80.29% <85.71%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Manciukic
Manciukic force-pushed the vhost-user-hotplug branch 2 times, most recently from 7b15947 to ecea4ba Compare September 11, 2026 10:02
@Manciukic
Manciukic marked this pull request as ready for review September 11, 2026 10:10
@Manciukic Manciukic added the Status: Awaiting review Indicates that a pull request is ready to be reviewed label Sep 11, 2026
@Manciukic
Manciukic enabled auto-merge September 16, 2026 11:46
update_mem_table handed the backend any region that had a file
descriptor. Memory restored from a snapshot file has one but is mapped
MAP_PRIVATE, so the backend would map its own copy of the file: writes
on either side never reach the other, and the backend's land in the
snapshot file.

Require MAP_SHARED as well, and name the error for what it means. The
test helper mapped its file private, which is the case now rejected; it
maps shared, as memfd-backed guest memory does.

Signed-off-by: Riccardo Mancini <mancio@amazon.com>
Hot-adding a vhost-user block was accepted with a 204 whenever the
transport allowed it, and failed only when the guest driver signalled
DRIVER_OK and activation reached update_mem_table. Guest memory is a
shared memfd mapping only when a vhost-user device is configured before
boot; a VM booted without one has anonymous memory, and a VM restored
from a snapshot file has a private mapping.

Check the same condition up front, after the transport check so a VM
that cannot hotplug at all still reports that first.

Signed-off-by: Riccardo Mancini <mancio@amazon.com>
Nothing exercised hot-adding a vhost-user block after boot, in either
direction. Add the accepted case -- a VM booted with a vhost-user drive
gains a second one, which the guest formats and writes through the
backend -- and the two rejected ones: a VM booted without any vhost-user
device, and a VM restored from a snapshot memory file. Hotplug needs
PCIe and ACPI in the guest, so the tests are pinned like the rest of the
hotplug suite.

Signed-off-by: Riccardo Mancini <mancio@amazon.com>
Record the new hot-add restriction in the hotplug limitations and in the
CHANGELOG.

Signed-off-by: Riccardo Mancini <mancio@amazon.com>
@Manciukic

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (68698adfe): ecea4ba01 -> 19af4ff35. The only conflict was the CHANGELOG entry landing next to #6218's; no code changes. Re-ran the per-commit build check, style, unit tests and the vhost-user integration tests on the new base, all green.

/// Failed to read vhost eventfd: No memory region found
VhostUserNoMemoryRegion,
/// Guest memory is not a shared file mapping, so the backend cannot map it
VhostUserMemoryNotShareable,

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.

I don't believe this is a valid error for vhost-user devices. We must not create them if we don't use sharable memfd for memory.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now the only way preventing that is that we can't snapshot, but in theory the code could restore it. A modified snapshot could in theory contain it and this makes the check more generic and robust for the future.

Comment on lines +653 to +657
pub fn shared_file_offset(&self) -> Option<&FileOffset> {
self.inner
.file_offset()
.filter(|_| self.inner.flags() & libc::MAP_SHARED != 0)
}

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.

how about just exposing is_shared function instead? This will convert the usage to just:

assert!(region.is_shared());
let Some(_file_offset) = region.file_offset() else {
  panic!("...")
}
let (mmap_handle, mmap_offset) = (_file_offset.file().as_raw_fd(), _file_offset.start());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which is longer and more complicated than the code above? no thanks. Maybe I can add a separate is_shared helper but I don't really see the point.

Comment on lines +361 to +362
def test_hotplug_after_file_restore_rejected(uvm, microvm_factory):
"""A vhost-user block cannot be hot-added to a VM restored from a snapshot file.

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.

what is going on with this "hot-add" business? FC supports "hotplug", so let's use this term everywhere in code and in commits.


// After the transport check, so a VM that cannot hotplug at all says so
// first. Without this the request succeeds and fails only at DRIVER_OK.
if config.is_vhost_user() && !vm.vhost_user_memory_shareable() {

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.

Can we just reject it unconditionally for now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have a release that supports it. Sure, it's dev preview, but if it works, why should we gate it?

use crate::vstate::memory;
use crate::vstate::memory::{GuestAddress, GuestRegionMmapExt};

pub(crate) fn create_mem(file: File, regions: &[(GuestAddress, usize)]) -> GuestMemoryMmap {

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.

This seems to also be used in test_update_mem_table_rejects_unshareable_memory() L783, shall we rather make create_mem have a flags argument?

VirtioDevices::Mmio(_) => return Err(VmmActionError::PciNotEnabled),
}

// After the transport check, so a VM that cannot hotplug at all says so

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.

classic AI comment that I'd remove

// first. Without this the request succeeds and fails only at DRIVER_OK.
if config.is_vhost_user() && !vm.vhost_user_memory_shareable() {
return Err(VmmActionError::NotSupported(
"vhost-user hot-add requires guest memory that a backend can map shared"

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.

Would a more user friendly error be something like "vhost-user can only be hotplugged when a vhost-user device was present at boot"?


@pin_pci(True)
@pin_guest_kernel(ACPI_GUEST_KERNELS)
def test_hotplug_vhost_user(uvm_vhost_user_booted_ro):

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.

Shall these go where the other hotplug tests are?

@not4s not4s 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.

The description says that after restoring from snapshot, the backend writes reached the snapshot memory file. Is this possible given the memory file is a read-only descriptor on the normal path?

I tried to reproduce and the writable shared mmap failed with EACCES

vm.ssh.check_output(f"mkfs.ext4 {dev}")
vm.ssh.check_output(f"mkdir -p /tmp/scratch && mount {dev} /tmp/scratch")
vm.ssh.check_output("echo vhost_user_hotplug > /tmp/scratch/probe")
assert (

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.

nit; should we unmount and remount before cat-ting or check the backing image after unmouning? The immediate cat content can come from the guest's page cache



@pin_pci(True)
def test_hotplug_without_memfd_rejected(uvm):

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.

The RuntimeError would also accept a 500 as well, so might be better to assert 400 for the rejection tests explicitly.

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

Labels

Status: Awaiting review Indicates that a pull request is ready to be reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants