baryl 0.0.4

Public SDK for Baryl, a full-system emulation and introspection engine
Documentation
//! [`Control`]: everything a component can reach, in one value.
//!
//! Every event handler is passed a `&mut Control`. It holds two things:
//!
//! - `ctl.subs`, a [`Subsystems`] — one handle per subsystem, so `ctl.subs.arch`
//!   walks page tables, `ctl.subs.engine` reads physical memory,
//!   `ctl.subs.enlighten` names processes, and so on.
//! - `ctl.core`, a `ComponentCoreRef` — the three things you can ask of the run
//!   itself: reset it, end it, or checkpoint it.
//!
//! There is no `Deref` between the two, so which half a line is reaching for is
//! written on the line.
//!
//! A handle for a subsystem this run did not load is `{NULL, NULL}`. What that
//! means is the subsystem's own to say, and each module says it: `enlighten`,
//! `fuzz` and `net` degrade to empty answers, while `arch`, `engine`,
//! `breakpoints` and `coverage` expect to be there. Name what you cannot run
//! without in `#[component("name", requires(...))]` and the run refuses to start
//! without it, rather than misbehaving once it has.

use crate::arch::Arch;

// Bindgen output cannot satisfy the workspace lints; the allow stops here.
mod generated {
    #![allow(non_camel_case_types, non_upper_case_globals, dead_code)]
    include!("generated.rs");
}
pub use generated::{
    BARYL_SUB_ARCH, BARYL_SUB_BREAKPOINTS, BARYL_SUB_CORPUS, BARYL_SUB_COVERAGE, BARYL_SUB_ENGINE,
    BARYL_SUB_ENLIGHTEN, BARYL_SUB_FUZZ, BARYL_SUB_NET, BARYL_SUB_STATS, Control, Subsystems,
};

// FIXME: I want us to design an API in front of control, so components stop reaching through
// ctl.subs or whatever

/// Nothing bound: every handle `{NULL, NULL}`, `isa` 0, `regs` null. Useful as
/// a placeholder in your own state; the one a handler is passed is never this.
impl Default for Control {
    fn default() -> Self {
        // SAFETY: every field is a handle of two pointers, a `u32`, or a raw
        // pointer, and all-zero is a valid value of each.
        unsafe { core::mem::zeroed() }
    }
}

/// Why [`Subsystems::regs`] could not hand back a register file.
///
/// Implements `Display` and `std::error::Error`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ControlError {
    /// Asked too early. The engine assembles the register table on its way to
    /// first VM entry, so nothing before `#[core(first_vm_entry)]` can read it.
    NotPublished,
    /// You asked for one ISA's register file and the run is on another —
    /// `want` is your `A::ID`, `found` is what the engine paired with. An
    /// x86-64 component on an aarch64 guest lands here.
    ArchMismatch { want: u32, found: u32 },
    /// The run named an ISA but published no table to go with it.
    NullRegs,
}

impl core::fmt::Display for ControlError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        match self {
            Self::NotPublished => write!(f, "the vm is not published until first VM entry"),
            Self::ArchMismatch { want, found } => {
                write!(f, "built for arch {want:#x}, engine paired with {found:#x}")
            },
            Self::NullRegs => write!(f, "the engine published no register table"),
        }
    }
}

impl core::error::Error for ControlError {}

impl Subsystems {
    /// The guest's register file, as the ISA `A` lays it out.
    ///
    /// The ISA is checked before the cast, so asking for `X64Regs` on a run
    /// that is not x86-64 is an error rather than a misread. The reference
    /// borrows `self`, and the values behind it move as the guest runs — read
    /// what you need at the exit you were called on rather than holding it.
    ///
    /// # Errors
    ///
    /// [`ControlError::NotPublished`] before the engine has assembled the
    /// table, which is any point before `#[core(first_vm_entry)]`.
    /// [`ControlError::ArchMismatch`] when the run is on a different ISA.
    /// [`ControlError::NullRegs`] when it named an ISA but published no table.
    ///
    /// # Examples
    ///
    /// ```ignore
    /// #[core(first_ring_three)]
    /// fn at_ring_three(&mut self, t: &mut Control) {
    ///     let Ok(regs): Result<&X64Regs, _> = t.subs.regs() else { return };
    ///     baryl::logging::info!("rip {:#x}, cr3 {:#x}", *regs.rip(), *regs.cr(3));
    /// }
    /// ```
    pub fn regs<A: Arch>(&self) -> Result<&A, ControlError> {
        match self.isa {
            0 => Err(ControlError::NotPublished),
            id if id != A::ID => Err(ControlError::ArchMismatch { want: A::ID, found: id }),
            _ if self.regs.is_null() => Err(ControlError::NullRegs),
            // SAFETY: `isa` says which standard the engine published `regs`
            // under, and it agrees with `A::ID`.
            _ => Ok(unsafe { &*self.regs.cast() }),
        }
    }

    /// Which ISA this run is on, as a `BARYL_ARCH_*` value — `None` until the
    /// register table is published at first VM entry.
    ///
    /// `Some` here is the same condition [`regs`](Self::regs) needs, so use
    /// this when all you want to know is whether the guest is up.
    pub fn arch_id(&self) -> Option<u32> {
        (self.isa != 0).then_some(self.isa)
    }

    /// The arch ABI revision the run was built against; `None` at the same
    /// times [`arch_id`](Self::arch_id) is.
    pub fn abi_version(&self) -> Option<u32> {
        (self.isa != 0).then_some(self.abi)
    }
}