diff --git a/alioth-cli/src/boot/boot.rs b/alioth-cli/src/boot/boot.rs index 9e10a2c8..64907718 100644 --- a/alioth-cli/src/boot/boot.rs +++ b/alioth-cli/src/boot/boot.rs @@ -31,7 +31,7 @@ use alioth::hv::{CocoSpec, HvSpec, Hypervisor}; use alioth::loader::{Executable, PayloadSpec}; use alioth::mem::{MemBackend, MemSpec}; #[cfg(target_os = "linux")] -use alioth::vfio::{VfioCdevSpec, VfioContainerSpec, VfioGroupSpec, VfioIoasSpec}; +use alioth::vfio::{VfioCdevSpec, VfioContainerSpec, VfioGroupSpec, VfioIoasSpec, VfioUserSpec}; #[cfg(target_os = "linux")] use alioth::virtio::DeviceId; use alioth::virtio::dev::balloon::BalloonSpec; @@ -182,6 +182,10 @@ pub struct BootArgs { #[arg(long, help(help_text::("Add a new VFIO container.")))] vfio_container: Vec, + #[cfg(target_os = "linux")] + #[arg(long, help(help_text::("Assign a vfio-user device to the guest.")))] + vfio_user: Vec, + #[arg(long)] #[arg(long, help(help_text::("Add a VirtIO balloon device.")))] balloon: Option, @@ -358,6 +362,11 @@ fn parse_args(mut args: BootArgs, objects: HashMap<&str, &str>) -> Result(hypervisor: &H, spec: VmSpec) -> Result, ali for (index, cdev_spec) in spec.vfio_cdev.into_iter().enumerate() { vm.add_vfio_cdev(format!("vfio-{index}").into(), cdev_spec)?; } + #[cfg(target_os = "linux")] + for (index, user_spec) in spec.vfio_user.into_iter().enumerate() { + vm.add_vfio_user_dev(format!("vfio-user-{index}").into(), user_spec)?; + } #[cfg(target_os = "linux")] for container_spec in spec.vfio_container.into_iter() { diff --git a/alioth-cli/src/boot/boot_test.rs b/alioth-cli/src/boot/boot_test.rs index a29bb889..055e7268 100644 --- a/alioth-cli/src/boot/boot_test.rs +++ b/alioth-cli/src/boot/boot_test.rs @@ -22,7 +22,7 @@ use alioth::device::net::MacAddr; use alioth::loader::{Executable, PayloadSpec}; use alioth::mem::{MemBackend, MemSpec}; #[cfg(target_os = "linux")] -use alioth::vfio::{VfioCdevSpec, VfioContainerSpec, VfioGroupSpec, VfioIoasSpec}; +use alioth::vfio::{VfioCdevSpec, VfioContainerSpec, VfioGroupSpec, VfioIoasSpec, VfioUserSpec}; use alioth::virtio::dev::balloon::BalloonSpec; use alioth::virtio::dev::blk::BlkFileSpec; use alioth::virtio::dev::entropy::EntropySpec; @@ -89,6 +89,8 @@ fn test_parse_args() { vfio_group: vec!["path=/dev/vfio/26,container=gpu_container,devices=id_gpus".into()], #[cfg(target_os = "linux")] vfio_container: vec!["name=gpu_container,dev_vfio=/dev/vfio/vfio".into()], + #[cfg(target_os = "linux")] + vfio_user: vec!["socket=/tmp/nvme.sock".into()], ..Default::default() }; let objects = HashMap::from([ @@ -212,6 +214,10 @@ fn test_parse_args() { container: Some("gpu_container".into()), devices: vec!["0000:06:0d.0".into(), "0000:06:0d.1".into()], }], + #[cfg(target_os = "linux")] + vfio_user: vec![VfioUserSpec { + socket: Path::new("/tmp/nvme.sock").into(), + }], }; assert_eq!(spec, want); } diff --git a/alioth-cli/src/boot/config.rs b/alioth-cli/src/boot/config.rs index 1be32663..a2c5c0c3 100644 --- a/alioth-cli/src/boot/config.rs +++ b/alioth-cli/src/boot/config.rs @@ -21,7 +21,7 @@ use alioth::device::console::ConsoleSpec; use alioth::device::fw_cfg::FwCfgItemSpec; use alioth::loader::PayloadSpec; #[cfg(target_os = "linux")] -use alioth::vfio::{VfioCdevSpec, VfioContainerSpec, VfioGroupSpec, VfioIoasSpec}; +use alioth::vfio::{VfioCdevSpec, VfioContainerSpec, VfioGroupSpec, VfioIoasSpec, VfioUserSpec}; use alioth::virtio::dev::balloon::BalloonSpec; use alioth::virtio::dev::blk::BlkFileSpec; use alioth::virtio::dev::entropy::EntropySpec; @@ -119,4 +119,6 @@ pub struct VmSpec { pub vfio_group: Vec, #[cfg(target_os = "linux")] pub vfio_container: Vec, + #[cfg(target_os = "linux")] + pub vfio_user: Vec, } diff --git a/alioth/src/sys/linux/vfio.rs b/alioth/src/sys/linux/vfio.rs index 4d7b7edb..97894f24 100644 --- a/alioth/src/sys/linux/vfio.rs +++ b/alioth/src/sys/linux/vfio.rs @@ -13,6 +13,7 @@ // limitations under the License. use bitfield::bitfield; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; use crate::sys::ioctl::ioctl_io; use crate::{ @@ -80,7 +81,7 @@ consts! { } #[repr(C)] -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, KnownLayout, Immutable, FromBytes, IntoBytes)] pub struct VfioRegionInfo { pub argsz: u32, pub flags: VfioRegionInfoFlag, @@ -116,7 +117,7 @@ consts! { } #[repr(C)] -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, KnownLayout, Immutable, FromBytes, IntoBytes)] pub struct VfioIrqInfo { pub argsz: u32, pub flags: VfioIrqInfoFlag, diff --git a/alioth/src/vfio/user/bindings.rs b/alioth/src/vfio/user/bindings.rs new file mode 100644 index 00000000..9c4fc5d8 --- /dev/null +++ b/alioth/src/vfio/user/bindings.rs @@ -0,0 +1,130 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use bitfield::bitfield; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +use crate::sys::vfio::{VfioDeviceInfoFlag, VfioIrqSetFlag}; +use crate::{bitflags, consts}; + +consts! { + pub struct VfioUserCmd(u16) { + VERSION = 1; + DMA_MAP = 2; + DMA_UNMAP = 3; + DEVICE_GET_INFO = 4; + DEVICE_GET_REGION_INFO = 5; + DEVICE_GET_REGION_IO_FDS = 6; + DEVICE_GET_IRQ_INFO = 7; + DEVICE_SET_IRQS = 8; + REGION_READ = 9; + REGION_WRITE = 10; + DMA_READ = 11; + DMA_WRITE = 12; + DEVICE_RESET = 13; + REGION_WRITE_MULTI = 15; + DEVICE_FEATURE = 16; + MIG_DATA_READ = 17; + MIG_DATA_WRITE = 18; + } +} + +consts! { + pub struct VfioUserMessageType(u8) { + COMMAND = 0; + REPLY = 1; + } +} + +bitfield! { + #[derive(Copy, Clone, Default, IntoBytes, FromBytes, Immutable, KnownLayout)] + pub struct VfioUserHeaderFlag(u32); + impl Debug; + impl new; + pub u8, from into VfioUserMessageType, ty, set_ty: 3, 0; + pub no_reply, set_no_reply: 4; + pub error, set_error: 5; +} + +#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, Immutable, Default)] +#[repr(C)] +pub struct VfioUserHeader { + pub msg_id: u16, + pub cmd: VfioUserCmd, + pub msg_size: u32, + pub flags: VfioUserHeaderFlag, + pub error_no: u32, +} + +#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, Immutable, Default)] +#[repr(C)] +pub struct VfioUserVersion { + pub major: u16, + pub minor: u16, +} + +#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, Immutable, Default)] +#[repr(C)] +pub struct VfioUserDeviceInfo { + pub argsz: u32, + pub flags: VfioDeviceInfoFlag, + pub num_regions: u32, + pub num_irqs: u32, +} + +#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, Immutable, Default)] +#[repr(C)] +pub struct VfioUserIrqSet { + pub argsz: u32, + pub flags: VfioIrqSetFlag, + pub index: u32, + pub start: u32, + pub count: u32, +} + +#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, Immutable, Default)] +#[repr(C)] +pub struct VfioUserRegionAccess { + pub offset: u64, + pub region: u32, + pub count: u32, +} + +bitflags! { + pub struct VfioUserDmaMapFlag(u32) { + READ = 1 << 0; + WRITE = 1 << 1; + MMAP = 1 << 2; + FILE_IO = 1 << 3; + } +} + +#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, Immutable, Default)] +#[repr(C)] +pub struct VfioUserDmaMap { + pub argsz: u32, + pub flags: VfioUserDmaMapFlag, + pub offset: u64, + pub addr: u64, + pub size: u64, +} + +#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, Immutable, Default)] +#[repr(C)] +pub struct VfioUserDmaUnmap { + pub argsz: u32, + pub flags: u32, + pub addr: u64, + pub size: u64, +} diff --git a/alioth/src/vfio/user/conn.rs b/alioth/src/vfio/user/conn.rs new file mode 100644 index 00000000..a88add10 --- /dev/null +++ b/alioth/src/vfio/user/conn.rs @@ -0,0 +1,325 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::array; +use std::io::{IoSlice, IoSliceMut}; +use std::mem::size_of; +use std::os::fd::{BorrowedFd, OwnedFd}; +use std::os::unix::net::UnixStream; + +use parking_lot::Mutex; +use zerocopy::IntoBytes; + +use crate::sys::vfio::{VfioIrqInfo, VfioRegionInfo}; +use crate::utils::uds::{recv_msg_with_fds, send_msg_with_fds}; +use crate::vfio::user::bindings::{ + VfioUserCmd, VfioUserDeviceInfo, VfioUserDmaMap, VfioUserDmaUnmap, VfioUserHeader, + VfioUserHeaderFlag, VfioUserIrqSet, VfioUserMessageType, VfioUserRegionAccess, VfioUserVersion, +}; +use crate::vfio::user::{Result, error}; + +#[derive(Debug)] +struct Session { + stream: UnixStream, + next_msg_id: u16, +} + +#[derive(Debug)] +pub struct VfioUserSession { + session: Mutex, +} + +impl VfioUserSession { + pub fn new(stream: UnixStream) -> Self { + VfioUserSession { + session: Mutex::new(Session { + stream, + next_msg_id: 0, + }), + } + } + + fn transact( + &self, + cmd: VfioUserCmd, + req_bufs: (&[u8], &[u8]), + req_fds: &[BorrowedFd<'_>], + resp_bufs: (&mut [u8], &mut [u8]), + resp_fds: &mut [Option], + ) -> Result { + let mut session = self.session.lock(); + let msg_id = session.next_msg_id; + session.next_msg_id = msg_id.wrapping_add(1); + let stream = &mut session.stream; + + let (req, data) = req_bufs; + let total_req_size = size_of::() + req.len() + data.len(); + + let header = VfioUserHeader { + msg_id, + cmd, + msg_size: total_req_size as u32, + flags: VfioUserHeaderFlag::new(VfioUserMessageType::COMMAND, false, false), + error_no: 0, + }; + let send_slices = [ + IoSlice::new(header.as_bytes()), + IoSlice::new(req), + IoSlice::new(data), + ]; + let done = send_msg_with_fds(stream, &send_slices, req_fds)?; + if done != total_req_size { + return error::PartialWrite { + want: total_req_size, + done, + } + .fail(); + } + + let mut reply_header = VfioUserHeader::default(); + let (resp, data) = resp_bufs; + let mut reply_header_slice = [ + IoSliceMut::new(reply_header.as_mut_bytes()), + IoSliceMut::new(resp), + IoSliceMut::new(data), + ]; + let bytes_read = recv_msg_with_fds(stream, &mut reply_header_slice, resp_fds)?; + if bytes_read < size_of::() + resp.len() { + return error::PartialRead { + want: size_of::() + resp.len(), + done: bytes_read, + } + .fail(); + } + if reply_header.msg_size != bytes_read as u32 { + return error::PartialRead { + want: reply_header.msg_size as usize, + done: bytes_read, + } + .fail(); + } + if reply_header.msg_id != msg_id { + return error::MsgId { + want: msg_id, + got: reply_header.msg_id, + } + .fail(); + } + if reply_header.cmd != cmd { + return error::Response { + want: cmd, + got: reply_header.cmd, + } + .fail(); + } + if reply_header.flags.ty() != VfioUserMessageType::REPLY { + return error::HeaderFlag { + flags: reply_header.flags, + } + .fail(); + } + if reply_header.flags.error() { + return error::ServerErr { + cmd, + code: reply_header.error_no, + } + .fail(); + } + + Ok(reply_header) + } + + pub fn negotiate_version(&self) -> Result<()> { + let version_hdr = VfioUserVersion { major: 0, minor: 2 }; + let caps_str = "{\"capabilities\":{\"max_fds\":32,\"max_data_xfer_size\":1048576}}\0"; + let caps_bytes = caps_str.as_bytes(); + + let mut resp = VfioUserVersion::default(); + let mut resp_buf = vec![0u8; 8192]; + + let reply = self.transact( + VfioUserCmd::VERSION, + (version_hdr.as_bytes(), caps_bytes), + &[], + (resp.as_mut_bytes(), resp_buf.as_mut_slice()), + &mut [], + )?; + + let server_major = resp.major; + let server_minor = resp.minor; + log::debug!("vfio-user server version: {server_major}.{server_minor}"); + if server_major != 0 { + return error::Version { + major: server_major, + minor: server_minor, + } + .fail(); + } + + let caps_reply_size = + reply.msg_size as usize - size_of::() - size_of::(); + if caps_reply_size > 0 { + let caps_reply = &resp_buf[..caps_reply_size]; + log::debug!( + "vfio-user server capabilities: {}", + String::from_utf8_lossy(caps_reply) + ); + } + + Ok(()) + } + + pub fn dma_map(&self, req: &VfioUserDmaMap, fd: BorrowedFd) -> Result<()> { + self.transact( + VfioUserCmd::DMA_MAP, + (req.as_bytes(), &[]), + &[fd], + (&mut [], &mut []), + &mut [], + )?; + Ok(()) + } + + pub fn dma_unmap(&self, req: &VfioUserDmaUnmap) -> Result<()> { + let mut resp = VfioUserDmaUnmap::default(); + self.transact( + VfioUserCmd::DMA_UNMAP, + (req.as_bytes(), &[]), + &[], + (resp.as_mut_bytes(), &mut []), + &mut [], + )?; + Ok(()) + } + + pub fn get_device_info(&self) -> Result { + let req = VfioUserDeviceInfo { + argsz: size_of::() as u32, + ..Default::default() + }; + let mut resp = VfioUserDeviceInfo::default(); + self.transact( + VfioUserCmd::DEVICE_GET_INFO, + (req.as_bytes(), &[]), + &[], + (resp.as_mut_bytes(), &mut []), + &mut [], + )?; + Ok(resp) + } + + pub fn get_region_info(&self, index: u32) -> Result<(VfioRegionInfo, Option)> { + let req = VfioRegionInfo { + argsz: size_of::() as u32, + index, + ..Default::default() + }; + let mut resp = VfioRegionInfo::default(); + let mut resp_fd = None; + + self.transact( + VfioUserCmd::DEVICE_GET_REGION_INFO, + (req.as_bytes(), &[]), + &[], + (resp.as_mut_bytes(), &mut []), + array::from_mut(&mut resp_fd), + )?; + + Ok((resp, resp_fd)) + } + + pub fn get_irq_info(&self, index: u32) -> Result { + let req = VfioIrqInfo { + argsz: size_of::() as u32, + index, + ..Default::default() + }; + let mut resp = VfioIrqInfo::default(); + + self.transact( + VfioUserCmd::DEVICE_GET_IRQ_INFO, + (req.as_bytes(), &[]), + &[], + (resp.as_mut_bytes(), &mut []), + &mut [], + )?; + + Ok(resp) + } + + pub fn reset(&self) -> Result<()> { + self.transact( + VfioUserCmd::DEVICE_RESET, + (&[], &[]), + &[], + (&mut [], &mut []), + &mut [], + )?; + Ok(()) + } + + pub fn set_irqs(&self, req: &VfioUserIrqSet, data: &[u8], fds: &[BorrowedFd]) -> Result<()> { + self.transact( + VfioUserCmd::DEVICE_SET_IRQS, + (req.as_bytes(), data), + fds, + (&mut [], &mut []), + &mut [], + )?; + Ok(()) + } + + pub fn read_region(&self, req: &VfioUserRegionAccess, buf: &mut [u8]) -> Result<()> { + let mut resp = VfioUserRegionAccess::default(); + self.transact( + VfioUserCmd::REGION_READ, + (req.as_bytes(), &[]), + &[], + (resp.as_mut_bytes(), buf), + &mut [], + )?; + if resp.count != buf.len() as u32 { + return error::PartialRead { + want: buf.len(), + done: resp.count as usize, + } + .fail(); + } + + Ok(()) + } + + pub fn write_region(&self, req: VfioUserRegionAccess, buf: &[u8]) -> Result<()> { + let mut resp = VfioUserRegionAccess::default(); + self.transact( + VfioUserCmd::REGION_WRITE, + (req.as_bytes(), buf), + &[], + (resp.as_mut_bytes(), &mut []), + &mut [], + )?; + if resp.count != buf.len() as u32 { + return error::PartialWrite { + want: buf.len(), + done: resp.count as usize, + } + .fail(); + } + Ok(()) + } +} + +#[cfg(test)] +#[path = "conn_test.rs"] +mod tests; diff --git a/alioth/src/vfio/user/conn_test.rs b/alioth/src/vfio/user/conn_test.rs new file mode 100644 index 00000000..26e39b99 --- /dev/null +++ b/alioth/src/vfio/user/conn_test.rs @@ -0,0 +1,101 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io::{IoSlice, IoSliceMut}; +use std::os::unix::net::UnixStream; +use std::thread; + +use assert_matches::assert_matches; +use zerocopy::{FromBytes, IntoBytes}; + +use crate::utils::uds::{recv_msg_with_fds, send_msg_with_fds}; +use crate::vfio::user::Error; +use crate::vfio::user::bindings::{ + VfioUserCmd, VfioUserHeader, VfioUserHeaderFlag, VfioUserMessageType, VfioUserVersion, +}; +use crate::vfio::user::conn::VfioUserSession; + +#[test] +fn test_vfio_user_version_server_mismatch() { + let (client, server) = UnixStream::pair().unwrap(); + let server_handle = thread::spawn(move || { + let mut header_buf = [0u8; size_of::()]; + let mut recv_fds = [const { None }; 32]; + let mut header_slice = [IoSliceMut::new(&mut header_buf)]; + recv_msg_with_fds(&server, &mut header_slice, &mut recv_fds).unwrap(); + let (req_header, _) = VfioUserHeader::read_from_prefix(&header_buf).unwrap(); + + // Server sends major version 1 (mismatch) + let reply_version = VfioUserVersion { major: 1, minor: 0 }; + let reply_hdr = VfioUserHeader { + msg_id: req_header.msg_id, + cmd: VfioUserCmd::VERSION, + msg_size: (size_of::() + size_of::()) as u32, + flags: VfioUserHeaderFlag::new(VfioUserMessageType::REPLY, false, false), + error_no: 0, + }; + let slices = [ + IoSlice::new(reply_hdr.as_bytes()), + IoSlice::new(reply_version.as_bytes()), + ]; + send_msg_with_fds(&server, &slices, &[]).unwrap(); + }); + + let session = VfioUserSession::new(client); + let res = session.negotiate_version(); + assert_matches!( + res, + Err(Error::Version { + major: 1, + minor: 0, + .. + }) + ); + + server_handle.join().unwrap(); +} + +#[test] +fn test_vfio_user_server_error_response() { + let (client, server) = UnixStream::pair().unwrap(); + let server_handle = thread::spawn(move || { + let mut req_header = VfioUserHeader::default(); + let mut header_slice = [IoSliceMut::new(req_header.as_mut_bytes())]; + recv_msg_with_fds(&server, &mut header_slice, &mut []).unwrap(); + + // Server replies with ERROR flag and EINVAL + let reply_hdr = VfioUserHeader { + msg_id: req_header.msg_id, + cmd: req_header.cmd, + msg_size: size_of::() as u32, + flags: VfioUserHeaderFlag::new(VfioUserMessageType::REPLY, false, true), + error_no: 22, + }; + let slices = [IoSlice::new(reply_hdr.as_bytes())]; + send_msg_with_fds(&server, &slices, &[]).unwrap(); + }); + + let session = VfioUserSession::new(client); + let res = session.reset(); + assert_matches!( + res, + Err(Error::ServerErr { + cmd: VfioUserCmd::DEVICE_RESET, + code: 22, + .. + }) + ); + + server_handle.join().unwrap(); +} diff --git a/alioth/src/vfio/user/device.rs b/alioth/src/vfio/user/device.rs new file mode 100644 index 00000000..1217db7d --- /dev/null +++ b/alioth/src/vfio/user/device.rs @@ -0,0 +1,232 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; +use std::fmt::Debug; +use std::io; +use std::io::ErrorKind; +use std::mem::size_of; +use std::os::fd::{BorrowedFd, OwnedFd}; +use std::sync::Arc; + +use parking_lot::RwLock; +use zerocopy::IntoBytes; + +use crate::errors::BoxTrace; +use crate::mem; +use crate::mem::LayoutChanged; +use crate::mem::mapped::ArcMemPages; +use crate::sys::vfio::{VfioDeviceInfo, VfioIrqInfo, VfioIrqSetFlag, VfioRegionInfo}; +use crate::vfio::Result; +use crate::vfio::device::Device; +use crate::vfio::user::bindings::{ + VfioUserDmaMap, VfioUserDmaMapFlag, VfioUserDmaUnmap, VfioUserIrqSet, VfioUserRegionAccess, +}; +use crate::vfio::user::conn::VfioUserSession; + +#[derive(Debug)] +pub struct VfioUserDevice { + session: Arc, + region_fds: RwLock>>, +} + +impl VfioUserDevice { + pub fn new(session: Arc) -> Result { + let dev = VfioUserDevice { + session, + region_fds: RwLock::new(HashMap::new()), + }; + + Ok(dev) + } +} + +impl Device for VfioUserDevice { + fn get_info(&self) -> Result { + let resp = self.session.get_device_info()?; + Ok(VfioDeviceInfo { + argsz: size_of::() as u32, + flags: resp.flags, + num_irqs: resp.num_irqs, + num_regions: resp.num_regions, + cap_offset: 0, + pad: 0, + }) + } + + fn get_region_info(&self, index: u32) -> Result { + let (resp, resp_fd) = self.session.get_region_info(index)?; + let mut fds = self.region_fds.write(); + if let Some(old) = fds.insert(index, resp_fd) { + fds.insert(index, old); + } + Ok(resp) + } + + fn get_irq_info(&self, index: u32) -> Result { + let resp = self.session.get_irq_info(index)?; + Ok(resp) + } + + fn reset(&self) -> Result<()> { + self.session.reset()?; + Ok(()) + } + + fn set_irq_eventfd( + &self, + index: u32, + start: u32, + eventfds: &[Option>], + ) -> Result<()> { + let mut send_fds = vec![]; + let mut fd_indices = vec![]; + for fd in eventfds { + if let Some(f) = fd { + fd_indices.push(send_fds.len() as i32); + send_fds.push(*f); + } else { + fd_indices.push(-1); + } + } + + let irq_set = VfioUserIrqSet { + argsz: (size_of::() + fd_indices.as_bytes().len()) as u32, + flags: VfioIrqSetFlag::DATA_EVENTFD | VfioIrqSetFlag::ACTION_TRIGGER, + index, + start, + count: eventfds.len() as u32, + }; + + self.session + .set_irqs(&irq_set, fd_indices.as_bytes(), &send_fds)?; + Ok(()) + } + + fn disable_irq(&self, index: u32) -> Result<()> { + let irq_set = VfioUserIrqSet { + argsz: size_of::() as u32, + flags: VfioIrqSetFlag::DATA_NONE | VfioIrqSetFlag::ACTION_TRIGGER, + index, + start: 0, + count: 0, + }; + self.session.set_irqs(&irq_set, &[], &[])?; + Ok(()) + } + + fn read_region(&self, region: &VfioRegionInfo, offset: u64, buf: &mut [u8]) -> Result<()> { + let req = VfioUserRegionAccess { + offset, + region: region.index, + count: buf.len() as u32, + }; + self.session.read_region(&req, buf)?; + Ok(()) + } + + fn write_region(&self, region: &VfioRegionInfo, offset: u64, buf: &[u8]) -> Result<()> { + let req = VfioUserRegionAccess { + offset, + region: region.index, + count: buf.len() as u32, + }; + self.session.write_region(req, buf)?; + Ok(()) + } + + fn get_region_mmap_fd(&self, index: u32) -> Result> { + if let Some(Some(fd)) = &self.region_fds.read().get(&index) { + Ok(Some(fd.try_clone()?)) + } else { + Ok(None) + } + } + + fn get_dma_buf_fd(&self, _index: u32, _offset: u64, _size: usize) -> Result { + Err(io::Error::new( + ErrorKind::Unsupported, + "dma-buf is not supported in vfio-user", + ) + .into()) + } +} + +#[derive(Debug)] +pub struct UpdateVfioUserMapping { + session: Arc, +} + +impl UpdateVfioUserMapping { + pub fn new(session: Arc) -> Self { + UpdateVfioUserMapping { session } + } +} + +impl LayoutChanged for UpdateVfioUserMapping { + fn ram_added(&self, gpa: u64, pages: &ArcMemPages) -> mem::Result<()> { + let Some((fd, offset)) = pages.fd() else { + log::warn!("No fd for pages at gpa {gpa:#x}, skipping mapping"); + return Ok(()); + }; + let map = VfioUserDmaMap { + argsz: size_of::() as u32, + flags: VfioUserDmaMapFlag::READ | VfioUserDmaMapFlag::WRITE, + offset, + addr: gpa, + size: pages.size(), + }; + let ret = self.session.dma_map(&map, fd); + ret.box_trace(mem::error::ChangeLayout)?; + Ok(()) + } + + fn ram_removed(&self, gpa: u64, pages: &ArcMemPages) -> mem::Result<()> { + if pages.fd().is_none() { + log::warn!("No fd for pages at gpa {gpa:#x}, skipping unmapping"); + return Ok(()); + }; + let unmap = VfioUserDmaUnmap { + argsz: size_of::() as u32, + flags: 0, + addr: gpa, + size: pages.size(), + }; + let ret = self.session.dma_unmap(&unmap); + ret.box_trace(mem::error::ChangeLayout)?; + Ok(()) + } + + fn dev_mem_added( + &self, + gpa: u64, + pages: &ArcMemPages, + _: Option, + ) -> mem::Result<()> { + self.ram_added(gpa, pages) + } + + fn dev_mem_removed( + &self, + gpa: u64, + pages: &ArcMemPages, + _: Option, + ) -> mem::Result<()> { + self.ram_removed(gpa, pages) + } +} + +#[cfg(test)] +#[path = "device_test.rs"] +mod tests; diff --git a/alioth/src/vfio/user/device_test.rs b/alioth/src/vfio/user/device_test.rs new file mode 100644 index 00000000..973486fb --- /dev/null +++ b/alioth/src/vfio/user/device_test.rs @@ -0,0 +1,362 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io::{IoSlice, IoSliceMut, Read}; +use std::os::fd::{AsFd, OwnedFd}; +use std::os::unix::net::UnixStream; +use std::sync::Arc; +use std::thread; + +use assert_matches::assert_matches; +use zerocopy::{FromBytes, IntoBytes}; + +use crate::mem::LayoutChanged; +use crate::mem::mapped::ArcMemPages; +use crate::sys::vfio::{ + VfioDeviceInfoFlag, VfioIrqInfo, VfioIrqInfoFlag, VfioRegionInfo, VfioRegionInfoFlag, +}; +use crate::utils::uds::{recv_msg_with_fds, send_msg_with_fds}; +use crate::vfio::device::Device; +use crate::vfio::user::bindings::{ + VfioUserCmd, VfioUserDeviceInfo, VfioUserDmaUnmap, VfioUserHeader, VfioUserHeaderFlag, + VfioUserMessageType, VfioUserRegionAccess, VfioUserVersion, +}; +use crate::vfio::user::conn::VfioUserSession; +use crate::vfio::user::device::{UpdateVfioUserMapping, VfioUserDevice}; + +fn handle_vfio_user_server(mut stream: &UnixStream) { + let dummy_file = tempfile::tempfile().unwrap(); + dummy_file.set_len(0x1000).unwrap(); + let mmap_fd: OwnedFd = dummy_file.into(); + + let mut req_header = VfioUserHeader::default(); + let mut payload_buf = [0u8; 4096]; + let mut recv_fds = [const { None }; 32]; + + loop { + for fd in &mut recv_fds { + *fd = None; + } + let mut header_slice = [IoSliceMut::new(req_header.as_mut_bytes())]; + let bytes = match recv_msg_with_fds(stream, &mut header_slice, &mut recv_fds) { + Ok(0) => break, // EOF + Ok(b) => b, + Err(e) => { + log::error!("server recv_msg error: {e:?}"); + break; + } + }; + if bytes != size_of::() { + log::error!("vfio-user server: failed to read a complete header"); + break; + } + let payload_size = req_header.msg_size as usize - size_of::(); + if payload_size > 0 + && let Err(e) = stream.read_exact(&mut payload_buf[..payload_size]) + { + log::error!("server read_exact payload error: {e:?}"); + break; + } + + match req_header.cmd { + VfioUserCmd::VERSION => { + let reply_version = VfioUserVersion { major: 0, minor: 2 }; + let cap_str = b"{\"capabilities\":{\"max_msg_fds\":32}}\0"; + let reply_size = + size_of::() + size_of::() + cap_str.len(); + let reply_hdr = VfioUserHeader { + msg_id: req_header.msg_id, + cmd: VfioUserCmd::VERSION, + msg_size: reply_size as u32, + flags: VfioUserHeaderFlag::new(VfioUserMessageType::REPLY, false, false), + error_no: 0, + }; + let slices = [ + IoSlice::new(reply_hdr.as_bytes()), + IoSlice::new(reply_version.as_bytes()), + IoSlice::new(cap_str), + ]; + send_msg_with_fds(stream, &slices, &[]).unwrap(); + } + VfioUserCmd::DEVICE_GET_INFO => { + let dev_info = VfioUserDeviceInfo { + argsz: size_of::() as u32, + flags: VfioDeviceInfoFlag::PCI | VfioDeviceInfoFlag::RESET, + num_regions: 2, + num_irqs: 2, + }; + let reply_hdr = VfioUserHeader { + msg_id: req_header.msg_id, + cmd: VfioUserCmd::DEVICE_GET_INFO, + msg_size: (size_of::() + size_of::()) + as u32, + flags: VfioUserHeaderFlag::new(VfioUserMessageType::REPLY, false, false), + error_no: 0, + }; + let slices = [ + IoSlice::new(reply_hdr.as_bytes()), + IoSlice::new(dev_info.as_bytes()), + ]; + send_msg_with_fds(stream, &slices, &[]).unwrap(); + } + VfioUserCmd::DEVICE_GET_REGION_INFO => { + let (reg_req, _) = VfioRegionInfo::read_from_prefix(&payload_buf).unwrap(); + let (reg_info, fd_to_send) = if reg_req.index == 0 { + ( + VfioRegionInfo { + argsz: size_of::() as u32, + flags: VfioRegionInfoFlag::READ + | VfioRegionInfoFlag::WRITE + | VfioRegionInfoFlag::MMAP, + index: 0, + cap_offset: 0, + size: 0x1000, + offset: 0, + }, + Some(mmap_fd.as_fd()), + ) + } else { + ( + VfioRegionInfo { + argsz: size_of::() as u32, + flags: VfioRegionInfoFlag::READ | VfioRegionInfoFlag::WRITE, + index: 1, + cap_offset: 0, + size: 0x1000, + offset: 0, + }, + None, + ) + }; + let reply_hdr = VfioUserHeader { + msg_id: req_header.msg_id, + cmd: VfioUserCmd::DEVICE_GET_REGION_INFO, + msg_size: (size_of::() + size_of::()) as u32, + flags: VfioUserHeaderFlag::new(VfioUserMessageType::REPLY, false, false), + error_no: 0, + }; + let slices = [ + IoSlice::new(reply_hdr.as_bytes()), + IoSlice::new(reg_info.as_bytes()), + ]; + if let Some(fd) = fd_to_send { + send_msg_with_fds(stream, &slices, &[fd]).unwrap(); + } else { + send_msg_with_fds(stream, &slices, &[]).unwrap(); + } + } + VfioUserCmd::DEVICE_GET_IRQ_INFO => { + let (irq_req, _) = VfioIrqInfo::read_from_prefix(&payload_buf).unwrap(); + let irq_info = VfioIrqInfo { + argsz: size_of::() as u32, + flags: VfioIrqInfoFlag::EVENTFD, + index: irq_req.index, + count: 4, + }; + let reply_hdr = VfioUserHeader { + msg_id: req_header.msg_id, + cmd: VfioUserCmd::DEVICE_GET_IRQ_INFO, + msg_size: (size_of::() + size_of::()) as u32, + flags: VfioUserHeaderFlag::new(VfioUserMessageType::REPLY, false, false), + error_no: 0, + }; + let slices = [ + IoSlice::new(reply_hdr.as_bytes()), + IoSlice::new(irq_info.as_bytes()), + ]; + send_msg_with_fds(stream, &slices, &[]).unwrap(); + } + VfioUserCmd::DEVICE_RESET => { + let reply_hdr = VfioUserHeader { + msg_id: req_header.msg_id, + cmd: VfioUserCmd::DEVICE_RESET, + msg_size: size_of::() as u32, + flags: VfioUserHeaderFlag::new(VfioUserMessageType::REPLY, false, false), + error_no: 0, + }; + let slices = [IoSlice::new(reply_hdr.as_bytes())]; + send_msg_with_fds(stream, &slices, &[]).unwrap(); + } + VfioUserCmd::DEVICE_SET_IRQS => { + let reply_hdr = VfioUserHeader { + msg_id: req_header.msg_id, + cmd: VfioUserCmd::DEVICE_SET_IRQS, + msg_size: size_of::() as u32, + flags: VfioUserHeaderFlag::new(VfioUserMessageType::REPLY, false, false), + error_no: 0, + }; + let slices = [IoSlice::new(reply_hdr.as_bytes())]; + send_msg_with_fds(stream, &slices, &[]).unwrap(); + } + VfioUserCmd::REGION_READ => { + let (access_req, _) = VfioUserRegionAccess::read_from_prefix(&payload_buf).unwrap(); + let access_resp = VfioUserRegionAccess { + offset: access_req.offset, + region: access_req.region, + count: access_req.count, + }; + let data = vec![0xaa; access_req.count as usize]; + let reply_hdr = VfioUserHeader { + msg_id: req_header.msg_id, + cmd: VfioUserCmd::REGION_READ, + msg_size: (size_of::() + + size_of::() + + data.len()) as u32, + flags: VfioUserHeaderFlag::new(VfioUserMessageType::REPLY, false, false), + error_no: 0, + }; + let slices = [ + IoSlice::new(reply_hdr.as_bytes()), + IoSlice::new(access_resp.as_bytes()), + IoSlice::new(&data), + ]; + send_msg_with_fds(stream, &slices, &[]).unwrap(); + } + VfioUserCmd::REGION_WRITE => { + let (access_req, _) = VfioUserRegionAccess::read_from_prefix(&payload_buf).unwrap(); + let access_resp = VfioUserRegionAccess { + offset: access_req.offset, + region: access_req.region, + count: access_req.count, + }; + let reply_hdr = VfioUserHeader { + msg_id: req_header.msg_id, + cmd: VfioUserCmd::REGION_WRITE, + msg_size: (size_of::() + size_of::()) + as u32, + flags: VfioUserHeaderFlag::new(VfioUserMessageType::REPLY, false, false), + error_no: 0, + }; + let slices = [ + IoSlice::new(reply_hdr.as_bytes()), + IoSlice::new(access_resp.as_bytes()), + ]; + send_msg_with_fds(stream, &slices, &[]).unwrap(); + } + VfioUserCmd::DMA_MAP => { + let reply_hdr = VfioUserHeader { + msg_id: req_header.msg_id, + cmd: VfioUserCmd::DMA_MAP, + msg_size: size_of::() as u32, + flags: VfioUserHeaderFlag::new(VfioUserMessageType::REPLY, false, false), + error_no: 0, + }; + let slices = [IoSlice::new(reply_hdr.as_bytes())]; + send_msg_with_fds(stream, &slices, &[]).unwrap(); + } + VfioUserCmd::DMA_UNMAP => { + let (unmap_req, _) = VfioUserDmaUnmap::read_from_prefix(&payload_buf).unwrap(); + let reply_hdr = VfioUserHeader { + msg_id: req_header.msg_id, + cmd: VfioUserCmd::DMA_UNMAP, + msg_size: (size_of::() + size_of::()) as u32, + flags: VfioUserHeaderFlag::new(VfioUserMessageType::REPLY, false, false), + error_no: 0, + }; + let slices = [ + IoSlice::new(reply_hdr.as_bytes()), + IoSlice::new(unmap_req.as_bytes()), + ]; + send_msg_with_fds(stream, &slices, &[]).unwrap(); + } + _ => { + let reply_hdr = VfioUserHeader { + msg_id: req_header.msg_id, + cmd: req_header.cmd, + msg_size: size_of::() as u32, + flags: VfioUserHeaderFlag::new(VfioUserMessageType::REPLY, false, true), + error_no: libc::ENOSYS as u32, + }; + let slices = [IoSlice::new(reply_hdr.as_bytes())]; + send_msg_with_fds(stream, &slices, &[]).unwrap(); + } + } + } +} + +#[test] +fn test_vfio_user_device_full_lifecycle() { + let (client, server) = UnixStream::pair().unwrap(); + let server_handle = thread::spawn(move || { + handle_vfio_user_server(&server); + }); + + let session = Arc::new(VfioUserSession::new(client)); + let dev = VfioUserDevice::new(session.clone()).unwrap(); + + // 1. Test get_info + let info = dev.get_info().unwrap(); + assert_eq!(info.num_regions, 2); + assert_eq!(info.num_irqs, 2); + + // 2. Test get_region_info + let reg0 = dev.get_region_info(0).unwrap(); + assert_eq!(reg0.size, 0x1000); + assert!(reg0.flags.contains(VfioRegionInfoFlag::MMAP)); + + let reg1 = dev.get_region_info(1).unwrap(); + assert_eq!(reg1.size, 0x1000); + assert!(!reg1.flags.contains(VfioRegionInfoFlag::MMAP)); + + // 3. Test get_region_mmap + let mmap0 = dev.get_region_mmap_fd(0).unwrap(); + assert!(mmap0.is_some()); + let mmap1 = dev.get_region_mmap_fd(1).unwrap(); + assert!(mmap1.is_none()); + + // 4. Test get_irq_info + let irq_info = dev.get_irq_info(0).unwrap(); + assert_eq!(irq_info.count, 4); + + // 5. Test read_region and write_region + let mut read_buf = [0u8; 16]; + dev.read_region(®0, 0, &mut read_buf).unwrap(); + assert_eq!(read_buf, [0xaa; 16]); + + let write_buf = [0x55; 16]; + dev.write_region(®0, 0, &write_buf).unwrap(); + + // 6. Test set_irq_eventfd and disable_irq + let eventfd_file = tempfile::tempfile().unwrap(); + let eventfd_borrowed = eventfd_file.as_fd(); + dev.set_irq_eventfd(0, 0, &[Some(eventfd_borrowed)]) + .unwrap(); + dev.disable_irq(0).unwrap(); + + // 7. Test reset + dev.reset().unwrap(); + + // 8. Test get_dma_buf_fd (unsupported) + assert_matches!(dev.get_dma_buf_fd(0, 0, 0x1000), Err(_)); + + // 9. Test DMA mapping and UpdateVfioUserMapping + let arc_anon = ArcMemPages::from_memfd(c"test_mem", 0x2000, None).unwrap(); + + let updater = UpdateVfioUserMapping::new(session.clone()); + // RAM add / remove + assert_matches!(updater.ram_added(0x1000_0000, &arc_anon), Ok(())); + assert_matches!(updater.ram_removed(0x1000_0000, &arc_anon), Ok(())); + + // Dev mem add / remove + assert_matches!(updater.dev_mem_added(0x2000_0000, &arc_anon, None), Ok(())); + assert_matches!( + updater.dev_mem_removed(0x2000_0000, &arc_anon, None), + Ok(()) + ); + + drop(updater); + drop(dev); + drop(session); + server_handle.join().unwrap(); +} diff --git a/alioth/src/vfio/user/user.rs b/alioth/src/vfio/user/user.rs new file mode 100644 index 00000000..9b314e77 --- /dev/null +++ b/alioth/src/vfio/user/user.rs @@ -0,0 +1,46 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub mod bindings; +pub mod conn; +pub mod device; + +use snafu::Snafu; + +use crate::errors::{DebugTrace, trace_error}; +use crate::vfio::user::bindings::{VfioUserCmd, VfioUserHeaderFlag}; + +#[trace_error] +#[derive(Snafu, DebugTrace)] +#[snafu(module, visibility(pub(crate)), context(suffix(false)))] +pub enum Error { + #[snafu(display("Error from OS"), context(false))] + System { error: std::io::Error }, + #[snafu(display("Unexpected vfio-user response, want {want:?}, got {got:?}"))] + Response { want: VfioUserCmd, got: VfioUserCmd }, + #[snafu(display("Unexpected vfio-user message id, want {want}, got {got}"))] + MsgId { want: u16, got: u16 }, + #[snafu(display("Unexpected vfio-user message flags {flags:?}"))] + HeaderFlag { flags: VfioUserHeaderFlag }, + #[snafu(display("Failed to send {want} bytes, only {done} bytes were sent"))] + PartialWrite { want: usize, done: usize }, + #[snafu(display("Failed to read {want} bytes, only {done} bytes were read"))] + PartialRead { want: usize, done: usize }, + #[snafu(display("Unsupported vfio-user version {major}.{minor}"))] + Version { major: u16, minor: u16 }, + #[snafu(display("Server error: cmd {cmd:?}, error code {code}"))] + ServerErr { cmd: VfioUserCmd, code: u32 }, +} + +pub type Result = std::result::Result; diff --git a/alioth/src/vfio/vfio.rs b/alioth/src/vfio/vfio.rs index 6f6a92b5..4dbc391b 100644 --- a/alioth/src/vfio/vfio.rs +++ b/alioth/src/vfio/vfio.rs @@ -18,6 +18,8 @@ pub mod device; pub mod group; pub mod iommu; pub mod pci; +#[path = "user/user.rs"] +pub mod user; use std::path::Path; @@ -47,6 +49,8 @@ pub enum Error { NotSupportedHeader { ty: u8 }, #[snafu(display("Setting container iommu to {new:?}, but it already has {current:?}"))] SetContainerIommu { current: VfioIommu, new: VfioIommu }, + #[snafu(display("vfio-user error"), context(false))] + VfioUser { source: Box }, } pub type Result = std::result::Result; @@ -86,6 +90,12 @@ pub struct VfioContainerSpec { pub dev_vfio: Option>, } +#[derive(Debug, PartialEq, Eq, Deserialize, Help)] +pub struct VfioUserSpec { + /// Path to the vfio-user UNIX domain socket. + pub socket: Box, +} + #[cfg(test)] #[path = "vfio_test.rs"] mod tests; diff --git a/alioth/src/vfio/vfio_test.rs b/alioth/src/vfio/vfio_test.rs index 9dab7ff1..0db8dcc4 100644 --- a/alioth/src/vfio/vfio_test.rs +++ b/alioth/src/vfio/vfio_test.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::vfio::{VfioCdevSpec, VfioContainerSpec, VfioGroupSpec, VfioIoasSpec}; +use crate::vfio::{VfioCdevSpec, VfioContainerSpec, VfioGroupSpec, VfioIoasSpec, VfioUserSpec}; #[test] fn test_vfio_specs_deserialization() { @@ -44,4 +44,9 @@ fn test_vfio_specs_deserialization() { container_spec.dev_vfio.unwrap().to_str().unwrap(), "/dev/vfio/vfio" ); + + // VfioUserSpec + let user_aco = "socket=/tmp/vfio-user.sock"; + let user_spec: VfioUserSpec = serde_aco::from_arg(user_aco).unwrap(); + assert_eq!(user_spec.socket.to_str().unwrap(), "/tmp/vfio-user.sock"); } diff --git a/alioth/src/vm/vm.rs b/alioth/src/vm/vm.rs index 30397715..042ebd03 100644 --- a/alioth/src/vm/vm.rs +++ b/alioth/src/vm/vm.rs @@ -64,7 +64,14 @@ use crate::vfio::iommu::{Ioas, Iommu, UpdateIommuIoas}; #[cfg(target_os = "linux")] use crate::vfio::pci::VfioPciDev; #[cfg(target_os = "linux")] -use crate::vfio::{VfioCdevSpec, VfioContainerSpec, VfioGroupSpec, VfioIoasSpec}; +use crate::vfio::user::conn::VfioUserSession; +#[cfg(target_os = "linux")] +use crate::vfio::user::device::{UpdateVfioUserMapping, VfioUserDevice}; +#[cfg(target_os = "linux")] +use crate::vfio::{VfioCdevSpec, VfioContainerSpec, VfioGroupSpec, VfioIoasSpec, VfioUserSpec}; +#[cfg(target_os = "linux")] +use std::os::unix::net::UnixStream; + use crate::virtio::dev::{DevSpec, Virtio, VirtioDevice}; use crate::virtio::pci::VirtioPciDevice; @@ -373,6 +380,30 @@ where Ok(()) } + #[cfg(target_os = "linux")] + pub fn add_vfio_user_dev(&self, name: Arc, spec: VfioUserSpec) -> Result<(), Error> { + let stream = UnixStream::connect(&spec.socket).map_err(crate::vfio::Error::from)?; + + let session = Arc::new(VfioUserSession::new(stream)); + session + .negotiate_version() + .map_err(crate::vfio::Error::from)?; + + let dev = VfioUserDevice::new(session.clone())?; + + let bdf = self.ctx.board.pci_bus.reserve(None).unwrap(); + let msi_sender = self.ctx.board.vm.create_msi_sender( + #[cfg(target_arch = "aarch64")] + u32::from(bdf.0), + )?; + let dev = VfioPciDev::new(name.clone(), dev, msi_sender)?; + self.add_pci_dev(Some(bdf), Arc::new(dev))?; + + let update = Box::new(UpdateVfioUserMapping::new(session.clone())); + self.ctx.board.memory.register_change_callback(update)?; + Ok(()) + } + pub fn add_vfio_container(&self, spec: VfioContainerSpec) -> Result, Error> { let mut containers = self.vfio_containers.lock(); if containers.contains_key(&spec.name) {