baryl 0.0.3

Public SDK for Baryl, a full-system emulation and introspection engine
//! Stopping the guest at an address.
//!
//! Reached as `ctl.subs.breakpoints`. Arm a guest **physical** address and your
//! handler runs every time the guest executes there, whichever virtual mapping
//! it came in through — resolve a VA with `ctl.subs.arch.translate_with_cr3`
//! first. Several components may watch one site; each holds its own claim and
//! gives it up separately.
//!
//! These calls read the subsystem's table without checking it is there, so
//! write `requires(breakpoints)` on your `#[component]` and let the run refuse
//! to start rather than reaching through a handle nothing filled in.

use crate::abi::{BpInsertError, PhysAddr};

// 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 BreakpointsVtable {
    /// A table with no calls in it. Start from this when filling one in, so a
    /// call you did not implement reads as absent rather than as garbage.
    pub const ABSENT: BreakpointsVtable =
        BreakpointsVtable { insert: None, remove: None, query: None };
}

/// A breakpoint handler, as you write it: `extern "C"`, but taking your own
/// type and your own `Control` instead of raw pointers.
///
/// `T` is whatever state the handler needs — the same value you pass as `obj`
/// to [`insert`](BreakpointsRef::insert) — and `C` is `Control`.
pub type BpFn<T, C> = extern "C" fn(&mut T, &mut C, PhysAddr);

impl BreakpointsRef {
    /// Watch guest physical address `pa`, calling `handler` with `obj` on every
    /// hit.
    ///
    /// `pa` is physical, so the handler fires however the guest reached the
    /// site — from any process, through any mapping. `obj` is borrowed for as
    /// long as the breakpoint is armed, and is also what identifies the claim:
    /// [`remove`](Self::remove) takes the same `pa` and `obj` back.
    ///
    /// # Errors
    ///
    /// [`BpInsertError::DuplicateOwner`] when this `obj` already watches `pa`.
    /// Another component watching the same site is not an error.
    ///
    /// # Panics
    ///
    /// If the breakpoints subsystem is loaded but exposes no `insert`.
    ///
    /// # Examples
    ///
    /// ```ignore
    /// extern "C" fn on_open(me: &mut Watcher, t: &mut Control, pa: PhysAddr) {
    ///     me.hits += 1;
    ///     baryl::logging::info!("sys_open #{} at {pa}", me.hits);
    ///     let _ = t;
    /// }
    ///
    /// #[core(first_ring_three)]
    /// fn arm(&mut self, t: &mut Control) {
    ///     let Some(va) = t.subs.enlighten.resolve_symbol(None, "do_sys_openat2") else { return };
    ///     // SAFETY: past first VM entry, so this cr3 names a live address space.
    ///     let Some(pa) = (unsafe { t.subs.arch.translate_with_cr3(VirtAddr(va), cr3) }) else {
    ///         return;
    ///     };
    ///     t.subs.breakpoints.insert(pa, self, on_open).expect("not armed yet");
    /// }
    /// ```
    pub fn insert<T, C>(
        &self,
        pa: PhysAddr,
        obj: &mut T,
        handler: BpFn<T, C>,
    ) -> Result<(), BpInsertError> {
        // SAFETY: both are `extern "C"`; `&mut T`, `&mut C` and `PhysAddr` are
        // thin values with the same FFI representation as the raw forms.
        let handler: BpHandler = unsafe { core::mem::transmute(handler) };
        let obj = core::ptr::from_mut(obj).cast();
        let insert = unsafe { (*self.vtable).insert }
            .expect("the breakpoints `.so` populates every BreakpointsVtable fn");
        let rc = unsafe { insert(*self, pa.0, obj, handler) };
        (rc == 0)
            .then_some(())
            .ok_or(BpInsertError::DuplicateOwner(pa))
    }

    /// Give up `obj`'s claim on `pa`. Anyone else watching the same site keeps
    /// theirs, and the site stays armed until the last claim goes.
    ///
    /// A claim that was never made is a no-op, and so is a run with no
    /// breakpoints implementation. Nothing is reported either way.
    pub fn remove<T>(&self, pa: PhysAddr, obj: &mut T) {
        // SAFETY: `vtable` is the breakpoints `.so`'s static table, valid for the process.
        let Some(remove) = (unsafe { (*self.vtable).remove }) else {
            return;
        };
        unsafe { remove(*self, pa.0, core::ptr::from_mut(obj).cast()) };
    }

    /// Whether *anyone* is watching `pa` — yours or another component's.
    ///
    /// `None` when the run offers no query at all, which is not the same
    /// answer as `Some(false)`.
    pub fn query(&self, pa: u64) -> Option<bool> {
        // SAFETY: as `remove`.
        let query = unsafe { (*self.vtable).query }?;
        Some(unsafe { query(*self, pa) } != 0)
    }
}