pub mod blk;
pub mod mmio;
pub mod queue;
pub mod rng;
use alloc::boxed::Box;
use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use core::fmt;
use crate::core::device::{Device, DeviceClass, PropertySpec};
use crate::core::error::Result;
use crate::core::props::{Props, ValueKind};
use crate::core::space::RamStore;
use crate::core::state::{ChunkReader, ChunkWriter};
use crate::dev::ata::Medium;
use queue::{Descriptor, Queue};
pub use blk::VirtioBlk;
pub use mmio::VirtioMmio;
pub use rng::VirtioRng;
pub const VENDOR_ID: u32 = 0x6d65_7372;
pub const DEVICE_ID_BLOCK: u32 = 2;
pub const DEVICE_ID_ENTROPY: u32 = 4;
pub const BLK_CLASS_NAME: &str = "virtio.blk";
pub const RNG_CLASS_NAME: &str = "virtio.rng";
pub trait Backend: Send + Sync + fmt::Debug {
fn device_id(&self) -> u32;
fn queue_count(&self) -> usize;
fn features(&self) -> u64 {
0
}
fn config_read(&self, offset: u64, dst: &mut [u8]);
fn config_write(&self, offset: u64, src: &[u8]) {
let (_, _) = (offset, src);
}
fn handle(&self, queue: usize, q: &Queue<'_>, chain: &[Descriptor]) -> u32;
fn reset(&self);
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let _ = w;
Ok(())
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let _ = r;
Ok(())
}
}
pub const DEFAULT_SLOT: &str = "disk";
pub fn blk_from_props(props: &Props) -> Result<VirtioMmio> {
let mut r = props.reader();
let media = r.optional_media("image")?;
let slot = media.map(crate::core::props::Media::name);
let image = media.map(crate::core::props::Media::to_bytes);
let size = r.or_size("size", 0)?;
let serial = r.or("serial", String::from("rsemu-virtio"))?;
let read_only = r.or("readonly", false)?;
r.finish()?;
let supplied = match props.hosts() {
Some(hosts) => {
let name = slot.unwrap_or(DEFAULT_SLOT);
crate::dev::ata::medium::get(hosts, name)?.and_then(|slot| slot.take())
}
None => None,
};
let bytes = match (&supplied, size, image.as_ref()) {
(Some(medium), _, _) => medium.capacity(),
(None, size, Some(image)) => size.max(image.len() as u64),
(None, size, None) => size,
};
if bytes == 0 {
return Err(crate::core::Error::Property(String::from(
"a `virtio.blk` needs a medium: give it a `size` (`size = 16M`) or an `image` \
media slot, or both to pad an image out to a larger disk — or install one under \
its media slot with `--drive disk=…`",
)));
}
let media: Arc<dyn Medium> = match supplied {
Some(medium) => medium,
None => {
let bytes = bytes.next_multiple_of(blk::SECTOR_SIZE);
let store = RamStore::new(bytes);
if let Some(image) = image {
RamStore::write_at(&store, 0, &image).map_err(|e| crate::core::Error::Config {
at: String::from(BLK_CLASS_NAME),
message: alloc::format!("the bound image did not fit: {e}"),
})?;
}
Arc::new(store)
}
};
Ok(VirtioMmio::new(
Arc::new(VirtioBlk::new(media, serial, read_only)?) as Arc<dyn Backend>,
&BLK_CLASS,
))
}
pub fn rng_from_props(props: &Props) -> Result<VirtioMmio> {
let mut r = props.reader();
let seed = r.or("seed", 0u64)?;
r.finish()?;
Ok(VirtioMmio::new(
Arc::new(VirtioRng::new(seed)) as Arc<dyn Backend>,
&RNG_CLASS,
))
}
pub static BLK_CLASS: DeviceClass = DeviceClass {
name: BLK_CLASS_NAME,
version: 1,
summary: "virtio block device on the MMIO transport, over a `dev::ata::Medium`",
properties: &[
PropertySpec {
name: "size",
kind: ValueKind::Size,
required: false,
summary: "how large the disk is, as in `size = 16M`; ignored when a host \
installed a medium under the media slot",
},
PropertySpec {
name: "image",
kind: ValueKind::Media,
required: false,
summary: "the media slot the disk is bound to; a host medium under that name \
wins, which is what `--drive disk=root.qcow2` installs",
},
PropertySpec {
name: "serial",
kind: ValueKind::Str,
required: false,
summary: "the serial number a `GET_ID` request reports",
},
PropertySpec {
name: "readonly",
kind: ValueKind::Bool,
required: false,
summary: "whether writes are refused (default false)",
},
],
construct: |props| Ok(Box::new(blk_from_props(props)?) as Box<dyn Device>),
};
pub static RNG_CLASS: DeviceClass = DeviceClass {
name: RNG_CLASS_NAME,
version: 1,
summary: "virtio entropy device on the MMIO transport, deterministically seeded",
properties: &[PropertySpec {
name: "seed",
kind: ValueKind::Uint,
required: false,
summary: "the generator's seed; the same seed gives the same bytes every run",
}],
construct: |props| Ok(Box::new(rng_from_props(props)?) as Box<dyn Device>),
};
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&BLK_CLASS)?;
registry.add(&RNG_CLASS)
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(BLK_CLASS_NAME, |props| Ok(Arc::new(blk_from_props(props)?)))?;
bindings.bind(RNG_CLASS_NAME, |props| Ok(Arc::new(rng_from_props(props)?)))
}
#[must_use]
pub fn schemas() -> Vec<crate::machine::validate::ClassSchema> {
use crate::machine::validate::{ClassSchema, PortDir, PropSchema};
alloc::vec![
ClassSchema::new(BLK_CLASS_NAME)
.prop(PropSchema::new("size", ValueKind::Size))
.prop(PropSchema::new("image", ValueKind::Media))
.prop(PropSchema::new("serial", ValueKind::Str))
.prop(PropSchema::new("readonly", ValueKind::Bool))
.region("")
.region("regs")
.port("irq", PortDir::Out),
ClassSchema::new(RNG_CLASS_NAME)
.prop(PropSchema::new("seed", ValueKind::Uint))
.region("")
.region("regs")
.port("irq", PortDir::Out),
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::props::Value;
use alloc::string::ToString;
#[test]
fn a_block_device_needs_a_medium() {
let e = blk_from_props(&Props::new())
.expect_err("no size and no image")
.to_string();
assert!(e.contains("medium"), "{e}");
let disk = blk_from_props(&Props::new().with("size", Value::Size(4096)))
.expect("a size is enough");
assert_eq!(disk.backend().device_id(), DEVICE_ID_BLOCK);
}
#[test]
fn a_medium_the_host_installed_wins_over_the_machine_files_size() {
use crate::core::hosts::HostObjects;
use crate::core::space::RamStore;
use crate::dev::ata::medium;
let hosts = alloc::sync::Arc::new(HostObjects::new());
let store: Arc<dyn Medium> = Arc::new(RamStore::new(8 * 512));
medium::install(&hosts, "disk", store).expect("nothing else claimed it");
let props = Props::new()
.with("size", Value::Size(64 * 1024))
.with_hosts(hosts);
let disk = blk_from_props(&props).expect("a medium is enough");
let backend = disk.backend();
assert_eq!(backend.device_id(), DEVICE_ID_BLOCK);
let mut config = [0u8; 8];
backend.config_read(0, &mut config);
assert_eq!(
u64::from_le_bytes(config),
8,
"the medium's eight sectors, not the 128 `size` asked for"
);
}
#[test]
fn an_entropy_device_takes_a_seed_and_nothing_else() {
let rng = rng_from_props(&Props::new().with("seed", 5u64)).expect("a seed is legal");
assert_eq!(rng.backend().device_id(), DEVICE_ID_ENTROPY);
assert!(rng_from_props(&Props::new().with("sed", 5u64)).is_err());
}
#[test]
fn both_classes_register_and_bind() {
let mut registry = crate::core::Registry::new();
register(&mut registry).expect("fresh registry");
assert!(registry.get(BLK_CLASS_NAME).is_some());
assert!(registry.get(RNG_CLASS_NAME).is_some());
let mut bindings = crate::machine::Bindings::new();
bind(&mut bindings).expect("fresh bindings");
assert_eq!(bindings.len(), 2);
assert_eq!(schemas().len(), 2);
}
}