Skip to main content

execsurface_observe/
lib.rs

1//! Linux metadata-only observation backend.
2//!
3//! On Linux x86_64 the implementation uses ptrace and reads only selected
4//! metadata pointers. It never dereferences argv or envp.
5
6use std::ffi::{CString, OsStr, OsString};
7use std::fmt;
8use std::io;
9use std::os::unix::ffi::OsStrExt;
10use std::sync::Mutex;
11
12use execsurface_model::Observation;
13
14pub const DEFAULT_EVENT_LIMIT: usize = 1_000_000;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct ObserveOptions {
18    pub event_limit: usize,
19}
20
21impl Default for ObserveOptions {
22    fn default() -> Self {
23        Self {
24            event_limit: DEFAULT_EVENT_LIMIT,
25        }
26    }
27}
28
29#[derive(Clone)]
30pub struct CommandSpec {
31    program: OsString,
32    args: Vec<OsString>,
33}
34
35impl CommandSpec {
36    pub fn new(program: impl Into<OsString>) -> Self {
37        Self {
38            program: program.into(),
39            args: Vec::new(),
40        }
41    }
42
43    pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
44        self.args.push(arg.into());
45        self
46    }
47
48    pub fn args<I, S>(mut self, args: I) -> Self
49    where
50        I: IntoIterator<Item = S>,
51        S: Into<OsString>,
52    {
53        self.args.extend(args.into_iter().map(Into::into));
54        self
55    }
56
57    fn c_argv(&self) -> Result<(CString, Vec<CString>), ObserveError> {
58        let program = cstring_from_os(&self.program)?;
59        let mut argv = Vec::with_capacity(self.args.len() + 1);
60        argv.push(cstring_from_os(&self.program)?);
61        for arg in &self.args {
62            argv.push(cstring_from_os(arg)?);
63        }
64        Ok((program, argv))
65    }
66}
67
68fn cstring_from_os(value: &OsStr) -> Result<CString, ObserveError> {
69    CString::new(value.as_bytes()).map_err(|_| {
70        ObserveError::InvalidCommand("command contains an interior NUL byte".to_owned())
71    })
72}
73
74#[derive(Debug)]
75pub enum ObserveError {
76    UnsupportedPlatform(&'static str),
77    InvalidCommand(String),
78    Os(io::Error),
79    Protocol(String),
80}
81
82impl fmt::Display for ObserveError {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match self {
85            Self::UnsupportedPlatform(message) => write!(f, "unsupported platform: {message}"),
86            Self::InvalidCommand(message) => write!(f, "invalid command: {message}"),
87            Self::Os(error) => write!(f, "observer OS error: {error}"),
88            Self::Protocol(message) => write!(f, "observer protocol error: {message}"),
89        }
90    }
91}
92
93impl std::error::Error for ObserveError {}
94
95impl From<io::Error> for ObserveError {
96    fn from(value: io::Error) -> Self {
97        Self::Os(value)
98    }
99}
100
101#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
102mod linux_ptrace;
103
104static OBSERVE_LOCK: Mutex<()> = Mutex::new(());
105
106pub fn observe_command(spec: &CommandSpec) -> Result<Observation, ObserveError> {
107    observe_command_with_options(spec, ObserveOptions::default())
108}
109
110pub fn observe_command_with_options(
111    spec: &CommandSpec,
112    options: ObserveOptions,
113) -> Result<Observation, ObserveError> {
114    let _session_guard = OBSERVE_LOCK.lock().map_err(|_| {
115        ObserveError::Protocol("observer session serialization lock was poisoned".to_owned())
116    })?;
117    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
118    {
119        linux_ptrace::observe(spec, options)
120    }
121
122    #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
123    {
124        let _ = (spec, options);
125        Err(ObserveError::UnsupportedPlatform(
126            "current observer supports Linux x86_64 only",
127        ))
128    }
129}
130
131#[cfg(test)]
132mod api_tests {
133    use super::*;
134    use std::os::unix::ffi::OsStringExt;
135
136    #[test]
137    fn invalid_command_metadata_returns_explicit_error() {
138        let invalid = OsString::from_vec(b"bad\0program".to_vec());
139        let result = observe_command(&CommandSpec::new(invalid));
140        assert!(matches!(result, Err(ObserveError::InvalidCommand(_))));
141    }
142
143    #[test]
144    fn default_event_budget_is_fail_closed_and_finite() {
145        let options = ObserveOptions::default();
146        assert_eq!(options.event_limit, DEFAULT_EVENT_LIMIT);
147        assert!(options.event_limit >= 100_000);
148        assert!(options.event_limit < usize::MAX);
149    }
150}