1use std::collections::HashMap;
4use std::ffi::OsString;
5use std::io;
6use std::os::unix::ffi::OsStringExt;
7use std::os::unix::process::CommandExt;
8use std::process::Child;
9use std::sync::{Arc, Condvar, Mutex};
10use std::time::Duration;
11
12use crate::platform::process::{ExactTraceEvent, ExactTraceEventKind, TraceOriginArtifact};
13
14const STACK_CAPTURE_BYTES: usize = 16 * 1024;
15const MODULE_MAP_CAPTURE_BYTES: usize = 256 * 1024;
16
17#[cfg(target_env = "musl")]
21type PtraceRequest = libc::c_int;
22#[cfg(not(target_env = "musl"))]
23type PtraceRequest = libc::c_uint;
24
25pub fn configure_exact_trace(command: &mut std::process::Command) -> io::Result<()> {
29 unsafe {
33 command.pre_exec(|| {
34 let result = libc::ptrace(
35 libc::PTRACE_TRACEME,
36 0,
37 std::ptr::null_mut::<libc::c_void>(),
38 std::ptr::null_mut::<libc::c_void>(),
39 );
40 if result == -1 {
41 return Err(io::Error::last_os_error());
42 }
43 Ok(())
44 });
45 }
46 Ok(())
47}
48
49#[derive(Default)]
50struct RootState {
51 exit_code: Option<i32>,
52 done: bool,
53 tracer_error: Option<String>,
54}
55
56struct Shared {
57 state: Mutex<RootState>,
58 wake: Condvar,
59}
60
61pub struct TracedChild {
63 pid: u32,
64 shared: Arc<Shared>,
65 stdin: Option<std::process::ChildStdin>,
66 stdout: Option<std::process::ChildStdout>,
67 stderr: Option<std::process::ChildStderr>,
68}
69
70impl TracedChild {
71 pub fn id(&self) -> u32 {
72 self.pid
73 }
74
75 pub fn try_wait_code(&self) -> io::Result<Option<i32>> {
76 let state = self.shared.state.lock().unwrap_or_else(|e| e.into_inner());
77 if let Some(error) = state.tracer_error.as_ref() {
78 return Err(io::Error::other(error.clone()));
79 }
80 Ok(state.exit_code)
81 }
82
83 pub fn kill(&mut self) -> io::Result<()> {
84 if unsafe { libc::kill(self.pid as libc::pid_t, libc::SIGKILL) } == -1 {
87 let error = io::Error::last_os_error();
88 if error.raw_os_error() != Some(libc::ESRCH) {
89 return Err(error);
90 }
91 }
92 Ok(())
93 }
94
95 pub fn take_stdin(&mut self) -> Option<std::process::ChildStdin> {
96 self.stdin.take()
97 }
98
99 pub fn take_stdout(&mut self) -> Option<std::process::ChildStdout> {
100 self.stdout.take()
101 }
102
103 pub fn take_stderr(&mut self) -> Option<std::process::ChildStderr> {
104 self.stderr.take()
105 }
106}
107
108pub fn start_exact_trace(
109 mut command: std::process::Command,
110 emit: Box<dyn Fn(ExactTraceEvent) + Send>,
111 complete: Box<dyn FnOnce() + Send>,
112) -> io::Result<TracedChild> {
113 configure_exact_trace(&mut command)?;
114 let shared = Arc::new(Shared {
115 state: Mutex::new(RootState::default()),
116 wake: Condvar::new(),
117 });
118 let thread_shared = Arc::clone(&shared);
119 let (setup_tx, setup_rx) = std::sync::mpsc::sync_channel(1);
120 let spawned = std::thread::Builder::new()
121 .name("rp-linux-ptrace".to_owned())
122 .spawn(move || {
123 let child = match command.spawn() {
124 Ok(child) => child,
125 Err(error) => {
126 let _ = setup_tx.send(Err(error));
127 return;
128 }
129 };
130 trace_loop(child, thread_shared, emit, complete, setup_tx);
131 });
132 if let Err(error) = spawned {
133 return Err(io::Error::other(format!(
134 "spawn ptrace supervisor: {error}"
135 )));
136 }
137 let setup = setup_rx
138 .recv()
139 .map_err(|_| io::Error::other("ptrace supervisor ended during launch setup"))??;
140 Ok(TracedChild {
141 pid: setup.pid,
142 shared,
143 stdin: setup.stdin,
144 stdout: setup.stdout,
145 stderr: setup.stderr,
146 })
147}
148
149struct TraceSetup {
150 pid: u32,
151 stdin: Option<std::process::ChildStdin>,
152 stdout: Option<std::process::ChildStdout>,
153 stderr: Option<std::process::ChildStderr>,
154}
155
156#[derive(Clone)]
157struct Tracee {
158 parent_pid: Option<u32>,
159 parent_start_key: Option<u64>,
160 start_key: Option<u64>,
161 process_leader: bool,
162 executable: Option<std::path::PathBuf>,
163 argv: Option<Vec<OsString>>,
164 origin: Option<TraceOriginArtifact>,
165}
166
167fn trace_loop(
168 mut child: Child,
169 shared: Arc<Shared>,
170 emit: Box<dyn Fn(ExactTraceEvent) + Send>,
171 complete: Box<dyn FnOnce() + Send>,
172 setup: std::sync::mpsc::SyncSender<io::Result<TraceSetup>>,
173) {
174 let root_pid = child.id();
175 let mut initial_status = 0;
176 if unsafe { libc::waitpid(root_pid as libc::pid_t, &mut initial_status, libc::__WALL) } == -1
179 || !libc::WIFSTOPPED(initial_status)
180 {
181 let message = "root did not reach its initial ptrace exec stop";
182 let _ = setup.send(Err(io::Error::other(message)));
183 cleanup_failed_setup(&mut child);
184 return;
185 }
186
187 let options = libc::PTRACE_O_TRACEFORK
188 | libc::PTRACE_O_TRACEVFORK
189 | libc::PTRACE_O_TRACECLONE
190 | libc::PTRACE_O_TRACEEXEC
191 | libc::PTRACE_O_TRACEEXIT;
192 if ptrace_value(libc::PTRACE_SETOPTIONS, root_pid, 0, options as usize).is_err() {
193 let message = "PTRACE_SETOPTIONS was denied";
194 let _ = setup.send(Err(io::Error::new(
195 io::ErrorKind::PermissionDenied,
196 message,
197 )));
198 cleanup_failed_setup(&mut child);
199 return;
200 }
201
202 let mut sequence = 1u64;
203 let mut tracees = HashMap::from([(
204 root_pid,
205 Tracee {
206 parent_pid: None,
207 parent_start_key: None,
208 start_key: process_start_key(root_pid),
209 process_leader: true,
210 executable: read_executable(root_pid),
211 argv: read_argv(root_pid),
212 origin: None,
213 },
214 )]);
215 if ptrace_value(libc::PTRACE_CONT, root_pid, 0, 0).is_err() {
216 let message = "failed to continue root after initial exec stop";
217 let _ = setup.send(Err(io::Error::other(message)));
218 detach_all(tracees.keys().copied());
219 cleanup_failed_setup(&mut child);
220 return;
221 }
222 let setup_result = TraceSetup {
223 pid: root_pid,
224 stdin: child.stdin.take(),
225 stdout: child.stdout.take(),
226 stderr: child.stderr.take(),
227 };
228 if setup.send(Ok(setup_result)).is_err() {
229 cleanup_failed_setup(&mut child);
230 return;
231 }
232
233 while !tracees.is_empty() {
234 let pids: Vec<u32> = tracees.keys().copied().collect();
235 let mut progressed = false;
236 for pid in pids {
237 let mut status = 0;
238 let waited = unsafe {
242 libc::waitpid(
243 pid as libc::pid_t,
244 &mut status,
245 libc::WNOHANG | libc::__WALL,
246 )
247 };
248 if waited == 0 {
249 continue;
250 }
251 if waited == -1 {
252 let error = io::Error::last_os_error();
253 let tracee = tracees.get(&pid).cloned();
254 if error.raw_os_error() == Some(libc::ECHILD)
255 && tracee.as_ref().is_some_and(|item| !item.process_leader)
256 {
257 tracees.remove(&pid);
263 continue;
264 }
265 abort_runtime_trace(
266 &mut child,
267 &shared,
268 &emit,
269 sequence,
270 pid,
271 tracee.as_ref(),
272 &tracees,
273 format!("ptrace wait ownership lost: {error}"),
274 );
275 complete();
276 return;
277 }
278 progressed = true;
279 if libc::WIFEXITED(status) || libc::WIFSIGNALED(status) {
280 if let Some(tracee) = tracees.remove(&pid) {
281 if tracee.process_leader {
282 let exit_code = libc::WIFEXITED(status).then(|| libc::WEXITSTATUS(status));
283 let signal = libc::WIFSIGNALED(status).then(|| libc::WTERMSIG(status));
284 if pid == root_pid {
285 let normalized = exit_code.unwrap_or_else(|| -signal.unwrap_or(0));
286 finish_root(&shared, normalized);
287 } else {
288 emit(event_for(
289 sequence,
290 pid,
291 &tracee,
292 ExactTraceEventKind::Exit {
293 exit_code,
294 signal,
295 raw_status: i64::from(status),
296 },
297 ));
298 sequence += 1;
299 }
300 }
301 }
302 continue;
303 }
304 if !libc::WIFSTOPPED(status) {
305 continue;
306 }
307
308 let signal = libc::WSTOPSIG(status);
309 let ptrace_event = status >> 16;
310 match ptrace_event {
311 libc::PTRACE_EVENT_FORK | libc::PTRACE_EVENT_VFORK | libc::PTRACE_EVENT_CLONE => {
312 let mut child_pid = 0usize;
313 match ptrace_value(
314 libc::PTRACE_GETEVENTMSG,
315 pid,
316 0,
317 (&raw mut child_pid) as usize,
318 ) {
319 Ok(_) => {
320 let child_pid = child_pid as u32;
321 let process_leader = if matches!(
322 ptrace_event,
323 libc::PTRACE_EVENT_FORK | libc::PTRACE_EVENT_VFORK
324 ) {
325 true
326 } else {
327 match thread_group_id(child_pid) {
328 Some(thread_group) => thread_group == child_pid,
329 None => {
330 tracees.insert(
336 child_pid,
337 Tracee {
338 parent_pid: None,
339 parent_start_key: None,
340 start_key: process_start_key(child_pid),
341 process_leader: true,
342 executable: None,
343 argv: None,
344 origin: None,
345 },
346 );
347 abort_runtime_trace(
348 &mut child,
349 &shared,
350 &emit,
351 sequence,
352 child_pid,
353 tracees.get(&child_pid),
354 &tracees,
355 "cannot classify PTRACE_EVENT_CLONE child".to_owned(),
356 );
357 complete();
358 return;
359 }
360 }
361 };
362 let parent_pid =
363 process_leader.then(|| thread_group_id(pid).unwrap_or(pid));
364 let tracee = Tracee {
365 parent_pid,
366 parent_start_key: parent_pid.and_then(process_start_key),
367 start_key: process_start_key(child_pid),
368 process_leader,
369 executable: None,
370 argv: None,
371 origin: process_leader.then(|| capture_origin(pid)),
372 };
373 let spawn_event = process_leader.then(|| {
374 event_for(
375 sequence,
376 child_pid,
377 &tracee,
378 ExactTraceEventKind::Spawn,
379 )
380 });
381 tracees.insert(child_pid, tracee);
382 if let Err(error) = ptrace_value(libc::PTRACE_CONT, pid, 0, 0) {
383 abort_runtime_trace(
384 &mut child,
385 &shared,
386 &emit,
387 sequence,
388 pid,
389 tracees.get(&pid),
390 &tracees,
391 format!("continue spawning tracee: {error}"),
392 );
393 complete();
394 return;
395 }
396 let mut child_status = 0;
397 let waited = unsafe {
401 libc::waitpid(
402 child_pid as libc::pid_t,
403 &mut child_status,
404 libc::__WALL,
405 )
406 };
407 if waited != child_pid as libc::pid_t
408 || !libc::WIFSTOPPED(child_status)
409 {
410 abort_runtime_trace(
411 &mut child,
412 &shared,
413 &emit,
414 sequence,
415 child_pid,
416 tracees.get(&child_pid),
417 &tracees,
418 "new tracee did not reach its initial stop".to_owned(),
419 );
420 complete();
421 return;
422 }
423 if let Err(error) = ptrace_value(libc::PTRACE_CONT, child_pid, 0, 0) {
424 abort_runtime_trace(
425 &mut child,
426 &shared,
427 &emit,
428 sequence,
429 child_pid,
430 tracees.get(&child_pid),
431 &tracees,
432 format!("continue new tracee: {error}"),
433 );
434 complete();
435 return;
436 }
437 if let Some(spawn_event) = spawn_event {
440 emit(spawn_event);
441 sequence += 1;
442 }
443 }
444 Err(error) => {
445 abort_runtime_trace(
446 &mut child,
447 &shared,
448 &emit,
449 sequence,
450 pid,
451 tracees.get(&pid),
452 &tracees,
453 format!("read fork/clone event child pid: {error}"),
454 );
455 complete();
456 return;
457 }
458 }
459 }
460 libc::PTRACE_EVENT_EXEC => {
461 let exec_event = if let Some(tracee) = tracees.get_mut(&pid) {
462 tracee.executable = read_executable(pid);
463 tracee.argv = read_argv(pid);
464 tracee
465 .process_leader
466 .then(|| event_for(sequence, pid, tracee, ExactTraceEventKind::Exec))
467 } else {
468 None
469 };
470 if let Err(error) = ptrace_value(libc::PTRACE_CONT, pid, 0, 0) {
471 abort_runtime_trace(
472 &mut child,
473 &shared,
474 &emit,
475 sequence,
476 pid,
477 tracees.get(&pid),
478 &tracees,
479 format!("continue after exec event: {error}"),
480 );
481 complete();
482 return;
483 }
484 if let Some(exec_event) = exec_event {
485 emit(exec_event);
486 sequence += 1;
487 }
488 }
489 libc::PTRACE_EVENT_EXIT => {
490 if let Err(error) = ptrace_value(libc::PTRACE_CONT, pid, 0, 0) {
491 abort_runtime_trace(
492 &mut child,
493 &shared,
494 &emit,
495 sequence,
496 pid,
497 tracees.get(&pid),
498 &tracees,
499 format!("continue after exit event: {error}"),
500 );
501 complete();
502 return;
503 }
504 }
505 _ => {
506 let forwarded = if signal == libc::SIGTRAP {
507 0
508 } else {
509 signal
510 };
511 if let Err(error) =
512 ptrace_value(libc::PTRACE_CONT, pid, 0, forwarded as usize)
513 {
514 abort_runtime_trace(
515 &mut child,
516 &shared,
517 &emit,
518 sequence,
519 pid,
520 tracees.get(&pid),
521 &tracees,
522 format!("continue after signal stop: {error}"),
523 );
524 complete();
525 return;
526 }
527 }
528 }
529 }
530 if !progressed {
531 std::thread::sleep(Duration::from_millis(1));
532 }
533 }
534 drop(child);
535 complete();
536}
537
538fn ptrace_value(
539 request: PtraceRequest,
540 pid: u32,
541 address: usize,
542 data: usize,
543) -> io::Result<libc::c_long> {
544 let value = unsafe {
548 libc::ptrace(
549 request,
550 pid as libc::pid_t,
551 address as *mut libc::c_void,
552 data as *mut libc::c_void,
553 )
554 };
555 if value == -1 {
556 Err(io::Error::last_os_error())
557 } else {
558 Ok(value)
559 }
560}
561
562fn emit_loss(
563 emit: &dyn Fn(ExactTraceEvent),
564 sequence: u64,
565 pid: u32,
566 tracee: Option<&Tracee>,
567 reason: String,
568) {
569 emit(ExactTraceEvent {
570 sequence,
571 pid,
572 parent_pid: tracee.and_then(|tracee| tracee.parent_pid),
573 parent_start_key: tracee.and_then(|tracee| tracee.parent_start_key),
574 start_key: tracee.and_then(|tracee| tracee.start_key),
575 timestamp: std::time::SystemTime::now(),
576 kind: ExactTraceEventKind::Loss { reason },
577 executable: None,
578 argv: None,
579 origin: None,
580 });
581}
582
583#[allow(clippy::too_many_arguments)]
584fn abort_runtime_trace(
585 child: &mut Child,
586 shared: &Shared,
587 emit: &dyn Fn(ExactTraceEvent),
588 sequence: u64,
589 pid: u32,
590 tracee: Option<&Tracee>,
591 tracees: &HashMap<u32, Tracee>,
592 reason: String,
593) {
594 emit_loss(emit, sequence, pid, tracee, reason.clone());
595 for tracee_pid in tracees.keys().copied().filter(|item| *item != child.id()) {
596 unsafe {
600 libc::kill(tracee_pid as libc::pid_t, libc::SIGKILL);
601 libc::waitpid(tracee_pid as libc::pid_t, std::ptr::null_mut(), libc::__WALL);
602 }
603 }
604 let _ = child.kill();
605 let _ = child.wait();
606 let mut state = shared.state.lock().unwrap_or_else(|error| error.into_inner());
607 state.tracer_error = Some(reason);
608 state.done = true;
609 shared.wake.notify_all();
610}
611
612fn capture_origin(thread_id: u32) -> TraceOriginArtifact {
613 let origin_pid = thread_group_id(thread_id).unwrap_or(thread_id);
614 let mut registers = vec![0u8; 1024];
615 let mut iov = libc::iovec {
616 iov_base: registers.as_mut_ptr().cast(),
617 iov_len: registers.len(),
618 };
619 if ptrace_value(
620 libc::PTRACE_GETREGSET,
621 thread_id,
622 libc::NT_PRSTATUS as usize,
623 (&raw mut iov) as usize,
624 )
625 .is_ok()
626 {
627 registers.truncate(iov.iov_len);
628 } else {
629 registers.clear();
630 }
631 let (stack_pointer, instruction_pointer) = read_syscall_pointers(thread_id);
632 let mut stack = vec![0u8; STACK_CAPTURE_BYTES];
633 if let Some(stack_pointer) = stack_pointer {
634 let local = libc::iovec {
635 iov_base: stack.as_mut_ptr().cast(),
636 iov_len: stack.len(),
637 };
638 let remote = libc::iovec {
639 iov_base: stack_pointer as *mut libc::c_void,
640 iov_len: stack.len(),
641 };
642 let read = unsafe {
646 libc::process_vm_readv(thread_id as libc::pid_t, &local, 1, &remote, 1, 0)
647 };
648 if read >= 0 {
649 stack.truncate(read as usize);
650 } else {
651 stack.clear();
652 }
653 } else {
654 stack.clear();
655 }
656 let mut module_map = std::fs::read(format!("/proc/{origin_pid}/maps")).unwrap_or_default();
657 let module_map_truncated = module_map.len() > MODULE_MAP_CAPTURE_BYTES;
658 module_map.truncate(MODULE_MAP_CAPTURE_BYTES);
659 TraceOriginArtifact {
660 origin_pid,
661 thread_id,
662 architecture: std::env::consts::ARCH.to_owned(),
663 register_format: format!("linux-nt-prstatus-{}", std::env::consts::ARCH),
664 executable: read_executable(origin_pid),
665 registers,
666 stack_pointer,
667 instruction_pointer,
668 truncated: stack.len() == STACK_CAPTURE_BYTES,
669 stack,
670 module_map,
671 module_map_truncated,
672 }
673}
674
675fn read_syscall_pointers(pid: u32) -> (Option<u64>, Option<u64>) {
676 let Ok(text) = std::fs::read_to_string(format!("/proc/{pid}/syscall")) else {
677 return (None, None);
678 };
679 let fields: Vec<&str> = text.split_ascii_whitespace().collect();
680 if fields.len() < 2 {
681 return (None, None);
682 }
683 let parse = |value: &str| u64::from_str_radix(value.trim_start_matches("0x"), 16).ok();
684 (parse(fields[fields.len() - 2]), parse(fields[fields.len() - 1]))
685}
686
687fn read_executable(pid: u32) -> Option<std::path::PathBuf> {
688 std::fs::read_link(format!("/proc/{pid}/exe")).ok()
689}
690
691fn read_argv(pid: u32) -> Option<Vec<OsString>> {
692 let bytes = std::fs::read(format!("/proc/{pid}/cmdline")).ok()?;
693 Some(
694 bytes
695 .split(|byte| *byte == 0)
696 .filter(|part| !part.is_empty())
697 .map(|part| OsString::from_vec(part.to_vec()))
698 .collect(),
699 )
700}
701
702fn process_start_key(pid: u32) -> Option<u64> {
703 let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
704 stat.get(stat.rfind(')')? + 1..)?
705 .split_ascii_whitespace()
706 .nth(19)?
707 .parse()
708 .ok()
709}
710
711fn thread_group_id(pid: u32) -> Option<u32> {
712 std::fs::read_to_string(format!("/proc/{pid}/status"))
713 .ok()?
714 .lines()
715 .find_map(|line| line.strip_prefix("Tgid:")?.trim().parse().ok())
716}
717
718fn event_for(
719 sequence: u64,
720 pid: u32,
721 tracee: &Tracee,
722 kind: ExactTraceEventKind,
723) -> ExactTraceEvent {
724 ExactTraceEvent {
725 sequence,
726 pid,
727 parent_pid: tracee.parent_pid,
728 parent_start_key: tracee.parent_start_key,
729 start_key: tracee.start_key,
730 timestamp: std::time::SystemTime::now(),
731 kind,
732 executable: tracee.executable.clone(),
733 argv: tracee.argv.clone(),
734 origin: tracee.origin.clone(),
735 }
736}
737
738fn detach_all(tracees: impl IntoIterator<Item = u32>) {
739 for pid in tracees {
740 let _ = ptrace_value(libc::PTRACE_DETACH, pid, 0, 0);
741 }
742}
743
744fn cleanup_failed_setup(child: &mut Child) {
745 let _ = child.kill();
746 let _ = child.wait();
747}
748
749fn finish_root(shared: &Shared, exit_code: i32) {
750 let mut state = shared.state.lock().unwrap_or_else(|e| e.into_inner());
751 state.exit_code = Some(exit_code);
752 state.done = true;
753 shared.wake.notify_all();
754}