diff --git a/Cargo.lock b/Cargo.lock index e08280b..d787ae6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -958,6 +958,24 @@ dependencies = [ "vmm-sys-util", ] +[[package]] +name = "vhost-device-template" +version = "0.1.0" +dependencies = [ + "assert_matches", + "clap", + "env_logger", + "libc", + "log", + "thiserror", + "vhost", + "vhost-user-backend", + "virtio-bindings", + "virtio-queue", + "vm-memory", + "vmm-sys-util", +] + [[package]] name = "vhost-device-vsock" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 6037899..f34d512 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,5 +7,6 @@ members = [ "vhost-device-rng", "vhost-device-scsi", "vhost-device-scmi", + "vhost-device-template", "vhost-device-vsock", ] diff --git a/README.md b/README.md index 19fb908..da9b847 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,10 @@ Here is the list of device backends that we support: - [SCSI](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-scsi/README.md) - [VSOCK](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-vsock/README.md) +The vhost-device workspace also provides a +[template](https://github.com/rust-vmm/vhost-device/blob/main/vhost-device-template/README.md) +to help new developers understand how to write their own vhost-user backend. + ### Staging Devices Implementing a proper VirtIO device requires co-ordination between the diff --git a/coverage_config_x86_64.json b/coverage_config_x86_64.json index 57da641..4fa22e1 100644 --- a/coverage_config_x86_64.json +++ b/coverage_config_x86_64.json @@ -1,5 +1,5 @@ { - "coverage_score": 77.28, + "coverage_score": 76.49, "exclude_path": "", "crate_features": "" } diff --git a/vhost-device-template/CHANGELOG.md b/vhost-device-template/CHANGELOG.md new file mode 100644 index 0000000..377ac4f --- /dev/null +++ b/vhost-device-template/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog +## [Unreleased] + +### Added + +### Changed + +### Fixed + +### Deprecated diff --git a/vhost-device-template/Cargo.toml b/vhost-device-template/Cargo.toml new file mode 100644 index 0000000..1509f6f --- /dev/null +++ b/vhost-device-template/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "vhost-device-template" +version = "0.1.0" +authors = ["Viresh Kumar "] +description = "Template for a vhost backend device" +repository = "https://github.com/rust-vmm/vhost-device" +readme = "README.md" +keywords = ["template", "vhost", "virt", "backend"] +license = "Apache-2.0 OR BSD-3-Clause" +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +xen = ["vm-memory/xen", "vhost/xen", "vhost-user-backend/xen"] + +[dependencies] +clap = { version = "4.4", features = ["derive"] } +env_logger = "0.10" +libc = "0.2" +log = "0.4" +thiserror = "1.0" +vhost = { version = "0.9", features = ["vhost-user-backend"] } +vhost-user-backend = "0.11" +virtio-bindings = "0.2.2" +virtio-queue = "0.10" +vm-memory = "0.13.1" +vmm-sys-util = "0.11" + +[dev-dependencies] +assert_matches = "1.5" +virtio-queue = { version = "0.10", features = ["test-utils"] } +vm-memory = { version = "0.13.1", features = ["backend-mmap", "backend-atomic"] } diff --git a/vhost-device-template/LICENSE-APACHE b/vhost-device-template/LICENSE-APACHE new file mode 120000 index 0000000..965b606 --- /dev/null +++ b/vhost-device-template/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/vhost-device-template/LICENSE-BSD-3-Clause b/vhost-device-template/LICENSE-BSD-3-Clause new file mode 120000 index 0000000..f2b0791 --- /dev/null +++ b/vhost-device-template/LICENSE-BSD-3-Clause @@ -0,0 +1 @@ +../LICENSE-BSD-3-Clause \ No newline at end of file diff --git a/vhost-device-template/README.md b/vhost-device-template/README.md new file mode 100644 index 0000000..4be683b --- /dev/null +++ b/vhost-device-template/README.md @@ -0,0 +1,37 @@ +# vhost-device-template - Template for a vhost-device backend implementation + +## Description +This program is a template for developers who intend to write a new vhost-device +backend. + +## Synopsis + +**vhost-device-template** [*OPTIONS*] + +## Options + +.. program:: vhost-device-template + +.. option:: -h, --help + + Print help. + +.. option:: -s, --socket-path=PATH + + Location of vhost-user Unix domain socket. This supports a single socket / + guest. + +## Examples + +The daemon should be started first: + +:: + + host# vhost-device-template --socket-path=vfoo.sock + +## License + +This project is licensed under either of + +- [Apache License](http://www.apache.org/licenses/LICENSE-2.0), Version 2.0 +- [BSD-3-Clause License](https://opensource.org/licenses/BSD-3-Clause) diff --git a/vhost-device-template/src/main.rs b/vhost-device-template/src/main.rs new file mode 100644 index 0000000..967cdc6 --- /dev/null +++ b/vhost-device-template/src/main.rs @@ -0,0 +1,146 @@ +// VIRTIO FOO Emulation via vhost-user +// +// Copyright 2023 Linaro Ltd. All Rights Reserved. +// Viresh Kumar +// +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause + +mod vhu_foo; + +use log::error; +use std::process::exit; +use std::sync::{Arc, RwLock}; +use std::thread::{spawn, JoinHandle}; + +use clap::Parser; +use thiserror::Error as ThisError; +use vhost_user_backend::VhostUserDaemon; +use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap}; + +use vhu_foo::VhostUserFooBackend; + +type Result = std::result::Result; + +#[derive(Debug, ThisError)] +/// Errors related to low level foo helpers +pub(crate) enum Error { + #[error("Could not create backend: {0}")] + CouldNotCreateBackend(vhu_foo::Error), + #[error("Could not create daemon: {0}")] + CouldNotCreateDaemon(vhost_user_backend::Error), + #[error("Fatal error: {0}")] + ServeFailed(vhost_user_backend::Error), +} + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +struct FooArgs { + /// Location of vhost-user Unix domain socket. + #[clap(short, long)] + socket_path: String, +} + +#[derive(PartialEq, Debug)] +struct FooConfiguration { + socket_path: String, +} + +impl TryFrom for FooConfiguration { + type Error = Error; + + fn try_from(args: FooArgs) -> Result { + Ok(FooConfiguration { + socket_path: args.socket_path.trim().to_string(), + }) + } +} + +#[derive(Copy, Clone)] +pub(crate) struct FooInfo { + counter: u32, +} + +impl FooInfo { + pub fn new() -> Self { + Self { counter: 0 } + } + + pub fn counter(&mut self) -> u32 { + self.counter += 1; + self.counter + } +} + +fn start_backend(args: FooArgs) -> Result<()> { + let config = FooConfiguration::try_from(args).unwrap(); + + let socket = config.socket_path.to_owned(); + let info = FooInfo::new(); + + let handle: JoinHandle> = spawn(move || loop { + // There isn't much value in complicating code here to return an error from the threads, + // and so the code uses unwrap() instead. The panic on a thread won't cause trouble to the + // main() function and should be safe for the daemon. + let backend = Arc::new(RwLock::new( + VhostUserFooBackend::new(info).map_err(Error::CouldNotCreateBackend)?, + )); + + let mut daemon = VhostUserDaemon::new( + String::from("vhost-device-template-backend"), + backend, + GuestMemoryAtomic::new(GuestMemoryMmap::new()), + ) + .map_err(Error::CouldNotCreateDaemon)?; + + daemon.serve(&socket).map_err(Error::ServeFailed)?; + }); + + handle.join().map_err(std::panic::resume_unwind).unwrap() +} + +fn main() { + env_logger::init(); + + if let Err(e) = start_backend(FooArgs::parse()) { + error!("{e}"); + exit(1); + } +} + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + + use super::*; + + impl FooArgs { + pub(crate) fn from_args(path: &str) -> FooArgs { + FooArgs { + socket_path: path.to_string(), + } + } + } + + #[test] + fn test_parse_successful() { + let socket_name = "vfoo.sock"; + + let cmd_args = FooArgs::from_args(socket_name); + let config = FooConfiguration::try_from(cmd_args).unwrap(); + + let expected_config = FooConfiguration { + socket_path: String::from(socket_name), + }; + + assert_eq!(config, expected_config); + } + + #[test] + fn test_fail_listener() { + // This will fail the listeners and thread will panic. + let socket_name = "~/path/not/present/foo"; + let cmd_args = FooArgs::from_args(socket_name); + + assert_matches!(start_backend(cmd_args).unwrap_err(), Error::ServeFailed(_)); + } +} diff --git a/vhost-device-template/src/vhu_foo.rs b/vhost-device-template/src/vhu_foo.rs new file mode 100644 index 0000000..4bf52f3 --- /dev/null +++ b/vhost-device-template/src/vhu_foo.rs @@ -0,0 +1,476 @@ +// vhost device foo +// +// Copyright 2023 Linaro Ltd. All Rights Reserved. +// Viresh Kumar +// +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause + +use log::{info, warn}; +use std::{ + convert, + io::{self, Result as IoResult}, +}; + +use thiserror::Error as ThisError; +use vhost::vhost_user::message::{VhostUserProtocolFeatures, VhostUserVirtioFeatures}; +use vhost_user_backend::{VhostUserBackendMut, VringRwLock, VringT}; +use virtio_bindings::bindings::virtio_config::{VIRTIO_F_NOTIFY_ON_EMPTY, VIRTIO_F_VERSION_1}; +use virtio_bindings::bindings::virtio_ring::{ + VIRTIO_RING_F_EVENT_IDX, VIRTIO_RING_F_INDIRECT_DESC, +}; +use virtio_queue::{DescriptorChain, QueueOwnedT}; +use vm_memory::{GuestAddressSpace, GuestMemoryAtomic, GuestMemoryLoadGuard, GuestMemoryMmap}; +use vmm_sys_util::epoll::EventSet; +use vmm_sys_util::eventfd::{EventFd, EFD_NONBLOCK}; + +use crate::FooInfo; + +const QUEUE_SIZE: usize = 1024; +const NUM_QUEUES: usize = 1; + +type Result = std::result::Result; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, ThisError)] +/// Errors related to vhost-device-foo daemon. +pub(crate) enum Error { + #[error("Failed to handle event, didn't match EPOLLIN")] + HandleEventNotEpollIn, + #[error("Failed to handle unknown event")] + HandleEventUnknown, + #[error("Descriptor not found")] + DescriptorNotFound, + #[error("Failed to send notification")] + NotificationFailed, + #[error("Failed to create new EventFd")] + EventFdFailed, +} + +impl convert::From for io::Error { + fn from(e: Error) -> Self { + io::Error::new(io::ErrorKind::Other, e) + } +} + +pub(crate) struct VhostUserFooBackend { + info: FooInfo, + event_idx: bool, + pub exit_event: EventFd, + mem: Option>, +} + +type FooDescriptorChain = DescriptorChain>>; + +impl VhostUserFooBackend { + pub fn new(info: FooInfo) -> Result { + Ok(VhostUserFooBackend { + info, + event_idx: false, + exit_event: EventFd::new(EFD_NONBLOCK).map_err(|_| Error::EventFdFailed)?, + mem: None, + }) + } + + /// Process the requests in the vring and dispatch replies + fn process_requests( + &mut self, + requests: Vec, + _vring: &VringRwLock, + ) -> Result<()> { + if requests.is_empty() { + info!("No pending requests"); + return Ok(()); + } + + // Iterate over each FOO request. + // + // The layout of the various structures, to be read from and written into the descriptor + // buffers, is defined in the Virtio specification for each protocol. + for desc_chain in requests.clone() { + let counter = self.info.counter(); + let descriptors: Vec<_> = desc_chain.clone().collect(); + + info!( + "Request number: {} contains {} descriptors", + counter, + descriptors.len(), + ); + + for (i, desc) in descriptors.iter().enumerate() { + let perm = if desc.is_write_only() { + "write only" + } else { + "read only" + }; + + // We now can iterate over the set of descriptors and process the messages. There + // will be a number of read only descriptors containing messages as defined by the + // specification. If any replies are needed, the driver should have placed one or + // more writable descriptors at the end for the device to use to reply. + info!("Length of the {} descriptor@{} is: {}", perm, i, desc.len()); + } + } + + Ok(()) + } + + /// Process the requests in the vring and dispatch replies + fn process_queue(&mut self, vring: &VringRwLock) -> Result<()> { + // Collect all pending requests + let requests: Vec<_> = vring + .get_mut() + .get_queue_mut() + .iter(self.mem.as_ref().unwrap().clone()) + .map_err(|_| Error::DescriptorNotFound)? + .collect(); + + if self.process_requests(requests, vring).is_ok() { + // Send notification once all the requests are processed + vring + .signal_used_queue() + .map_err(|_| Error::NotificationFailed)?; + } + + Ok(()) + } +} + +/// VhostUserBackendMut trait methods +impl VhostUserBackendMut for VhostUserFooBackend { + type Vring = VringRwLock; + type Bitmap = (); + + fn num_queues(&self) -> usize { + NUM_QUEUES + } + + fn max_queue_size(&self) -> usize { + QUEUE_SIZE + } + + fn features(&self) -> u64 { + // this matches the current libvhost defaults except VHOST_F_LOG_ALL + 1 << VIRTIO_F_VERSION_1 + | 1 << VIRTIO_F_NOTIFY_ON_EMPTY + | 1 << VIRTIO_RING_F_INDIRECT_DESC + | 1 << VIRTIO_RING_F_EVENT_IDX + + // Protocol features are optional and must not be defined unless required and must be + // accompanied by the supporting PROTOCOL_FEATURES bits in features. + | VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits() + } + + fn protocol_features(&self) -> VhostUserProtocolFeatures { + VhostUserProtocolFeatures::MQ + } + + fn set_event_idx(&mut self, enabled: bool) { + self.event_idx = enabled; + } + + fn update_memory(&mut self, mem: GuestMemoryAtomic) -> IoResult<()> { + self.mem = Some(mem.memory()); + Ok(()) + } + + fn handle_event( + &mut self, + device_event: u16, + evset: EventSet, + vrings: &[VringRwLock], + _thread_id: usize, + ) -> IoResult<()> { + if evset != EventSet::IN { + return Err(Error::HandleEventNotEpollIn.into()); + } + + match device_event { + 0 => { + let vring = &vrings[0]; + + if self.event_idx { + // vm-virtio's Queue implementation only checks avail_index + // once, so to properly support EVENT_IDX we need to keep + // calling process_queue() until it stops finding new + // requests on the queue. + loop { + vring.disable_notification().unwrap(); + self.process_queue(vring)?; + if !vring.enable_notification().unwrap() { + break; + } + } + } else { + // Without EVENT_IDX, a single call is enough. + self.process_queue(vring)?; + } + } + + _ => { + warn!("unhandled device_event: {}", device_event); + return Err(Error::HandleEventUnknown.into()); + } + } + Ok(()) + } + + fn exit_event(&self, _thread_index: usize) -> Option { + self.exit_event.try_clone().ok() + } +} + +#[cfg(test)] +mod tests { + use std::mem::size_of; + use vhost_user_backend::{VhostUserBackendMut, VringRwLock, VringT}; + use virtio_bindings::bindings::virtio_ring::{VRING_DESC_F_NEXT, VRING_DESC_F_WRITE}; + use virtio_queue::{mock::MockSplitQueue, Descriptor, Queue}; + use vm_memory::{Address, ByteValued, Bytes, GuestAddress, GuestMemoryAtomic, GuestMemoryMmap}; + + use super::*; + + #[derive(Copy, Clone, Default)] + #[repr(C)] + struct VirtioFooOutHdr { + a: u16, + b: u16, + c: u32, + } + // SAFETY: The layout of the structure is fixed and can be initialized by + // reading its content from byte array. + unsafe impl ByteValued for VirtioFooOutHdr {} + + #[derive(Copy, Clone, Default)] + #[repr(C)] + struct VirtioFooInHdr { + d: u8, + } + // SAFETY: The layout of the structure is fixed and can be initialized by + // reading its content from byte array. + unsafe impl ByteValued for VirtioFooInHdr {} + + fn init() -> ( + VhostUserFooBackend, + GuestMemoryAtomic, + VringRwLock, + ) { + let backend = VhostUserFooBackend::new(FooInfo::new()).unwrap(); + let mem = GuestMemoryAtomic::new( + GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(), + ); + let vring = VringRwLock::new(mem.clone(), 16).unwrap(); + + (backend, mem, vring) + } + + // Prepares a single chain of descriptors + fn prepare_descriptors( + mut next_addr: u64, + mem: &GuestMemoryLoadGuard>, + buf: &mut Vec, + ) -> Vec { + let mut descriptors = Vec::new(); + let mut index = 0; + + // Out header descriptor + let out_hdr = VirtioFooOutHdr { + a: 0x10, + b: 0x11, + c: 0x20, + }; + + let desc_out = Descriptor::new( + next_addr, + size_of::() as u32, + VRING_DESC_F_NEXT as u16, + index + 1, + ); + next_addr += desc_out.len() as u64; + index += 1; + + mem.write_obj::(out_hdr, desc_out.addr()) + .unwrap(); + descriptors.push(desc_out); + + // Buf descriptor: optional + if !buf.is_empty() { + let desc_buf = Descriptor::new( + next_addr, + buf.len() as u32, + (VRING_DESC_F_WRITE | VRING_DESC_F_NEXT) as u16, + index + 1, + ); + next_addr += desc_buf.len() as u64; + + mem.write(buf, desc_buf.addr()).unwrap(); + descriptors.push(desc_buf); + } + + // In response descriptor + let desc_in = Descriptor::new( + next_addr, + size_of::() as u32, + VRING_DESC_F_WRITE as u16, + 0, + ); + descriptors.push(desc_in); + descriptors + } + + // Prepares a single chain of descriptors + fn prepare_desc_chain(buf: &mut Vec) -> (VhostUserFooBackend, VringRwLock) { + let (mut backend, mem, vring) = init(); + let mem_handle = mem.memory(); + let vq = MockSplitQueue::new(&*mem_handle, 16); + let next_addr = vq.desc_table().total_size() + 0x100; + + let descriptors = prepare_descriptors(next_addr, &mem_handle, buf); + + vq.build_desc_chain(&descriptors).unwrap(); + + // Put the descriptor index 0 in the first available ring position. + mem_handle + .write_obj(0u16, vq.avail_addr().unchecked_add(4)) + .unwrap(); + + // Set `avail_idx` to 1. + mem_handle + .write_obj(1u16, vq.avail_addr().unchecked_add(2)) + .unwrap(); + + vring.set_queue_size(16); + vring + .set_queue_info(vq.desc_table_addr().0, vq.avail_addr().0, vq.used_addr().0) + .unwrap(); + vring.set_queue_ready(true); + + backend.update_memory(mem).unwrap(); + + (backend, vring) + } + + // Prepares a chain of descriptors + fn prepare_desc_chains( + mem: &GuestMemoryAtomic, + buf: &mut Vec, + ) -> FooDescriptorChain { + let mem_handle = mem.memory(); + let vq = MockSplitQueue::new(&*mem_handle, 16); + let next_addr = vq.desc_table().total_size() + 0x100; + + let descriptors = prepare_descriptors(next_addr, &mem_handle, buf); + + for (idx, desc) in descriptors.iter().enumerate() { + vq.desc_table().store(idx as u16, *desc).unwrap(); + } + + // Put the descriptor index 0 in the first available ring position. + mem_handle + .write_obj(0u16, vq.avail_addr().unchecked_add(4)) + .unwrap(); + + // Set `avail_idx` to 1. + mem_handle + .write_obj(1u16, vq.avail_addr().unchecked_add(2)) + .unwrap(); + + // Create descriptor chain from pre-filled memory + vq.create_queue::() + .unwrap() + .iter(mem_handle) + .unwrap() + .next() + .unwrap() + } + + #[test] + fn process_requests_no_desc() { + let (mut backend, _, vring) = init(); + + // Descriptor chain size zero, shouldn't fail + backend + .process_requests(Vec::::new(), &vring) + .unwrap(); + } + + #[test] + fn process_request_single() { + // Single valid descriptor + let mut buf: Vec = vec![0; 30]; + let (mut backend, vring) = prepare_desc_chain(&mut buf); + backend.process_queue(&vring).unwrap(); + } + + #[test] + fn process_requests_multi() { + // Multiple valid descriptors + let (mut backend, mem, vring) = init(); + + let mut bufs: Vec> = vec![vec![0; 30]; 6]; + let desc_chains = vec![ + prepare_desc_chains(&mem, &mut bufs[0]), + prepare_desc_chains(&mem, &mut bufs[1]), + prepare_desc_chains(&mem, &mut bufs[2]), + prepare_desc_chains(&mem, &mut bufs[3]), + prepare_desc_chains(&mem, &mut bufs[4]), + prepare_desc_chains(&mem, &mut bufs[5]), + ]; + + backend + .process_requests(desc_chains.clone(), &vring) + .unwrap(); + } + + #[test] + fn verify_backend() { + let info = FooInfo::new(); + let mut backend = VhostUserFooBackend::new(info).unwrap(); + + assert_eq!(backend.num_queues(), NUM_QUEUES); + assert_eq!(backend.max_queue_size(), QUEUE_SIZE); + assert_eq!(backend.features(), 0x171000000); + assert_eq!(backend.protocol_features(), VhostUserProtocolFeatures::MQ); + + assert_eq!(backend.queues_per_thread(), vec![0xffff_ffff]); + assert_eq!(backend.get_config(0, 0), vec![]); + + backend.set_event_idx(true); + assert!(backend.event_idx); + + assert!(backend.exit_event(0).is_some()); + + let mem = GuestMemoryAtomic::new( + GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(), + ); + backend.update_memory(mem.clone()).unwrap(); + + let vring = VringRwLock::new(mem, 0x1000).unwrap(); + vring.set_queue_info(0x100, 0x200, 0x300).unwrap(); + vring.set_queue_ready(true); + + assert_eq!( + backend + .handle_event(0, EventSet::OUT, &[vring.clone()], 0) + .unwrap_err() + .kind(), + io::ErrorKind::Other + ); + + assert_eq!( + backend + .handle_event(1, EventSet::IN, &[vring.clone()], 0) + .unwrap_err() + .kind(), + io::ErrorKind::Other + ); + + // Hit the loop part + backend.set_event_idx(true); + backend + .handle_event(0, EventSet::IN, &[vring.clone()], 0) + .unwrap(); + + // Hit the non-loop part + backend.set_event_idx(false); + backend.handle_event(0, EventSet::IN, &[vring], 0).unwrap(); + } +}