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