vm-info 0.1.0

Inspect Linux virtual memory structure
Documentation
use std::{fmt, fs, io, num, path};
use std::io::BufRead;

use super::regex;

use super::ProcessId;

/// Permissions for a mapped region.
#[derive(Clone)]
pub struct Permissions {
    read: bool,
    write: bool,
    execute: bool,
    shared: bool,
}

impl Permissions {
    pub fn read(&self) -> bool {
        self.read
    }
    pub fn write(&self) -> bool {
        self.write
    }
    pub fn execute(&self) -> bool {
        self.execute
    }
    /// True iff `private()` is false.
    pub fn shared(&self) -> bool {
        self.shared
    }
    /// True iff `shared()` is false.
    pub fn private(&self) -> bool {
        !self.shared
    }
}

impl fmt::Debug for Permissions {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let read = if self.read { 'r' } else { '-' };
        let write = if self.write { 'w' } else { '-' };
        let execute = if self.execute { 'x' } else { '-' };
        let shared = if self.shared { 's' } else { 'p' };
        write!(f, "{}{}{}{}", read, write, execute, shared)
    }
}

/// Metadata for a mapped virtual memory region. See the `proc(5)` manpage.
#[derive(Clone, Debug)]
pub struct MemoryRegion {
    /// Start of the address in the process's address space.
    pub start_address: usize,
    pub end_address: usize,
    pub permissions: Permissions,
    /// Offset into the mapped file
    pub offset: usize,
    /// Device major number
    // 12 bits currently
    pub dev_major: u32,
    /// Device minor number
    // 20 bits
    pub dev_minor: u32,
    /// inode, if available
    pub inode: Option<u64>,
    /// Filename, or pseudo-path (e.g. `[stack]`), or `None` for anonymous mappings
    pub pathname: Option<String>,
}

#[derive(Debug)]
pub enum Error {
    ParseFailed,
    IoError(io::Error),
}

impl From<io::Error> for Error {
    fn from(e: io::Error) -> Self {
        Error::IoError(e)
    }
}

impl From<num::ParseIntError> for Error {
    fn from(_: num::ParseIntError) -> Self {
        Error::ParseFailed
    }
}

/// Iterator across VM regions.
pub struct MemoryRegionIter {
    reader: io::BufReader<fs::File>,
    buf: String,
    finished: bool,
}

impl MemoryRegionIter {
    fn parse_buf(&mut self) -> Result<MemoryRegion, Error> {
        lazy_static! {
            static ref RE: regex::Regex = regex::Regex::new(
                "^([0-9a-f]+)-([0-9a-f]+) ([a-z-]{4}) ([0-9a-f]+) (\\d+):(\\d+) (\\d+) +(.*)$")
                .expect("regex is valid");
        }

        let captures = RE.captures(self.buf.trim_right_matches('\n'))
            .ok_or(Error::ParseFailed)?;
        let start_address = usize::from_str_radix(&captures[1], 16)?;
        let end_address = usize::from_str_radix(&captures[2], 16)?;
        let p_bytes = &captures[3].as_bytes();
        let permissions = Permissions {
            read: p_bytes[0] == b'r',
            write: p_bytes[1] == b'w',
            execute: p_bytes[2] == b'x',
            shared: p_bytes[3] == b's',
        };
        let offset = usize::from_str_radix(&captures[4], 16)?;
        let dev_major = u32::from_str_radix(&captures[5], 16)?;
        let dev_minor = u32::from_str_radix(&captures[6], 16)?;
        let inode = u64::from_str_radix(&captures[7], 10)?;
        let inode = if inode == 0 { None } else { Some(inode) };
        let pathname = &captures[8];
        let pathname = if pathname.is_empty() {
            None
        } else {
            Some(String::from(pathname))
        };

        Ok(MemoryRegion {
            start_address,
            end_address,
            permissions,
            offset,
            dev_major,
            dev_minor,
            inode,
            pathname,
        })
    }
}

impl Iterator for MemoryRegionIter {
    type Item = Result<MemoryRegion, Error>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.finished {
            return None;
        }

        self.buf.clear();

        match self.reader.read_line(&mut self.buf) {
            Ok(bytes_read) => {
                if bytes_read == 0 {
                    self.finished = true;
                    None
                } else {
                    Some(self.parse_buf())
                }
            }
            Err(e) => Some(Err(Error::IoError(e))),
        }
    }
}

