baryl 0.0.2

Public SDK for Baryl, a full-system emulation and introspection engine
//! Guest physical memory, the instruction clock, and the emulated NIC's
//! address.
//!
//! Reached as `ctl.subs.engine`. This is the machine below translation: an
//! address here is a guest *physical* address, and nothing walks page tables on
//! your behalf. Reach for `ctl.subs.arch` when you have a virtual address.
//!
//! Not every engine implements every call. Each one below fails closed on a
//! call its engine does not offer — `None`, an error, or a documented zero —
//! so a component built against a richer engine degrades rather than
//! misbehaves. It does assume the engine subsystem itself is loaded: write
//! `requires(engine)` on your `#[component]`.

use crate::abi::progress_thunk;
/// Re-exported so a caller of the scans below reaches these without a second
/// import.
pub use crate::abi::{BARYL_SCAN_NO_MATCH, ProgressFn, ProgressSink, ScanRange};

// 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::*;

impl EngineVtable {
    /// A table with no calls in it.
    ///
    /// A capability is present exactly when its pointer is filled in, so an
    /// engine writes the calls it has and closes the literal with `..ABSENT`.
    /// New calls are appended over time; starting from this means a table built
    /// today still compiles when one arrives.
    pub const ABSENT: EngineVtable = EngineVtable {
        phys_to_host_translate: None,
        phys_read_into: None,
        phys_write_from: None,
        phys_scan_for_pattern: None,
        phys_scan_for_u64: None,
        phys_scan_for_u32: None,
        jit_flush_page: None,
        icount: None,
        clamp_icount_budget: None,
        guest_mac: None,
    };
}

/// A physical read or write did not happen: the address is outside the
/// machine's RAM, or the engine offers no such call.
///
/// Carries no detail — the ABI answers with a bare status, so there is nothing
/// more specific to report. Implements `Display` and `std::error::Error`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PhysAccessError;

impl core::fmt::Display for PhysAccessError {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "physical memory access failed")
    }
}

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

impl EngRef {
    /// One little-endian `u64` at guest physical `pa` — a page-table entry, a
    /// list pointer, a counter.
    ///
    /// `None` for an address outside RAM, or an engine that cannot read
    /// physical memory at all.
    pub fn read_phys_u64(&self, pa: u64) -> Option<u64> {
        // SAFETY: `vtable` is the engine `.so`'s static table, valid for the process.
        let read = unsafe { (*self.vtable).phys_read_into }?;
        let mut buf = [0u8; 8];
        let rc = unsafe { read(*self, pa, buf.as_mut_ptr(), 8) };
        (rc == 0).then(|| u64::from_le_bytes(buf))
    }

    /// A host pointer straight at guest physical `pa`, and how many bytes
    /// remain in that RAM region.
    ///
    /// The fast path when you are about to touch a lot of bytes and a copy per
    /// access is too much. The length bounds you: it runs to the end of that
    /// RAM region, not to the end of RAM, so a span crossing a region boundary
    /// needs a second call. Writing through the pointer writes guest memory
    /// with nothing watching.
    ///
    /// `None` for an address outside RAM, or an engine that hands out no host
    /// pointers.
    pub fn phys_host(&self, pa: u64) -> Option<(*mut u8, u64)> {
        // SAFETY: as `read_phys_u64`.
        let host_fn = unsafe { (*self.vtable).phys_to_host_translate }?;
        let mut len = 0u64;
        let p = unsafe { host_fn(*self, pa, &raw mut len) };
        (!p.is_null()).then_some((p, len))
    }

    /// Fill `buf` from guest physical `pa`; `buf.len()` says how much.
    ///
    /// # Errors
    ///
    /// [`PhysAccessError`] if any of the span is outside RAM, or the engine
    /// cannot read physical memory. `buf` may have been partly written either
    /// way — do not read it after an error.
    pub fn read_phys_into(&self, buf: &mut [u8], pa: u64) -> Result<(), PhysAccessError> {
        // SAFETY: as `read_phys_u64`; `buf` bounds the write.
        let f = unsafe { (*self.vtable).phys_read_into }.ok_or(PhysAccessError)?;
        match unsafe { f(*self, pa, buf.as_mut_ptr(), buf.len() as u64) } {
            0 => Ok(()),
            _ => Err(PhysAccessError),
        }
    }

    /// Write `buf` into guest memory at physical `pa`.
    ///
    /// The guest sees this as if it had written the bytes itself. Code the JIT
    /// has already translated is a separate matter — follow a write over
    /// executable memory with [`jit_flush_page`](Self::jit_flush_page).
    ///
    /// # Errors
    ///
    /// [`PhysAccessError`] if any of the span is outside RAM, or the engine
    /// cannot write physical memory. A partial write may have landed.
    pub fn write_phys_from(&self, buf: &[u8], pa: u64) -> Result<(), PhysAccessError> {
        // SAFETY: as `read_phys_u64`; `buf` bounds the read.
        let f = unsafe { (*self.vtable).phys_write_from }.ok_or(PhysAccessError)?;
        match unsafe { f(*self, pa, buf.as_ptr(), buf.len() as u64) } {
            0 => Ok(()),
            _ => Err(PhysAccessError),
        }
    }

