mirror of
https://github.com/rust-vmm/vhost-device.git
synced 2025-12-30 09:46:55 +00:00
Merge pull request #119 from mathieupoirier/vhost-device-rng-v8
RNG: Initial vhost-device RNG implementation (v8)
This commit is contained in:
commit
f36dfae52c
133
Cargo.lock
generated
133
Cargo.lock
generated
@ -89,6 +89,36 @@ dependencies = [
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "epoll"
|
||||
version = "4.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20df693c700404f7e19d4d6fae6b15215d2913c27955d2b9d6f2c0f537511cd0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2"
|
||||
dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.11.2"
|
||||
@ -126,6 +156,15 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
@ -162,6 +201,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
@ -204,6 +249,45 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.5.4"
|
||||
@ -221,6 +305,15 @@ version = "0.6.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
@ -238,6 +331,20 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"remove_dir_all",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.2"
|
||||
@ -314,6 +421,26 @@ dependencies = [
|
||||
"vmm-sys-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vhost-device-rng"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"env_logger",
|
||||
"epoll",
|
||||
"libc",
|
||||
"log",
|
||||
"rand",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"vhost",
|
||||
"vhost-user-backend",
|
||||
"virtio-bindings",
|
||||
"virtio-queue",
|
||||
"vm-memory",
|
||||
"vmm-sys-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vhost-user-backend"
|
||||
version = "0.1.0"
|
||||
@ -367,6 +494,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.2+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
||||
@ -2,4 +2,5 @@
|
||||
|
||||
members = [
|
||||
"i2c",
|
||||
"rng",
|
||||
]
|
||||
|
||||
@ -9,6 +9,7 @@ crates.
|
||||
Here is the list of device backends that we support:
|
||||
|
||||
- [I2C](https://github.com/rust-vmm/vhost-device/blob/main/i2c/README.md)
|
||||
- [RNG](https://github.com/rust-vmm/vhost-device/blob/main/rng/README.md)
|
||||
|
||||
## Testing and Code Coverage
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"coverage_score": 85.5,
|
||||
"coverage_score": 85.2,
|
||||
"exclude_path": "",
|
||||
"crate_features": ""
|
||||
}
|
||||
|
||||
30
rng/Cargo.toml
Normal file
30
rng/Cargo.toml
Normal file
@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "vhost-device-rng"
|
||||
version = "0.1.0"
|
||||
authors = ["Mathieu Poirier <mathieu.poirier@linaro.org>"]
|
||||
description = "vhost RNG backend device"
|
||||
repository = "https://github.com/rust-vmm/vhost-device"
|
||||
readme = "README.md"
|
||||
keywords = ["rng", "vhost", "virt", "backend"]
|
||||
license = "Apache-2.0 OR BSD-3-Clause"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = ">=3.0", features = ["derive"] }
|
||||
env_logger = ">=0.9"
|
||||
epoll = "4.3"
|
||||
libc = ">=0.2.95"
|
||||
log = ">=0.4.6"
|
||||
rand = ">=0.8.5"
|
||||
tempfile = "3.2.0"
|
||||
thiserror = "1.0"
|
||||
vhost = { version = "0.3", features = ["vhost-user-slave"] }
|
||||
vhost-user-backend = "0.1"
|
||||
virtio-bindings = ">=0.1"
|
||||
virtio-queue = "0.1"
|
||||
vm-memory = ">=0.7"
|
||||
vmm-sys-util = ">=0.9.0"
|
||||
|
||||
[dev-dependencies]
|
||||
virtio-queue = { version = "0.1", features = ["test-utils"] }
|
||||
vm-memory = { version = "0.7.0", features = ["backend-mmap", "backend-atomic"] }
|
||||
73
rng/README.md
Normal file
73
rng/README.md
Normal file
@ -0,0 +1,73 @@
|
||||
# vhost-device-rng - RNG emulation backend daemon
|
||||
|
||||
## Description
|
||||
This program is a vhost-user backend that emulates a VirtIO random number
|
||||
generator (RNG). It uses the host's random number generator pool,
|
||||
/dev/urandom by default but configurable at will, to satisfy requests from
|
||||
guests.
|
||||
|
||||
The daemon is designed to respect limitation on possible random generator
|
||||
hardware using the --max-bytes and --period options. As such 5 kilobyte per
|
||||
second would translate to "--max-bytes 5000 --period 1000". If an application
|
||||
requests more bytes than the allowed limit the thread will block until the
|
||||
start of a new period. The daemon will automatically split the available
|
||||
bandwidth equally between the guest when several threads are requested.
|
||||
|
||||
Thought developed and tested with QEMU, the implemenation is based on the
|
||||
vhost-user protocol and as such should be interoperable with other virtual
|
||||
machine managers. Please see below for working examples.
|
||||
|
||||
## Synopsis
|
||||
|
||||
**vhost-device-rng** [*OPTIONS*]
|
||||
|
||||
## Options
|
||||
|
||||
.. program:: vhost-device-rng
|
||||
|
||||
.. option:: -h, --help
|
||||
|
||||
Print help.
|
||||
|
||||
.. option:: -s, --socket-path=PATH
|
||||
|
||||
Location of vhost-user Unix domain sockets, this path will be suffixed with
|
||||
0,1,2..socket_count-1.
|
||||
|
||||
.. option:: -f, --filename
|
||||
Random number generator source file, defaults to /dev/urandom.
|
||||
|
||||
.. option:: -c, --socket-count=INT
|
||||
|
||||
Number of guests (sockets) to attach to, default set to 1.
|
||||
|
||||
.. option:: -p, --period
|
||||
|
||||
Rate, in milliseconds, at which the RNG hardware can generate random data.
|
||||
Used in conjunction with the --max-bytes option.
|
||||
|
||||
.. option:: -m, --max-bytes
|
||||
|
||||
In conjuction with the --period parameter, provides the maximum number of byte
|
||||
per milliseconds a RNG device can generate.
|
||||
|
||||
## Examples
|
||||
|
||||
The daemon should be started first:
|
||||
|
||||
::
|
||||
|
||||
host# vhost-device-rng --socket-path=/some/path/rng.sock -c 1 -m 512 -p 1000
|
||||
|
||||
Note that from the above command the socket path "/some/path/rng.sock0" will be
|
||||
created. This in turn needs to be communicated as a chardev socket to QEMU in order
|
||||
for the backend RNG device to communicate with the vhost RNG daemon:
|
||||
|
||||
::
|
||||
|
||||
host# qemu-system -M virt \
|
||||
-object memory-backend-file,id=mem,size=4G,mem-path=/dev/shm,share=on \
|
||||
-chardev socket,path=/some/path/rng.sock0,id=rng0 \
|
||||
-device vhost-user-rng-pci,chardev=rng0 \
|
||||
-numa node,memdev=mem \
|
||||
...
|
||||
244
rng/src/main.rs
Normal file
244
rng/src/main.rs
Normal file
@ -0,0 +1,244 @@
|
||||
//
|
||||
// Copyright 2022 Linaro Ltd. All Rights Reserved.
|
||||
// Mathieu Poirier <mathieu.poirier@linaro.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
mod vhu_rng;
|
||||
|
||||
use log::{info, warn};
|
||||
use std::convert::TryFrom;
|
||||
use std::fs::File;
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
use std::thread;
|
||||
|
||||
use clap::Parser;
|
||||
use thiserror::Error as ThisError;
|
||||
use vhost::{vhost_user, vhost_user::Listener};
|
||||
use vhost_user_backend::VhostUserDaemon;
|
||||
use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap};
|
||||
|
||||
use vhu_rng::VuRngBackend;
|
||||
|
||||
// Chosen to replicate the max period found in QEMU's vhost-user-rng
|
||||
// and virtio-rng implementations.
|
||||
const VHU_RNG_MAX_PERIOD_MS: u128 = 65536;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, PartialEq, ThisError)]
|
||||
/// Errors related to vhost-device-rng daemon.
|
||||
pub enum Error {
|
||||
#[error("RNG source file doesn't exists or can't be accessed")]
|
||||
AccessRngSourceFile,
|
||||
#[error("Period is too big: {0}")]
|
||||
InvalidPeriodInput(u128),
|
||||
#[error("Wrong socket count: {0}")]
|
||||
InvalidSocketCount(u32),
|
||||
#[error("Threads can't be joined")]
|
||||
FailedJoiningThreads,
|
||||
}
|
||||
|
||||
#[derive(Clone, Parser, Debug, PartialEq)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
struct RngArgs {
|
||||
// Time needed (in ms) to transfer max-bytes amount of byte.
|
||||
#[clap(short, long, default_value_t = VHU_RNG_MAX_PERIOD_MS)]
|
||||
period: u128,
|
||||
|
||||
// Maximum amount of byte that can be transferred in a period.
|
||||
#[clap(short, long, default_value_t = usize::MAX)]
|
||||
max_bytes: usize,
|
||||
|
||||
// Number of guests (sockets) to connect to.
|
||||
#[clap(short = 'c', long, default_value_t = 1)]
|
||||
socket_count: u32,
|
||||
|
||||
// Location of vhost-user Unix domain socket. This is suffixed by 0,1,2..socket_count-1.
|
||||
#[clap(short, long)]
|
||||
socket_path: String,
|
||||
|
||||
// Where to get the RNG data from. Defaults to /dev/urandom.
|
||||
#[clap(short = 'f', long, default_value = "/dev/urandom")]
|
||||
rng_source: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct VuRngConfig {
|
||||
pub period_ms: u128,
|
||||
pub max_bytes: usize,
|
||||
pub count: u32,
|
||||
pub socket_path: String,
|
||||
pub rng_source: String,
|
||||
}
|
||||
|
||||
impl TryFrom<RngArgs> for VuRngConfig {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(args: RngArgs) -> Result<Self> {
|
||||
if args.period == 0 || args.period > VHU_RNG_MAX_PERIOD_MS {
|
||||
return Err(Error::InvalidPeriodInput(args.period));
|
||||
}
|
||||
|
||||
if args.socket_count == 0 {
|
||||
return Err(Error::InvalidSocketCount(args.socket_count));
|
||||
}
|
||||
|
||||
// Divide available bandwidth by the number of threads in order
|
||||
// to avoid overwhelming the HW.
|
||||
let max_bytes = args.max_bytes / args.socket_count as usize;
|
||||
let socket_path = args.socket_path.trim().to_string();
|
||||
let rng_source = args.rng_source.trim().to_string();
|
||||
|
||||
Ok(VuRngConfig {
|
||||
period_ms: args.period,
|
||||
max_bytes,
|
||||
count: args.socket_count,
|
||||
socket_path,
|
||||
rng_source,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_backend(config: VuRngConfig) -> Result<()> {
|
||||
let mut handles = Vec::new();
|
||||
let file = File::open(&config.rng_source).map_err(|_| Error::AccessRngSourceFile)?;
|
||||
let random_file = Arc::new(Mutex::new(file));
|
||||
|
||||
for i in 0..config.count {
|
||||
let socket = format!("{}{}", config.socket_path.to_owned(), i);
|
||||
let period_ms = config.period_ms;
|
||||
let max_bytes = config.max_bytes;
|
||||
let random = Arc::clone(&random_file);
|
||||
|
||||
let handle = thread::spawn(move || loop {
|
||||
// If creating the VuRngBackend isn't successull there isn't much else to do than
|
||||
// killing the thread, which .unwrap() does. When that happens an error code is
|
||||
// generated and displayed by the runtime mechanic. Killing a thread doesn't affect
|
||||
// the other threads spun-off by the daemon.
|
||||
let vu_rng_backend = Arc::new(RwLock::new(
|
||||
VuRngBackend::new(random.clone(), period_ms, max_bytes).unwrap(),
|
||||
));
|
||||
|
||||
let mut daemon = VhostUserDaemon::new(
|
||||
String::from("vhost-user-RNG-daemon"),
|
||||
Arc::clone(&vu_rng_backend),
|
||||
GuestMemoryAtomic::new(GuestMemoryMmap::new()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let listener = Listener::new(socket.clone(), true).unwrap();
|
||||
daemon.start(listener).unwrap();
|
||||
|
||||
match daemon.wait() {
|
||||
Ok(()) => {
|
||||
info!("Stopping cleanly.");
|
||||
}
|
||||
Err(vhost_user_backend::Error::HandleRequest(
|
||||
vhost_user::Error::PartialMessage,
|
||||
)) => {
|
||||
info!("vhost-user connection closed with partial message. If the VM is shutting down, this is expected behavior; otherwise, it might be a bug.");
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Error running daemon: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// No matter the result, we need to shut down the worker thread.
|
||||
vu_rng_backend
|
||||
.read()
|
||||
.unwrap()
|
||||
.exit_event
|
||||
.write(1)
|
||||
.expect("Shutting down worker thread");
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.join().map_err(|_| Error::FailedJoiningThreads)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
start_backend(VuRngConfig::try_from(RngArgs::parse()).unwrap())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn verify_cmd_line_arguments() {
|
||||
// All parameters have default values, except for the socket path. White spaces are
|
||||
// introduced on purpose to make sure Strings are trimmed properly.
|
||||
let default_args: RngArgs = Parser::parse_from(&["", "-s /some/socket_path "]);
|
||||
|
||||
// A valid configuration that should be equal to the above default configuration.
|
||||
let args = RngArgs {
|
||||
period: VHU_RNG_MAX_PERIOD_MS,
|
||||
max_bytes: usize::MAX,
|
||||
socket_count: 1,
|
||||
socket_path: "/some/socket_path".to_string(),
|
||||
rng_source: "/dev/urandom".to_string(),
|
||||
};
|
||||
|
||||
// All configuration elements should be what we expect them to be. Using
|
||||
// VuRngConfig::try_from() ensures that strings have been properly trimmed.
|
||||
assert_eq!(
|
||||
VuRngConfig::try_from(default_args),
|
||||
VuRngConfig::try_from(args.clone())
|
||||
);
|
||||
|
||||
// Setting a invalid period should trigger an InvalidPeriodInput error.
|
||||
let mut invalid_period_args = args.clone();
|
||||
invalid_period_args.period = VHU_RNG_MAX_PERIOD_MS + 1;
|
||||
assert_eq!(
|
||||
VuRngConfig::try_from(invalid_period_args),
|
||||
Err(Error::InvalidPeriodInput(VHU_RNG_MAX_PERIOD_MS + 1))
|
||||
);
|
||||
|
||||
// Setting the socket count to 0 should trigger an InvalidSocketCount error.
|
||||
let mut invalid_socket_count_args = args;
|
||||
invalid_socket_count_args.socket_count = 0;
|
||||
assert_eq!(
|
||||
VuRngConfig::try_from(invalid_socket_count_args),
|
||||
Err(Error::InvalidSocketCount(0))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_start_backend() {
|
||||
let dir = tempdir().unwrap();
|
||||
let random_path = dir.path().join("urandom");
|
||||
let _random_file = File::create(random_path.clone());
|
||||
|
||||
let mut config = VuRngConfig {
|
||||
period_ms: 1000,
|
||||
max_bytes: 512,
|
||||
count: 1,
|
||||
socket_path: "/invalid/path".to_string(),
|
||||
rng_source: "/invalid/path".to_string(),
|
||||
};
|
||||
|
||||
// An invalid RNG source file should trigger an AccessRngSourceFile error.
|
||||
assert_eq!(
|
||||
start_backend(config.clone()).unwrap_err(),
|
||||
Error::AccessRngSourceFile
|
||||
);
|
||||
|
||||
// Set the RNG source to something valid, forcing the code to check the validity
|
||||
// of the socket file. Since the latter is invalid the vhost_user::Listener will
|
||||
// throw an error, forcing the thread to exit and the call to handle.join() to fail.
|
||||
config.rng_source = random_path.to_str().unwrap().to_string();
|
||||
assert_eq!(
|
||||
start_backend(config).unwrap_err(),
|
||||
Error::FailedJoiningThreads
|
||||
);
|
||||
}
|
||||
}
|
||||
552
rng/src/vhu_rng.rs
Normal file
552
rng/src/vhu_rng.rs
Normal file
@ -0,0 +1,552 @@
|
||||
// VIRTIO RNG Emulation via vhost-user
|
||||
//
|
||||
// Copyright 2022 Linaro Ltd. All Rights Reserved.
|
||||
// Mathieu Poirier <mathieu.poirier@linaro.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use log::warn;
|
||||
use std::io::Read;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread::sleep;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::{convert, io, result};
|
||||
|
||||
use thiserror::Error as ThisError;
|
||||
use vhost::vhost_user::message::{VhostUserProtocolFeatures, VhostUserVirtioFeatures};
|
||||
use vhost_user_backend::{VhostUserBackendMut, VringRwLock, VringT};
|
||||
use virtio_bindings::bindings::virtio_net::{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;
|
||||
use vm_memory::{Bytes, GuestMemoryAtomic, GuestMemoryLoadGuard, GuestMemoryMmap};
|
||||
use vmm_sys_util::epoll::EventSet;
|
||||
use vmm_sys_util::eventfd::{EventFd, EFD_NONBLOCK};
|
||||
|
||||
const QUEUE_SIZE: usize = 1024;
|
||||
const NUM_QUEUES: usize = 1;
|
||||
|
||||
type Result<T> = std::result::Result<T, VuRngError>;
|
||||
type RngDescriptorChain = DescriptorChain<GuestMemoryLoadGuard<GuestMemoryMmap<()>>>;
|
||||
|
||||
#[derive(Debug, PartialEq, ThisError)]
|
||||
/// Errors related to vhost-device-rng daemon.
|
||||
pub enum VuRngError {
|
||||
#[error("Descriptor not found")]
|
||||
DescriptorNotFound,
|
||||
#[error("Notification send failed")]
|
||||
SendNotificationFailed,
|
||||
#[error("Can't create eventFd")]
|
||||
EventFdError,
|
||||
#[error("Failed to handle event")]
|
||||
HandleEventNotEpollIn,
|
||||
#[error("Unknown device event")]
|
||||
HandleEventUnknownEvent,
|
||||
#[error("Too many descriptors: {0}")]
|
||||
UnexpectedDescriptorCount(usize),
|
||||
#[error("Unexpected Read Descriptor")]
|
||||
UnexpectedReadDescriptor,
|
||||
#[error("Failed to access RNG source")]
|
||||
UnexpectedRngSourceAccessError,
|
||||
#[error("Failed to read from the RNG source")]
|
||||
UnexpectedRngSourceError,
|
||||
#[error("Previous Time value is later than current time")]
|
||||
UnexpectedTimerValue,
|
||||
#[error("Unexpected VirtQueue error")]
|
||||
UnexpectedVirtQueueError,
|
||||
}
|
||||
|
||||
impl convert::From<VuRngError> for io::Error {
|
||||
fn from(e: VuRngError) -> Self {
|
||||
io::Error::new(io::ErrorKind::Other, e)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct VuRngTimerConfig {
|
||||
period_ms: u128,
|
||||
period_start: Instant,
|
||||
max_bytes: usize,
|
||||
quota_remaining: usize,
|
||||
}
|
||||
|
||||
impl VuRngTimerConfig {
|
||||
pub fn new(period_ms: u128, max_bytes: usize) -> Self {
|
||||
VuRngTimerConfig {
|
||||
period_ms,
|
||||
period_start: Instant::now(),
|
||||
max_bytes,
|
||||
quota_remaining: max_bytes,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VuRngBackend<T: Read> {
|
||||
event_idx: bool,
|
||||
timer: VuRngTimerConfig,
|
||||
rng_source: Arc<Mutex<T>>,
|
||||
pub exit_event: EventFd,
|
||||
}
|
||||
|
||||
impl<T: Read> VuRngBackend<T> {
|
||||
/// Create a new virtio rng device that gets random data from /dev/urandom.
|
||||
pub fn new(
|
||||
rng_source: Arc<Mutex<T>>,
|
||||
period_ms: u128,
|
||||
max_bytes: usize,
|
||||
) -> std::result::Result<Self, std::io::Error> {
|
||||
Ok(VuRngBackend {
|
||||
event_idx: false,
|
||||
rng_source,
|
||||
timer: VuRngTimerConfig::new(period_ms, max_bytes),
|
||||
exit_event: EventFd::new(EFD_NONBLOCK).map_err(|_| VuRngError::EventFdError)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn process_requests(
|
||||
&mut self,
|
||||
requests: Vec<RngDescriptorChain>,
|
||||
vring: &VringRwLock,
|
||||
) -> Result<bool> {
|
||||
if requests.is_empty() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
for desc_chain in requests {
|
||||
let descriptors: Vec<_> = desc_chain.clone().collect();
|
||||
|
||||
if descriptors.len() != 1 {
|
||||
return Err(VuRngError::UnexpectedDescriptorCount(descriptors.len()));
|
||||
}
|
||||
|
||||
let descriptor = descriptors[0];
|
||||
let mut to_read = descriptor.len() as usize;
|
||||
let mut timer = &mut self.timer;
|
||||
|
||||
if !descriptor.is_write_only() {
|
||||
return Err(VuRngError::UnexpectedReadDescriptor);
|
||||
}
|
||||
|
||||
// Get the current time
|
||||
let now = Instant::now();
|
||||
|
||||
// Check how much time has passed since we started the last period.
|
||||
match now.checked_duration_since(timer.period_start) {
|
||||
Some(duration) => {
|
||||
let elapsed = duration.as_millis();
|
||||
|
||||
if elapsed >= timer.period_ms {
|
||||
// More time has passed than a full period, reset time
|
||||
// and quota.
|
||||
timer.period_start = now;
|
||||
timer.quota_remaining = timer.max_bytes;
|
||||
} else {
|
||||
// If we are out of bytes for the current period. Block until
|
||||
// the start of the next period.
|
||||
if timer.quota_remaining == 0 {
|
||||
let to_sleep = timer.period_ms - elapsed;
|
||||
|
||||
sleep(Duration::from_millis(to_sleep as u64));
|
||||
timer.period_start = Instant::now();
|
||||
timer.quota_remaining = timer.max_bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => return Err(VuRngError::UnexpectedTimerValue),
|
||||
};
|
||||
|
||||
if timer.quota_remaining < to_read {
|
||||
to_read = timer.quota_remaining;
|
||||
}
|
||||
|
||||
let mut rng_source = self
|
||||
.rng_source
|
||||
.lock()
|
||||
.map_err(|_| VuRngError::UnexpectedRngSourceAccessError)?;
|
||||
|
||||
let len = desc_chain
|
||||
.memory()
|
||||
.read_from(descriptor.addr(), &mut *rng_source, to_read as usize)
|
||||
.map_err(|_| VuRngError::UnexpectedRngSourceError)?;
|
||||
|
||||
timer.quota_remaining -= len;
|
||||
|
||||
if vring.add_used(desc_chain.head_index(), len as u32).is_err() {
|
||||
warn!("Couldn't return used descriptors to the ring");
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Process the requests in the vring and dispatch replies
|
||||
fn process_queue(&mut self, vring: &VringRwLock) -> Result<bool> {
|
||||
let requests: Vec<_> = vring
|
||||
.get_mut()
|
||||
.get_queue_mut()
|
||||
.iter()
|
||||
.map_err(|_| VuRngError::DescriptorNotFound)?
|
||||
.collect();
|
||||
|
||||
if self.process_requests(requests, vring)? {
|
||||
// Send notification once all the requests are processed
|
||||
vring
|
||||
.signal_used_queue()
|
||||
.map_err(|_| VuRngError::SendNotificationFailed)?;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
/// VhostUserBackend trait methods
|
||||
impl<T: 'static + Read + Sync + Send> VhostUserBackendMut<VringRwLock, ()> for VuRngBackend<T> {
|
||||
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
|
||||
| VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits()
|
||||
}
|
||||
|
||||
fn protocol_features(&self) -> VhostUserProtocolFeatures {
|
||||
VhostUserProtocolFeatures::MQ
|
||||
}
|
||||
|
||||
fn set_event_idx(&mut self, enabled: bool) {
|
||||
dbg!(self.event_idx = enabled);
|
||||
}
|
||||
|
||||
fn update_memory(
|
||||
&mut self,
|
||||
_mem: GuestMemoryAtomic<GuestMemoryMmap>,
|
||||
) -> result::Result<(), io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
device_event: u16,
|
||||
evset: EventSet,
|
||||
vrings: &[VringRwLock],
|
||||
_thread_id: usize,
|
||||
) -> result::Result<bool, io::Error> {
|
||||
if evset != EventSet::IN {
|
||||
return Err(VuRngError::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(VuRngError::HandleEventUnknownEvent.into());
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn exit_event(&self, _thread_index: usize) -> Option<EventFd> {
|
||||
self.exit_event.try_clone().ok()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::{ErrorKind, Read};
|
||||
|
||||
use virtio_queue::defs::{VIRTQ_DESC_F_NEXT, VIRTQ_DESC_F_WRITE};
|
||||
use virtio_queue::{mock::MockSplitQueue, Descriptor};
|
||||
use vm_memory::{Address, GuestAddress, GuestMemoryAtomic, GuestMemoryMmap};
|
||||
|
||||
// Add VuRngBackend accessor to artificially manipulate internal fields
|
||||
impl<T: Read> VuRngBackend<T> {
|
||||
// For testing purposes modify time synthetically
|
||||
pub(crate) fn time_add(&mut self, duration: Duration) {
|
||||
if let Some(t) = self.timer.period_start.checked_add(duration) {
|
||||
self.timer.period_start = t;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn time_sub(&mut self, duration: Duration) {
|
||||
if let Some(t) = self.timer.period_start.checked_sub(duration) {
|
||||
self.timer.period_start = t;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn time_now(&mut self) {
|
||||
self.timer.period_start = Instant::now();
|
||||
}
|
||||
|
||||
pub(crate) fn set_quota(&mut self, quota: usize) {
|
||||
self.timer.quota_remaining = quota;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a mock RNG source for testing purposes
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct MockRng {
|
||||
permission_denied: bool,
|
||||
}
|
||||
|
||||
impl MockRng {
|
||||
fn new(permission_denied: bool) -> Self {
|
||||
MockRng { permission_denied }
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for MockRng {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
match self.permission_denied {
|
||||
true => Err(std::io::Error::from(ErrorKind::PermissionDenied)),
|
||||
false => {
|
||||
buf[0] = rand::random::<u8>();
|
||||
Ok(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_desc_chain(count: u16, flags: u16) -> RngDescriptorChain {
|
||||
let mem = GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap();
|
||||
let vq = MockSplitQueue::new(&mem, 16);
|
||||
|
||||
//Create a descriptor chain with @count descriptors.
|
||||
for i in 0..count {
|
||||
let desc_flags = if i < count - 1 {
|
||||
flags | VIRTQ_DESC_F_NEXT
|
||||
} else {
|
||||
flags & !VIRTQ_DESC_F_NEXT
|
||||
};
|
||||
|
||||
let desc = Descriptor::new((0x100 * (i + 1)) as u64, 0x200, desc_flags, i + 1);
|
||||
vq.desc_table().store(i, desc);
|
||||
}
|
||||
|
||||
// Put the descriptor index 0 in the first available ring position.
|
||||
mem.write_obj(0u16, vq.avail_addr().unchecked_add(4))
|
||||
.unwrap();
|
||||
|
||||
// Set `avail_idx` to 1.
|
||||
mem.write_obj(1u16, vq.avail_addr().unchecked_add(2))
|
||||
.unwrap();
|
||||
|
||||
// Create descriptor chain from pre-filled memory
|
||||
vq.create_queue(GuestMemoryAtomic::<GuestMemoryMmap>::new(mem.clone()))
|
||||
.iter()
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_chain_descriptors() {
|
||||
let random_source = Arc::new(Mutex::new(MockRng::new(false)));
|
||||
let mut backend = VuRngBackend::new(random_source, 1000, 512).unwrap();
|
||||
// Any number of descriptor higher than 1 will generate an error
|
||||
let count = 4;
|
||||
|
||||
// Artificial memory
|
||||
let mem = GuestMemoryAtomic::new(
|
||||
GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(),
|
||||
);
|
||||
|
||||
// Artificial Vring
|
||||
let vring = VringRwLock::new(mem, 0x1000);
|
||||
|
||||
// The guest driver is supposed to send us only unchained descriptors
|
||||
let desc_chain = build_desc_chain(count, VIRTQ_DESC_F_WRITE);
|
||||
assert_eq!(
|
||||
backend
|
||||
.process_requests(vec![desc_chain], &vring)
|
||||
.unwrap_err(),
|
||||
VuRngError::UnexpectedDescriptorCount(count as usize)
|
||||
);
|
||||
|
||||
// The guest driver is supposed to send us only write descriptors
|
||||
let desc_chain = build_desc_chain(1, 0);
|
||||
assert_eq!(
|
||||
backend
|
||||
.process_requests(vec![desc_chain], &vring)
|
||||
.unwrap_err(),
|
||||
VuRngError::UnexpectedReadDescriptor
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_timer() {
|
||||
let random_source = Arc::new(Mutex::new(MockRng::new(false)));
|
||||
let mut backend = VuRngBackend::new(random_source, 1000, 512).unwrap();
|
||||
|
||||
// Artificial memory
|
||||
let mem = GuestMemoryAtomic::new(
|
||||
GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(),
|
||||
);
|
||||
|
||||
// Artificial Vring
|
||||
let vring = VringRwLock::new(mem, 0x1000);
|
||||
|
||||
// Artificially set the period start time 5 seconds in the future
|
||||
backend.time_add(Duration::from_secs(5));
|
||||
|
||||
// Checking for a start time in the future throws a VuRngError::UnexpectedTimerValue
|
||||
assert_eq!(
|
||||
backend
|
||||
.process_requests(vec![build_desc_chain(1, VIRTQ_DESC_F_WRITE)], &vring)
|
||||
.unwrap_err(),
|
||||
VuRngError::UnexpectedTimerValue
|
||||
);
|
||||
|
||||
// Artificially set the period start time to 10 second. This will simulate a
|
||||
// condition where the the period has been exeeded and for the quota to be reset
|
||||
// to its maximum value.
|
||||
backend.time_sub(Duration::from_secs(10));
|
||||
assert!(backend
|
||||
.process_requests(vec![build_desc_chain(1, VIRTQ_DESC_F_WRITE)], &vring)
|
||||
.unwrap());
|
||||
|
||||
// Reset time to right now and set remaining quota to 0. This will simulate a
|
||||
// condition where the quota for a period has been exceeded and force the execution
|
||||
// thread to wait for the start of the next period before serving requets.
|
||||
backend.time_now();
|
||||
backend.set_quota(0);
|
||||
assert!(backend
|
||||
.process_requests(vec![build_desc_chain(1, VIRTQ_DESC_F_WRITE)], &vring)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_file_access() {
|
||||
// Crate a mock RNG source that can't be accessed.
|
||||
let random_source = Arc::new(Mutex::new(MockRng::new(true)));
|
||||
let mut backend = VuRngBackend::new(random_source, 1000, 512).unwrap();
|
||||
|
||||
// Artificial memory
|
||||
let mem = GuestMemoryAtomic::new(
|
||||
GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(),
|
||||
);
|
||||
|
||||
// Artificial Vring
|
||||
let vring = VringRwLock::new(mem, 0x1000);
|
||||
|
||||
// Any type of error while reading an RNG source will throw a VuRngError::UnexpectedRngSourceError.
|
||||
assert_eq!(
|
||||
backend
|
||||
.process_requests(vec![build_desc_chain(1, VIRTQ_DESC_F_WRITE)], &vring)
|
||||
.unwrap_err(),
|
||||
VuRngError::UnexpectedRngSourceError
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_handle_event() {
|
||||
let random_source = Arc::new(Mutex::new(MockRng::new(false)));
|
||||
let mut backend = VuRngBackend::new(random_source, 1000, 512).unwrap();
|
||||
|
||||
// Artificial memory
|
||||
let mem = GuestMemoryAtomic::new(
|
||||
GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(),
|
||||
);
|
||||
|
||||
// Artificial Vring
|
||||
let vring = VringRwLock::new(mem, 0x1000);
|
||||
|
||||
// Currently handles EventSet::IN only, otherwise an error is generated.
|
||||
assert_eq!(
|
||||
backend
|
||||
.handle_event(0, EventSet::OUT, &[vring.clone()], 0)
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
io::ErrorKind::Other
|
||||
);
|
||||
|
||||
// Currently handles a single device event, anything higher than 0 will generate
|
||||
// an error.
|
||||
assert_eq!(
|
||||
backend
|
||||
.handle_event(1, EventSet::IN, &[vring.clone()], 0)
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
io::ErrorKind::Other
|
||||
);
|
||||
|
||||
// backend.event_idx is set to false by default, which will call backend.process_queue()
|
||||
// a single time. Since there is no descriptor in the vring backend.process_requests()
|
||||
// will return immediately.
|
||||
backend
|
||||
.handle_event(0, EventSet::IN, &[vring.clone()], 0)
|
||||
.unwrap();
|
||||
|
||||
// Set backend.event_idx to true in order to call backend.process_queue() multiple time
|
||||
backend.set_event_idx(true);
|
||||
backend.handle_event(0, EventSet::IN, &[vring], 0).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_backend() {
|
||||
let random_source = Arc::new(Mutex::new(MockRng::new(false)));
|
||||
let mut backend = VuRngBackend::new(random_source, 1000, 512).unwrap();
|
||||
|
||||
// Artificial memory
|
||||
let mem = GuestMemoryAtomic::new(
|
||||
GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(),
|
||||
);
|
||||
|
||||
// Artificial Vring
|
||||
let vring = VringRwLock::new(mem.clone(), 0x1000);
|
||||
|
||||
// Empty descriptor chain should be ignored
|
||||
assert!(backend
|
||||
.process_requests(Vec::<RngDescriptorChain>::new(), &vring)
|
||||
.unwrap());
|
||||
|
||||
// The capacity of descriptors is 512 byte as set in build_desc_chain(). Set the
|
||||
// quota value to half of that to simulate a condition where there is less antropy
|
||||
// available than the capacity of the descriptor buffer.
|
||||
backend.set_quota(0x100);
|
||||
assert!(backend
|
||||
.process_requests(vec![build_desc_chain(1, VIRTQ_DESC_F_WRITE)], &vring)
|
||||
.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![]);
|
||||
assert!(backend.update_memory(mem).is_ok());
|
||||
|
||||
backend.set_event_idx(true);
|
||||
assert!(backend.event_idx);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user