1#[deny(clippy::pedantic, clippy::format_push_string)]
59#[allow(
61 clippy::cast_possible_truncation,
62 clippy::cast_possible_wrap,
63 clippy::cast_precision_loss,
64 clippy::missing_errors_doc,
65 clippy::missing_panics_doc,
66 clippy::must_use_candidate,
67 clippy::redundant_closure_for_method_calls,
68 clippy::struct_excessive_bools
69)]
70pub mod arch;
71pub mod args;
72pub mod style;
73pub mod syscall_info;
74
75use anyhow::{anyhow, Result};
76use comfy_table::modifiers::UTF8_ROUND_CORNERS;
77use comfy_table::presets::UTF8_BORDERS_ONLY;
78use comfy_table::CellAlignment::Right;
79use comfy_table::{Cell, ContentArrangement, Row, Table};
80use libc::user_regs_struct;
81use nix::sys::personality::{self, Persona};
82use nix::sys::ptrace::{self, Event};
83use nix::sys::signal::Signal;
84use nix::sys::wait::{wait, WaitStatus};
85use nix::unistd::Pid;
86use std::collections::HashMap;
87use std::fs;
88use std::io::Write;
89use std::os::unix::process::CommandExt;
90use std::process::{Command, Stdio};
91use std::time::{Duration, SystemTime};
92use style::StyleConfig;
93use syscalls::{Sysno, SysnoMap, SysnoSet};
94use uzers::get_user_by_name;
95
96use crate::args::{Args, Filter};
97use crate::syscall_info::{RetCode, SyscallInfo};
98
99const STRING_LIMIT: usize = 32;
100
101pub struct Tracer<W: Write> {
102 pid: Pid,
103 args: Args,
104 string_limit: Option<usize>,
105 filter: Filter,
106 syscalls_time: SysnoMap<Duration>,
107 syscalls_pass: SysnoMap<u64>,
108 syscalls_fail: SysnoMap<u64>,
109 style_config: StyleConfig,
110 output: W,
111}
112
113impl<W: Write> Tracer<W> {
114 pub fn new(pid: Pid, args: Args, output: W, style_config: StyleConfig) -> Result<Self> {
115 Ok(Self {
116 pid,
117 filter: args.create_filter()?,
118 string_limit: if args.no_abbrev {
119 None
120 } else {
121 Some(args.string_limit.unwrap_or(STRING_LIMIT))
122 },
123 args,
124 syscalls_time: SysnoMap::from_iter(
125 SysnoSet::all().iter().map(|v| (v, Duration::default())),
126 ),
127 syscalls_pass: SysnoMap::from_iter(SysnoSet::all().iter().map(|v| (v, 0))),
128 syscalls_fail: SysnoMap::from_iter(SysnoSet::all().iter().map(|v| (v, 0))),
129 style_config,
130 output,
131 })
132 }
133
134 pub fn set_output(&mut self, output: W) {
135 self.output = output;
136 }
137
138 #[allow(clippy::too_many_lines)]
139 pub fn run_tracer(&mut self) -> Result<()> {
140 let mut start_times = HashMap::<Pid, Option<SystemTime>>::new();
142 start_times.insert(self.pid, None);
143
144 let mut options_initialized = false;
145 let mut entry_regs = None;
146
147 loop {
148 let status = wait()?;
149
150 if !options_initialized {
151 if self.args.follow_forks {
152 arch::ptrace_init_options_fork(self.pid)?;
153 } else {
154 arch::ptrace_init_options(self.pid)?;
155 }
156 options_initialized = true;
157 }
158
159 match status {
160 WaitStatus::Stopped(pid, signal) => {
162 if signal == Signal::SIGTRAP {
171 self.log_standard_syscall(pid, None, None, None)?;
172 self.issue_ptrace_syscall_request(pid, None)?;
173 continue;
174 }
175
176 if signal == Signal::SIGSTOP {
180 if self.args.follow_forks {
181 start_times.insert(pid, None);
182
183 if !self.args.summary_only {
184 writeln!(&mut self.output, "Attaching to child {}", pid,)?;
185 }
186 }
187
188 self.issue_ptrace_syscall_request(pid, None)?;
189 continue;
190 }
191
192 if signal == Signal::SIGCHLD {
197 self.issue_ptrace_syscall_request(pid, Some(signal))?;
198 continue;
199 }
200
201 ptrace::cont(pid, signal)?;
205 }
206 WaitStatus::Exited(pid, _) => {
208 if self.pid == pid {
211 break;
212 } else {
213 continue;
214 };
215 }
216 WaitStatus::PtraceEvent(pid, _, code) => {
218 if code == Event::PTRACE_EVENT_EXIT as i32 && self.is_exit_syscall(pid)? {
221 self.log_standard_syscall(pid, None, None, None)?;
222 }
223
224 self.issue_ptrace_syscall_request(pid, None)?;
225 }
226 WaitStatus::PtraceSyscall(pid) => {
228 let event = ptrace::getevent(pid)? as u8;
233
234 let timestamp = Some(SystemTime::now());
237
238 if let Some(syscall_start_time) = start_times.get_mut(&pid) {
240 if event == 2 {
241 self.log_standard_syscall(
242 pid,
243 entry_regs,
244 *syscall_start_time,
245 timestamp,
246 )?;
247 *syscall_start_time = None;
248 } else {
249 *syscall_start_time = timestamp;
250 entry_regs = Some(self.get_registers(pid)?);
251 }
252 } else {
253 return Err(anyhow!("Unable to get start time for tracee {}", pid));
254 }
255
256 self.issue_ptrace_syscall_request(pid, None)?;
257 }
258 WaitStatus::Signaled(pid, signal, coredump) => {
260 writeln!(
261 &mut self.output,
262 "Child {} terminated by signal {} {}",
263 pid,
264 signal,
265 if coredump { "(core dumped)" } else { "" }
266 )?;
267 break;
268 }
269 WaitStatus::Continued(_) | WaitStatus::StillAlive => {
272 continue;
273 }
274 }
275 }
276
277 if !self.args.json && (self.args.summary_only || self.args.summary) {
278 if !self.args.summary_only {
279 writeln!(&mut self.output)?;
281 }
282 self.report_summary()?;
283 }
284
285 Ok(())
286 }
287
288 pub fn report_summary(&mut self) -> Result<()> {
289 let headers = vec!["% time", "time", "time/call", "calls", "errors", "syscall"];
290 let mut table = Table::new();
291 table
292 .load_preset(UTF8_BORDERS_ONLY)
293 .apply_modifier(UTF8_ROUND_CORNERS)
294 .set_content_arrangement(ContentArrangement::Dynamic)
295 .set_header(&headers);
296
297 for i in 0..headers.len() {
298 table.column_mut(i).unwrap().set_cell_alignment(Right);
299 }
300
301 let mut sorted_sysno: Vec<_> = self.filter.all_enabled().iter().collect();
302 sorted_sysno.sort_by_key(|k| k.name());
303 let t_time: Duration = self.syscalls_time.values().sum();
304
305 for sysno in sorted_sysno {
306 let (Some(pass), Some(fail), Some(time)) = (
307 self.syscalls_pass.get(sysno),
308 self.syscalls_fail.get(sysno),
309 self.syscalls_time.get(sysno),
310 ) else {
311 continue;
312 };
313
314 let calls = pass + fail;
315 if calls == 0 {
316 continue;
317 }
318
319 let time_percent = if !t_time.is_zero() {
320 time.as_secs_f32() / t_time.as_secs_f32() * 100f32
321 } else {
322 0f32
323 };
324
325 table.add_row(vec![
326 Cell::new(format!("{time_percent:.1}%")),
327 Cell::new(format!("{}µs", time.as_micros())),
328 Cell::new(format!("{:.1}ns", time.as_nanos() as f64 / calls as f64)),
329 Cell::new(format!("{calls}")),
330 Cell::new(format!("{fail}")),
331 Cell::new(sysno.name()),
332 ]);
333 }
334
335 let failed = self.syscalls_fail.values().sum::<u64>();
337 let calls: u64 = self.syscalls_pass.values().sum::<u64>() + failed;
338 let totals: Row = vec![
339 Cell::new("100%"),
340 Cell::new(format!("{}µs", t_time.as_micros())),
341 Cell::new(format!("{:.1}ns", t_time.as_nanos() as f64 / calls as f64)),
342 Cell::new(calls),
343 Cell::new(failed.to_string()),
344 Cell::new("total"),
345 ]
346 .into();
347
348 let divider_row: Vec<String> = table
353 .column_max_content_widths()
354 .iter()
355 .copied()
356 .enumerate()
357 .map(|(idx, val)| {
358 let cell_at_idx = totals.cell_iter().nth(idx).unwrap();
359 (val as usize).max(cell_at_idx.content().len())
360 })
361 .map(|v| str::repeat("-", v))
362 .collect();
363 table.add_row(divider_row);
364 table.add_row(totals);
365
366 if !self.args.summary_only {
367 writeln!(&mut self.output)?;
369 }
370 writeln!(&mut self.output, "{table}")?;
371
372 Ok(())
373 }
374
375 fn log_standard_syscall(
376 &mut self,
377 pid: Pid,
378 entry_regs: Option<user_regs_struct>,
379 syscall_start_time: Option<SystemTime>,
380 syscall_end_time: Option<SystemTime>,
381 ) -> Result<()> {
382 let register_data = self.parse_register_data(pid);
383 if let Err(e) = register_data {
384 eprintln!("{e}");
385 return Ok(());
386 }
387 let (syscall_number, registers) = register_data.unwrap();
388
389 let ret_code = match syscall_number {
393 Sysno::exit | Sysno::exit_group => RetCode::from_raw(0),
394 _ => {
395 #[cfg(target_arch = "x86_64")]
396 let code = RetCode::from_raw(registers.rax);
397 #[cfg(target_arch = "riscv64")]
398 let code = RetCode::from_raw(registers.a7);
399 #[cfg(target_arch = "aarch64")]
400 let code = RetCode::from_raw(registers.regs[0]);
401 match code {
402 RetCode::Err(_) => self.syscalls_fail[syscall_number] += 1,
403 _ => self.syscalls_pass[syscall_number] += 1,
404 }
405 code
406 }
407 };
408
409 let registers = entry_regs.unwrap_or(registers);
410
411 if self.filter.matches(syscall_number, ret_code) {
412 let elapsed = syscall_start_time.map_or(Duration::default(), |start_time| {
413 let end_time = syscall_end_time.unwrap_or(SystemTime::now());
414 end_time.duration_since(start_time).unwrap_or_default()
415 });
416
417 if syscall_start_time.is_some() {
418 self.syscalls_time[syscall_number] += elapsed;
419 }
420
421 if !self.args.summary_only {
422 let info = SyscallInfo::new(pid, syscall_number, ret_code, registers, elapsed);
423 self.write_syscall_info(&info)?;
424 }
425 }
426
427 Ok(())
428 }
429
430 fn write_syscall_info(&mut self, info: &SyscallInfo) -> Result<()> {
431 if self.args.json {
432 let json = serde_json::to_string(&info)?;
433 Ok(writeln!(&mut self.output, "{json}")?)
434 } else {
435 info.write_syscall(
436 self.style_config.clone(),
437 self.string_limit,
438 self.args.syscall_number,
439 self.args.syscall_times,
440 &mut self.output,
441 )
442 }
443 }
444
445 fn issue_ptrace_syscall_request(&self, pid: Pid, signal: Option<Signal>) -> Result<()> {
447 ptrace::syscall(pid, signal)
448 .map_err(|_| anyhow!("Unable to issue a PTRACE_SYSCALL request in tracee {}", pid))
449 }
450
451 fn get_registers(&self, pid: Pid) -> Result<user_regs_struct> {
453 ptrace::getregs(pid).map_err(|_| anyhow!("Unable to get registers from tracee {}", pid))
454 }
455
456 fn get_syscall(&self, registers: user_regs_struct) -> Result<Sysno> {
457 #[cfg(target_arch = "x86_64")]
458 let reg = registers.orig_rax;
459 #[cfg(target_arch = "riscv64")]
460 let reg = registers.a7;
461 #[cfg(target_arch = "aarch64")]
462 let reg = registers.regs[8];
463
464 Ok(u32::try_from(reg)
465 .map_err(|_| anyhow!("Invalid syscall number {reg}"))?
466 .into())
467 }
468
469 fn parse_register_data(&self, pid: Pid) -> Result<(Sysno, user_regs_struct)> {
471 let registers = self.get_registers(pid)?;
472 let syscall_number = self.get_syscall(registers)?;
473
474 Ok((syscall_number, registers))
475 }
476
477 fn is_exit_syscall(&self, pid: Pid) -> Result<bool> {
478 self.get_registers(pid).map(|registers| {
479 #[cfg(target_arch = "x86_64")]
480 let reg = registers.orig_rax;
481 #[cfg(target_arch = "riscv64")]
482 let reg = registers.a7;
483 #[cfg(target_arch = "aarch64")]
484 let reg = registers.regs[8];
485 reg == Sysno::exit as u64 || reg == Sysno::exit_group as u64
486 })
487 }
488}
489
490pub fn run_tracee(command: &[String], envs: &[String], username: &Option<String>) -> Result<()> {
491 ptrace::traceme()?;
492 personality::set(Persona::ADDR_NO_RANDOMIZE)
493 .map_err(|_| anyhow!("Unable to set ADDR_NO_RANDOMIZE"))?;
494 let mut binary = command
495 .first()
496 .ok_or_else(|| anyhow!("No command"))?
497 .to_string();
498 if let Ok(bin) = fs::canonicalize(&binary) {
499 binary = bin
500 .to_str()
501 .ok_or_else(|| anyhow!("Invalid binary path"))?
502 .to_string()
503 }
504 let mut cmd = Command::new(binary);
505 cmd.args(command[1..].iter()).stdout(Stdio::null());
506
507 for token in envs {
508 let mut parts = token.splitn(2, '=');
509 match (parts.next(), parts.next()) {
510 (Some(key), Some(value)) => cmd.env(key, value),
511 (Some(key), None) => cmd.env_remove(key),
512 _ => unreachable!(),
513 };
514 }
515
516 if let Some(username) = username {
517 if let Some(user) = get_user_by_name(username) {
518 cmd.uid(user.uid());
519 }
520 }
521
522 let _ = cmd.exec();
523
524 Ok(())
525}