use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use nix::sys::ptrace;
use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
use nix::unistd::Pid;
#[derive(Debug, Clone)]
pub struct ProcessTraceConfig {
pub max_syscalls: usize,
pub timeout: Duration,
pub enable_source: bool,
pub otlp_endpoint: Option<String>,
pub rate_limit: u32,
pub anomaly_threshold: f32,
}
impl Default for ProcessTraceConfig {
fn default() -> Self {
Self {
max_syscalls: 1000,
timeout: Duration::from_millis(100),
enable_source: false,
otlp_endpoint: None,
rate_limit: 100,
anomaly_threshold: 3.0,
}
}
}
impl ProcessTraceConfig {
#[must_use]
pub fn with_max_syscalls(mut self, max: usize) -> Self {
self.max_syscalls = max;
self
}
#[must_use]
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn with_source(mut self, enable: bool) -> Self {
self.enable_source = enable;
self
}
#[must_use]
pub fn with_otlp(mut self, endpoint: String) -> Self {
self.otlp_endpoint = Some(endpoint);
self
}
#[must_use]
pub fn with_rate_limit(mut self, rate: u32) -> Self {
self.rate_limit = rate;
self
}
#[must_use]
pub fn with_source_correlation(self, enable: bool) -> Self {
self.with_source(enable)
}
#[must_use]
pub fn with_anomaly_threshold(mut self, threshold: f32) -> Self {
self.anomaly_threshold = threshold;
self
}
}
#[derive(Debug, thiserror::Error)]
pub enum TracerError {
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("Process {pid} not found")]
ProcessNotFound {
pid: u32,
},
#[error("Process {pid} is already being traced")]
AlreadyTraced {
pid: u32,
},
#[error("Process {pid} exited during tracing")]
ProcessExited {
pid: u32,
},
#[error("Ptrace error: {0}")]
PtraceError(#[from] nix::Error),
#[error("Timeout after {0:?}")]
Timeout(Duration),
#[error("Rate limit exceeded: {current}/s > {limit}/s")]
RateLimitExceeded {
current: u32,
limit: u32,
},
#[error("OTLP export error: {0}")]
OtlpError(String),
#[error("DWARF error: {0}")]
DwarfError(String),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Not attached to any process")]
NotAttached,
}
static_assertions::assert_impl_all!(TracerError: Send, Sync);
static_assertions::assert_impl_all!(ProcessTraceConfig: Send, Sync);
static_assertions::assert_impl_all!(SyscallEvent: Send, Sync);
static_assertions::assert_impl_all!(SyscallBreakdown: Send, Sync);
static_assertions::assert_impl_all!(SyscallAnomaly: Send, Sync);
static_assertions::assert_impl_all!(SourceLocation: Send, Sync);
static_assertions::assert_impl_all!(OtlpAttribute: Send, Sync);
static_assertions::assert_impl_all!(OtlpSpan: Send, Sync);
static_assertions::assert_impl_all!(TraceResult: Send, Sync);
#[derive(Debug, Clone)]
pub struct SyscallEvent {
pub syscall: String,
pub syscall_nr: i64,
pub duration: Duration,
pub result: i64,
pub timestamp: Instant,
}
impl SyscallEvent {
pub fn new(syscall: String, syscall_nr: i64, duration: Duration, result: i64) -> Self {
Self { syscall, syscall_nr, duration, result, timestamp: Instant::now() }
}
}
#[derive(Debug, Clone, Default)]
pub struct SyscallBreakdown {
pub mmap_us: u64,
pub futex_us: u64,
pub ioctl_us: u64,
pub read_us: u64,
pub write_us: u64,
pub other_us: u64,
pub compute_us: u64,
pub syscall_count: u64,
pub syscall_counts: HashMap<String, u64>,
pub total_us: u64,
}
impl SyscallBreakdown {
pub fn from_events(events: &[SyscallEvent], total_duration_us: u64) -> Self {
let mut breakdown = Self { total_us: total_duration_us, ..Default::default() };
let mut syscall_time_us: u64 = 0;
for event in events {
let duration_us = event.duration.as_micros() as u64;
syscall_time_us += duration_us;
breakdown.syscall_count += 1;
*breakdown.syscall_counts.entry(event.syscall.clone()).or_insert(0) += 1;
match event.syscall.as_str() {
"mmap" | "munmap" | "mprotect" | "brk" | "mremap" => {
breakdown.mmap_us += duration_us;
}
"futex" => breakdown.futex_us += duration_us,
"ioctl" => breakdown.ioctl_us += duration_us,
"read" | "pread64" | "readv" | "preadv" | "preadv2" => {
breakdown.read_us += duration_us;
}
"write" | "pwrite64" | "writev" | "pwritev" | "pwritev2" => {
breakdown.write_us += duration_us;
}
_ => breakdown.other_us += duration_us,
}
}
breakdown.compute_us = total_duration_us.saturating_sub(syscall_time_us);
breakdown
}
pub fn syscall_time_us(&self) -> u64 {
self.mmap_us + self.futex_us + self.ioctl_us + self.read_us + self.write_us + self.other_us
}
pub fn efficiency(&self) -> f64 {
if self.total_us == 0 {
1.0
} else {
self.compute_us as f64 / self.total_us as f64
}
}
pub fn sorted_categories(&self) -> Vec<(&'static str, u64)> {
let mut categories = vec![
("mmap", self.mmap_us),
("futex", self.futex_us),
("ioctl", self.ioctl_us),
("read", self.read_us),
("write", self.write_us),
("other", self.other_us),
("compute", self.compute_us),
];
categories.sort_by(|a, b| b.1.cmp(&a.1));
categories
}
}
#[derive(Debug, Clone, Default)]
pub struct SyscallBaseline {
pub mean_us: HashMap<String, f64>,
pub std_us: HashMap<String, f64>,
pub sample_count: u64,
}
impl SyscallBaseline {
pub fn from_events(events: &[SyscallEvent]) -> Self {
let mut category_times: HashMap<String, Vec<f64>> = HashMap::new();
for event in events {
let category = categorize_syscall(&event.syscall);
category_times
.entry(category.to_string())
.or_default()
.push(event.duration.as_micros() as f64);
}
let mut mean_us = HashMap::new();
let mut std_us = HashMap::new();
for (category, times) in &category_times {
if times.is_empty() {
continue;
}
let n = times.len() as f64;
let mean = times.iter().sum::<f64>() / n;
mean_us.insert(category.clone(), mean);
if times.len() > 1 {
let variance = times.iter().map(|t| (t - mean).powi(2)).sum::<f64>() / (n - 1.0);
std_us.insert(category.clone(), variance.sqrt());
} else {
std_us.insert(category.clone(), mean.max(1.0));
}
}
Self { mean_us, std_us, sample_count: events.len() as u64 }
}
pub fn has_category(&self, category: &str) -> bool {
self.mean_us.contains_key(category)
}
}
#[derive(Debug, Clone)]
pub struct SyscallAnomaly {
pub syscall: String,
pub duration_us: u64,
pub zscore: f32,
pub expected_us: f64,
pub category: String,
}
#[derive(Debug, Clone)]
pub struct SourceLocation {
pub file: String,
pub line: u32,
pub function: Option<String>,
}
#[derive(Debug, Clone)]
pub struct OtlpAttribute {
pub key: String,
pub value: OtlpValue,
}
#[derive(Debug, Clone)]
pub enum OtlpValue {
Int(i64),
Float(f64),
String(String),
}
#[derive(Debug, Clone)]
pub struct OtlpSpan {
pub name: String,
pub trace_id: [u8; 16],
pub span_id: [u8; 8],
pub attributes: Vec<OtlpAttribute>,
}
#[derive(Debug, Clone)]
pub struct TraceResult {
pub pid: u32,
pub duration: Duration,
pub breakdown: SyscallBreakdown,
pub max_zscore: f32,
pub anomalies: Vec<SyscallAnomaly>,
pub source_locations: Vec<SourceLocation>,
pub events: Vec<SyscallEvent>,
}
impl TraceResult {
pub fn new(pid: u32, duration: Duration, events: Vec<SyscallEvent>) -> Self {
let breakdown = SyscallBreakdown::from_events(&events, duration.as_micros() as u64);
Self {
pid,
duration,
breakdown,
max_zscore: 0.0,
anomalies: Vec::new(),
source_locations: Vec::new(),
events,
}
}
pub fn with_baseline(mut self, baseline: &SyscallBaseline, threshold: f32) -> Self {
let mut max_z: f32 = 0.0;
for event in &self.events {
let category = categorize_syscall(&event.syscall);
let z = zscore(event, baseline);
if z > max_z {
max_z = z;
}
if z > threshold {
let expected = baseline.mean_us.get(category).copied().unwrap_or(0.0);
self.anomalies.push(SyscallAnomaly {
syscall: event.syscall.clone(),
duration_us: event.duration.as_micros() as u64,
zscore: z,
expected_us: expected,
category: category.to_string(),
});
}
}
self.max_zscore = max_z;
self
}
pub fn to_otlp_span(&self) -> OtlpSpan {
use std::time::{SystemTime, UNIX_EPOCH};
let now =
SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos() as u64;
let mut trace_id = [0u8; 16];
trace_id[0..8].copy_from_slice(&now.to_be_bytes());
trace_id[8..12].copy_from_slice(&self.pid.to_be_bytes());
let mut span_id = [0u8; 8];
span_id[0..4].copy_from_slice(&self.pid.to_be_bytes());
span_id[4..8].copy_from_slice(&(now as u32).to_be_bytes());
let attributes = vec![
OtlpAttribute {
key: "process.pid".to_string(),
value: OtlpValue::Int(i64::from(self.pid)),
},
OtlpAttribute {
key: "syscall.count".to_string(),
value: OtlpValue::Int(self.events.len() as i64),
},
OtlpAttribute {
key: "syscall.mmap_us".to_string(),
value: OtlpValue::Int(self.breakdown.mmap_us as i64),
},
OtlpAttribute {
key: "syscall.futex_us".to_string(),
value: OtlpValue::Int(self.breakdown.futex_us as i64),
},
OtlpAttribute {
key: "syscall.ioctl_us".to_string(),
value: OtlpValue::Int(self.breakdown.ioctl_us as i64),
},
OtlpAttribute {
key: "syscall.read_us".to_string(),
value: OtlpValue::Int(self.breakdown.read_us as i64),
},
OtlpAttribute {
key: "syscall.write_us".to_string(),
value: OtlpValue::Int(self.breakdown.write_us as i64),
},
OtlpAttribute {
key: "syscall.compute_us".to_string(),
value: OtlpValue::Int(self.breakdown.compute_us as i64),
},
OtlpAttribute {
key: "anomaly.max_zscore".to_string(),
value: OtlpValue::Float(f64::from(self.max_zscore)),
},
OtlpAttribute {
key: "anomaly.count".to_string(),
value: OtlpValue::Int(self.anomalies.len() as i64),
},
];
OtlpSpan { name: format!("process.trace.{}", self.pid), trace_id, span_id, attributes }
}
}
#[derive(Debug)]
pub struct ProcessTrace {
pid: u32,
config: ProcessTraceConfig,
_start_time: Instant,
events: Vec<SyscallEvent>,
baseline: Option<SyscallBaseline>,
attached: bool,
_trace_count: Arc<AtomicU64>,
_rate_limit_window: Instant,
}
impl ProcessTrace {
pub fn pid(&self) -> u32 {
self.pid
}
pub fn is_attached(&self) -> bool {
self.attached
}
pub fn event_count(&self) -> usize {
self.events.len()
}
pub fn set_baseline(&mut self, baseline: SyscallBaseline) {
self.baseline = Some(baseline);
}
pub fn baseline(&self) -> Option<&SyscallBaseline> {
self.baseline.as_ref()
}
}
static GLOBAL_TRACE_COUNT: AtomicU64 = AtomicU64::new(0);
static GLOBAL_RATE_WINDOW: AtomicU64 = AtomicU64::new(0);
fn check_rate_limit(config: &ProcessTraceConfig) -> Result<(), TracerError> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("System clock before UNIX epoch")
.as_secs();
let window = GLOBAL_RATE_WINDOW.load(Ordering::Relaxed);
if now > window {
GLOBAL_RATE_WINDOW.store(now, Ordering::Relaxed);
GLOBAL_TRACE_COUNT.store(1, Ordering::Relaxed);
Ok(())
} else {
let count = GLOBAL_TRACE_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
if count > config.rate_limit as u64 {
Err(TracerError::RateLimitExceeded { current: count as u32, limit: config.rate_limit })
} else {
Ok(())
}
}
}
fn categorize_syscall(name: &str) -> &'static str {
match name {
"mmap" | "munmap" | "mprotect" | "brk" | "mremap" => "mmap",
"futex" => "futex",
"ioctl" => "ioctl",
"read" | "pread64" | "readv" | "preadv" | "preadv2" => "read",
"write" | "pwrite64" | "writev" | "pwritev" | "pwritev2" => "write",
_ => "other",
}
}
pub fn syscall_name(nr: i64) -> &'static str {
match nr {
0 => "read",
1 => "write",
2 => "open",
3 => "close",
4 => "stat",
5 => "fstat",
6 => "lstat",
7 => "poll",
8 => "lseek",
9 => "mmap",
10 => "mprotect",
11 => "munmap",
12 => "brk",
13 => "rt_sigaction",
14 => "rt_sigprocmask",
15 => "rt_sigreturn",
16 => "ioctl",
17 => "pread64",
18 => "pwrite64",
19 => "readv",
20 => "writev",
21 => "access",
22 => "pipe",
23 => "select",
24 => "sched_yield",
25 => "mremap",
35 => "nanosleep",
56 => "clone",
57 => "fork",
58 => "vfork",
59 => "execve",
60 => "exit",
61 => "wait4",
62 => "kill",
202 => "futex",
_ => "unknown",
}
}
pub fn zscore(event: &SyscallEvent, baseline: &SyscallBaseline) -> f32 {
let category = categorize_syscall(&event.syscall);
let mean = match baseline.mean_us.get(category) {
Some(&m) => m,
None => return 0.0, };
let std = match baseline.std_us.get(category) {
Some(&s) if s > 0.0 => s,
_ => return 0.0, };
let duration = event.duration.as_micros() as f64;
((duration - mean) / std) as f32
}
pub fn compute_baseline(events: &[SyscallEvent]) -> SyscallBaseline {
SyscallBaseline::from_events(events)
}
fn has_ptrace_capability() -> bool {
let Ok(status) = std::fs::read_to_string("/proc/self/status") else {
return false;
};
let Some(cap_line) = status.lines().find(|l| l.starts_with("CapEff:")) else {
return false;
};
let Some(hex) = cap_line.split_whitespace().nth(1) else {
return false;
};
let Ok(caps) = u64::from_str_radix(hex, 16) else {
return false;
};
(caps & (1 << 19)) != 0
}
fn ptrace_scope_allows_tracing() -> bool {
std::fs::read_to_string("/proc/sys/kernel/yama/ptrace_scope")
.map(|scope| scope.trim() == "0")
.unwrap_or(false)
}
pub fn is_available() -> bool {
nix::unistd::geteuid().is_root() || has_ptrace_capability() || ptrace_scope_allows_tracing()
}
fn can_trace_pid(pid: u32) -> Result<bool, TracerError> {
let proc_path = format!("/proc/{}", pid);
if !std::path::Path::new(&proc_path).exists() {
return Err(TracerError::ProcessNotFound { pid });
}
if nix::unistd::geteuid().is_root() {
return Ok(true);
}
let status = std::fs::read_to_string(format!("/proc/{}/status", pid))?;
let uid_line = status
.lines()
.find(|l| l.starts_with("Uid:"))
.ok_or(TracerError::ProcessNotFound { pid })?;
let real_uid: u32 = uid_line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse().ok())
.ok_or(TracerError::ProcessNotFound { pid })?;
Ok(real_uid == nix::unistd::getuid().as_raw())
}
pub fn attach(pid: u32, config: ProcessTraceConfig) -> Result<ProcessTrace, TracerError> {
check_rate_limit(&config)?;
if !can_trace_pid(pid)? {
return Err(TracerError::PermissionDenied(format!(
"Cannot trace PID {} (not owner and not root)",
pid
)));
}
let nix_pid = Pid::from_raw(pid as i32);
ptrace::attach(nix_pid).map_err(|e| match e {
nix::Error::EPERM => TracerError::PermissionDenied(format!("EPERM attaching to {}", pid)),
nix::Error::ESRCH => TracerError::ProcessNotFound { pid },
_ => TracerError::PtraceError(e),
})?;
match waitpid(nix_pid, Some(WaitPidFlag::WSTOPPED)) {
Ok(WaitStatus::Stopped(_, _)) => {}
Ok(WaitStatus::Exited(_, _)) => {
return Err(TracerError::ProcessExited { pid });
}
Ok(_) => {}
Err(e) => {
let _ = ptrace::detach(nix_pid, None);
return Err(TracerError::PtraceError(e));
}
}
if let Err(e) = ptrace::setoptions(
nix_pid,
ptrace::Options::PTRACE_O_TRACESYSGOOD | ptrace::Options::PTRACE_O_TRACEEXEC,
) {
let _ = ptrace::detach(nix_pid, None);
return Err(TracerError::PtraceError(e));
}
Ok(ProcessTrace {
pid,
config,
_start_time: Instant::now(),
events: Vec::new(),
baseline: None,
attached: true,
_trace_count: Arc::new(AtomicU64::new(0)),
_rate_limit_window: Instant::now(),
})
}
pub fn detach(mut trace: ProcessTrace) -> Result<(), TracerError> {
if !trace.attached {
return Ok(());
}
contract_pre_error_handling!();
let nix_pid = Pid::from_raw(trace.pid as i32);
ptrace::detach(nix_pid, None).map_err(|e| match e {
nix::Error::ESRCH => TracerError::ProcessExited { pid: trace.pid },
_ => TracerError::PtraceError(e),
})?;
trace.attached = false;
Ok(())
}
struct SyscallCollector {
events: Vec<SyscallEvent>,
in_syscall: bool,
syscall_start: Instant,
current_syscall_nr: i64,
}
impl SyscallCollector {
fn new() -> Self {
Self {
events: Vec::new(),
in_syscall: false,
syscall_start: Instant::now(),
current_syscall_nr: 0,
}
}
fn handle_syscall_stop(&mut self, nix_pid: Pid) -> Result<(), TracerError> {
if self.in_syscall {
let duration = self.syscall_start.elapsed();
let regs = crate::arch::PtraceRegs::get_nix(nix_pid)?;
let result = regs.syscall_return();
self.events.push(SyscallEvent {
syscall: syscall_name(self.current_syscall_nr).to_string(),
syscall_nr: self.current_syscall_nr,
duration,
result,
timestamp: self.syscall_start,
});
self.in_syscall = false;
} else {
let regs = crate::arch::PtraceRegs::get_nix(nix_pid)?;
self.current_syscall_nr = regs.syscall_number();
self.syscall_start = Instant::now();
self.in_syscall = true;
}
Ok(())
}
}
fn handle_wait_status(
status: Result<WaitStatus, nix::Error>,
nix_pid: Pid,
collector: &mut SyscallCollector,
pid: u32,
attached: &mut bool,
) -> Result<bool, TracerError> {
match status {
Ok(WaitStatus::PtraceSyscall(_)) => {
collector.handle_syscall_stop(nix_pid)?;
ptrace::syscall(nix_pid, None)?;
Ok(false)
}
Ok(WaitStatus::Exited(_, _) | WaitStatus::Signaled(_, _, _)) => {
*attached = false;
Err(TracerError::ProcessExited { pid })
}
Ok(WaitStatus::Stopped(_, sig)) => {
ptrace::syscall(nix_pid, Some(sig))?;
Ok(false)
}
Ok(WaitStatus::StillAlive) => {
std::thread::sleep(Duration::from_micros(100));
Ok(false)
}
Ok(_) => {
ptrace::syscall(nix_pid, None)?;
Ok(false)
}
Err(nix::Error::ECHILD) => {
*attached = false;
Err(TracerError::ProcessExited { pid })
}
Err(e) => Err(TracerError::PtraceError(e)),
}
}
pub fn collect(trace: &mut ProcessTrace) -> Result<TraceResult, TracerError> {
if !trace.attached {
return Err(TracerError::NotAttached);
}
contract_pre_error_handling!();
let nix_pid = Pid::from_raw(trace.pid as i32);
let start = Instant::now();
let timeout = trace.config.timeout;
let max_syscalls = trace.config.max_syscalls;
let mut collector = SyscallCollector::new();
ptrace::syscall(nix_pid, None)?;
loop {
if start.elapsed() > timeout || collector.events.len() >= max_syscalls {
break;
}
let status = waitpid(nix_pid, Some(WaitPidFlag::WNOHANG));
if handle_wait_status(status, nix_pid, &mut collector, trace.pid, &mut trace.attached)? {
break;
}
}
let duration = start.elapsed();
let mut result = TraceResult::new(trace.pid, duration, collector.events);
if let Some(baseline) = &trace.baseline {
result = result.with_baseline(baseline, 3.0);
}
trace.events.extend(result.events.clone());
Ok(result)
}
pub fn stream_syscalls(pid: u32, config: ProcessTraceConfig) -> Result<SyscallStream, TracerError> {
let trace = attach(pid, config)?;
Ok(SyscallStream { trace })
}
pub struct SyscallStream {
trace: ProcessTrace,
}
impl Iterator for SyscallStream {
type Item = Result<SyscallEvent, TracerError>;
fn next(&mut self) -> Option<Self::Item> {
if !self.trace.attached {
return None;
}
let nix_pid = Pid::from_raw(self.trace.pid as i32);
let mut in_syscall = false;
let mut syscall_start = Instant::now();
let mut current_syscall_nr: i64 = 0;
if let Err(e) = ptrace::syscall(nix_pid, None) {
self.trace.attached = false;
return Some(Err(TracerError::PtraceError(e)));
}
loop {
match waitpid(nix_pid, None) {
Ok(WaitStatus::PtraceSyscall(_)) => {
if in_syscall {
let duration = syscall_start.elapsed();
let regs = match crate::arch::PtraceRegs::get_nix(nix_pid) {
Ok(r) => r,
Err(e) => return Some(Err(TracerError::PtraceError(e))),
};
let result = regs.syscall_return();
return Some(Ok(SyscallEvent {
syscall: syscall_name(current_syscall_nr).to_string(),
syscall_nr: current_syscall_nr,
duration,
result,
timestamp: syscall_start,
}));
} else {
let regs = match crate::arch::PtraceRegs::get_nix(nix_pid) {
Ok(r) => r,
Err(e) => return Some(Err(TracerError::PtraceError(e))),
};
current_syscall_nr = regs.syscall_number();
syscall_start = Instant::now();
in_syscall = true;
if let Err(e) = ptrace::syscall(nix_pid, None) {
return Some(Err(TracerError::PtraceError(e)));
}
}
}
Ok(WaitStatus::Exited(_, _) | WaitStatus::Signaled(_, _, _)) => {
self.trace.attached = false;
return None;
}
Ok(WaitStatus::Stopped(_, sig)) => {
if let Err(e) = ptrace::syscall(nix_pid, Some(sig)) {
return Some(Err(TracerError::PtraceError(e)));
}
}
Ok(_) => {
if let Err(e) = ptrace::syscall(nix_pid, None) {
return Some(Err(TracerError::PtraceError(e)));
}
}
Err(nix::Error::ECHILD) => {
self.trace.attached = false;
return None;
}
Err(e) => {
return Some(Err(TracerError::PtraceError(e)));
}
}
}
}
}
impl Drop for SyscallStream {
fn drop(&mut self) {
if self.trace.attached {
let _ = ptrace::detach(Pid::from_raw(self.trace.pid as i32), None);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[ignore] fn test_f001_attach_valid_pid() {
if !is_available() {
eprintln!("Skipping F001: no ptrace permissions");
return;
}
let mut child = std::process::Command::new("sleep")
.arg("10")
.spawn()
.expect("Failed to spawn test process");
let child_pid = child.id();
let config = ProcessTraceConfig::default();
let result = attach(child_pid, config);
std::process::Command::new("kill").args(["-9", &child_pid.to_string()]).output().ok();
let _ = child.wait();
match result {
Ok(trace) => {
assert!(trace.is_attached());
assert_eq!(trace.pid(), child_pid);
let _ = detach(trace);
}
Err(TracerError::PermissionDenied(_)) => {
}
Err(e) => {
panic!("Unexpected error: {:?}", e);
}
}
}
#[test]
fn test_f002_attach_invalid_pid() {
let config = ProcessTraceConfig::default();
let result = attach(99999999, config);
assert!(
matches!(result, Err(TracerError::ProcessNotFound { .. })),
"Expected ProcessNotFound, got {:?}",
result
);
}
#[test]
fn test_f003_attach_no_permission() {
if nix::unistd::geteuid().is_root() {
eprintln!("Skipping F003: running as root");
return;
}
let config = ProcessTraceConfig::default();
let result = attach(1, config);
assert!(
matches!(
result,
Err(TracerError::PermissionDenied(_) | TracerError::ProcessNotFound { .. })
),
"Expected PermissionDenied or ProcessNotFound, got {:?}",
result
);
}
#[test]
fn test_f004_detach_releases() {
if !is_available() {
eprintln!("Skipping F004: no ptrace permissions");
return;
}
let trace = ProcessTrace {
pid: 12345,
config: ProcessTraceConfig::default(),
_start_time: Instant::now(),
events: Vec::new(),
baseline: None,
attached: false, _trace_count: Arc::new(AtomicU64::new(0)),
_rate_limit_window: Instant::now(),
};
let result = detach(trace);
assert!(result.is_ok());
}
#[test]
fn test_f005_collect_has_events() {
let events = vec![
SyscallEvent::new("read".to_string(), 0, Duration::from_micros(100), 1024),
SyscallEvent::new("write".to_string(), 1, Duration::from_micros(50), 10),
];
let result = TraceResult::new(1234, Duration::from_millis(10), events);
assert!(!result.events.is_empty(), "Events should not be empty");
assert_eq!(result.events.len(), 2);
}
#[test]
fn test_f006_collect_timeout() {
let config = ProcessTraceConfig::default().with_timeout(Duration::from_millis(50));
assert_eq!(config.timeout, Duration::from_millis(50));
}
#[test]
fn test_f007_collect_max_syscalls() {
let config = ProcessTraceConfig::default().with_max_syscalls(500);
assert_eq!(config.max_syscalls, 500);
}
#[test]
fn test_f008_stream_yields() {
fn assert_iterator<T: Iterator>() {}
assert_iterator::<SyscallStream>();
}
#[test]
fn test_f009_baseline_empty() {
let events: Vec<SyscallEvent> = vec![];
let baseline = compute_baseline(&events);
assert_eq!(baseline.sample_count, 0);
assert!(baseline.mean_us.is_empty());
}
#[test]
fn test_f010_zscore_zero_std() {
let mut baseline = SyscallBaseline::default();
baseline.mean_us.insert("read".to_string(), 100.0);
baseline.std_us.insert("read".to_string(), 0.0);
let event = SyscallEvent::new("read".to_string(), 0, Duration::from_micros(200), 0);
let z = zscore(&event, &baseline);
assert!(!z.is_nan(), "zscore should not return NaN for zero std");
assert_eq!(z, 0.0);
}
#[test]
fn test_f011_rate_limit() {
GLOBAL_TRACE_COUNT.store(0, Ordering::Relaxed);
GLOBAL_RATE_WINDOW.store(0, Ordering::Relaxed);
let config = ProcessTraceConfig::default().with_rate_limit(5);
for _ in 0..5 {
assert!(check_rate_limit(&config).is_ok());
}
let result = check_rate_limit(&config);
assert!(
matches!(result, Err(TracerError::RateLimitExceeded { .. })),
"Expected RateLimitExceeded, got {:?}",
result
);
}
#[test]
fn test_f012_double_attach() {
let err = TracerError::AlreadyTraced { pid: 1234 };
assert!(matches!(err, TracerError::AlreadyTraced { pid: 1234 }));
}
#[test]
fn test_f013_reattach() {
let mut trace = ProcessTrace {
pid: 12345,
config: ProcessTraceConfig::default(),
_start_time: Instant::now(),
events: Vec::new(),
baseline: None,
attached: true,
_trace_count: Arc::new(AtomicU64::new(0)),
_rate_limit_window: Instant::now(),
};
trace.attached = false;
assert!(!trace.is_attached());
}
#[test]
fn test_f014_process_exit() {
let err = TracerError::ProcessExited { pid: 1234 };
let msg = format!("{}", err);
assert!(msg.contains("1234"));
}
#[test]
fn test_f015_event_duration() {
let event = SyscallEvent::new("read".to_string(), 0, Duration::from_micros(100), 0);
assert!(event.duration.as_micros() > 0);
}
#[test]
fn test_f016_breakdown_sum() {
let events = vec![
SyscallEvent::new("mmap".to_string(), 9, Duration::from_micros(100), 0),
SyscallEvent::new("futex".to_string(), 202, Duration::from_micros(50), 0),
SyscallEvent::new("read".to_string(), 0, Duration::from_micros(75), 0),
SyscallEvent::new("write".to_string(), 1, Duration::from_micros(25), 0),
];
let total_us = 500;
let breakdown = SyscallBreakdown::from_events(&events, total_us);
let syscall_sum = breakdown.mmap_us
+ breakdown.futex_us
+ breakdown.ioctl_us
+ breakdown.read_us
+ breakdown.write_us
+ breakdown.other_us;
assert_eq!(syscall_sum + breakdown.compute_us, total_us, "Breakdown should sum to total");
}
#[test]
fn test_f017_result_zscore() {
let events = vec![SyscallEvent::new("read".to_string(), 0, Duration::from_micros(100), 0)];
let result = TraceResult::new(1234, Duration::from_millis(10), events);
assert!(!result.max_zscore.is_nan());
assert!(!result.max_zscore.is_infinite());
}
#[test]
fn test_f018_source_locations() {
let loc = SourceLocation {
file: "test.rs".to_string(),
line: 42,
function: Some("test_fn".to_string()),
};
assert_eq!(loc.file, "test.rs");
assert_eq!(loc.line, 42);
assert_eq!(loc.function, Some("test_fn".to_string()));
}
#[test]
fn test_f019_otlp_span() {
let events = vec![SyscallEvent::new("read".to_string(), 0, Duration::from_micros(100), 0)];
let result = TraceResult::new(1234, Duration::from_millis(10), events);
assert_eq!(result.pid, 1234);
}
#[test]
fn test_f020_error_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<TracerError>();
}
#[test]
fn test_categorize_syscall() {
assert_eq!(categorize_syscall("mmap"), "mmap");
assert_eq!(categorize_syscall("munmap"), "mmap");
assert_eq!(categorize_syscall("mprotect"), "mmap");
assert_eq!(categorize_syscall("futex"), "futex");
assert_eq!(categorize_syscall("ioctl"), "ioctl");
assert_eq!(categorize_syscall("read"), "read");
assert_eq!(categorize_syscall("pread64"), "read");
assert_eq!(categorize_syscall("write"), "write");
assert_eq!(categorize_syscall("pwrite64"), "write");
assert_eq!(categorize_syscall("unknown_syscall"), "other");
}
#[test]
fn test_syscall_name() {
assert_eq!(syscall_name(0), "read");
assert_eq!(syscall_name(1), "write");
assert_eq!(syscall_name(9), "mmap");
assert_eq!(syscall_name(202), "futex");
assert_eq!(syscall_name(99999), "unknown");
}
#[test]
fn test_config_builder() {
let config = ProcessTraceConfig::default()
.with_max_syscalls(500)
.with_timeout(Duration::from_millis(200))
.with_source(true)
.with_otlp("http://localhost:4317".to_string())
.with_rate_limit(50);
assert_eq!(config.max_syscalls, 500);
assert_eq!(config.timeout, Duration::from_millis(200));
assert!(config.enable_source);
assert_eq!(config.otlp_endpoint, Some("http://localhost:4317".to_string()));
assert_eq!(config.rate_limit, 50);
}
#[test]
fn test_syscall_baseline_from_events() {
let events = vec![
SyscallEvent::new("read".to_string(), 0, Duration::from_micros(100), 0),
SyscallEvent::new("read".to_string(), 0, Duration::from_micros(200), 0),
SyscallEvent::new("read".to_string(), 0, Duration::from_micros(150), 0),
SyscallEvent::new("write".to_string(), 1, Duration::from_micros(50), 0),
];
let baseline = compute_baseline(&events);
assert_eq!(baseline.sample_count, 4);
assert!(baseline.mean_us.contains_key("read"));
assert!(baseline.std_us.contains_key("read"));
let read_mean = baseline.mean_us.get("read").unwrap();
assert!((read_mean - 150.0).abs() < 0.1);
}
#[test]
fn test_zscore_calculation() {
let mut baseline = SyscallBaseline::default();
baseline.mean_us.insert("read".to_string(), 100.0);
baseline.std_us.insert("read".to_string(), 20.0);
let event = SyscallEvent::new("read".to_string(), 0, Duration::from_micros(140), 0);
let z = zscore(&event, &baseline);
assert!((z - 2.0).abs() < 0.1);
}
#[test]
fn test_trace_result_with_baseline() {
let events = vec![
SyscallEvent::new("read".to_string(), 0, Duration::from_micros(100), 0),
SyscallEvent::new("read".to_string(), 0, Duration::from_micros(500), 0), ];
let mut baseline = SyscallBaseline::default();
baseline.mean_us.insert("read".to_string(), 100.0);
baseline.std_us.insert("read".to_string(), 50.0);
baseline.sample_count = 100;
let result =
TraceResult::new(1234, Duration::from_millis(10), events).with_baseline(&baseline, 3.0);
assert!(!result.anomalies.is_empty());
assert!(result.max_zscore > 3.0);
}
#[test]
fn test_breakdown_efficiency() {
let events = vec![
SyscallEvent::new("read".to_string(), 0, Duration::from_micros(200), 0),
SyscallEvent::new("write".to_string(), 1, Duration::from_micros(100), 0),
];
let breakdown = SyscallBreakdown::from_events(&events, 1000);
assert_eq!(breakdown.read_us, 200);
assert_eq!(breakdown.write_us, 100);
assert_eq!(breakdown.compute_us, 700);
assert!((breakdown.efficiency() - 0.7).abs() < 0.01);
}
#[test]
fn test_breakdown_sorted_categories() {
let events = vec![
SyscallEvent::new("mmap".to_string(), 9, Duration::from_micros(500), 0),
SyscallEvent::new("read".to_string(), 0, Duration::from_micros(100), 0),
];
let breakdown = SyscallBreakdown::from_events(&events, 1000);
let sorted = breakdown.sorted_categories();
assert_eq!(sorted[0].0, "mmap");
assert_eq!(sorted[0].1, 500);
}
#[test]
fn test_is_available() {
let _ = is_available();
}
#[test]
fn test_baseline_has_category() {
let mut baseline = SyscallBaseline::default();
assert!(!baseline.has_category("read"));
baseline.mean_us.insert("read".to_string(), 100.0);
baseline.std_us.insert("read".to_string(), 10.0);
assert!(baseline.has_category("read"));
assert!(!baseline.has_category("write"));
}
#[test]
fn test_breakdown_default() {
let breakdown = SyscallBreakdown::default();
assert_eq!(breakdown.mmap_us, 0);
assert_eq!(breakdown.futex_us, 0);
assert_eq!(breakdown.ioctl_us, 0);
assert_eq!(breakdown.read_us, 0);
assert_eq!(breakdown.write_us, 0);
assert_eq!(breakdown.other_us, 0);
assert_eq!(breakdown.compute_us, 0);
assert_eq!(breakdown.syscall_count, 0);
assert_eq!(breakdown.total_us, 0);
}
#[test]
fn test_breakdown_efficiency_zero_total() {
let breakdown = SyscallBreakdown::default();
assert!((breakdown.efficiency() - 1.0).abs() < 0.01);
}
#[test]
fn test_breakdown_syscall_time_us() {
let mut breakdown = SyscallBreakdown::default();
breakdown.mmap_us = 100;
breakdown.futex_us = 50;
breakdown.ioctl_us = 30;
breakdown.read_us = 200;
breakdown.write_us = 150;
breakdown.other_us = 70;
assert_eq!(breakdown.syscall_time_us(), 600);
}
#[test]
fn test_trace_result_new() {
let events =
vec![SyscallEvent::new("read".to_string(), 0, Duration::from_micros(100), 1024)];
let result = TraceResult::new(12345, Duration::from_millis(10), events);
assert_eq!(result.pid, 12345);
assert_eq!(result.events.len(), 1);
assert_eq!(result.max_zscore, 0.0);
assert!(result.anomalies.is_empty());
}
#[test]
fn test_syscall_event_new() {
let event = SyscallEvent::new("mmap".to_string(), 9, Duration::from_micros(500), 0);
assert_eq!(event.syscall, "mmap");
assert_eq!(event.syscall_nr, 9);
assert_eq!(event.result, 0);
}
#[test]
fn test_source_location() {
let loc = SourceLocation {
file: "/home/test/main.rs".to_string(),
line: 42,
function: Some("main".to_string()),
};
assert_eq!(loc.file, "/home/test/main.rs");
assert_eq!(loc.line, 42);
assert_eq!(loc.function, Some("main".to_string()));
}
#[test]
fn test_syscall_anomaly() {
let anomaly = SyscallAnomaly {
syscall: "futex".to_string(),
duration_us: 50000,
zscore: 5.5,
expected_us: 1000.0,
category: "futex".to_string(),
};
assert_eq!(anomaly.syscall, "futex");
assert_eq!(anomaly.duration_us, 50000);
assert!((anomaly.zscore - 5.5).abs() < 0.01);
}
#[test]
fn test_process_trace_accessors() {
let trace = ProcessTrace {
pid: 9999,
config: ProcessTraceConfig::default(),
_start_time: Instant::now(),
events: vec![SyscallEvent::new("read".to_string(), 0, Duration::from_micros(100), 0)],
baseline: None,
attached: true,
_trace_count: Arc::new(AtomicU64::new(0)),
_rate_limit_window: Instant::now(),
};
assert_eq!(trace.pid(), 9999);
assert!(trace.is_attached());
assert_eq!(trace.event_count(), 1);
assert!(trace.baseline().is_none());
}
#[test]
fn test_process_trace_set_baseline() {
let mut trace = ProcessTrace {
pid: 9999,
config: ProcessTraceConfig::default(),
_start_time: Instant::now(),
events: Vec::new(),
baseline: None,
attached: true,
_trace_count: Arc::new(AtomicU64::new(0)),
_rate_limit_window: Instant::now(),
};
let mut baseline = SyscallBaseline::default();
baseline.mean_us.insert("read".to_string(), 100.0);
baseline.std_us.insert("read".to_string(), 20.0);
trace.set_baseline(baseline);
assert!(trace.baseline().is_some());
}
#[test]
fn test_categorize_syscall_all() {
assert_eq!(categorize_syscall("mmap"), "mmap");
assert_eq!(categorize_syscall("munmap"), "mmap");
assert_eq!(categorize_syscall("mprotect"), "mmap");
assert_eq!(categorize_syscall("brk"), "mmap");
assert_eq!(categorize_syscall("mremap"), "mmap");
assert_eq!(categorize_syscall("futex"), "futex");
assert_eq!(categorize_syscall("ioctl"), "ioctl");
assert_eq!(categorize_syscall("read"), "read");
assert_eq!(categorize_syscall("pread64"), "read");
assert_eq!(categorize_syscall("readv"), "read");
assert_eq!(categorize_syscall("preadv"), "read");
assert_eq!(categorize_syscall("preadv2"), "read");
assert_eq!(categorize_syscall("write"), "write");
assert_eq!(categorize_syscall("pwrite64"), "write");
assert_eq!(categorize_syscall("writev"), "write");
assert_eq!(categorize_syscall("pwritev"), "write");
assert_eq!(categorize_syscall("pwritev2"), "write");
assert_eq!(categorize_syscall("socket"), "other");
assert_eq!(categorize_syscall("connect"), "other");
}
#[test]
fn test_syscall_name_extended() {
assert_eq!(syscall_name(0), "read");
assert_eq!(syscall_name(1), "write");
assert_eq!(syscall_name(2), "open");
assert_eq!(syscall_name(3), "close");
assert_eq!(syscall_name(4), "stat");
assert_eq!(syscall_name(5), "fstat");
assert_eq!(syscall_name(6), "lstat");
assert_eq!(syscall_name(7), "poll");
assert_eq!(syscall_name(8), "lseek");
assert_eq!(syscall_name(9), "mmap");
assert_eq!(syscall_name(10), "mprotect");
assert_eq!(syscall_name(11), "munmap");
assert_eq!(syscall_name(12), "brk");
assert_eq!(syscall_name(13), "rt_sigaction");
assert_eq!(syscall_name(14), "rt_sigprocmask");
assert_eq!(syscall_name(15), "rt_sigreturn");
assert_eq!(syscall_name(16), "ioctl");
assert_eq!(syscall_name(17), "pread64");
assert_eq!(syscall_name(18), "pwrite64");
assert_eq!(syscall_name(19), "readv");
assert_eq!(syscall_name(20), "writev");
assert_eq!(syscall_name(21), "access");
assert_eq!(syscall_name(22), "pipe");
assert_eq!(syscall_name(23), "select");
assert_eq!(syscall_name(24), "sched_yield");
assert_eq!(syscall_name(25), "mremap");
assert_eq!(syscall_name(35), "nanosleep");
assert_eq!(syscall_name(56), "clone");
assert_eq!(syscall_name(57), "fork");
assert_eq!(syscall_name(58), "vfork");
assert_eq!(syscall_name(59), "execve");
assert_eq!(syscall_name(60), "exit");
assert_eq!(syscall_name(61), "wait4");
assert_eq!(syscall_name(62), "kill");
assert_eq!(syscall_name(202), "futex");
assert_eq!(syscall_name(9999), "unknown");
}
#[test]
fn test_breakdown_with_all_categories() {
let events = vec![
SyscallEvent::new("mmap".to_string(), 9, Duration::from_micros(100), 0),
SyscallEvent::new("munmap".to_string(), 11, Duration::from_micros(50), 0),
SyscallEvent::new("futex".to_string(), 202, Duration::from_micros(200), 0),
SyscallEvent::new("ioctl".to_string(), 16, Duration::from_micros(150), 0),
SyscallEvent::new("read".to_string(), 0, Duration::from_micros(80), 0),
SyscallEvent::new("pread64".to_string(), 17, Duration::from_micros(90), 0),
SyscallEvent::new("write".to_string(), 1, Duration::from_micros(70), 0),
SyscallEvent::new("writev".to_string(), 20, Duration::from_micros(60), 0),
SyscallEvent::new("socket".to_string(), 41, Duration::from_micros(40), 0),
];
let breakdown = SyscallBreakdown::from_events(&events, 2000);
assert_eq!(breakdown.mmap_us, 150); assert_eq!(breakdown.futex_us, 200);
assert_eq!(breakdown.ioctl_us, 150);
assert_eq!(breakdown.read_us, 170); assert_eq!(breakdown.write_us, 130); assert_eq!(breakdown.other_us, 40); assert_eq!(breakdown.syscall_count, 9);
assert_eq!(breakdown.compute_us, 1160);
}
#[test]
fn test_zscore_no_baseline() {
let baseline = SyscallBaseline::default();
let event = SyscallEvent::new("read".to_string(), 0, Duration::from_micros(100), 0);
let z = zscore(&event, &baseline);
assert_eq!(z, 0.0);
}
#[test]
fn test_trace_result_with_baseline_no_anomalies() {
let events = vec![SyscallEvent::new("read".to_string(), 0, Duration::from_micros(100), 0)];
let mut baseline = SyscallBaseline::default();
baseline.mean_us.insert("read".to_string(), 100.0);
baseline.std_us.insert("read".to_string(), 50.0);
baseline.sample_count = 100;
let result =
TraceResult::new(1234, Duration::from_millis(10), events).with_baseline(&baseline, 3.0);
assert!(result.anomalies.is_empty());
assert!((result.max_zscore - 0.0).abs() < 0.01);
}
#[test]
fn test_error_display() {
let errors = vec![
TracerError::PermissionDenied("test".to_string()),
TracerError::ProcessNotFound { pid: 1234 },
TracerError::AlreadyTraced { pid: 1234 },
TracerError::ProcessExited { pid: 1234 },
TracerError::Timeout(Duration::from_secs(1)),
TracerError::RateLimitExceeded { current: 200, limit: 100 },
TracerError::OtlpError("test".to_string()),
TracerError::DwarfError("test".to_string()),
TracerError::NotAttached,
];
for err in errors {
let msg = format!("{}", err);
assert!(!msg.is_empty());
}
}
}