Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ptrace support for BSDs #949

Merged
merged 1 commit into from Oct 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -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/
Expand Down
8 changes: 7 additions & 1 deletion src/sys/mod.rs
Expand Up @@ -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")]
Expand Down
170 changes: 170 additions & 0 deletions 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",
Susurrus marked this conversation as resolved.
Show resolved Hide resolved
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 {
Susurrus marked this conversation as resolved.
Show resolved Hide resolved
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<c_int> {
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<T: Into<Option<Signal>>>(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<T: Into<Option<Signal>>>(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<c_int> {
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<()> {
Susurrus marked this conversation as resolved.
Show resolved Hide resolved
unsafe { ptrace_other(Request::PT_WRITE_D, pid, addr, data).map(|_| ()) }
}
28 changes: 26 additions & 2 deletions src/sys/ptrace.rs → src/sys/ptrace/linux.rs
Expand Up @@ -173,8 +173,10 @@ libc_bitflags! {
pub unsafe fn ptrace(request: Request, pid: Pid, addr: *mut c_void, data: *mut c_void) -> Result<c_long> {
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)
}
}
Expand Down Expand Up @@ -320,6 +322,15 @@ pub fn cont<T: Into<Option<Signal>>>(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<()> {
Susurrus marked this conversation as resolved.
Show resolved Hide resolved
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, ...)`
///
Expand Down Expand Up @@ -354,3 +365,16 @@ pub fn step<T: Into<Option<Signal>>>(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<c_long> {
Susurrus marked this conversation as resolved.
Show resolved Hide resolved
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<()> {
Susurrus marked this conversation as resolved.
Show resolved Hide resolved
Susurrus marked this conversation as resolved.
Show resolved Hide resolved
unsafe {
ptrace_other(Request::PTRACE_POKEDATA, pid, addr, data).map(|_| ())
}
}
22 changes: 22 additions & 0 deletions 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::*;
7 changes: 6 additions & 1 deletion test/sys/mod.rs
Expand Up @@ -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;
28 changes: 23 additions & 5 deletions 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;
Expand All @@ -11,25 +13,29 @@ 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) ||
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you allowing EINVAL?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://www.freebsd.org/cgi/man.cgi?query=ptrace the BSDs seem to use EINVAL for attempting to attach to yourself. I don't know if all BSDs do that or if some mimic linux and do EPERM so I just had it as that for now. I can investigate and make it more specific though

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NetBSD returns 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);
}

// 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);
}

// 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!");
Expand All @@ -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) {
Expand All @@ -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
Expand All @@ -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"),
}
},
Expand Down