execsurface-observe 0.1.0-alpha.2

Linux metadata-only observer for AETHER X ExecSurface
Documentation
//! Linux metadata-only observation backend.
//!
//! On Linux x86_64 the implementation uses ptrace and reads only selected
//! metadata pointers. It never dereferences argv or envp.

use std::ffi::{CString, OsStr, OsString};
use std::fmt;
use std::io;
use std::os::unix::ffi::OsStrExt;
use std::sync::Mutex;

use execsurface_model::Observation;

pub const DEFAULT_EVENT_LIMIT: usize = 1_000_000;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ObserveOptions {
    pub event_limit: usize,
}

impl Default for ObserveOptions {
    fn default() -> Self {
        Self {
            event_limit: DEFAULT_EVENT_LIMIT,
        }
    }
}

#[derive(Clone)]
pub struct CommandSpec {
    program: OsString,
    args: Vec<OsString>,
}

impl CommandSpec {
    pub fn new(program: impl Into<OsString>) -> Self {
        Self {
            program: program.into(),
            args: Vec::new(),
        }
    }

    pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
        self.args.push(arg.into());
        self
    }

    pub fn args<I, S>(mut self, args: I) -> Self
    where
        I: IntoIterator<Item = S>,
        S: Into<OsString>,
    {
        self.args.extend(args.into_iter().map(Into::into));
        self
    }

    fn c_argv(&self) -> Result<(CString, Vec<CString>), ObserveError> {
        let program = cstring_from_os(&self.program)?;
        let mut argv = Vec::with_capacity(self.args.len() + 1);
        argv.push(cstring_from_os(&self.program)?);
        for arg in &self.args {
            argv.push(cstring_from_os(arg)?);
        }
        Ok((program, argv))
    }
}

fn cstring_from_os(value: &OsStr) -> Result<CString, ObserveError> {
    CString::new(value.as_bytes()).map_err(|_| {
        ObserveError::InvalidCommand("command contains an interior NUL byte".to_owned())
    })
}

#[derive(Debug)]
pub enum ObserveError {
    UnsupportedPlatform(&'static str),
    InvalidCommand(String),
    Os(io::Error),
    Protocol(String),
}

impl fmt::Display for ObserveError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::UnsupportedPlatform(message) => write!(f, "unsupported platform: {message}"),
            Self::InvalidCommand(message) => write!(f, "invalid command: {message}"),
            Self::Os(error) => write!(f, "observer OS error: {error}"),
            Self::Protocol(message) => write!(f, "observer protocol error: {message}"),
        }
    }
}

impl std::error::Error for ObserveError {}

impl From<io::Error> for ObserveError {
    fn from(value: io::Error) -> Self {
        Self::Os(value)
    }
}

#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
mod linux_ptrace;

static OBSERVE_LOCK: Mutex<()> = Mutex::new(());

pub fn observe_command(spec: &CommandSpec) -> Result<Observation, ObserveError> {
    observe_command_with_options(spec, ObserveOptions::default())
}

pub fn observe_command_with_options(
    spec: &CommandSpec,
    options: ObserveOptions,
) -> Result<Observation, ObserveError> {
    let _session_guard = OBSERVE_LOCK.lock().map_err(|_| {
        ObserveError::Protocol("observer session serialization lock was poisoned".to_owned())
    })?;
    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
    {
        linux_ptrace::observe(spec, options)
    }

    #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
    {
        let _ = (spec, options);
        Err(ObserveError::UnsupportedPlatform(
            "current observer supports Linux x86_64 only",
        ))
    }
}

#[cfg(test)]
mod api_tests {
    use super::*;
    use std::os::unix::ffi::OsStringExt;

    #[test]
    fn invalid_command_metadata_returns_explicit_error() {
        let invalid = OsString::from_vec(b"bad\0program".to_vec());
        let result = observe_command(&CommandSpec::new(invalid));
        assert!(matches!(result, Err(ObserveError::InvalidCommand(_))));
    }

    #[test]
    fn default_event_budget_is_fail_closed_and_finite() {
        let options = ObserveOptions::default();
        assert_eq!(options.event_limit, DEFAULT_EVENT_LIMIT);
        assert!(options.event_limit >= 100_000);
        assert!(options.event_limit < usize::MAX);
    }
}