From bfc7e35a96e09861363023d0c81fd8eb4796a522 Mon Sep 17 00:00:00 2001 From: ice1000 Date: Thu, 22 Nov 2018 03:28:43 -0500 Subject: [PATCH] Squash master changes Signed-off-by: ice1000 --- .gitignore | 1 + .travis.yml | 16 ++- README.md | 2 + src/filesystems.rs | 87 +++++++++++++++ src/lib.rs | 3 + src/parsers.rs | 14 +++ src/pid/cwd.rs | 6 ++ src/pid/environ.rs | 186 ++++++++++++++++++++++++++++++++ src/pid/io.rs | 118 ++++++++++++++++++++ src/pid/limits.rs | 40 +++---- src/pid/maps.rs | 250 +++++++++++++++++++++++++++++++++++++++++++ src/pid/mod.rs | 18 ++-- src/pid/mountinfo.rs | 8 +- src/pid/stat.rs | 106 +++++++++--------- src/pid/statm.rs | 18 ++-- src/pid/status.rs | 21 ++-- src/unmangle.rs | 99 +++++++++++++++++ 17 files changed, 894 insertions(+), 99 deletions(-) create mode 100644 src/filesystems.rs create mode 100644 src/pid/environ.rs create mode 100644 src/pid/io.rs create mode 100644 src/pid/maps.rs create mode 100644 src/unmangle.rs diff --git a/.gitignore b/.gitignore index a9d37c5..64f40ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target Cargo.lock +.idea diff --git a/.travis.yml b/.travis.yml index c249513..63ed78b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,11 +5,25 @@ rust: - stable - nightly +env: +- +- CARGO_BUILD_TARGET=arm-linux-androideabi +- CARGO_BUILD_TARGET=x86_64-linux-android +- CARGO_BUILD_TARGET=aarch64-linux-android + os: - linux +matrix: + exclude: + rust: 1.13.0 + env: CARGO_BUILD_TARGET=x86_64-linux-android + script: + - if [[ $CARGO_BUILD_TARGET ]]; then + rustup target add $CARGO_BUILD_TARGET; + fi - cargo build --verbose - - if [[ $TRAVIS_RUST_VERSION = nightly* ]]; then + - if [[ $TRAVIS_RUST_VERSION = nightly* && -z "$CARGO_BUILD_TARGET" ]]; then env RUST_BACKTRACE=1 cargo test -v; fi diff --git a/README.md b/README.md index 1814cc0..07c2eca 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,9 @@ currently the following interfaces are provided: * `/proc/loadavg` * `/proc//cwd` +* `/proc//environ` * `/proc//limits` +* `/proc//maps` * `/proc//mountinfo` * `/proc//stat` * `/proc//statm` diff --git a/src/filesystems.rs b/src/filesystems.rs new file mode 100644 index 0000000..bc35397 --- /dev/null +++ b/src/filesystems.rs @@ -0,0 +1,87 @@ +//! Supported filesystems from `/proc/filesystems`. + +use std::fs::File; +use std::io::{BufRead, BufReader, Result}; + +use nom::tab; + +use parsers::{map_result, parse_line}; + +/// Supported filesystems. +/// +/// This is a list of filesystems which which are supported by the +/// kernel, namely filesystems which were compiled into the kernel +/// or whose kernel modules are currently loaded. If a filesystem +/// is marked with "nodev", this means that it does not require a +/// block device to be mounted (e.g., virtual filesystem, network +/// filesystem). +/// +/// See `man 5 proc` and `Linux/fs/filesystems.c`. +#[derive(Debug, Default, PartialEq)] +pub struct Filesystem { + /// The filesystem does not require a block device to be mounted (e.g., virtual filesytems, network filesystems). + pub nodev: bool, + /// The name of the filesystem (e.g. "ext4"). + pub name: String, +} + +/// Parses a filesystem entry according to filesystems file format. +named!(parse_filesystem, + do_parse!(nodev: opt!(tag!("nodev")) >> tab >> + name: parse_line >> + ( Filesystem { nodev: nodev.is_some(), name: name } ))); + +/// Returns the supported filesystems. +pub fn filesystems() -> Result> { + let mut file = try!(File::open("/proc/filesystems")); + let mut r = Vec::new(); + for line in BufReader::new(&mut file).lines() { + let fs = try!(map_result(parse_filesystem(try!(line).as_bytes()))); + r.push(fs); + } + Ok(r) +} + +#[cfg(test)] +pub mod tests { + use super::{Filesystem, parse_filesystem, filesystems}; + + /// Test parsing a single filesystems entry (positive check). + #[test] + fn test_parse_filesystem() { + let entry = + b"\text4"; + let got_fs = parse_filesystem(entry).unwrap().1; + let want_fs = Filesystem { + nodev: false, + name: "ext4".to_string(), + }; + assert_eq!(got_fs, want_fs); + } + + /// Test parsing a single filesystems entry with nodev (positive check). + #[test] + fn test_parse_nodev_filesystem() { + let entry = + b"nodev\tfuse"; + let got_fs = parse_filesystem(entry).unwrap().1; + let want_fs = Filesystem { + nodev: true, + name: "fuse".to_string(), + }; + assert_eq!(got_fs, want_fs); + } + + /// Test parsing a single filesystem entry (negative check). + #[test] + fn test_parse_filesystem_error() { + let entry = b"garbage"; + parse_filesystem(entry).unwrap_err(); + } + + /// Test that the system filesystems file can be parsed. + #[test] + fn test_filesystems() { + filesystems().unwrap(); + } +} diff --git a/src/lib.rs b/src/lib.rs index 24cc2ba..53f54f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,10 +13,13 @@ extern crate libc; #[macro_use] mod parsers; +mod unmangle; +mod filesystems; mod loadavg; pub mod pid; pub mod sys; pub mod net; +pub use filesystems::{Filesystem, filesystems}; pub use loadavg::{LoadAvg, loadavg}; diff --git a/src/parsers.rs b/src/parsers.rs index f460f4f..c511d8e 100644 --- a/src/parsers.rs +++ b/src/parsers.rs @@ -130,6 +130,10 @@ named!(pub parse_u32, named!(pub parse_u64, map_res!(map_res!(digit, str::from_utf8), FromStr::from_str)); +/// Parses a u128 in base-10 format. +named!(pub parse_u128, + map_res!(map_res!(digit, str::from_utf8), FromStr::from_str)); + /// Parses a usize in base-10 format. named!(pub parse_usize, map_res!(map_res!(digit, str::from_utf8), FromStr::from_str)); @@ -164,11 +168,21 @@ named!(pub parse_u32_octal, map_res!(map_res!(alphanumeric, str::from_utf8), |s| u32::from_str_radix(s, 8))); +/// Parses a u16 in base-8 format. +named!(pub parse_u16_octal, + map_res!(map_res!(alphanumeric, str::from_utf8), + |s| u16::from_str_radix(s, 8))); + /// Parses a u64 in base-16 format. named!(pub parse_u64_hex, map_res!(map_res!(alphanumeric, str::from_utf8), |s| u64::from_str_radix(s, 16))); +/// Parses a usize in base-16 format. +named!(pub parse_usize_hex, + map_res!(map_res!(alphanumeric, str::from_utf8), + |s| usize::from_str_radix(s, 16))); + /// Reverses the bits in a byte. fn reverse(n: u8) -> u8 { // stackoverflow.com/questions/2602823/in-c-c-whats-the-simplest-way-to-reverse-the-order-of-bits-in-a-byte diff --git a/src/pid/cwd.rs b/src/pid/cwd.rs index 04b257c..1b30e3d 100644 --- a/src/pid/cwd.rs +++ b/src/pid/cwd.rs @@ -13,6 +13,12 @@ pub fn cwd(pid: pid_t) -> Result { fs::read_link(format!("/proc/{}/cwd", pid)) } +/// Gets path of current working directory for the process with the provided +/// pid and tid. +pub fn cwd_task(pid: pid_t, tid: pid_t) -> Result { + fs::read_link(format!("/proc/{}/task/{}/cwd", pid, tid)) +} + /// Gets path of current working directory for the current process. pub fn cwd_self() -> Result { fs::read_link("/proc/self/cwd") diff --git a/src/pid/environ.rs b/src/pid/environ.rs new file mode 100644 index 0000000..a697458 --- /dev/null +++ b/src/pid/environ.rs @@ -0,0 +1,186 @@ +//! Process initial environment from `/proc/[pid]/environ`. + +use std::ffi::OsStr; +use std::fs::File; +use std::io::{Error, ErrorKind, Read, Result}; +use std::os::unix::ffi::OsStrExt; +use std::path::Path; +use std::iter::Iterator; + +use libc::pid_t; +use nom::{self, IResult}; + +/// An environment holder. +/// +/// Use the `into_iter` method to access environment variables as key-value pairs. +#[derive(Debug, Clone)] +pub struct Environ { + data: Vec, +} + +/// A lazy iterator over environment variables. +pub struct EnvironIter<'a> { + data_pointer: &'a [u8], +} + +impl<'a> Iterator for EnvironIter<'a> { + /// Since the data is parsed on the fly, a parsing error could be encountered, hence using an + /// `io::Result` as an iterator item. + type Item = Result<(&'a OsStr, &'a OsStr)>; + + fn next(&mut self) -> Option { + if self.data_pointer.is_empty() { + return None; + } + match parse_pair(self.data_pointer) { + IResult::Done(data, parsed) => { + self.data_pointer = data; + Some(Ok(parsed)) + } + IResult::Incomplete(_) => None, + IResult::Error(err) => Some(Err(Error::new( + ErrorKind::InvalidInput, + format!("Unable to parse input: {:?}", err), + ))), + } + } +} + +impl<'a> IntoIterator for &'a Environ { + type Item = Result<(&'a OsStr, &'a OsStr)>; + type IntoIter = EnvironIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + EnvironIter { + data_pointer: &self.data, + } + } +} + +/// Extracts name of a variable. Also consumes a delimiter. +fn get_name(src: &[u8]) -> IResult<&[u8], &OsStr> { + // Calculate position of the *equal* sign. + let pos = match src.iter().skip(1).position(|c| c == &b'=') { + Some(p) => p, + None => return IResult::Error(error_position!(nom::ErrorKind::Custom(0), src)), + }; + IResult::Done(&src[pos + 2..], from_bytes(&src[..pos + 1])) +} + +/// Parses "key=value" pair. +named!( + parse_pair<&[u8], (&OsStr, &OsStr)>, + tuple!(get_name, map!(take_until_and_consume!("\0"), from_bytes)) +); + +/// A helper function to convert a slice of bytes to an `OsString`. +fn from_bytes(s: &[u8]) -> &OsStr { + OsStr::from_bytes(s) +} + +/// Parses the provided environ file. +fn environ_path>(path: P) -> Result { + let mut buf = Vec::new(); + File::open(path)?.read_to_end(&mut buf)?; + Ok(Environ { data: buf }) +} + +/// Returns initial environment for the process with the provided pid as key-value pairs. +pub fn environ(pid: pid_t) -> Result { + environ_path(format!("/proc/{}/environ", pid)) +} + +/// Returns initial environment for the process with the provided process id and thread id +/// as key-value pairs. +pub fn environ_task(pid: pid_t, tid: pid_t) -> Result { + environ_path(format!("/proc/{}/task/{}/environ", pid, tid)) +} + +/// Returns initial environment for the current process as key-value pairs. +pub fn environ_self() -> Result { + environ_path("/proc/self/environ") +} + +#[cfg(test)] +mod test { + use super::*; + use std::collections::BTreeMap; + + #[test] + fn test_get_name() { + let src = &b"FOO=BAR"[..]; + assert_eq!(get_name(src), IResult::Done(&b"BAR"[..], OsStr::new("FOO"))); + let src = &b"FOO="[..]; + assert_eq!(get_name(src), IResult::Done(&b""[..], OsStr::new("FOO"))); + let src = &b"=FOO=BAR"[..]; + assert_eq!( + get_name(src), + IResult::Done(&b"BAR"[..], OsStr::new("=FOO")) + ); + let src = &b"=FOO="[..]; + assert_eq!(get_name(src), IResult::Done(&b""[..], OsStr::new("=FOO"))); + } + + #[test] + fn test_pair() { + let source = &b"FOO=BAR\0123"[..]; + assert_eq!( + parse_pair(source), + IResult::Done(&b"123"[..], (OsStr::new("FOO"), OsStr::new("BAR"))) + ); + let source = &b"FOO=\0123"[..]; + assert_eq!( + parse_pair(source), + IResult::Done(&b"123"[..], (OsStr::new("FOO"), OsStr::new(""))) + ); + let source = &b"=FOO=BAR\0-"[..]; + assert_eq!( + parse_pair(source), + IResult::Done(&b"-"[..], (OsStr::new("=FOO"), OsStr::new("BAR"))) + ); + let source = &b"=FOO=\0-"[..]; + assert_eq!( + parse_pair(source), + IResult::Done(&b"-"[..], (OsStr::new("=FOO"), OsStr::new(""))) + ); + } + + #[test] + fn test_iter() { + let env = Environ { + data: b"key1=val1\0=key2=val 2\0key3=val3\0".to_vec(), + }; + // Here's how you convert the env into a vector. + let pairs_vec: Result> = env.into_iter().collect(); + let pairs_vec = match pairs_vec { + Err(e) => panic!("Parsing has failed: {:?}", e), + Ok(pairs) => pairs, + }; + assert_eq!( + pairs_vec, + vec![ + (OsStr::new("key1"), OsStr::new("val1")), + (OsStr::new("=key2"), OsStr::new("val 2")), + (OsStr::new("key3"), OsStr::new("val3")), + ] + ); + // And here's how you create a map. + let pairs_map: Result> = env.into_iter().collect(); + let pairs_map = match pairs_map { + Err(e) => panic!("Parsing has failed: {:?}", e), + Ok(pairs) => pairs, + }; + assert_eq!(pairs_map.get(OsStr::new("key1")), Some(&OsStr::new("val1"))); + assert_eq!( + pairs_map.get(OsStr::new("=key2")), + Some(&OsStr::new("val 2")) + ); + assert_eq!(pairs_map.get(OsStr::new("key3")), Some(&OsStr::new("val3"))); + } + + #[test] + fn test_environ_self() { + let env = environ_self().unwrap(); + assert!(env.into_iter().all(|x| x.is_ok())); + } +} diff --git a/src/pid/io.rs b/src/pid/io.rs new file mode 100644 index 0000000..43d1626 --- /dev/null +++ b/src/pid/io.rs @@ -0,0 +1,118 @@ +//! Concerning the I/O information of a process, from +//! `/proc/[pid]/io`. + +use std::fs::File; +use std::io::Result; + +use libc::pid_t; +use nom::{ + IResult, + Err, + ErrorKind, + line_ending, + space, +}; + +use parsers::{ + map_result, + parse_usize, + read_to_end, +}; + +/// The I/O information of a process +#[derive(Debug, Default, PartialEq, Eq, Hash)] +pub struct Io { + pub rchar: usize, + pub wchar: usize, + pub syscr: usize, + pub syscw: usize, + pub read_bytes: usize, + pub write_bytes: usize, + pub cancelled_write_bytes: usize, +} + +named!(opt_space>, opt!(space)); +named!(parse_rchar, chain!(opt_space ~ tag!("rchar:") ~ opt_space ~ s: parse_usize ~ line_ending, || { s })); +named!(parse_wchar, chain!(opt_space ~ tag!("wchar:") ~ opt_space ~ s: parse_usize ~ line_ending, || { s })); +named!(parse_syscr, chain!(opt_space ~ tag!("syscr:") ~ opt_space ~ s: parse_usize ~ line_ending, || { s })); +named!(parse_syscw, chain!(opt_space ~ tag!("syscw:") ~ opt_space ~ s: parse_usize ~ line_ending, || { s })); +named!(parse_read_bytes, chain!(opt_space ~ tag!("read_bytes:") ~ opt_space ~ s: parse_usize ~ line_ending, || { s })); +named!(parse_write_bytes, chain!(opt_space ~ tag!("write_bytes:") ~ opt_space ~ s: parse_usize ~ line_ending, || { s })); +named!(parse_cancelled_write_bytes, chain!(opt_space ~ tag!("cancelled_write_bytes:") ~ opt_space ~ s: parse_usize ~ line_ending, || { s })); + +fn parse_io(mut input: &[u8]) -> IResult<&[u8], Io> { + let mut io: Io = Default::default(); + loop { + let original_len = input.len(); + let (rest, ()) = try_parse!(input, + alt!( parse_rchar => { |value| io.rchar = value } + | parse_wchar => { |value| io.wchar = value } + | parse_syscr => { |value| io.syscr = value } + | parse_syscw => { |value| io.syscw = value } + | parse_read_bytes => { |value| io.read_bytes = value } + | parse_write_bytes => { |value| io.write_bytes = value } + | parse_cancelled_write_bytes => { |value| io.cancelled_write_bytes = value } + )); + let final_len = rest.len(); + if final_len == 0 { + break IResult::Done(&[], io); + } else if original_len == final_len { + break IResult::Error(Err::Position(ErrorKind::Tag, rest)); + } + input = rest; + } +} + +/// Parses the provided stat file. +fn io_file(file: &mut File) -> Result { + let mut buf = [0; 1024]; // A typical io file is about 100 bytes + map_result(parse_io(read_to_end(file, &mut buf)?)) +} + +/// Returns I/O information for the process with the provided pid. +pub fn io(pid: pid_t) -> Result { + io_file(&mut File::open(&format!("/proc/{}/io", pid))?) +} + +/// Returns I/O information for the current process. +pub fn io_self() -> Result { + io_file(&mut File::open("/proc/self/io")?) +} + +/// Returns I/O information from the thread with the provided parent process ID and thread ID. +pub fn io_task(process_id: pid_t, thread_id: pid_t) -> Result { + io_file(&mut File::open(&format!("/proc/{}/task/{}/io", process_id, thread_id))?) +} + +#[cfg(test)] +pub mod tests { + use parsers::tests::unwrap; + use libc::getpid; + use super::{Io, io, io_self, parse_io}; + + #[test] + fn test_io() { + io_self().unwrap(); + io(unsafe { getpid() }).unwrap(); + } + + #[test] + fn test_parse_io() { + let text = b"rchar: 4685194216 + wchar: 2920419824 + syscr: 1687286 + syscw: 708998 + read_bytes: 2938340352 + write_bytes: 2464854016 + cancelled_write_bytes: 592056320 +"; + let io: Io = unwrap(parse_io(text)); + assert_eq!(4685194216, io.rchar); + assert_eq!(2920419824, io.wchar); + assert_eq!(1687286, io.syscr); + assert_eq!(708998, io.syscw); + assert_eq!(2938340352, io.read_bytes); + assert_eq!(2464854016, io.write_bytes); + assert_eq!(592056320, io.cancelled_write_bytes); + } +} \ No newline at end of file diff --git a/src/pid/limits.rs b/src/pid/limits.rs index bbdb9ea..188d718 100644 --- a/src/pid/limits.rs +++ b/src/pid/limits.rs @@ -77,22 +77,22 @@ named!(parse_limits( &[u8] ) -> Limits, tag!("Max realtime priority") >> max_realtime_priority: parse_limit_usize >> tag!("Max realtime timeout") >> max_realtime_timeout: parse_limit_micros >> tag!("us") >> (Limits { - max_cpu_time: max_cpu_time, - max_file_size: max_file_size, - max_data_size: max_data_size, - max_stack_size: max_stack_size, - max_core_file_size: max_core_file_size, - max_resident_set: max_resident_set, - max_processes: max_processes, - max_open_files: max_open_files, - max_locked_memory: max_locked_memory, - max_address_space: max_address_space, - max_file_locks: max_file_locks, - max_pending_signals: max_pending_signals, - max_msgqueue_size: max_msgqueue_size, - max_nice_priority: max_nice_priority, - max_realtime_priority: max_realtime_priority, - max_realtime_timeout: max_realtime_timeout, + max_cpu_time, + max_file_size, + max_data_size, + max_stack_size, + max_core_file_size, + max_resident_set, + max_processes, + max_open_files, + max_locked_memory, + max_address_space, + max_file_locks, + max_pending_signals, + max_msgqueue_size, + max_nice_priority, + max_realtime_priority, + max_realtime_timeout, }) )) ); @@ -160,22 +160,22 @@ pub struct Limits { /// Parses the provided limits file. fn limits_file(file: &mut File) -> Result { let mut buf = [0; 2048]; // A typical limits file is about 1350 bytes - map_result(parse_limits(try!(read_to_end(file, &mut buf)))) + map_result(parse_limits(read_to_end(file, &mut buf)?)) } /// Returns resource limit information from the process with the provided pid. pub fn limits(pid: pid_t) -> Result { - limits_file(&mut try!(File::open(&format!("/proc/{}/limits", pid)))) + limits_file(&mut File::open(&format!("/proc/{}/limits", pid))?) } /// Returns resource limit information for the current process. pub fn limits_self() -> Result { - limits_file(&mut try!(File::open("/proc/self/limits"))) + limits_file(&mut File::open("/proc/self/limits")?) } /// Returns resource limit information from the thread with the provided parent process ID and thread ID. pub fn limits_task(process_id: pid_t, thread_id: pid_t) -> Result { - limits_file(&mut try!(File::open(&format!("/proc/{}/task/{}/limits", process_id, thread_id)))) + limits_file(&mut File::open(&format!("/proc/{}/task/{}/limits", process_id, thread_id))?) } #[cfg(test)] diff --git a/src/pid/maps.rs b/src/pid/maps.rs new file mode 100644 index 0000000..0ec3eba --- /dev/null +++ b/src/pid/maps.rs @@ -0,0 +1,250 @@ +//! Process memory mappings information from `/proc/[pid]/maps`. + +use std::ffi::OsString; +use std::io::{self, BufRead}; +use std::os::unix::ffi::OsStringExt; +use std::path::PathBuf; +use std::{fs, ops}; + +use libc; +use libc::pid_t; +use nom::{rest, space}; + +use parsers::{map_result, parse_usize_hex, parse_u32_hex, parse_u64, parse_u64_hex}; +use unmangle::unmangled_path; + +/// Process memory mapping information. +/// +/// Due to the way paths are encoded by the kernel before exposing them in +/// `/proc/[pid]/maps`, the parsing of `path` and `is_deleted` is +/// ambiguous. For example, all the following path/deleted combinations: +/// +/// - `/tmp/a\nfile` *(deleted file)* +/// - `/tmp/a\nfile (deleted)` *(existing file)* +/// - `/tmp/a\\012file` *(deleted file)* +/// - `/tmp/a\\012file (deleted)` *(existing file)* +/// +/// will be mangled by the kernel and decoded by this module as: +/// +/// ```rust,ignore +/// MemoryMapping ( +/// ..., +/// path: PathBuf::from("/tmp/a\nfile"), +/// is_deleted:true, +/// ) +/// ``` +/// +/// If the `path` of a mapping is required for other than purely informational +/// uses (such as opening and/or memory mapping it), a more reliable source +/// (such as `/proc/[pid]/map_files`) should be used, if available. The open +/// file `(dev, inode)` should also be checked against the values provided by +/// the mapping. +/// +/// See `man 5 proc`, `Linux/fs/proc/task_mmu.c`, and `Linux/fs/seq_file.c`. +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct MemoryMapping { + /// Address range that this mapping occupies in the process virtual memory + /// space. + pub range: ops::Range, + /// Whether pages in this mapping may be read. + pub is_readable: bool, + /// Whether pages in this mapping may be written. + pub is_writable: bool, + /// Whether pages in this mapping may be executed. + pub is_executable: bool, + /// Whether this mapping is shared. + pub is_shared: bool, + /// Offset into the file backing this mapping (for non-anonymous mappings). + pub offset: u64, + /// Device containing the file backing this mapping (for non-anonymous + /// mappings). + pub dev: libc::dev_t, + /// Inode of the file backing this mapping (for non-anonymous mappings). + pub inode: libc::ino_t, + /// Path to the file backing this mapping (for non-anonymous mappings), + /// pseudo-path (such as `[stack]`, `[heap]`, or `[vdso]`) for some special + /// anonymous mappings, or empty path for other anonymous mappings. + pub path: PathBuf, + /// Whether the file backing this mapping has been deleted (for + /// non-anonymous mappings). + pub is_deleted: bool, +} + +impl MemoryMapping { + /// Returns `true` if this is an anonymous mapping. + pub fn is_anonymous(&self) -> bool { + self.inode == 0 + } +} + +/// Parsed `pathname` field. +#[derive(Debug, PartialEq, Eq, Hash)] +struct Pathname { + path: PathBuf, + is_deleted: bool, +} + +impl Pathname { + /// Parses a `pathname` field. + fn from_bytes(bytes: &[u8]) -> Self { + let mut path = unmangled_path(bytes, b"\n"); + let deleted_suffix = b" (deleted)"; + let is_deleted = path.ends_with(deleted_suffix); + if is_deleted { + let length = path.len() - deleted_suffix.len(); + path.truncate(length); + } + Pathname { + path: PathBuf::from(OsString::from_vec(path)), + is_deleted, + } + } +} + +/// Parses read permission flag. +named!(perms_read<&[u8], bool>, map!(one_of!("r-"), |c| c == 'r')); + +/// Parses write permission flag. +named!(perms_write<&[u8], bool>, map!(one_of!("w-"), |c| c == 'w')); + +/// Parses execute permission flag. +named!(perms_execute<&[u8], bool>, map!(one_of!("x-"), |c| c == 'x')); + +/// Parses shared/private permission flag. +named!(perms_shared<&[u8], bool>, map!(one_of!("sp"), |c| c == 's')); + +/// Parses a maps entry. +named!(parse_maps_entry<&[u8], MemoryMapping>, do_parse!( + start: parse_usize_hex >> tag!("-") >> + end: parse_usize_hex >> space >> + is_readable: perms_read >> + is_writable: perms_write >> + is_executable: perms_execute >> + is_shared: perms_shared >> space >> + offset: parse_u64_hex >> space >> + major: parse_u32_hex >> tag!(":") >> + minor: parse_u32_hex >> space >> + inode: parse_u64 >> space >> + pathname: map!(rest, Pathname::from_bytes) >> + (MemoryMapping { + range: ops::Range{start, end}, + is_readable, + is_writable, + is_executable, + is_shared, + offset, + dev: unsafe {libc::makedev(major, minor)}, + inode, + path: pathname.path, + is_deleted: pathname.is_deleted, + }) +)); + +/// Parses the provided maps file. +fn maps_file(file: &mut R) -> io::Result> { + io::BufReader::new(file) + .split(b'\n') + .map(|line| map_result(parse_maps_entry(&line?))) + .collect() +} + +/// Returns mapped memory regions information for the process with the provided +/// pid. +pub fn maps(pid: pid_t) -> io::Result> { + maps_file(&mut fs::File::open(format!("/proc/{}/maps", pid))?) +} + +/// Returns mapped memory regions information for the process with the provided +/// pid and tid. +pub fn maps_task(pid: pid_t, tid: pid_t) -> io::Result> { + maps_file(&mut fs::File::open(format!("/proc/{}/task/{}/maps", pid, tid))?) +} + +/// Returns mapped memory regions information for the current process. +pub fn maps_self() -> io::Result> { + maps_file(&mut fs::File::open("/proc/self/maps")?) +} + +#[cfg(test)] +pub mod tests { + use std::path::Path; + + use super::*; + + /// Test that the current process maps file can be parsed. + #[test] + fn test_maps() { + maps_self().unwrap(); + } + + #[test] + fn test_maps_file() { + let maps_text = b"\ +5643a788f000-5643a7897000 r-xp 00000000 fd:01 8650756 /bin/cat +7f0540a43000-7f0540a47000 rw-p 00000000 00:00 0 \n"; + let mut buf = io::Cursor::new(maps_text.as_ref()); + let maps = maps_file(&mut buf).unwrap(); + assert_eq!(2, maps.len()); + } + + #[test] + fn test_maps_file_pathname_ends_with_cr() { + let maps_text = b"\ +5643a788f000-5643a7897000 r-xp 00000000 fd:01 8650756 /bin/cat\r +5643a9412000-5643a9433000 rw-p 00000000 00:00 0 [heap] +"; + let mut buf = io::Cursor::new(maps_text.as_ref()); + let maps = maps_file(&mut buf).unwrap(); + assert_eq!(2, maps.len()); + assert_eq!(Path::new("/bin/cat\r"), maps[0].path); + } + + #[test] + fn test_parse_maps_entry() { + let maps_entry_text = b"\ +5643a788f000-5643a7897000 r-xp 00000000 fd:01 8650756 /bin/cat"; + + let map = parse_maps_entry(maps_entry_text).to_result().unwrap(); + assert_eq!(0x5643a788f000, map.range.start); + assert_eq!(0x5643a7897000, map.range.end); + assert!(map.is_readable); + assert!(!map.is_writable); + assert!(map.is_executable); + assert!(!map.is_shared); + assert_eq!(0, map.offset); + assert_eq!(unsafe { libc::makedev(0xfd, 0x1) }, map.dev); + assert_eq!(8650756, map.inode); + assert_eq!(Path::new("/bin/cat"), map.path); + assert!(!map.is_deleted); + } + + #[test] + fn test_parse_maps_entry_no_path() { + let maps_entry_text = b"\ +7f8ec1d99000-7f8ec1dbe000 rw-p 00000000 00:00 0 "; + + let map = parse_maps_entry(maps_entry_text).to_result().unwrap(); + assert_eq!(Path::new(""), map.path); + assert!(!map.is_deleted); + assert!(map.is_anonymous()); + } + + #[test] + fn test_pathname_from_bytes() { + let pathname = Pathname::from_bytes(b"/bin/cat"); + assert_eq!(Path::new("/bin/cat"), pathname.path); + assert!(!pathname.is_deleted); + + let pathname = Pathname::from_bytes(b"/bin/cat (deleted)"); + assert_eq!(Path::new("/bin/cat"), pathname.path); + assert!(pathname.is_deleted); + + let pathname = Pathname::from_bytes(br"/bin/a program"); + assert_eq!(Path::new("/bin/a program"), pathname.path); + assert!(!pathname.is_deleted); + + let pathname = Pathname::from_bytes(br"/bin/a\012program"); + assert_eq!(Path::new("/bin/a\nprogram"), pathname.path); + assert!(!pathname.is_deleted); + } +} diff --git a/src/pid/mod.rs b/src/pid/mod.rs index 1feebb5..3417af0 100644 --- a/src/pid/mod.rs +++ b/src/pid/mod.rs @@ -2,17 +2,23 @@ mod cwd; mod limits; +mod maps; mod mountinfo; mod stat; mod statm; mod status; +mod environ; +mod io; -pub use pid::cwd::{cwd, cwd_self}; -pub use pid::limits::{Limit, Limits, limits, limits_self}; -pub use pid::mountinfo::{Mountinfo, mountinfo, mountinfo_self}; -pub use pid::statm::{Statm, statm, statm_self}; -pub use pid::status::{SeccompMode, Status, status, status_self}; -pub use pid::stat::{Stat, stat, stat_self}; +pub use pid::cwd::{cwd, cwd_self, cwd_task}; +pub use pid::limits::{Limit, Limits, limits, limits_self, limits_task}; +pub use pid::mountinfo::{Mountinfo, mountinfo, mountinfo_self, mountinfo_task}; +pub use pid::statm::{Statm, statm, statm_self, statm_task}; +pub use pid::status::{SeccompMode, Status, status, status_self, status_task}; +pub use pid::stat::{Stat, stat, stat_self, stat_task}; +pub use pid::environ::{Environ, environ, environ_self, environ_task}; +pub use pid::maps::{MemoryMapping, maps, maps_self, maps_task}; +pub use pid::io::{Io, io, io_self, io_task}; /// The state of a process. #[derive(Debug, PartialEq, Eq, Hash)] diff --git a/src/pid/mountinfo.rs b/src/pid/mountinfo.rs index b8dd7b3..a9a5ce6 100644 --- a/src/pid/mountinfo.rs +++ b/src/pid/mountinfo.rs @@ -233,7 +233,7 @@ named!(parse_mountinfo_entry, fn mountinfo_file(file: &mut File) -> Result> { let mut r = Vec::new(); for line in BufReader::new(file).lines() { - let mi = try!(map_result(parse_mountinfo_entry(try!(line).as_bytes()))); + let mi = map_result(parse_mountinfo_entry(line?.as_bytes()))?; r.push(mi); } Ok(r) @@ -241,17 +241,17 @@ fn mountinfo_file(file: &mut File) -> Result> { /// Returns mounts information for the process with the provided pid. pub fn mountinfo(pid: pid_t) -> Result> { - mountinfo_file(&mut try!(File::open(&format!("/proc/{}/mountinfo", pid)))) + mountinfo_file(&mut File::open(&format!("/proc/{}/mountinfo", pid))?) } /// Returns mounts information for the current process. pub fn mountinfo_self() -> Result> { - mountinfo_file(&mut try!(File::open("/proc/self/mountinfo"))) + mountinfo_file(&mut File::open("/proc/self/mountinfo")?) } /// Returns mounts information from the thread with the provided parent process ID and thread ID. pub fn mountinfo_task(process_id: pid_t, thread_id: pid_t) -> Result> { - mountinfo_file(&mut try!(File::open(&format!("/proc/{}/task/{}/mountinfo", process_id, thread_id)))) + mountinfo_file(&mut File::open(&format!("/proc/{}/task/{}/mountinfo", process_id, thread_id))?) } #[cfg(test)] diff --git a/src/pid/stat.rs b/src/pid/stat.rs index 17fbc88..d15c889 100644 --- a/src/pid/stat.rs +++ b/src/pid/stat.rs @@ -249,77 +249,77 @@ fn parse_stat(input: &[u8]) -> IResult<&[u8], Stat> { let (rest, exit_code) = try_parse!(rest, l!(parse_i32 )); IResult::Done(rest, Stat { - pid : pid, - command : command, - state : state, - ppid : ppid, - pgrp : pgrp, - session : session, - tty_nr : tty_nr, - tty_pgrp : tty_pgrp, - flags : flags, - minflt : minflt, - cminflt : cminflt, - majflt : majflt, - cmajflt : cmajflt, - utime : utime, - stime : stime, - cutime : cutime, - cstime : cstime, - priority : priority, - nice : nice, - num_threads : num_threads, - start_time : start_time, - vsize : vsize, - rss : rss, - rsslim : rsslim, - start_code : start_code, - end_code : end_code, - startstack : startstack, - kstkeep : kstkeep, - kstkeip : kstkeip, - signal : signal, - blocked : blocked, - sigignore : sigignore, - sigcatch : sigcatch, - wchan : wchan, - exit_signal : exit_signal, - processor : processor, - rt_priority : rt_priority, - policy : policy, - delayacct_blkio_ticks : delayacct_blkio_ticks, - guest_time : guest_time, - cguest_time : cguest_time, - start_data : start_data, - end_data : end_data, - start_brk : start_brk, - arg_start : arg_start, - arg_end : arg_end, - env_start : env_start, - env_end : env_end, - exit_code : exit_code, + pid, + command, + state, + ppid, + pgrp, + session, + tty_nr, + tty_pgrp, + flags, + minflt, + cminflt, + majflt, + cmajflt, + utime, + stime, + cutime, + cstime, + priority, + nice, + num_threads, + start_time, + vsize, + rss, + rsslim, + start_code, + end_code, + startstack, + kstkeep, + kstkeip, + signal, + blocked, + sigignore, + sigcatch, + wchan, + exit_signal, + processor, + rt_priority, + policy, + delayacct_blkio_ticks, + guest_time, + cguest_time, + start_data, + end_data, + start_brk, + arg_start, + arg_end, + env_start, + env_end, + exit_code, }) } /// Parses the provided stat file. fn stat_file(file: &mut File) -> Result { let mut buf = [0; 1024]; // A typical statm file is about 300 bytes - map_result(parse_stat(try!(read_to_end(file, &mut buf)))) + map_result(parse_stat(read_to_end(file, &mut buf)?)) } /// Returns status information for the process with the provided pid. pub fn stat(pid: pid_t) -> Result { - stat_file(&mut try!(File::open(&format!("/proc/{}/stat", pid)))) + stat_file(&mut File::open(&format!("/proc/{}/stat", pid))?) } /// Returns status information for the current process. pub fn stat_self() -> Result { - stat_file(&mut try!(File::open("/proc/self/stat"))) + stat_file(&mut File::open("/proc/self/stat")?) } /// Returns status information from the thread with the provided parent process ID and thread ID. pub fn stat_task(process_id: pid_t, thread_id: pid_t) -> Result { - stat_file(&mut try!(File::open(&format!("/proc/{}/task/{}/stat", process_id, thread_id)))) + stat_file(&mut File::open(&format!("/proc/{}/task/{}/stat", process_id, thread_id))?) } #[cfg(test)] diff --git a/src/pid/statm.rs b/src/pid/statm.rs index 24a3137..341ab09 100644 --- a/src/pid/statm.rs +++ b/src/pid/statm.rs @@ -36,31 +36,31 @@ named!(parse_statm, digit ~ space ~ // lib - unused since linux 2.6 data: parse_usize ~ space ~ digit ~ line_ending, // dt - unused since linux 2.6 - || { Statm { size: size, - resident: resident, - share: share, - text: text, - data: data } })); + || { Statm { size, + resident, + share, + text, + data } })); /// Parses the provided statm file. fn statm_file(file: &mut File) -> Result { let mut buf = [0; 256]; // A typical statm file is about 25 bytes - map_result(parse_statm(try!(read_to_end(file, &mut buf)))) + map_result(parse_statm(read_to_end(file, &mut buf)?)) } /// Returns memory status information for the process with the provided pid. pub fn statm(pid: pid_t) -> Result { - statm_file(&mut try!(File::open(&format!("/proc/{}/statm", pid)))) + statm_file(&mut File::open(&format!("/proc/{}/statm", pid))?) } /// Returns memory status information for the current process. pub fn statm_self() -> Result { - statm_file(&mut try!(File::open("/proc/self/statm"))) + statm_file(&mut File::open("/proc/self/statm")?) } /// Returns memory status information from the thread with the provided parent process ID and thread ID. pub fn statm_task(process_id: pid_t, thread_id: pid_t) -> Result { - statm_file(&mut try!(File::open(&format!("/proc/{}/task/{}/statm", process_id, thread_id)))) + statm_file(&mut File::open(&format!("/proc/{}/task/{}/statm", process_id, thread_id))?) } #[cfg(test)] diff --git a/src/pid/status.rs b/src/pid/status.rs index a8cc3ad..5b6cf92 100644 --- a/src/pid/status.rs +++ b/src/pid/status.rs @@ -21,6 +21,9 @@ use parsers::{ parse_u64_hex, read_to_end }; + +#[cfg(all(target_os = "android", target_arch = "arm"))] +use parsers::parse_u16_octal; use pid::State; /// The Secure Computing state of a process. @@ -175,6 +178,7 @@ pub struct Status { pub voluntary_ctxt_switches: u64, /// Number of involuntary context switches. pub nonvoluntary_ctxt_switches: u64, + pub speculation_store_nypass: String, } /// Parse the status state format. @@ -188,6 +192,9 @@ named!(parse_status_state, | tag!("Z (zombie)") => { |_| State::Zombie })); named!(parse_command, delimited!(tag!("Name:\t"), parse_line, line_ending)); +#[cfg(all(target_os = "android", target_arch = "arm"))] +named!(parse_umask, delimited!(tag!("Umask:\t"), parse_u16_octal, line_ending)); +#[cfg(not(all(target_os = "android", target_arch = "arm")))] named!(parse_umask, delimited!(tag!("Umask:\t"), parse_u32_octal, line_ending)); named!(parse_state, delimited!(tag!("State:\t"), parse_status_state, line_ending)); named!(parse_pid, delimited!(tag!("Tgid:\t"), parse_i32, line_ending)); @@ -255,8 +262,9 @@ named!(parse_mems_allowed >, delimited!(tag!("Mems_allowed:\t"), parse named!(parse_cpus_allowed_list<()>, chain!(tag!("Cpus_allowed_list:\t") ~ not_line_ending ~ line_ending, || { () })); named!(parse_mems_allowed_list<()>, chain!(tag!("Mems_allowed_list:\t") ~ not_line_ending ~ line_ending, || { () })); -named!(parse_voluntary_ctxt_switches, delimited!(tag!("voluntary_ctxt_switches:\t"), parse_u64, line_ending)); -named!(parse_nonvoluntary_ctxt_switches, delimited!(tag!("nonvoluntary_ctxt_switches:\t"), parse_u64, line_ending)); +named!(parse_speculation_store_nypass, delimited!(tag!("Speculation_Store_Bypass:\t"), parse_line, line_ending)); +named!(parse_voluntary_ctxt_switches, delimited!(tag!("voluntary_ctxt_switches:\t"), parse_u64, line_ending)); +named!(parse_nonvoluntary_ctxt_switches, delimited!(tag!("nonvoluntary_ctxt_switches:\t"), parse_u64, line_ending)); /// Parse the status format. fn parse_status(i: &[u8]) -> IResult<&[u8], Status> { @@ -327,6 +335,7 @@ fn parse_status(i: &[u8]) -> IResult<&[u8], Status> { | parse_mems_allowed_list | parse_voluntary_ctxt_switches => { |value| status.voluntary_ctxt_switches = value } | parse_nonvoluntary_ctxt_switches => { |value| status.nonvoluntary_ctxt_switches = value } + | parse_speculation_store_nypass => { |value| status.speculation_store_nypass = value } ) ), { |_| { status }}) @@ -335,22 +344,22 @@ fn parse_status(i: &[u8]) -> IResult<&[u8], Status> { /// Parses the provided status file. fn status_file(file: &mut File) -> Result { let mut buf = [0; 2048]; // A typical status file is about 1000 bytes - map_result(parse_status(try!(read_to_end(file, &mut buf)))) + map_result(parse_status(read_to_end(file, &mut buf)?)) } /// Returns memory status information for the process with the provided pid. pub fn status(pid: pid_t) -> Result { - status_file(&mut try!(File::open(&format!("/proc/{}/status", pid)))) + status_file(&mut File::open(&format!("/proc/{}/status", pid))?) } /// Returns memory status information for the current process. pub fn status_self() -> Result { - status_file(&mut try!(File::open("/proc/self/status"))) + status_file(&mut File::open("/proc/self/status")?) } /// Returns memory status information from the thread with the provided parent process ID and thread ID. pub fn status_task(process_id: pid_t, thread_id: pid_t) -> Result { - status_file(&mut try!(File::open(&format!("/proc/{}/task/{}/status", process_id, thread_id)))) + status_file(&mut File::open(&format!("/proc/{}/task/{}/status", process_id, thread_id))?) } #[cfg(test)] diff --git a/src/unmangle.rs b/src/unmangle.rs new file mode 100644 index 0000000..f3741a0 --- /dev/null +++ b/src/unmangle.rs @@ -0,0 +1,99 @@ +//! Path unmangling functions. +//! +//! Paths included in several files under `/proc` are mangled, i.e., some +//! characters are replaced by the corresponding octal escape sequence `\nnn`. +//! +//! For example, the following files: +//! +//! - `/proc/[pid]/maps` +//! - `/proc/[pid]/smaps` +//! - `/proc/[pid]/numa_maps` +//! - `/proc/swaps` +//! +//! contain mangled paths. +//! +//! This module provides the [`unmangled_path`] function to reverse the +//! mangling (decoding the escape sequences). +//! +//! Note that, unless `\` is included in the set of the escaped characters +//! (which is *not* the case in any of the previous files), the mangling is +//! actually non-reversible (i.e., the demangling is ambiguous). +//! +//! See `mangle_path` in `Linux/fs/seq_file.c` for details on the mangling +//! algorithm. +//! +//! [`unmangled_path`]: fn.unmangled_path.html + +use std::str; +use std::num::ParseIntError; + +/// Converts a bytes slice in a given base to an integer. +fn u8_from_bytes_radix(bytes: &[u8], radix: u32) -> Result { + let s = unsafe { str::from_utf8_unchecked(bytes) }; + u8::from_str_radix(s, radix) +} + +/// Returns an iterator that yields the unmangled representation of `path` as +/// bytes. +/// +/// This struct is used by the [`unmangled_path`] function. See its +/// documentation for more. +/// +/// [`unmangled_path`]: fn.unmangled_path.html +struct UnmangledPath<'a> { + /// Slice of the source path to be unmangled. + path: &'a [u8], + /// Escaped chars that should be decoded (e.g., b"\n "). + escaped: &'a [u8], +} + +impl<'a> Iterator for UnmangledPath<'a> { + type Item = u8; + + fn next(&mut self) -> Option { + if self.path.len() >= 4 && self.path[0] == b'\\' { + if let Ok(c) = u8_from_bytes_radix(&self.path[1..4], 8) { + if self.escaped.contains(&c) { + self.path = &self.path[4..]; + return Some(c); + } + } + } + self.path.split_first().map(|(&c, rest)| { + self.path = rest; + c + }) + } +} + +/// Returns a `Vec` containing the unmangled representation of `path`. +/// +/// Octal escape sequences `\nnn` for characters included in `escaped` are +/// decoded. +/// +/// This reverses the escaping done by `mangle_path` in `Linux/fs/seq_file.c`. +/// +/// # Examples +/// +/// To decode only escaped newlines (leaving other escaped sequences alone): +/// +/// ```rust,ignore +/// let path = unmangled_path(br"a\012\040path", b"\n"); +/// assert_eq!(path, b"a\n\\040path"); +/// ``` +pub fn unmangled_path(path: &[u8], escaped: &[u8]) -> Vec { + UnmangledPath { path, escaped }.collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_unmangle_path() { + assert_eq!(unmangled_path(b"abcd", b"\n"), b"abcd"); + assert_eq!(unmangled_path(br"a\012path", b"\n"), b"a\npath"); + assert_eq!(unmangled_path(br"a\012\040path", b"\n"), b"a\n\\040path"); + assert_eq!(unmangled_path(br"a\012\040path", b"\n "), b"a\n path"); + } +}