From f1573a7efa8368507d581adeb3bff9bbf5aa0ead Mon Sep 17 00:00:00 2001 From: xd009642 Date: Sat, 6 Oct 2018 15:31:33 +0100 Subject: [PATCH] Added ptrace support for BSDs * Moved ptrace API into it's own module with cfg'ed modules exported for linux/android or BSDs. * Replicated current linux API for BSD * Added API functions to peek and poke memory to avoid needing to replicate deprecated linux API and remaining feature complete * Added helper function for `PTRACE_KILL` requests * Updated tests based on new API changes * Added addition kill calls to `test_ptrace_cont` as inferior death doesn't happen immediately on OSX which caused issues in the tests. --- CHANGELOG.md | 3 + src/sys/mod.rs | 8 +- src/sys/ptrace/bsd.rs | 170 +++++++++++++++++++++++++ src/sys/{ptrace.rs => ptrace/linux.rs} | 28 +++- src/sys/ptrace/mod.rs | 22 ++++ test/sys/mod.rs | 7 +- test/sys/test_ptrace.rs | 28 +++- 7 files changed, 257 insertions(+), 9 deletions(-) create mode 100644 src/sys/ptrace/bsd.rs rename src/sys/{ptrace.rs => ptrace/linux.rs} (93%) create mode 100644 src/sys/ptrace/mod.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b1543f4f0..3f6cef9008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). ([#956](https://github.com/nix-rust/nix/pull/956)) - Added a `fchownat` wrapper. ([#955](https://github.com/nix-rust/nix/pull/955)) +- Added support for `ptrace` on BSD operating systems ([#949](https://github.com/nix-rust/nix/pull/949)) +- Added `ptrace` functions for reads and writes to tracee memory and ptrace kill + ([#949](https://github.com/nix-rust/nix/pull/949)) ### Changed - Increased required Rust version to 1.22.1/ diff --git a/src/sys/mod.rs b/src/sys/mod.rs index e6c7880c1e..72d5964910 100644 --- a/src/sys/mod.rs +++ b/src/sys/mod.rs @@ -38,7 +38,13 @@ pub mod mman; pub mod pthread; -#[cfg(any(target_os = "android", target_os = "linux"))] +#[cfg(any(target_os = "android", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "linux", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd"))] pub mod ptrace; #[cfg(target_os = "linux")] diff --git a/src/sys/ptrace/bsd.rs b/src/sys/ptrace/bsd.rs new file mode 100644 index 0000000000..71cfd80a99 --- /dev/null +++ b/src/sys/ptrace/bsd.rs @@ -0,0 +1,170 @@ +use errno::Errno; +use libc::{self, c_int}; +use std::ptr; +use sys::signal::Signal; +use unistd::Pid; +use Result; + +pub type RequestType = c_int; + +cfg_if! { + if #[cfg(any(target_os = "dragonfly", + target_os = "freebsd", + target_os = "macos", + target_os = "openbsd"))] { + #[doc(hidden)] + pub type AddressType = *mut ::libc::c_char; + } else { + #[doc(hidden)] + pub type AddressType = *mut ::libc::c_void; + } +} + +libc_enum! { + #[repr(i32)] + /// Ptrace Request enum defining the action to be taken. + pub enum Request { + PT_TRACE_ME, + PT_READ_I, + PT_READ_D, + #[cfg(target_os = "macos")] + PT_READ_U, + PT_WRITE_I, + PT_WRITE_D, + #[cfg(target_os = "macos")] + PT_WRITE_U, + PT_CONTINUE, + PT_KILL, + #[cfg(any(any(target_os = "dragonfly", + target_os = "freebsd", + target_os = "macos"), + all(target_os = "openbsd", target_arch = "x86_64"), + all(target_os = "netbsd", any(target_arch = "x86_64", + target_arch = "powerpc"))))] + PT_STEP, + PT_ATTACH, + PT_DETACH, + #[cfg(target_os = "macos")] + PT_SIGEXC, + #[cfg(target_os = "macos")] + PT_THUPDATE, + #[cfg(target_os = "macos")] + PT_ATTACHEXC + } +} + +unsafe fn ptrace_other( + request: Request, + pid: Pid, + addr: AddressType, + data: c_int, +) -> Result { + Errno::result(libc::ptrace( + request as RequestType, + libc::pid_t::from(pid), + addr, + data, + )).map(|_| 0) +} + +/// Sets the process as traceable, as with `ptrace(PT_TRACEME, ...)` +/// +/// Indicates that this process is to be traced by its parent. +/// This is the only ptrace request to be issued by the tracee. +pub fn traceme() -> Result<()> { + unsafe { ptrace_other(Request::PT_TRACE_ME, Pid::from_raw(0), ptr::null_mut(), 0).map(|_| ()) } +} + +/// Attach to a running process, as with `ptrace(PT_ATTACH, ...)` +/// +/// Attaches to the process specified in pid, making it a tracee of the calling process. +pub fn attach(pid: Pid) -> Result<()> { + unsafe { ptrace_other(Request::PT_ATTACH, pid, ptr::null_mut(), 0).map(|_| ()) } +} + +/// Detaches the current running process, as with `ptrace(PT_DETACH, ...)` +/// +/// Detaches from the process specified in pid allowing it to run freely +pub fn detach(pid: Pid) -> Result<()> { + unsafe { ptrace_other(Request::PT_DETACH, pid, ptr::null_mut(), 0).map(|_| ()) } +} + +/// Restart the stopped tracee process, as with `ptrace(PTRACE_CONT, ...)` +/// +/// Continues the execution of the process with PID `pid`, optionally +/// delivering a signal specified by `sig`. +pub fn cont>>(pid: Pid, sig: T) -> Result<()> { + let data = match sig.into() { + Some(s) => s as c_int, + None => 0, + }; + unsafe { + // Ignore the useless return value + ptrace_other(Request::PT_CONTINUE, pid, 1 as AddressType, data).map(|_| ()) + } +} + +/// Issues a kill request as with `ptrace(PT_KILL, ...)` +/// +/// This request is equivalent to `ptrace(PT_CONTINUE, ..., SIGKILL);` +pub fn kill(pid: Pid) -> Result<()> { + unsafe { + ptrace_other(Request::PT_KILL, pid, 0 as AddressType, 0).map(|_| ()) + } +} + +/// Move the stopped tracee process forward by a single step as with +/// `ptrace(PT_STEP, ...)` +/// +/// Advances the execution of the process with PID `pid` by a single step optionally delivering a +/// signal specified by `sig`. +/// +/// # Example +/// ```rust +/// extern crate nix; +/// use nix::sys::ptrace::step; +/// use nix::unistd::Pid; +/// use nix::sys::signal::Signal; +/// use nix::sys::wait::*; +/// fn main() { +/// // If a process changes state to the stopped state because of a SIGUSR1 +/// // signal, this will step the process forward and forward the user +/// // signal to the stopped process +/// match waitpid(Pid::from_raw(-1), None) { +/// Ok(WaitStatus::Stopped(pid, Signal::SIGUSR1)) => { +/// let _ = step(pid, Signal::SIGUSR1); +/// } +/// _ => {}, +/// } +/// } +/// ``` +#[cfg( + any( + any(target_os = "dragonfly", target_os = "freebsd", target_os = "macos"), + all(target_os = "openbsd", target_arch = "x86_64"), + all(target_os = "netbsd", + any(target_arch = "x86_64", target_arch = "powerpc") + ) + ) +)] +pub fn step>>(pid: Pid, sig: T) -> Result<()> { + let data = match sig.into() { + Some(s) => s as c_int, + None => 0, + }; + unsafe { ptrace_other(Request::PT_STEP, pid, ptr::null_mut(), data).map(|_| ()) } +} + +/// Reads a word from a processes memory at the given address +pub fn read(pid: Pid, addr: AddressType) -> Result { + unsafe { + // Traditionally there was a difference between reading data or + // instruction memory but not in modern systems. + ptrace_other(Request::PT_READ_D, pid, addr, 0) + } +} + +/// Writes a word into the processes memory at the given address +pub fn write(pid: Pid, addr: AddressType, data: c_int) -> Result<()> { + unsafe { ptrace_other(Request::PT_WRITE_D, pid, addr, data).map(|_| ()) } +} diff --git a/src/sys/ptrace.rs b/src/sys/ptrace/linux.rs similarity index 93% rename from src/sys/ptrace.rs rename to src/sys/ptrace/linux.rs index 4bb7e06f60..70404ade29 100644 --- a/src/sys/ptrace.rs +++ b/src/sys/ptrace/linux.rs @@ -173,8 +173,10 @@ libc_bitflags! { pub unsafe fn ptrace(request: Request, pid: Pid, addr: *mut c_void, data: *mut c_void) -> Result { use self::Request::*; match request { - PTRACE_PEEKTEXT | PTRACE_PEEKDATA | PTRACE_PEEKUSER => ptrace_peek(request, pid, addr, data), - PTRACE_GETSIGINFO | PTRACE_GETEVENTMSG | PTRACE_SETSIGINFO | PTRACE_SETOPTIONS => Err(Error::UnsupportedOperation), + PTRACE_PEEKTEXT | PTRACE_PEEKDATA | PTRACE_PEEKUSER | PTRACE_GETSIGINFO | + PTRACE_GETEVENTMSG | PTRACE_SETSIGINFO | PTRACE_SETOPTIONS | + PTRACE_POKETEXT | PTRACE_POKEDATA | PTRACE_POKEUSER | + PTRACE_KILL => Err(Error::UnsupportedOperation), _ => ptrace_other(request, pid, addr, data) } } @@ -320,6 +322,15 @@ pub fn cont>>(pid: Pid, sig: T) -> Result<()> { } } +/// Issues a kill request as with `ptrace(PTRACE_KILL, ...)` +/// +/// This request is equivalent to `ptrace(PTRACE_CONT, ..., SIGKILL);` +pub fn kill(pid: Pid) -> Result<()> { + unsafe { + ptrace_other(Request::PTRACE_KILL, pid, ptr::null_mut(), ptr::null_mut()).map(|_| ()) + } +} + /// Move the stopped tracee process forward by a single step as with /// `ptrace(PTRACE_SINGLESTEP, ...)` /// @@ -354,3 +365,16 @@ pub fn step>>(pid: Pid, sig: T) -> Result<()> { ptrace_other(Request::PTRACE_SINGLESTEP, pid, ptr::null_mut(), data).map(|_| ()) } } + + +/// Reads a word from a processes memory at the given address +pub fn read(pid: Pid, addr: *mut c_void) -> Result { + ptrace_peek(Request::PTRACE_PEEKDATA, pid, addr, ptr::null_mut()) +} + +/// Writes a word into the processes memory at the given address +pub fn write(pid: Pid, addr: *mut c_void, data: *mut c_void) -> Result<()> { + unsafe { + ptrace_other(Request::PTRACE_POKEDATA, pid, addr, data).map(|_| ()) + } +} diff --git a/src/sys/ptrace/mod.rs b/src/sys/ptrace/mod.rs new file mode 100644 index 0000000000..782c30409b --- /dev/null +++ b/src/sys/ptrace/mod.rs @@ -0,0 +1,22 @@ +///! Provides helpers for making ptrace system calls + +#[cfg(any(target_os = "android", target_os = "linux"))] +mod linux; + +#[cfg(any(target_os = "android", target_os = "linux"))] +pub use self::linux::*; + +#[cfg(any(target_os = "dragonfly", + target_os = "freebsd", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd"))] +mod bsd; + +#[cfg(any(target_os = "dragonfly", + target_os = "freebsd", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd" + ))] +pub use self::bsd::*; diff --git a/test/sys/mod.rs b/test/sys/mod.rs index bdc079d84c..46f2912394 100644 --- a/test/sys/mod.rs +++ b/test/sys/mod.rs @@ -27,5 +27,10 @@ mod test_uio; mod test_epoll; mod test_pthread; #[cfg(any(target_os = "android", - target_os = "linux"))] + target_os = "dragonfly", + target_os = "freebsd", + target_os = "linux", + target_os = "macos", + target_os = "netbsd", + target_os = "openbsd"))] mod test_ptrace; diff --git a/test/sys/test_ptrace.rs b/test/sys/test_ptrace.rs index a15a7921f6..24d9b522ee 100644 --- a/test/sys/test_ptrace.rs +++ b/test/sys/test_ptrace.rs @@ -1,7 +1,9 @@ use nix::Error; use nix::errno::Errno; use nix::unistd::getpid; -use nix::sys::ptrace::{self, Options}; +use nix::sys::ptrace; +#[cfg(any(target_os = "android", target_os = "linux"))] +use nix::sys::ptrace::Options; #[cfg(any(target_os = "android", target_os = "linux"))] use std::mem; @@ -11,11 +13,13 @@ fn test_ptrace() { // Just make sure ptrace can be called at all, for now. // FIXME: qemu-user doesn't implement ptrace on all arches, so permit ENOSYS let err = ptrace::attach(getpid()).unwrap_err(); - assert!(err == Error::Sys(Errno::EPERM) || err == Error::Sys(Errno::ENOSYS)); + assert!(err == Error::Sys(Errno::EPERM) || err == Error::Sys(Errno::EINVAL) || + err == Error::Sys(Errno::ENOSYS)); } // Just make sure ptrace_setoptions can be called at all, for now. #[test] +#[cfg(any(target_os = "android", target_os = "linux"))] fn test_ptrace_setoptions() { let err = ptrace::setoptions(getpid(), Options::PTRACE_O_TRACESYSGOOD).unwrap_err(); assert!(err != Error::UnsupportedOperation); @@ -23,6 +27,7 @@ fn test_ptrace_setoptions() { // Just make sure ptrace_getevent can be called at all, for now. #[test] +#[cfg(any(target_os = "android", target_os = "linux"))] fn test_ptrace_getevent() { let err = ptrace::getevent(getpid()).unwrap_err(); assert!(err != Error::UnsupportedOperation); @@ -30,6 +35,7 @@ fn test_ptrace_getevent() { // Just make sure ptrace_getsiginfo can be called at all, for now. #[test] +#[cfg(any(target_os = "android", target_os = "linux"))] fn test_ptrace_getsiginfo() { if let Err(Error::UnsupportedOperation) = ptrace::getsiginfo(getpid()) { panic!("ptrace_getsiginfo returns Error::UnsupportedOperation!"); @@ -38,6 +44,7 @@ fn test_ptrace_getsiginfo() { // Just make sure ptrace_setsiginfo can be called at all, for now. #[test] +#[cfg(any(target_os = "android", target_os = "linux"))] fn test_ptrace_setsiginfo() { let siginfo = unsafe { mem::uninitialized() }; if let Err(Error::UnsupportedOperation) = ptrace::setsiginfo(getpid(), &siginfo) { @@ -50,10 +57,12 @@ fn test_ptrace_setsiginfo() { fn test_ptrace_cont() { use nix::sys::ptrace; use nix::sys::signal::{raise, Signal}; - use nix::sys::wait::{waitpid, WaitStatus}; + use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus}; use nix::unistd::fork; use nix::unistd::ForkResult::*; + let _m = ::FORK_MTX.lock().expect("Mutex got poisoned by another test"); + // FIXME: qemu-user doesn't implement ptrace on all architectures // and retunrs ENOSYS in this case. // We (ab)use this behavior to detect the affected platforms @@ -79,9 +88,18 @@ fn test_ptrace_cont() { assert_eq!(waitpid(child, None), Ok(WaitStatus::Stopped(child, Signal::SIGTRAP))); ptrace::cont(child, None).unwrap(); assert_eq!(waitpid(child, None), Ok(WaitStatus::Stopped(child, Signal::SIGTRAP))); - ptrace::cont(child, Signal::SIGKILL).unwrap(); + ptrace::cont(child, Some(Signal::SIGKILL)).unwrap(); match waitpid(child, None) { - Ok(WaitStatus::Signaled(pid, Signal::SIGKILL, _)) if pid == child => {} + Ok(WaitStatus::Signaled(pid, Signal::SIGKILL, _)) if pid == child => { + // FIXME It's been observed on some systems (apple) the + // tracee may not be killed but remain as a zombie process + // affecting other wait based tests. Add an extra kill just + // to make sure there are no zombies. + let _ = waitpid(child, Some(WaitPidFlag::WNOHANG)); + while ptrace::cont(child, Some(Signal::SIGKILL)).is_ok() { + let _ = waitpid(child, Some(WaitPidFlag::WNOHANG)); + } + } _ => panic!("The process should have been killed"), } },