-
Notifications
You must be signed in to change notification settings - Fork 2.6k
fix(vmm): reject vhost-user hot-add when memory cannot be shared #6204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
f86d41c
08407e5
f2cb533
19af4ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -472,6 +472,15 @@ impl DeviceManager { | |
| VirtioDevices::Mmio(_) => return Err(VmmActionError::PciNotEnabled), | ||
| } | ||
|
|
||
| // 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() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we just reject it unconditionally for now?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
| return Err(VmmActionError::NotSupported( | ||
| "vhost-user hot-add requires guest memory that a backend can map shared" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"? |
||
| .to_string(), | ||
| )); | ||
| } | ||
|
|
||
| let device = match config { | ||
| HotplugDeviceConfig::Block(cfg) => Self::hotplug_make_block(cfg)?, | ||
| HotplugDeviceConfig::Pmem(cfg) => Self::hotplug_make_pmem(vm.clone(), cfg)?, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,8 +51,8 @@ pub enum VhostUserError { | |
| VhostUserSetVringKick(VhostError), | ||
| /// Set vring enable failed: {0} | ||
| VhostUserSetVringEnable(VhostError), | ||
| /// 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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| /// Invalid used address | ||
| UsedAddress(GuestMemoryError), | ||
| } | ||
|
|
@@ -368,12 +368,10 @@ impl<T: VhostUserHandleBackend> VhostUserHandleImpl<T> { | |
| let mut regions: Vec<VhostUserMemoryRegionInfo> = Vec::new(); | ||
|
|
||
| for region in mem.iter() { | ||
| let (mmap_handle, mmap_offset) = match region.file_offset() { | ||
| Some(_file_offset) => (_file_offset.file().as_raw_fd(), _file_offset.start()), | ||
| None => { | ||
| return Err(VhostUserError::VhostUserNoMemoryRegion); | ||
| } | ||
| }; | ||
| let file_offset = region | ||
| .shared_file_offset() | ||
| .ok_or(VhostUserError::VhostUserMemoryNotShareable)?; | ||
| let (mmap_handle, mmap_offset) = (file_offset.file().as_raw_fd(), file_offset.start()); | ||
|
|
||
| let vhost_user_net_reg = VhostUserMemoryRegionInfo { | ||
| guest_phys_addr: region.start_addr().raw_value(), | ||
|
|
@@ -484,7 +482,7 @@ pub(crate) mod tests { | |
| GuestMemoryMmap::from_regions( | ||
| memory::create( | ||
| regions.iter().copied(), | ||
| libc::MAP_PRIVATE, | ||
| libc::MAP_SHARED, | ||
| Some(file), | ||
| false, | ||
| libc::MADV_NORMAL, | ||
|
|
@@ -760,6 +758,48 @@ pub(crate) mod tests { | |
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_update_mem_table_rejects_unshareable_memory() { | ||
| struct NoBackend; | ||
| impl VhostUserHandleBackend for NoBackend {} | ||
|
|
||
| let vuh = VhostUserHandleImpl { | ||
| vu: NoBackend, | ||
| socket_path: "".to_string(), | ||
| }; | ||
| let region_size = 0x10000; | ||
| let regions = [(GuestAddress(0), region_size)]; | ||
|
|
||
| // Anonymous memory has no file to hand over. | ||
| let anon = crate::test_utils::single_region_mem(region_size); | ||
| assert!(matches!( | ||
| vuh.update_mem_table(&anon), | ||
| Err(VhostUserError::VhostUserMemoryNotShareable) | ||
| )); | ||
|
|
||
| // A private file mapping has one, but the backend would map its own copy. | ||
| let file = TempFile::new().unwrap().into_file(); | ||
| file.set_len(region_size as u64).unwrap(); | ||
| let private = GuestMemoryMmap::from_regions( | ||
| memory::create( | ||
| regions.iter().copied(), | ||
| libc::MAP_PRIVATE, | ||
| Some(file), | ||
| false, | ||
| libc::MADV_NORMAL, | ||
| ) | ||
| .unwrap() | ||
| .into_iter() | ||
| .map(|region| GuestRegionMmapExt::dram_from_mmap_region(region, 0)) | ||
| .collect(), | ||
| ) | ||
| .unwrap(); | ||
| assert!(matches!( | ||
| vuh.update_mem_table(&private), | ||
| Err(VhostUserError::VhostUserMemoryNotShareable) | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_update_mem_table() { | ||
| struct MockFrontend { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -647,6 +647,15 @@ impl<'a> GuestMemorySlot<'a> { | |
| } | ||
|
|
||
| impl GuestRegionMmapExt { | ||
| /// The backing file, if another process could map the same pages from it: | ||
| /// file-backed and `MAP_SHARED`. A private mapping copies on write, so a | ||
| /// second mapping of the file diverges from this one. | ||
| pub fn shared_file_offset(&self) -> Option<&FileOffset> { | ||
| self.inner | ||
| .file_offset() | ||
| .filter(|_| self.inner.flags() & libc::MAP_SHARED != 0) | ||
| } | ||
|
Comment on lines
+653
to
+657
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how about just exposing 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());
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| /// Adds a DRAM region which only contains a single plugged slot | ||
| pub(crate) fn dram_from_mmap_region(region: GuestRegionMmap, slot: u32) -> Self { | ||
| let slot_size = u64_to_usize(region.len()); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ | |
| import pytest | ||
|
|
||
| import host_tools.drive as drive_tools | ||
| from framework.artifacts import ACPI_GUEST_KERNELS, pin_guest_kernel, pin_pci | ||
| from framework.utils_drive import partuuid_and_disk_path | ||
| from host_tools.fcmetrics import FcDeviceMetrics | ||
|
|
||
|
|
@@ -300,6 +301,86 @@ def test_partuuid_update(uvm_vhost_user_plain_any, rootfs): | |
| vhost_user_block_metrics.validate(vm) | ||
|
|
||
|
|
||
| @pin_pci(True) | ||
| @pin_guest_kernel(ACPI_GUEST_KERNELS) | ||
| def test_hotplug_vhost_user(uvm_vhost_user_booted_ro): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shall these go where the other hotplug tests are? |
||
| """A vhost-user block can be hot-added to a VM booted with one. | ||
|
|
||
| Booting with a vhost-user drive makes guest memory memfd-backed, which is | ||
| what the backend of a hot-added drive maps. The new drive must appear in | ||
| the guest and carry data through the backend. | ||
| """ | ||
| vm = uvm_vhost_user_booted_ro | ||
| _, before, _ = vm.ssh.check_output("ls /sys/block") | ||
|
|
||
| fs = drive_tools.FilesystemFile(size=16) | ||
| vm.add_vhost_user_drive("scratch", fs.path) | ||
|
|
||
| # No hotplug notification yet, so the guest rescans the bus itself. | ||
| vm.ssh.check_output("echo 1 > /sys/bus/pci/rescan") | ||
| _, after, _ = vm.ssh.check_output("ls /sys/block") | ||
| new = set(after.split()) - set(before.split()) | ||
| assert len(new) == 1, new | ||
| dev = f"/dev/{new.pop()}" | ||
|
|
||
| # The rootfs is read-only; /tmp is writable. | ||
| 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 ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| vm.ssh.check_output("cat /tmp/scratch/probe").stdout.strip() | ||
| == "vhost_user_hotplug" | ||
| ) | ||
| vm.ssh.check_output("umount /tmp/scratch") | ||
|
|
||
|
|
||
| @pin_pci(True) | ||
| def test_hotplug_without_memfd_rejected(uvm): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| """A vhost-user block cannot be hot-added to a VM booted without one. | ||
|
|
||
| Guest memory is memfd-backed only when a vhost-user device is configured | ||
| before boot. The request must be refused up front rather than accepted and | ||
| failed at DRIVER_OK. It is refused before any backend connection, so no | ||
| backend is started here. | ||
| """ | ||
| vm = uvm | ||
| vm.spawn(log_level="Info") | ||
| vm.basic_config() | ||
| vm.add_net_iface() | ||
| vm.start() | ||
|
|
||
| with pytest.raises(RuntimeError, match="vhost-user hot-add requires guest memory"): | ||
| vm.api.drive.put( | ||
| drive_id="vub0", | ||
| socket="/tmp/nonexistent-vhost-user.sock", | ||
| is_root_device=False, | ||
| ) | ||
|
|
||
|
|
||
| @pin_pci(True) | ||
| 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. | ||
|
Comment on lines
+361
to
+362
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
|
||
| The restored memory is a private mapping of the snapshot file: it has a | ||
| descriptor, yet a backend given it would map its own copy of the file. The | ||
| request must be refused rather than accepted and failed at DRIVER_OK. | ||
| """ | ||
| vm = uvm | ||
| vm.spawn(log_level="Info") | ||
| vm.basic_config() | ||
| vm.add_net_iface() | ||
| vm.start() | ||
| snapshot = vm.snapshot_full() | ||
|
|
||
| restored = microvm_factory.build_from_snapshot(snapshot) | ||
| with pytest.raises(RuntimeError, match="vhost-user hot-add requires guest memory"): | ||
| restored.api.drive.put( | ||
| drive_id="vub0", | ||
| socket="/tmp/nonexistent-vhost-user.sock", | ||
| is_root_device=False, | ||
| ) | ||
|
|
||
|
|
||
| def test_config_change(uvm): | ||
| """ | ||
| Verify handling of block device resize. | ||
|
|
||
There was a problem hiding this comment.
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