#[deny(clippy::pedantic, clippy::format_push_string)]
#[allow(
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_precision_loss,
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::must_use_candidate,
clippy::redundant_closure_for_method_calls,
clippy::struct_excessive_bools
)]
pub mod arch;
pub mod args;
pub mod style;
pub mod syscall_info;
use anyhow::{anyhow, Result};
use comfy_table::modifiers::UTF8_ROUND_CORNERS;
use comfy_table::presets::UTF8_BORDERS_ONLY;
use comfy_table::CellAlignment::Right;
use comfy_table::{Cell, ContentArrangement, Row, Table};
use libc::user_regs_struct;
use nix::sys::personality::{self, Persona};
use nix::sys::ptrace::{self, Event};
use nix::sys::signal::Signal;
use nix::sys::wait::{wait, WaitStatus};
use nix::unistd::Pid;
use std::collections::HashMap;
use std::fs;
use std::io::Write;
use std::os::unix::process::CommandExt;
use std::process::{Command, Stdio};
use std::time::{Duration, SystemTime};
use style::StyleConfig;
use syscalls::{Sysno, SysnoMap, SysnoSet};
use uzers::get_user_by_name;
use crate::args::{Args, Filter};
use crate::syscall_info::{RetCode, SyscallArgs, SyscallInfo};
const STRING_LIMIT: usize = 32;
pub struct Tracer<W: Write> {
pid: Pid,
args: Args,
string_limit: Option<usize>,
filter: Filter,
syscalls_time: SysnoMap<Duration>,
syscalls_pass: SysnoMap<u64>,
syscalls_fail: SysnoMap<u64>,
style_config: StyleConfig,
output: W,
exec_retry_counts: std::collections::HashMap<Pid, usize>,
}
impl<W: Write> Tracer<W> {
pub fn new(pid: Pid, args: Args, output: W, style_config: StyleConfig) -> Result<Self> {
Ok(Self {
pid,
filter: args.create_filter()?,
string_limit: if args.no_abbrev {
None
} else {
Some(args.string_limit.unwrap_or(STRING_LIMIT))
},
args,
syscalls_time: SysnoMap::from_iter(
SysnoSet::all().iter().map(|v| (v, Duration::default())),
),
syscalls_pass: SysnoMap::from_iter(SysnoSet::all().iter().map(|v| (v, 0))),
syscalls_fail: SysnoMap::from_iter(SysnoSet::all().iter().map(|v| (v, 0))),
style_config,
output,
exec_retry_counts: HashMap::new(),
})
}
pub fn set_output(&mut self, output: W) {
self.output = output;
}
#[allow(clippy::too_many_lines)]
pub fn run_tracer(&mut self) -> Result<()> {
let mut start_times = HashMap::<Pid, Option<SystemTime>>::new();
let mut pending_args = HashMap::<Pid, Option<SyscallArgs>>::new();
start_times.insert(self.pid, None);
pending_args.insert(self.pid, None);
let mut options_initialized = false;
let mut entry_regs = None;
loop {
let status = wait()?;
if !options_initialized {
if self.args.follow_forks {
arch::ptrace_init_options_fork(self.pid)?;
} else {
arch::ptrace_init_options(self.pid)?;
}
options_initialized = true;
}
match status {
WaitStatus::Stopped(pid, signal) => {
if signal == Signal::SIGTRAP {
let regs = entry_regs.take();
let pre = pending_args.remove(&pid).unwrap_or(None);
if regs.is_none() {
if let Ok(cur_regs) = self.get_registers(pid) {
if let Ok(sysno) = self.get_syscall(cur_regs) {
if sysno == Sysno::execve || sysno == Sysno::execveat {
let args_now = arch::parse_args(pid, sysno, cur_regs);
self.log_standard_syscall(
pid,
Some(cur_regs),
Some(args_now),
None,
None,
)?;
self.issue_ptrace_syscall_request(pid, None)?;
continue;
}
}
}
}
self.log_standard_syscall(pid, regs, pre, None, None)?;
self.issue_ptrace_syscall_request(pid, None)?;
continue;
}
if signal == Signal::SIGSTOP {
if self.args.follow_forks {
start_times.insert(pid, None);
if !self.args.summary_only {
writeln!(&mut self.output, "Attaching to child {}", pid,)?;
}
}
self.issue_ptrace_syscall_request(pid, None)?;
continue;
}
if signal == Signal::SIGCHLD {
self.issue_ptrace_syscall_request(pid, Some(signal))?;
continue;
}
ptrace::cont(pid, signal)?;
}
WaitStatus::Exited(pid, _) => {
if self.pid == pid {
break;
} else {
continue;
};
}
WaitStatus::PtraceEvent(pid, _, code) => {
if code == Event::PTRACE_EVENT_EXEC as i32 {
let pre = pending_args.remove(&pid).unwrap_or(None);
if let Some(args_now) = pre {
self.log_exec_event(pid, args_now)?;
} else if let Ok(regs) = self.get_registers(pid) {
if let Ok(sysno) = self.get_syscall(regs) {
if sysno == Sysno::execve || sysno == Sysno::execveat {
let args_now = arch::parse_args(pid, sysno, regs);
self.log_exec_event(pid, args_now)?;
}
}
}
}
if code == Event::PTRACE_EVENT_EXIT as i32 && self.is_exit_syscall(pid)? {
let pre = pending_args.remove(&pid).unwrap_or(None);
self.log_standard_syscall(pid, None, pre, None, None)?;
}
self.issue_ptrace_syscall_request(pid, None)?;
}
WaitStatus::PtraceSyscall(pid) => {
let event = ptrace::getevent(pid)? as u8;
let timestamp = Some(SystemTime::now());
if let Some(syscall_start_time) = start_times.get_mut(&pid) {
if event == 2 {
let pre = pending_args.remove(&pid).unwrap_or(None);
self.log_standard_syscall(
pid,
entry_regs,
pre,
*syscall_start_time,
timestamp,
)?;
*syscall_start_time = None;
} else {
*syscall_start_time = timestamp;
let regs = self.get_registers(pid)?;
entry_regs = Some(regs);
if let Ok(sysno) = self.get_syscall(regs) {
if sysno == Sysno::execve || sysno == Sysno::execveat {
let args = arch::parse_args(pid, sysno, regs);
pending_args.insert(pid, Some(args));
} else {
pending_args.insert(pid, None);
}
}
}
} else {
return Err(anyhow!("Unable to get start time for tracee {}", pid));
}
self.issue_ptrace_syscall_request(pid, None)?;
}
WaitStatus::Signaled(pid, signal, coredump) => {
writeln!(
&mut self.output,
"Child {} terminated by signal {} {}",
pid,
signal,
if coredump { "(core dumped)" } else { "" }
)?;
break;
}
WaitStatus::Continued(_) | WaitStatus::StillAlive => {
continue;
}
}
}
if !self.args.json && (self.args.summary_only || self.args.summary) {
if !self.args.summary_only {
writeln!(&mut self.output)?;
}
self.report_summary()?;
}
Ok(())
}
pub fn report_summary(&mut self) -> Result<()> {
let headers = vec!["% time", "time", "time/call", "calls", "errors", "syscall"];
let mut table = Table::new();
table
.load_preset(UTF8_BORDERS_ONLY)
.apply_modifier(UTF8_ROUND_CORNERS)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(&headers);
for i in 0..headers.len() {
table.column_mut(i).unwrap().set_cell_alignment(Right);
}
let mut sorted_sysno: Vec<_> = self.filter.all_enabled().iter().collect();
sorted_sysno.sort_by_key(|k| k.name());
let t_time: Duration = self.syscalls_time.values().sum();
for sysno in sorted_sysno {
let (Some(pass), Some(fail), Some(time)) = (
self.syscalls_pass.get(sysno),
self.syscalls_fail.get(sysno),
self.syscalls_time.get(sysno),
) else {
continue;
};
let calls = pass + fail;
if calls == 0 {
continue;
}
let time_percent = if !t_time.is_zero() {
time.as_secs_f32() / t_time.as_secs_f32() * 100f32
} else {
0f32
};
table.add_row(vec![
Cell::new(format!("{time_percent:.1}%")),
Cell::new(format!("{}µs", time.as_micros())),
Cell::new(format!("{:.1}ns", time.as_nanos() as f64 / calls as f64)),
Cell::new(format!("{calls}")),
Cell::new(format!("{fail}")),
Cell::new(sysno.name()),
]);
}
let failed = self.syscalls_fail.values().sum::<u64>();
let calls: u64 = self.syscalls_pass.values().sum::<u64>() + failed;
let totals: Row = vec![
Cell::new("100%"),
Cell::new(format!("{}µs", t_time.as_micros())),
Cell::new(format!("{:.1}ns", t_time.as_nanos() as f64 / calls as f64)),
Cell::new(calls),
Cell::new(failed.to_string()),
Cell::new("total"),
]
.into();
let divider_row: Vec<String> = table
.column_max_content_widths()
.iter()
.copied()
.enumerate()
.map(|(idx, val)| {
let cell_at_idx = totals.cell_iter().nth(idx).unwrap();
(val as usize).max(cell_at_idx.content().len())
})
.map(|v| str::repeat("-", v))
.collect();
table.add_row(divider_row);
table.add_row(totals);
if !self.args.summary_only {
writeln!(&mut self.output)?;
}
writeln!(&mut self.output, "{table}")?;
Ok(())
}
fn log_standard_syscall(
&mut self,
pid: Pid,
entry_regs: Option<user_regs_struct>,
pre_parsed_args: Option<SyscallArgs>,
syscall_start_time: Option<SystemTime>,
syscall_end_time: Option<SystemTime>,
) -> Result<()> {
let register_data = self.parse_register_data(pid);
if let Err(e) = register_data {
eprintln!("{e}");
return Ok(());
}
let (syscall_number, registers) = register_data.unwrap();
let ret_code = match syscall_number {
Sysno::exit | Sysno::exit_group => RetCode::from_raw(0),
_ => {
#[cfg(target_arch = "x86_64")]
let code = RetCode::from_raw(registers.rax);
#[cfg(target_arch = "riscv64")]
let code = RetCode::from_raw(registers.a7);
#[cfg(target_arch = "aarch64")]
let code = RetCode::from_raw(registers.regs[0]);
match code {
RetCode::Err(_) => self.syscalls_fail[syscall_number] += 1,
_ => self.syscalls_pass[syscall_number] += 1,
}
code
}
};
let registers = entry_regs.unwrap_or(registers);
if self.args.collapse_exec_retries
&& (syscall_number == Sysno::execve || syscall_number == Sysno::execveat)
{
if let RetCode::Err(errno) = ret_code {
if errno == -2 {
let counter = self.exec_retry_counts.entry(pid).or_default();
*counter += 1;
return Ok(());
}
} else {
if let Some(count) = self.exec_retry_counts.remove(&pid) {
if count > 0 {
writeln!(
&mut self.output,
"[{}] execve: collapsed {} failed attempts",
pid, count
)?;
}
}
}
}
if self.filter.matches(syscall_number, ret_code) {
let elapsed = syscall_start_time.map_or(Duration::default(), |start_time| {
let end_time = syscall_end_time.unwrap_or(SystemTime::now());
end_time.duration_since(start_time).unwrap_or_default()
});
if syscall_start_time.is_some() {
self.syscalls_time[syscall_number] += elapsed;
}
if !self.args.summary_only {
let args = pre_parsed_args
.unwrap_or_else(|| arch::parse_args(pid, syscall_number, registers));
let info = SyscallInfo {
typ: "SYSCALL",
pid,
syscall: syscall_number,
args,
result: ret_code,
duration: elapsed,
};
self.write_syscall_info(&info)?;
}
}
Ok(())
}
fn log_exec_event(&mut self, pid: Pid, args: SyscallArgs) -> Result<()> {
let info = SyscallInfo {
typ: "SYSCALL",
pid,
syscall: Sysno::execve,
args,
result: RetCode::Ok(0),
duration: Duration::default(),
};
self.write_syscall_info(&info)?;
Ok(())
}
fn write_syscall_info(&mut self, info: &SyscallInfo) -> Result<()> {
if self.args.json {
let json = serde_json::to_string(&info)?;
Ok(writeln!(&mut self.output, "{json}")?)
} else {
info.write_syscall(
self.style_config.clone(),
self.string_limit,
self.args.syscall_number,
self.args.syscall_times,
&mut self.output,
)
}
}
fn issue_ptrace_syscall_request(&self, pid: Pid, signal: Option<Signal>) -> Result<()> {
ptrace::syscall(pid, signal)
.map_err(|_| anyhow!("Unable to issue a PTRACE_SYSCALL request in tracee {}", pid))
}
fn get_registers(&self, pid: Pid) -> Result<user_regs_struct> {
ptrace::getregs(pid).map_err(|_| anyhow!("Unable to get registers from tracee {}", pid))
}
fn get_syscall(&self, registers: user_regs_struct) -> Result<Sysno> {
#[cfg(target_arch = "x86_64")]
let reg = registers.orig_rax;
#[cfg(target_arch = "riscv64")]
let reg = registers.a7;
#[cfg(target_arch = "aarch64")]
let reg = registers.regs[8];
Ok(u32::try_from(reg)
.map_err(|_| anyhow!("Invalid syscall number {reg}"))?
.into())
}
fn parse_register_data(&self, pid: Pid) -> Result<(Sysno, user_regs_struct)> {
let registers = self.get_registers(pid)?;
let syscall_number = self.get_syscall(registers)?;
Ok((syscall_number, registers))
}
fn is_exit_syscall(&self, pid: Pid) -> Result<bool> {
self.get_registers(pid).map(|registers| {
#[cfg(target_arch = "x86_64")]
let reg = registers.orig_rax;
#[cfg(target_arch = "riscv64")]
let reg = registers.a7;
#[cfg(target_arch = "aarch64")]
let reg = registers.regs[8];
reg == Sysno::exit as u64 || reg == Sysno::exit_group as u64
})
}
}
pub fn run_tracee(command: &[String], envs: &[String], username: &Option<String>) -> Result<()> {
ptrace::traceme()?;
if std::env::var_os("LURK_DISABLE_SIGSTOP").is_none() {
nix::sys::signal::raise(Signal::SIGSTOP).map_err(|_| anyhow!("Unable to raise SIGSTOP"))?;
}
personality::set(Persona::ADDR_NO_RANDOMIZE)
.map_err(|_| anyhow!("Unable to set ADDR_NO_RANDOMIZE"))?;
let mut binary = command
.first()
.ok_or_else(|| anyhow!("No command"))?
.to_string();
if let Ok(bin) = fs::canonicalize(&binary) {
binary = bin
.to_str()
.ok_or_else(|| anyhow!("Invalid binary path"))?
.to_string()
}
let mut cmd = Command::new(binary);
cmd.args(command[1..].iter()).stdout(Stdio::null());
for token in envs {
let mut parts = token.splitn(2, '=');
match (parts.next(), parts.next()) {
(Some(key), Some(value)) => cmd.env(key, value),
(Some(key), None) => cmd.env_remove(key),
_ => unreachable!(),
};
}
if let Some(username) = username {
if let Some(user) = get_user_by_name(username) {
cmd.uid(user.uid());
}
}
let _ = cmd.exec();
Ok(())
}