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}