rsemu 0.0.2

A multiplatform emulator in pure Rust, built bottom-up on a generic framework.
Documentation
//! virtio: the transport-agnostic core, the MMIO transport, and two devices.
//!
//! `ROADMAP.md` §7 asks for "a transport-agnostic core (virtqueues, feature
//! negotiation) with PCI and MMIO transports", and this is the first half of
//! that: [`queue`] and [`Backend`] know nothing about MMIO, and [`mmio`] knows
//! nothing about block devices. A PCI transport slots in beside `mmio` without
//! either of the two device models changing.
//!
//! | Module | Covers |
//! | --- | --- |
//! | [`queue`] | split virtqueues: descriptor chains, available and used rings |
//! | [`mmio`] | the MMIO transport register block and the status handshake |
//! | [`blk`] | virtio-blk (device ID 2) over a byte-slice medium |
//! | [`rng`] | virtio-rng (device ID 4), deterministically seeded |
//!
//! # Source, and one prohibition
//!
//! Everything here is from *Virtual I/O Device (VIRTIO) Version 1.2*, OASIS
//! Standard — free, complete and normative. `ROADMAP.md` §1 names Linux's
//! virtio **drivers** as the most common way the provenance rule gets broken,
//! precisely because they are the obvious place to look when a device will not
//! probe. No driver source of any licence was opened for any part of this, and
//! the specification answered every question that came up.

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::state::{ChunkReader, ChunkWriter};

use queue::{Descriptor, Queue};

pub use blk::VirtioBlk;
pub use mmio::VirtioMmio;
pub use rng::VirtioRng;

/// The `VendorID` register's value: ASCII `rsem`, little-endian.
///
/// The specification does not assign these, and no driver may condition on one
/// (§4.2.2), so it is only ever seen by a human reading a register dump.
pub const VENDOR_ID: u32 = 0x6d65_7372;

/// virtio-blk (§5.2).
pub const DEVICE_ID_BLOCK: u32 = 2;
/// virtio-rng (§5.4).
pub const DEVICE_ID_ENTROPY: u32 = 4;

/// The class name for a block device on the MMIO transport.
pub const BLK_CLASS_NAME: &str = "virtio.blk";
/// The class name for an entropy device on the MMIO transport.
pub const RNG_CLASS_NAME: &str = "virtio.rng";

/// What a virtio device *is*, with no transport in it.
///
/// A transport handles feature negotiation, the status handshake and the
/// rings; a backend answers three questions — what am I, what does my
/// configuration space say, and what do I do with a chain.
pub trait Backend: Send + Sync + fmt::Debug {
    /// The device type (§5): 2 for block, 4 for entropy.
    fn device_id(&self) -> u32;

    /// How many virtqueues it has.
    fn queue_count(&self) -> usize;

    /// The feature bits it offers, *not* including `VIRTIO_F_VERSION_1`, which
    /// every modern transport adds for itself.
    fn features(&self) -> u64 {
        0
    }

    /// Fill `dst` from the device configuration space at `offset`.
    ///
    /// Bytes past the end of the configuration read as zero, because a driver
    /// reads the whole space before it knows which features are on.
    fn config_read(&self, offset: u64, dst: &mut [u8]);

    /// Take a configuration-space write. Most devices have none.
    fn config_write(&self, offset: u64, src: &[u8]) {
        let (_, _) = (offset, src);
    }

    /// Do whatever one descriptor chain asks, returning how many bytes were
    /// written into it — the length that goes in the used ring (§2.7.8).
    fn handle(&self, queue: usize, q: &Queue<'_>, chain: &[Descriptor]) -> u32;

    /// Return to the state a `Status` write of zero implies.
    fn reset(&self);

    /// Serialize whatever of this device is architectural state.
    ///
    /// # Errors
    ///
    /// Whatever the writer refuses.
    fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
        let _ = w;
        Ok(())
    }

    /// Restore what [`save`](Backend::save) wrote.
    ///
    /// # Errors
    ///
    /// Whatever the reader refuses, or a snapshot that does not match this
    /// device's shape.
    fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
        let _ = r;
        Ok(())
    }
}

/// Build a `virtio.blk` from machine-description properties.
///
/// # Errors
///
/// [`Error::Property`](crate::core::Error::Property) if neither `size` nor
/// `image` was given, or if a property this class does not know was.
pub fn blk_from_props(props: &Props) -> Result<VirtioMmio> {
    let mut r = props.reader();
    let image = r.optional_media("image")?.map(|m| m.to_bytes().to_vec());
    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 mut bytes = image.unwrap_or_default();
    let size = usize::try_from(size).unwrap_or(usize::MAX);
    if size > bytes.len() {
        bytes.resize(size, 0);
    }
    if bytes.is_empty() {
        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",
        )));
    }
    Ok(VirtioMmio::new(
        Arc::new(VirtioBlk::new(bytes, serial, read_only)) as Arc<dyn Backend>,
        &BLK_CLASS,
    ))
}

/// Build a `virtio.rng` from machine-description properties.
///
/// # Errors
///
/// [`Error::Property`](crate::core::Error::Property) if a property this class
/// does not know was given.
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,
    ))
}

/// The `virtio.blk` device class.
pub static BLK_CLASS: DeviceClass = DeviceClass {
    name: BLK_CLASS_NAME,
    version: 1,
    summary: "virtio block device on the MMIO transport, over an in-memory medium",
    properties: &[
        PropertySpec {
            name: "size",
            kind: ValueKind::Size,
            required: false,
            summary: "how large the disk is, as in `size = 16M`",
        },
        PropertySpec {
            name: "image",
            kind: ValueKind::Media,
            required: false,
            summary: "the disk's contents, as the name of a media slot",
        },
        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>),
};

/// The `virtio.rng` device class.
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>),
};

/// Add both virtio classes to a registry.
///
/// # Errors
///
/// [`Error::Config`](crate::core::Error::Config) if a name is already claimed.
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
    registry.add(&BLK_CLASS)?;
    registry.add(&RNG_CLASS)
}

/// Bind both virtio classes into the machine graph.
///
/// # Errors
///
/// As [`register`].
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)?)))
}

/// What the validator should know about the virtio classes.
#[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 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);
    }
}