From 8eba32b3953022f9d1b9613292759e0c5ed93093 Mon Sep 17 00:00:00 2001 From: Changyuan Lyu Date: Sun, 2 Aug 2026 09:48:10 -0700 Subject: [PATCH 1/4] feat(vfio-user): add protocol bindings Assisted-by: Antigravity:Gemini-3.7-Flash Signed-off-by: Changyuan Lyu --- alioth/src/vfio/user/bindings.rs | 130 +++++++++++++++++++++++++++++++ alioth/src/vfio/user/user.rs | 15 ++++ alioth/src/vfio/vfio.rs | 2 + 3 files changed, 147 insertions(+) create mode 100644 alioth/src/vfio/user/bindings.rs create mode 100644 alioth/src/vfio/user/user.rs 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/user.rs b/alioth/src/vfio/user/user.rs new file mode 100644 index 00000000..b3f57124 --- /dev/null +++ b/alioth/src/vfio/user/user.rs @@ -0,0 +1,15 @@ +// 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; diff --git a/alioth/src/vfio/vfio.rs b/alioth/src/vfio/vfio.rs index 6f6a92b5..5ffc2404 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; From 86f29c4b4ac2dcf367f48bb6938dbc72aee5b332 Mon Sep 17 00:00:00 2001 From: Changyuan Lyu Date: Sun, 2 Aug 2026 09:48:43 -0700 Subject: [PATCH 2/4] feat(vfio-user): add connection and session manager Assisted-by: Antigravity:Gemini-3.7-Flash Signed-off-by: Changyuan Lyu --- alioth/src/sys/linux/vfio.rs | 5 +- alioth/src/vfio/user/conn.rs | 325 ++++++++++++++++++++++++++++++ alioth/src/vfio/user/conn_test.rs | 101 ++++++++++ alioth/src/vfio/user/user.rs | 30 +++ alioth/src/vfio/vfio.rs | 2 + 5 files changed, 461 insertions(+), 2 deletions(-) create mode 100644 alioth/src/vfio/user/conn.rs create mode 100644 alioth/src/vfio/user/conn_test.rs 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/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/user.rs b/alioth/src/vfio/user/user.rs index b3f57124..ba356ad2 100644 --- a/alioth/src/vfio/user/user.rs +++ b/alioth/src/vfio/user/user.rs @@ -13,3 +13,33 @@ // limitations under the License. pub mod bindings; +pub mod conn; + +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 5ffc2404..fc1fbfc5 100644 --- a/alioth/src/vfio/vfio.rs +++ b/alioth/src/vfio/vfio.rs @@ -49,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; From 248fe8f1006dcddccbd62051b8d5162c80423ad0 Mon Sep 17 00:00:00 2001 From: Changyuan Lyu Date: Sun, 2 Aug 2026 09:48:59 -0700 Subject: [PATCH 3/4] feat(vfio-user): implement trait Device and LayoutChanged Create VfioUserDevice struct wrapping VfioUserSession. Implement the abstract Device trait to route guest accesses, IRQ settings, and reset commands. Implement UpdateVfioUserMapping (LayoutChanged callback) to sync DMA mappings. Assisted-by: Antigravity:Gemini-3.7-Flash Signed-off-by: Changyuan Lyu --- alioth/src/vfio/user/device.rs | 232 ++++++++++++++++++ alioth/src/vfio/user/device_test.rs | 362 ++++++++++++++++++++++++++++ alioth/src/vfio/user/user.rs | 1 + 3 files changed, 595 insertions(+) create mode 100644 alioth/src/vfio/user/device.rs create mode 100644 alioth/src/vfio/user/device_test.rs 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 index ba356ad2..9b314e77 100644 --- a/alioth/src/vfio/user/user.rs +++ b/alioth/src/vfio/user/user.rs @@ -14,6 +14,7 @@ pub mod bindings; pub mod conn; +pub mod device; use snafu::Snafu; From af11fb82e90b05ad3bcfcd276e43ad2d3f99344b Mon Sep 17 00:00:00 2001 From: Changyuan Lyu Date: Sun, 2 Aug 2026 09:49:25 -0700 Subject: [PATCH 4/4] feat(vfio-user): integrate into VM and CLI Implement add_vfio_user_dev in Machine to connect, negotiate version, register DMA callback, and register VfioPciDev. Update CLI configuration and boot parsing to expose --vfio-user socket= command line flag. Register memory change callback after successful device addition to prevent leaking open sockets on failure. TAG=agy CONV=563b5413-8af4-45cc-a898-1abde6e4000b --- alioth-cli/src/boot/boot.rs | 15 ++++++++++++++- alioth-cli/src/boot/boot_test.rs | 8 +++++++- alioth-cli/src/boot/config.rs | 4 +++- alioth/src/vfio/vfio.rs | 6 ++++++ alioth/src/vfio/vfio_test.rs | 7 ++++++- alioth/src/vm/vm.rs | 33 +++++++++++++++++++++++++++++++- 6 files changed, 68 insertions(+), 5 deletions(-) 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/vfio/vfio.rs b/alioth/src/vfio/vfio.rs index fc1fbfc5..4dbc391b 100644 --- a/alioth/src/vfio/vfio.rs +++ b/alioth/src/vfio/vfio.rs @@ -90,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) {