From c1a342778d8fdc0b146a85787bc16d9859a300b7 Mon Sep 17 00:00:00 2001 From: Geoffrey Thomas Date: Tue, 28 Mar 2017 16:55:36 -0400 Subject: [PATCH 1/2] Add WaitStatus::PtraceSyscall for use with PTRACE_O_TRACESYSGOOD The recommended way to trace syscalls with ptrace is to set the PTRACE_O_TRACESYSGOOD option, to distinguish syscall stops from receiving an actual SIGTRAP. In C, this would cause WSTOPSIG to return SIGTRAP | 0x80, but nix wants to parse that as an actual signal. Add another wait status type for syscall stops (in the language of the ptrace(2) manpage, "PTRACE_EVENT stops" and "Syscall-stops" are different things), and mask out bit 0x80 from signals before trying to parse it. Closes #550 --- CHANGELOG.md | 3 +++ src/sys/wait.rs | 19 ++++++++++++++-- test/sys/test_wait.rs | 50 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 817a89f45e..b7edf991c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Added `cfmakeraw`, `cfsetspeed`, and `tcgetsid`. ([#527](https://github.com/nix-rust/nix/pull/527)) - Added "bad none", "bad write_ptr", "bad write_int", and "bad readwrite" variants to the `ioctl!` macro. ([#670](https://github.com/nix-rust/nix/pull/670)) +- On Linux and Android, added support for receiving `PTRACE_O_TRACESYSGOOD` + events from `wait` and `waitpid` using `WaitStatus::PtraceSyscall` + ([#566](https://github.com/nix-rust/nix/pull/566)). ### Changed - Changed `ioctl!(write ...)` into `ioctl!(write_ptr ...)` and `ioctl!(write_int ..)` variants diff --git a/src/sys/wait.rs b/src/sys/wait.rs index ee0beade24..7033422da2 100644 --- a/src/sys/wait.rs +++ b/src/sys/wait.rs @@ -47,6 +47,8 @@ pub enum WaitStatus { Stopped(Pid, Signal), #[cfg(any(target_os = "linux", target_os = "android"))] PtraceEvent(Pid, Signal, c_int), + #[cfg(any(target_os = "linux", target_os = "android"))] + PtraceSyscall(Pid), Continued(Pid), StillAlive } @@ -56,6 +58,7 @@ pub enum WaitStatus { mod status { use sys::signal::Signal; use libc::c_int; + use libc::SIGTRAP; pub fn exited(status: i32) -> bool { (status & 0x7F) == 0 @@ -82,7 +85,17 @@ mod status { } pub fn stop_signal(status: i32) -> Signal { - Signal::from_c_int((status & 0xFF00) >> 8).unwrap() + // Keep only 7 bits of the signal: the high bit + // is used to indicate syscall stops, below. + Signal::from_c_int((status & 0x7F00) >> 8).unwrap() + } + + pub fn syscall_stop(status: i32) -> bool { + // From ptrace(2), setting PTRACE_O_TRACESYSGOOD has the effect + // of delivering SIGTRAP | 0x80 as the signal number for syscall + // stops. This allows easily distinguishing syscall stops from + // genuine SIGTRAP signals. + ((status & 0xFF00) >> 8) == SIGTRAP | 0x80 } pub fn stop_additional(status: i32) -> c_int { @@ -196,7 +209,9 @@ fn decode(pid : Pid, status: i32) -> WaitStatus { if #[cfg(any(target_os = "linux", target_os = "android"))] { fn decode_stopped(pid: Pid, status: i32) -> WaitStatus { let status_additional = status::stop_additional(status); - if status_additional == 0 { + if status::syscall_stop(status) { + WaitStatus::PtraceSyscall(pid) + } else if status_additional == 0 { WaitStatus::Stopped(pid, status::stop_signal(status)) } else { WaitStatus::PtraceEvent(pid, status::stop_signal(status), status::stop_additional(status)) diff --git a/test/sys/test_wait.rs b/test/sys/test_wait.rs index 2e28d9e78f..5f6c923184 100644 --- a/test/sys/test_wait.rs +++ b/test/sys/test_wait.rs @@ -34,3 +34,53 @@ fn test_wait_exit() { Err(_) => panic!("Error: Fork Failed") } } + +#[cfg(any(target_os = "linux", target_os = "android"))] +// FIXME: qemu-user doesn't implement ptrace on most arches +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +mod ptrace { + use nix::sys::ptrace::*; + use nix::sys::ptrace::ptrace::*; + use nix::sys::signal::*; + use nix::sys::wait::*; + use nix::unistd::*; + use nix::unistd::ForkResult::*; + use std::{ptr, process}; + + fn ptrace_child() -> ! { + let _ = ptrace(PTRACE_TRACEME, Pid::from_raw(0), ptr::null_mut(), ptr::null_mut()); + // As recommended by ptrace(2), raise SIGTRAP to pause the child + // until the parent is ready to continue + let _ = raise(SIGTRAP); + process::exit(0) + } + + fn ptrace_parent(child: Pid) { + // Wait for the raised SIGTRAP + assert_eq!(waitpid(child, None), Ok(WaitStatus::Stopped(child, SIGTRAP))); + // We want to test a syscall stop and a PTRACE_EVENT stop + assert!(ptrace_setoptions(child, PTRACE_O_TRACESYSGOOD | PTRACE_O_TRACEEXIT).is_ok()); + + // First, stop on the next system call, which will be exit() + assert!(ptrace(PTRACE_SYSCALL, child, ptr::null_mut(), ptr::null_mut()).is_ok()); + assert_eq!(waitpid(child, None), Ok(WaitStatus::PtraceSyscall(child))); + // Then get the ptrace event for the process exiting + assert!(ptrace(PTRACE_CONT, child, ptr::null_mut(), ptr::null_mut()).is_ok()); + assert_eq!(waitpid(child, None), Ok(WaitStatus::PtraceEvent(child, SIGTRAP, PTRACE_EVENT_EXIT))); + // Finally get the normal wait() result, now that the process has exited + assert!(ptrace(PTRACE_CONT, child, ptr::null_mut(), ptr::null_mut()).is_ok()); + assert_eq!(waitpid(child, None), Ok(WaitStatus::Exited(child, 0))); + } + + #[test] + fn test_wait_ptrace() { + #[allow(unused_variables)] + let m = ::FORK_MTX.lock().expect("Mutex got poisoned by another test"); + + match fork() { + Ok(Child) => ptrace_child(), + Ok(Parent { child }) => ptrace_parent(child), + Err(_) => panic!("Error: Fork Failed") + } + } +} From d292984dc0853cd6820d26b44ed721d56b20ea40 Mon Sep 17 00:00:00 2001 From: Geoffrey Thomas Date: Mon, 19 Jun 2017 23:51:25 -0400 Subject: [PATCH 2/2] Document WaitStatus and its variants --- src/sys/wait.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/sys/wait.rs b/src/sys/wait.rs index 7033422da2..f31f666d6d 100644 --- a/src/sys/wait.rs +++ b/src/sys/wait.rs @@ -40,16 +40,56 @@ libc_bitflags!( target_os = "android"))] const WSTOPPED: WaitPidFlag = WUNTRACED; +/// Possible return values from `wait()` or `waitpid()`. +/// +/// Each status (other than `StillAlive`) describes a state transition +/// in a child process `Pid`, such as the process exiting or stopping, +/// plus additional data about the transition if any. +/// +/// Note that there are two Linux-specific enum variants, `PtraceEvent` +/// and `PtraceSyscall`. Portable code should avoid exhaustively +/// matching on `WaitStatus`. #[derive(Eq, PartialEq, Clone, Copy, Debug)] pub enum WaitStatus { + /// The process exited normally (as with `exit()` or returning from + /// `main`) with the given exit code. This case matches the C macro + /// `WIFEXITED(status)`; the second field is `WEXITSTATUS(status)`. Exited(Pid, i8), + /// The process was killed by the given signal. The third field + /// indicates whether the signal generated a core dump. This case + /// matches the C macro `WIFSIGNALED(status)`; the last two fields + /// correspond to `WTERMSIG(status)` and `WCOREDUMP(status)`. Signaled(Pid, Signal, bool), + /// The process is alive, but was stopped by the given signal. This + /// is only reported if `WaitPidFlag::WUNTRACED` was passed. This + /// case matches the C macro `WIFSTOPPED(status)`; the second field + /// is `WSTOPSIG(status)`. Stopped(Pid, Signal), + /// The traced process was stopped by a `PTRACE_EVENT_*` event. See + /// [`nix::sys::ptrace`] and [`ptrace`(2)] for more information. All + /// currently-defined events use `SIGTRAP` as the signal; the third + /// field is the `PTRACE_EVENT_*` value of the event. + /// + /// [`nix::sys::ptrace`]: ../ptrace/index.html + /// [`ptrace`(2)]: http://man7.org/linux/man-pages/man2/ptrace.2.html #[cfg(any(target_os = "linux", target_os = "android"))] PtraceEvent(Pid, Signal, c_int), + /// The traced process was stopped by execution of a system call, + /// and `PTRACE_O_TRACESYSGOOD` is in effect. See [`ptrace`(2)] for + /// more information. + /// + /// [`ptrace`(2)]: http://man7.org/linux/man-pages/man2/ptrace.2.html #[cfg(any(target_os = "linux", target_os = "android"))] PtraceSyscall(Pid), + /// The process was previously stopped but has resumed execution + /// after receiving a `SIGCONT` signal. This is only reported if + /// `WaitPidFlag::WCONTINUED` was passed. This case matches the C + /// macro `WIFCONTINUED(status)`. Continued(Pid), + /// There are currently no state changes to report in any awaited + /// child process. This is only returned if `WaitPidFlag::WNOHANG` + /// was used (otherwise `wait()` or `waitpid()` would block until + /// there was something to report). StillAlive }