Skip to main content

stracers_core/
tracer.rs

1use std::collections::{HashMap, HashSet};
2use std::ffi::CString;
3use std::io;
4
5use nix::sys::ptrace;
6use nix::sys::wait::WaitStatus;
7use nix::unistd::{ForkResult, Pid, execvp, fork};
8
9use crate::arch;
10use crate::event::SyscallEvent;
11use crate::platform;
12
13/// Options for the tracer.
14pub struct TracerOptions {
15    /// Follow child processes created by fork/vfork/clone.
16    pub follow_forks: bool,
17}
18
19impl Default for TracerOptions {
20    fn default() -> Self {
21        Self {
22            follow_forks: false,
23        }
24    }
25}
26
27pub struct Tracer {
28    /// The initial tracee PID (either spawned or attached).
29    root_pid: Pid,
30    /// All currently-traced PIDs.
31    traced: HashSet<i32>,
32    /// Whether each PID is currently inside a syscall (entry seen, exit pending).
33    in_syscall: HashMap<i32, bool>,
34    /// Syscall entry events waiting for the corresponding exit.
35    pending_entry: HashMap<i32, SyscallEvent>,
36    /// Whether we attached to an existing process (vs spawned).
37    attached: bool,
38    options: TracerOptions,
39}
40
41impl Tracer {
42    /// Fork a child process, set up ptrace, and exec the given command.
43    pub fn spawn(command: &[String], options: TracerOptions) -> io::Result<Self> {
44        if command.is_empty() {
45            return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty command"));
46        }
47
48        let c_args: Vec<CString> = command
49            .iter()
50            .map(|s| CString::new(s.as_str()).unwrap())
51            .collect();
52
53        match unsafe { fork() }.map_err(|e| io::Error::from_raw_os_error(e as i32))? {
54            ForkResult::Child => {
55                platform::traceme()?;
56                execvp(&c_args[0], &c_args)
57                    .map_err(|e| io::Error::from_raw_os_error(e as i32))?;
58                unreachable!()
59            }
60            ForkResult::Parent { child } => {
61                // Wait for the initial SIGTRAP from the child's exec.
62                platform::wait(child)?;
63                platform::set_options(child, Self::ptrace_options(&options))?;
64                platform::syscall_continue(child, None)?;
65
66                let mut traced = HashSet::new();
67                traced.insert(child.as_raw());
68
69                Ok(Tracer {
70                    root_pid: child,
71                    traced,
72                    in_syscall: HashMap::new(),
73                    pending_entry: HashMap::new(),
74                    attached: false,
75                    options,
76                })
77            }
78        }
79    }
80
81    /// Attach to an already-running process.
82    pub fn attach(pid: i32, options: TracerOptions) -> io::Result<Self> {
83        let target = Pid::from_raw(pid);
84        platform::attach(target)?;
85        // Wait for the SIGSTOP delivered by PTRACE_ATTACH.
86        platform::wait(target)?;
87        platform::set_options(target, Self::ptrace_options(&options))?;
88        platform::syscall_continue(target, None)?;
89
90        let mut traced = HashSet::new();
91        traced.insert(pid);
92
93        Ok(Tracer {
94            root_pid: target,
95            traced,
96            in_syscall: HashMap::new(),
97            pending_entry: HashMap::new(),
98            attached: true,
99            options,
100        })
101    }
102
103    fn ptrace_options(opts: &TracerOptions) -> ptrace::Options {
104        let mut flags = ptrace::Options::PTRACE_O_TRACESYSGOOD
105            | ptrace::Options::PTRACE_O_TRACEEXEC;
106        if opts.follow_forks {
107            flags |= ptrace::Options::PTRACE_O_TRACEFORK
108                | ptrace::Options::PTRACE_O_TRACEVFORK
109                | ptrace::Options::PTRACE_O_TRACECLONE;
110        }
111        flags
112    }
113
114    /// Run the ptrace loop until all tracees exit.
115    /// Calls `on_event` for each completed syscall (after both entry and exit).
116    /// Returns the root tracee's exit code.
117    pub fn run<F: FnMut(&SyscallEvent)>(&mut self, mut on_event: F) -> io::Result<i32> {
118        let mut root_exit_code: Option<i32> = None;
119
120        loop {
121            if self.traced.is_empty() {
122                return Ok(root_exit_code.unwrap_or(0));
123            }
124
125            // Wait for any traced child when following forks, otherwise wait for root.
126            let status = if self.options.follow_forks {
127                platform::wait_any()
128            } else {
129                platform::wait(self.root_pid)
130            };
131
132            let status = match status {
133                Ok(s) => s,
134                Err(e) if e.raw_os_error() == Some(10) /* ECHILD */ => {
135                    return Ok(root_exit_code.unwrap_or(0));
136                }
137                Err(e) => return Err(e),
138            };
139
140            match status {
141                WaitStatus::PtraceSyscall(pid) => {
142                    let regs = platform::get_registers(pid)?;
143                    let raw_pid = pid.as_raw();
144
145                    if !self.in_syscall.get(&raw_pid).copied().unwrap_or(false) {
146                        // Syscall entry
147                        self.in_syscall.insert(raw_pid, true);
148                        let (number, args) = arch::extract_syscall_entry(&regs);
149                        let name = arch::syscall_name(number);
150                        let decoded_args = arch::decode_entry_args(pid, number, &args);
151                        let event = SyscallEvent {
152                            pid: raw_pid,
153                            number,
154                            name,
155                            args,
156                            ret: None,
157                            decoded_args,
158                        };
159                        self.pending_entry.insert(raw_pid, event);
160                    } else {
161                        // Syscall exit
162                        self.in_syscall.insert(raw_pid, false);
163                        if let Some(mut event) = self.pending_entry.remove(&raw_pid) {
164                            let ret = arch::extract_return_value(&regs);
165                            event.ret = Some(ret);
166                            arch::decode_exit_args(
167                                pid,
168                                event.number,
169                                &event.args,
170                                ret,
171                                &mut event.decoded_args,
172                            );
173                            on_event(&event);
174                        }
175                    }
176                    platform::syscall_continue(pid, None)?;
177                }
178                WaitStatus::PtraceEvent(pid, _sig, event) => {
179                    if event == nix::libc::PTRACE_EVENT_FORK as i32
180                        || event == nix::libc::PTRACE_EVENT_VFORK as i32
181                        || event == nix::libc::PTRACE_EVENT_CLONE as i32
182                    {
183                        // A new child was created.
184                        if let Ok(new_pid) = platform::get_event(pid) {
185                            let new_pid_raw = new_pid as i32;
186                            self.traced.insert(new_pid_raw);
187                            let new_pid_nix = Pid::from_raw(new_pid_raw);
188                            // Wait for the new child to stop, then configure and resume.
189                            let _ = platform::wait(new_pid_nix);
190                            let _ = platform::set_options(
191                                new_pid_nix,
192                                Self::ptrace_options(&self.options),
193                            );
194                            let _ = platform::syscall_continue(new_pid_nix, None);
195                        }
196                    } else if event == nix::libc::PTRACE_EVENT_EXEC as i32 {
197                        // After PTRACE_EVENT_EXEC, the kernel still delivers a
198                        // syscall-exit-stop for the execve. Keep in_syscall=true
199                        // so that exit is correctly paired with the pending entry.
200                    }
201                    platform::syscall_continue(pid, None)?;
202                }
203                WaitStatus::Exited(pid, code) => {
204                    let raw_pid = pid.as_raw();
205                    self.traced.remove(&raw_pid);
206                    self.in_syscall.remove(&raw_pid);
207                    self.pending_entry.remove(&raw_pid);
208                    if pid == self.root_pid {
209                        root_exit_code = Some(code);
210                    }
211                    if !self.options.follow_forks || self.traced.is_empty() {
212                        return Ok(root_exit_code.unwrap_or(code));
213                    }
214                }
215                WaitStatus::Signaled(pid, sig, _core_dumped) => {
216                    let raw_pid = pid.as_raw();
217                    self.traced.remove(&raw_pid);
218                    self.in_syscall.remove(&raw_pid);
219                    self.pending_entry.remove(&raw_pid);
220                    if pid == self.root_pid {
221                        root_exit_code = Some(128 + sig as i32);
222                    }
223                    if !self.options.follow_forks || self.traced.is_empty() {
224                        return Ok(root_exit_code.unwrap_or(128 + sig as i32));
225                    }
226                }
227                WaitStatus::Stopped(pid, sig) => {
228                    // Real signal — forward it to the tracee.
229                    platform::syscall_continue(pid, Some(sig))?;
230                }
231                _ => {
232                    // StillAlive or other — shouldn't happen with blocking wait.
233                }
234            }
235        }
236    }
237}
238
239impl Drop for Tracer {
240    fn drop(&mut self) {
241        // If we attached, try to detach cleanly from any remaining tracees.
242        if self.attached {
243            for &pid in &self.traced {
244                let _ = platform::detach(Pid::from_raw(pid), None);
245            }
246        }
247    }
248}