execsurface_observe/
lib.rs1use 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}