Skip to main content

proot/
tracer.rs

1//! The ptrace event loop.  Runs on the thread that forked the first tracee.
2//!
3//! Tracees are seized, so stops are unambiguous: seccomp-trace stops for the
4//! tier-two syscalls, exec events (to keep `/proc/self/exe` bookkeeping
5//! honest), clone/fork events (to copy bookkeeping to the child), and
6//! `PTRACE_EVENT_STOP` for group-stops and freshly attached children.  A
7//! group-stop gets `PTRACE_LISTEN` so job control keeps working; every other
8//! stop is resumed.  Signal-delivery stops are forwarded unchanged.
9
10use crate::note;
11use crate::state::Ctx;
12use crate::sys::{errno, strerror};
13use crate::syscall::traced;
14use crate::tracee::ptrace;
15use crate::tracee::regs::Regs;
16use std::sync::Arc;
17
18/// Runs the loop until every tracee has exited.
19///
20/// # Arguments
21/// * `ctx` - Shared supervisor state.
22/// * `root` - Pid of the first tracee, whose exit status becomes ours.
23///
24/// # Returns
25/// The exit code to report: the root tracee's status, or 128 plus the signal.
26pub fn run(ctx: &Arc<Ctx>, root: i32) -> i32 {
27    let mut exit_code = 0;
28    // Live tracees.  The loop ends when this empties, not when waitpid runs out
29    // of children: a non-tracee child (an inherited helper, a pipe reader)
30    // must not keep the supervisor alive.
31    let mut tracees: std::collections::HashSet<i32> = std::collections::HashSet::new();
32    tracees.insert(root);
33    // Children whose first stop came before their creator's fork or clone
34    // event.  Each waits there until the event copies the creator's state to
35    // it: run on, it would exec in no view, and the event would then
36    // overwrite what its exec recorded (`/proc/self/exe` read as the parent).
37    let mut unregistered: std::collections::HashSet<i32> = std::collections::HashSet::new();
38    loop {
39        let mut status = 0;
40        let pid = unsafe { libc::waitpid(-1, &mut status, libc::__WALL) };
41        if pid < 0 {
42            match errno() {
43                libc::EINTR => continue,
44                libc::ECHILD => break,
45                e => {
46                    eprintln!("proot: waitpid: {}", strerror(e));
47                    break;
48                }
49            }
50        }
51        if libc::WIFEXITED(status) || libc::WIFSIGNALED(status) {
52            if pid == root {
53                exit_code = if libc::WIFEXITED(status) {
54                    libc::WEXITSTATUS(status)
55                } else {
56                    128 + libc::WTERMSIG(status)
57                };
58            }
59            ctx.forget(pid);
60            tracees.remove(&pid);
61            // A creator killed in its fork stop never reports the event: a
62            // child left waiting for it goes on with no state copied.
63            unregistered.retain(|&child| {
64                let waits = creator(child).is_some_and(|c| tracees.contains(&c));
65                if !waits {
66                    resume(ctx, child);
67                }
68                waits
69            });
70            if tracees.is_empty() {
71                break;
72            }
73            continue;
74        }
75        if !libc::WIFSTOPPED(status) {
76            continue;
77        }
78        let sig = libc::WSTOPSIG(status);
79        let event = (status >> 16) & 0xff;
80        let mut deliver = 0;
81        note!(
82            ctx,
83            3,
84            "pid {pid}: stop sig={sig} event={event} status={status:#x}"
85        );
86        match event {
87            libc::PTRACE_EVENT_SECCOMP => match Regs::fetch(pid) {
88                Ok(mut regs) => {
89                    traced::handle(ctx, &mut regs);
90                    if let Err(e) = regs.commit() {
91                        note!(ctx, 1, "pid {pid}: setregs: {}", strerror(e));
92                    }
93                }
94                Err(e) => note!(ctx, 1, "pid {pid}: getregs: {}", strerror(e)),
95            },
96            libc::PTRACE_EVENT_FORK | libc::PTRACE_EVENT_VFORK | libc::PTRACE_EVENT_CLONE => {
97                if let Ok(child) = ptrace::geteventmsg(pid) {
98                    let child = child as i32;
99                    tracees.insert(child);
100                    ctx.fork(pid, child);
101                    if unregistered.remove(&child) {
102                        resume(ctx, child);
103                    }
104                }
105            }
106            libc::PTRACE_EVENT_EXEC => {
107                // Threads other than the leader vanish on exec; the leader keeps
108                // the record set by the execve handler.
109            }
110            libc::PTRACE_EVENT_STOP => {
111                // A new child may report its first stop before the parent's
112                // clone event; it is held until the event registers it.
113                if tracees.insert(pid) && sig == libc::SIGTRAP {
114                    unregistered.insert(pid);
115                    continue;
116                }
117                // Group-stop: keep the tracee stopped (job control, SIGSTOP from
118                // outside) but stay able to hear about it.  Anything else here is
119                // a freshly attached child or a PTRACE_INTERRUPT: just resume.
120                if matches!(sig, libc::SIGSTOP | libc::SIGTSTP | libc::SIGTTIN | libc::SIGTTOU) {
121                    if let Err(e) = ptrace::listen(pid)
122                        && e != libc::ESRCH
123                    {
124                        note!(ctx, 1, "pid {pid}: listen: {}", strerror(e));
125                    }
126                    continue;
127                }
128            }
129            0 => {
130                // Signal-delivery stop: the tracee gets exactly what it was sent.
131                deliver = sig;
132                let mut si_code = 0;
133                if matches!(
134                    sig,
135                    libc::SIGTRAP
136                        | libc::SIGSEGV
137                        | libc::SIGILL
138                        | libc::SIGABRT
139                        | libc::SIGBUS
140                        | libc::SIGSYS
141                ) {
142                    if let Ok(si) = ptrace::getsiginfo(pid) {
143                        si_code = si.si_code;
144                        // si_pid/si_addr share a union; both are printed since the
145                        // meaning depends on si_code (SI_USER/SI_TKILL: sender pid).
146                        let (spid, addr) = unsafe { (si.si_pid(), si.si_addr() as usize) };
147                        note!(
148                            ctx,
149                            1,
150                            "pid {pid}: fatal signal {sig} si_code={} si_pid={spid} si_addr={addr:#x}",
151                            si.si_code
152                        );
153                    }
154                    // An `int3` breakpoint is what a program executes when it believes a
155                    // debugger is attached and wants it to take over.  Behave like that
156                    // debugger: resume past it.  A real crash sequence (`int3; ud2`)
157                    // still dies on the `ud2`.
158                    if sig == libc::SIGTRAP
159                        && si_code == 0x80
160                        && let Ok(regs) = Regs::fetch(pid)
161                    {
162                        let mut byte = [0u8; 1];
163                        if ctx
164                            .mem
165                            .read(pid as u32, regs.ip().wrapping_sub(1), &mut byte)
166                            .is_ok()
167                            && byte[0] == 0xcc
168                        {
169                            note!(ctx, 1, "pid {pid}: breakpoint at {:#x} swallowed", regs.ip() - 1);
170                            deliver = 0;
171                        }
172                    }
173                    if let Ok(regs) = Regs::fetch(pid) {
174                        let ip = regs.ip();
175                        let where_ = std::fs::read_to_string(format!("/proc/{pid}/maps"))
176                            .ok()
177                            .and_then(|m| {
178                                m.lines().find_map(|l| {
179                                    let mut f = l.split_whitespace();
180                                    let range = f.next()?;
181                                    let (lo, hi) = range.split_once('-')?;
182                                    let (lo, hi) = (
183                                        u64::from_str_radix(lo, 16).ok()?,
184                                        u64::from_str_radix(hi, 16).ok()?,
185                                    );
186                                    if ip < lo || ip >= hi {
187                                        return None;
188                                    }
189                                    let _perms = f.next()?;
190                                    let off = u64::from_str_radix(f.next()?, 16).ok()?;
191                                    let path = f.nth(2).unwrap_or("[anon]");
192                                    Some(format!("{path}+{:#x}", ip - lo + off))
193                                })
194                            })
195                            .unwrap_or_else(|| "?".into());
196                        note!(ctx, 1, "pid {pid}: ip={ip:#x} in {where_}");
197                        note!(ctx, 1, "pid {pid}: {}", address_space_summary(pid));
198                    }
199                }
200            }
201            _ => {}
202        }
203        if let Err(e) = ptrace::cont(pid, deliver)
204            && e != libc::ESRCH
205        {
206            note!(ctx, 1, "pid {pid}: cont: {}", strerror(e));
207        }
208    }
209    exit_code
210}
211
212/// Resumes a tracee held at its first stop.
213fn resume(ctx: &Ctx, pid: i32) {
214    if let Err(e) = ptrace::cont(pid, 0)
215        && e != libc::ESRCH
216    {
217        note!(ctx, 1, "pid {pid}: cont: {}", strerror(e));
218    }
219}
220
221/// The tracee whose fork or clone event registers `pid`: its thread group's
222/// leader for a new thread, else its parent process.  `None` once it is gone.
223///
224/// # Arguments
225/// * `pid` - A tracee held at its first stop.
226fn creator(pid: i32) -> Option<i32> {
227    let status = std::fs::read_to_string(format!("/proc/{pid}/status")).ok()?;
228    let field = |name: &str| {
229        status
230            .lines()
231            .find_map(|l| l.strip_prefix(name))
232            .and_then(|v| v.trim().parse::<i32>().ok())
233    };
234    let tgid = field("Tgid:")?;
235    if tgid != pid { Some(tgid) } else { field("PPid:") }
236}
237
238/// One line describing a tracee's address space, for fatal-signal reports:
239/// where the heap and stack are, how much is mapped, and the largest free gap
240/// below 4 GiB (what a 32-bit process has left to allocate from).
241///
242/// # Arguments
243/// * `pid` - Tracee thread id.
244fn address_space_summary(pid: i32) -> String {
245    let Ok(maps) = std::fs::read_to_string(format!("/proc/{pid}/maps")) else {
246        return "maps unavailable".into();
247    };
248    let mut ranges: Vec<(u64, u64)> = Vec::new();
249    let (mut heap, mut stack, mut total) = (String::from("none"), String::from("none"), 0u64);
250    for line in maps.lines() {
251        let mut f = line.split_whitespace();
252        let Some(range) = f.next() else { continue };
253        let Some((lo, hi)) = range.split_once('-') else {
254            continue;
255        };
256        let (Ok(lo), Ok(hi)) = (u64::from_str_radix(lo, 16), u64::from_str_radix(hi, 16)) else {
257            continue;
258        };
259        total += hi - lo;
260        ranges.push((lo, hi));
261        match f.nth(4) {
262            Some("[heap]") => heap = format!("{lo:#x}-{hi:#x} ({} MiB)", (hi - lo) >> 20),
263            Some("[stack]") => stack = format!("{lo:#x}-{hi:#x}"),
264            _ => {}
265        }
266    }
267    ranges.sort_unstable();
268    let mut gap = 0u64;
269    let mut cursor = 0x10000u64;
270    for (lo, hi) in &ranges {
271        if *lo >= 0xffff_f000 {
272            break;
273        }
274        gap = gap.max(lo.saturating_sub(cursor));
275        cursor = cursor.max(*hi);
276    }
277    gap = gap.max(0xffff_f000u64.saturating_sub(cursor));
278    format!(
279        "{} mappings, {} MiB mapped, heap {heap}, stack {stack}, largest free gap below 4G {} MiB",
280        ranges.len(),
281        total >> 20,
282        gap >> 20
283    )
284}
285
286/// Ptrace options for every tracee.
287///
288/// Tracees always die with the supervisor: once the listener is gone every
289/// translated syscall would fail with `ENOSYS`, so an orphaned tracee is
290/// never useful.  `--kill-on-exit` is therefore accepted but implied.
291pub fn options(ctx: &Ctx) -> i64 {
292    let _ = ctx;
293    ptrace::DEFAULT_OPTIONS | ptrace::OPTION_EXITKILL
294}