/// Load all mappings out of Linux procfs.
///
/// This may fail depending on permissions setup.
pub fn iter_mappings(pid: ProcessId) -> io::Result<MemoryRegionIter> {
    let path = match pid {
        ProcessId::SelfPid => String::from("/proc/self/maps"),
        ProcessId::Num(n) => format!("/proc/{}/maps", n),
    };

    iter_mapping_path(path)
}

fn iter_mapping_path<P: AsRef<path::Path>>(path: P) -> io::Result<MemoryRegionIter> {
    let f = fs::File::open(path)?;
    let reader = io::BufReader::new(f);

    Ok(MemoryRegionIter {
        reader,
        buf: String::new(),
        finished: false,
    })
}

#[cfg(test)]
mod tests {
    extern crate libc;

    use std::path;
    use super::*;

    #[test]
    fn sample_maps_file() {
        let path = path::Path::new("src/test-data/obexd-map.txt");

        let mappings = iter_mapping_path(path)
            .unwrap()
            .map(|r| r.unwrap())
            .collect::<Vec<MemoryRegion>>();

        assert_eq!(80, mappings.len());

        let first = &mappings[0];
        assert_eq!(94858956906496, first.start_address);
        assert_eq!(94858957352960, first.end_address);
        assert!(first.permissions.read());
        assert!(!first.permissions.write());
        assert!(first.permissions.execute());
        assert!(!first.permissions.shared());
        assert!(first.permissions.private());
        assert_eq!(0, first.offset);
        assert_eq!(8, first.dev_major);
        assert_eq!(3, first.dev_minor);
        assert_eq!(Some(58219319), first.inode);
        assert_eq!(
            "/usr/libexec/bluetooth/obexd",
            first.pathname.as_ref().unwrap()
        );

        let anon = &mappings[12];
        assert_eq!(0, anon.offset);
        assert_eq!(0, anon.dev_major);
        assert_eq!(0, anon.dev_minor);
        assert_eq!(None, anon.inode);
        assert_eq!(None, anon.pathname.as_ref());

        // has offset
        let libpthread = &mappings[50];
        assert!(!libpthread.permissions.read());
        assert!(!libpthread.permissions.write());
        assert!(!libpthread.permissions.execute());
        assert!(!libpthread.permissions.shared());
        assert!(libpthread.permissions.private());
        assert_eq!(106496, libpthread.offset);

        // different permissions, has inode
        let gconv = &mappings[71];
        assert!(gconv.permissions.read());
        assert!(!gconv.permissions.write());
        assert!(!gconv.permissions.execute());
        assert!(gconv.permissions.shared());
        assert!(!gconv.permissions.private());
        assert_eq!(3, gconv.dev_minor);
        assert_eq!(Some(55705632), gconv.inode);

        let last = &mappings[79];
        assert_eq!(18446744073699065856, last.start_address);
        assert_eq!(18446744073699069952, last.end_address);
        assert!(last.permissions.read());
        assert!(!last.permissions.write());
        assert!(last.permissions.execute());
        assert!(!last.permissions.shared());
        assert!(last.permissions.private());
        assert_eq!(0, last.offset);
        assert_eq!(0, last.dev_major);
        assert_eq!(0, last.dev_minor);
        assert_eq!(None, last.inode);
        assert_eq!("[vsyscall]", last.pathname.as_ref().unwrap());
    }

    #[test]
    fn read_self() {
        // can find stack
        assert_eq!(
            1,
            iter_mappings(ProcessId::SelfPid)
                .unwrap()
                .filter_map(|r| r.unwrap().pathname.clone())
                .filter(|p| p == "[stack]")
                .count()
        )
    }

    #[test]
    fn read_own_pid() {
        let pid = unsafe { libc::getpid() as u32 };

        // can find stack
        assert_eq!(
            1,
            iter_mappings(ProcessId::Num(pid))
                .unwrap()
                .filter_map(|r| r.unwrap().pathname.clone())
                .filter(|p| p == "[stack]")
                .count()
        )
    }

    #[test]
    fn permissions_debug() {
        let p1 = Permissions {
            read: false,
            write: false,
            execute: false,
            shared: false,
        };

        assert_eq!("---p", format!("{:?}", p1));

        let p2 = Permissions {
            read: true,
            write: true,
            execute: true,
            shared: true,
        };


        assert_eq!("rwxs", format!("{:?}", p2));
    }
}