    /// The address of the first `pat` in `range`, searching guest physical
    /// memory.
    ///
    /// `progress` is called as the sweep advances, with bytes done and bytes
    /// total; pass `None` to opt out. A whole-RAM scan is not quick, and this
    /// is what lets a caller show that it is alive.
    ///
    /// `None` covers three things at once — no match, an argument the engine
    /// rejected, and an engine with no pattern scan. Use `ScanRange::WHOLE` and
    /// a sane pattern and it means "not found".
    ///
    /// # Examples
    ///
    /// ```ignore
    /// let mut seen = |done, total| baryl::logging::info!("{done}/{total}");
    /// let sink: Option<ProgressSink<'_>> = Some(&mut seen);
    ///
    /// match t.subs.engine.phys_scan_for_pattern(ScanRange::WHOLE, b"MZ\x90\x00", sink) {
    ///     Some(pa) => baryl::logging::info!("PE header at {pa:#x}"),
    ///     None => baryl::logging::info!("no match"),
    /// }
    /// ```
    pub fn phys_scan_for_pattern(
        &self,
        range: ScanRange,
        pat: &[u8],
        progress: Option<ProgressSink<'_>>,
    ) -> Option<u64> {
        // SAFETY: as `read_phys_u64`; `pat` bounds the read, and the thunk pair
        // is borrowed from `progress`, which outlives the call.
        let f = unsafe { (*self.vtable).phys_scan_for_pattern }?;
        let mut progress = progress;
        let (obj, cb) = progress_thunk(&mut progress);
        let hit = unsafe {
            f(
                *self,
                range.start,
                range.end,
                range.stride,
                pat.as_ptr(),
                pat.len() as u64,
                obj,
                cb,
            )
        };
        (hit != BARYL_SCAN_NO_MATCH).then_some(hit)
    }

    /// The address of the first little-endian `target` in `range`.
    ///
    /// The pattern scan over eight bytes, so `u64` finds a stored pointer
    /// rather than its spelling. `None` as for
    /// [`phys_scan_for_pattern`](Self::phys_scan_for_pattern).
    pub fn phys_scan_for_u64(
        &self,
        range: ScanRange,
        target: u64,
        progress: Option<ProgressSink<'_>>,
    ) -> Option<u64> {
        // SAFETY: as `phys_scan_for_pattern`.
        let f = unsafe { (*self.vtable).phys_scan_for_u64 }?;
        let mut progress = progress;
        let (obj, cb) = progress_thunk(&mut progress);
        let hit = unsafe { f(*self, range.start, range.end, range.stride, target, obj, cb) };
        (hit != BARYL_SCAN_NO_MATCH).then_some(hit)
    }

    /// The same over four bytes.
    ///
    /// `None` as for [`phys_scan_for_pattern`](Self::phys_scan_for_pattern).
    pub fn phys_scan_for_u32(
        &self,
        range: ScanRange,
        target: u32,
        progress: Option<ProgressSink<'_>>,
    ) -> Option<u64> {
        // SAFETY: as `phys_scan_for_pattern`.
        let f = unsafe { (*self.vtable).phys_scan_for_u32 }?;
        let mut progress = progress;
        let (obj, cb) = progress_thunk(&mut progress);
        let hit = unsafe { f(*self, range.start, range.end, range.stride, target, obj, cb) };
        (hit != BARYL_SCAN_NO_MATCH).then_some(hit)
    }

    /// Throw away every translation the JIT holds for the page `pa` is in.
    ///
    /// Call this after writing over guest code, or the guest keeps executing
    /// the instructions that were there when the page was translated. A no-op
    /// on an interpreting engine, which has nothing cached.
    pub fn jit_flush_page(&self, pa: u64) {
        // SAFETY: as `read_phys_u64`.
        let Some(flush) = (unsafe { (*self.vtable).jit_flush_page }) else {
            return;
        };
        unsafe { flush(*self, pa) };
    }

    /// Guest instructions retired so far — the run's clock.
    ///
    /// Exact wherever you read it, including in the middle of a slice, so two
    /// readings subtract to the instructions between them. Answers 0 before the
    /// machine is up, and on an engine that does not count instructions, so it
    /// is not a liveness test.
    ///
    /// # Examples
    ///
    /// ```ignore
    /// let before = t.subs.engine.icount();
    /// // ... let the guest run to the next event ...
    /// baryl::logging::info!("{} instructions", t.subs.engine.icount() - before);
    /// ```
    pub fn icount(&self) -> u64 {
        // SAFETY: as `read_phys_u64`.
        let Some(f) = (unsafe { (*self.vtable).icount }) else {
            return 0;
        };
        unsafe { f(*self) }
    }

    /// Cut the live budget so at most `max` more instructions run before the
    /// guest stops and `#[core(icount_expiration)]` fires.
    ///
    /// Shrinking only. Answers what was granted; 0 means nothing changed —
    /// either the budget was already tighter than `max`, or this engine has no
    /// budget to clamp. Either way the existing budget stands.
    pub fn clamp_icount_budget(&self, max: u64) -> u64 {
        // SAFETY: as `read_phys_u64`.
        let Some(f) = (unsafe { (*self.vtable).clamp_icount_budget }) else {
            return 0;
        };
        unsafe { f(*self, max) }
    }

    /// The MAC address of the guest's emulated NIC — the address a frame built
    /// for the guest has to be sent to.
    ///
    /// `None` for a machine with no NIC, one whose NIC is not up yet, and an
    /// engine that reads no network device at all.
    pub fn guest_mac(&self) -> Option<[u8; 6]> {
        // SAFETY: as `read_phys_u64`; `out` is this frame's own six bytes.
        let f = unsafe { (*self.vtable).guest_mac }?;
        let mut out = [0u8; 6];
        (unsafe { f(*self, out.as_mut_ptr()) } == 0).then_some(out)
    }
}