1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
//! Get virtual memory maps from another process
//!
//! This crate provides a function—[`get_process_maps`](linux_maps/fn.get_process_maps.html)
//! that returns a Vec of [`MapRange`](linux_maps/struct.MapRange.html) structs.
//!
//! This code works on Linux, macOS, and Windows. Each operating system has a different
//! implementation, but the functions and structs for all OSes share the same
//! interface - so this can be used generically across operating systems.
//!
//! Note: on macOS this requires root access, and even with root will still not
//! work on processes that have System Integrity Protection enabled
//! (anything in /usr/bin for example).
//!
//! # Example
//!
//! ```rust,no_run
//! use proc_maps::{get_process_maps, MapRange, Pid};
//!
//! let maps = get_process_maps(123456 as Pid).unwrap();
//! for map in maps {
//!    println!("Filename {:?} Address {} Size {}", map.filename(), map.start(), map.size());
//! }
//! ```

extern crate libc;

#[cfg(target_os = "macos")]
extern crate anyhow;
#[cfg(target_os = "macos")]
extern crate libproc;
#[cfg(target_os = "macos")]
extern crate mach2;
#[cfg(windows)]
extern crate winapi;

#[cfg(target_os = "macos")]
pub mod mac_maps;
#[cfg(target_os = "macos")]
pub use mac_maps::{get_process_maps, MapRange, Pid};

#[cfg(any(target_os = "linux", target_os = "android"))]
pub mod linux_maps;
#[cfg(any(target_os = "linux", target_os = "android"))]
pub use linux_maps::{get_process_maps, MapRange, Pid};

#[cfg(windows)]
pub mod win_maps;
#[cfg(windows)]
pub use win_maps::{get_process_maps, MapRange, Pid};

#[cfg(target_os = "freebsd")]
pub mod freebsd_maps;
#[cfg(target_os = "freebsd")]
pub use freebsd_maps::{get_process_maps, MapRange, Pid};

/// Trait to implement on MapRange, to provide an implementation.
///
/// By using a private trait, and providing an inherent implementation, we ensure the provided methods
/// are the same for all supported OSes.
trait MapRangeImpl {
    /// Returns the size of this MapRange in bytes
    fn size(&self) -> usize;
    /// Returns the address this MapRange starts at
    fn start(&self) -> usize;
    /// Returns the filename of the loaded module
    fn filename(&self) -> Option<&std::path::Path>;
    /// Returns whether this range contains executable code
    fn is_exec(&self) -> bool;
    /// Returns whether this range contains writeable memory
    fn is_write(&self) -> bool;
    /// Returns whether this range contains readable memory
    fn is_read(&self) -> bool;
}

impl MapRange {
    /// Returns the size of this MapRange in bytes
    #[inline]
    pub fn size(&self) -> usize {
        MapRangeImpl::size(self)
    }
    /// Returns the address this MapRange starts at
    #[inline]
    pub fn start(&self) -> usize {
        MapRangeImpl::start(self)
    }
    /// Returns the filename of the loaded module
    #[inline]
    pub fn filename(&self) -> Option<&std::path::Path> {
        MapRangeImpl::filename(self)
    }
    /// Returns whether this range contains executable code
    #[inline]
    pub fn is_exec(&self) -> bool {
        MapRangeImpl::is_exec(self)
    }
    /// Returns whether this range contains writeable memory
    #[inline]
    pub fn is_write(&self) -> bool {
        MapRangeImpl::is_write(self)
    }
    /// Returns whether this range contains readable memory
    #[inline]
    pub fn is_read(&self) -> bool {
        MapRangeImpl::is_read(self)
    }
}

fn map_contain_addr(map: &MapRange, addr: usize) -> bool {
    let start = map.start();
    (addr >= start) && (addr < (start + map.size()))
}

/// Returns whether or not any MapRange contains the given address
/// Note: this will only work correctly on macOS and Linux.
pub fn maps_contain_addr(addr: usize, maps: &[MapRange]) -> bool {
    maps.iter().any(|map| map_contain_addr(map, addr))
}

#[cfg(test)]
mod tests {
    use crate::get_process_maps;
    use crate::Pid;

    #[cfg(not(target_os = "windows"))]
    fn test_process_path() -> Option<std::path::PathBuf> {
        std::env::current_exe().ok().and_then(|p| {
            p.parent().map(|p| {
                p.with_file_name("test")
                    .with_extension(std::env::consts::EXE_EXTENSION)
            })
        })
    }

    #[cfg(not(target_os = "freebsd"))]
    #[test]
    fn test_map_from_test_binary_present() -> () {
        let maps = get_process_maps(std::process::id() as Pid).unwrap();

        let region = maps.iter().find(|map| {
            if let Some(filename) = map.filename() {
                filename.to_string_lossy().contains("proc_maps")
            } else {
                false
            }
        });

        assert!(
            region.is_some(),
            "We should have a map for the current test process"
        );
    }

    #[cfg(not(target_os = "windows"))]
    #[test]
    fn test_map_from_invoked_binary_present() -> () {
        let path = test_process_path().unwrap();
        if !path.exists() {
            println!("Skipping test because the 'test' binary hasn't been built");
            return;
        }

        let mut have_expected_map = false;
        // The maps aren't populated immediately on Linux, so retry a few times if needed
        for _ in 1..10 {
            let mut child = std::process::Command::new(&path)
                .spawn()
                .expect("failed to execute test process");

            let maps = get_process_maps(child.id() as Pid).unwrap();

            child.kill().expect("failed to kill test process");

            let region = maps.iter().find(|map| {
                if let Some(filename) = map.filename() {
                    filename.to_string_lossy().contains("/test")
                } else {
                    false
                }
            });

            if region.is_some() {
                have_expected_map = true;
                break;
            } else {
                std::thread::sleep(std::time::Duration::from_millis(100));
            }
        }

        assert!(
            have_expected_map,
            "We should have a map from the binary we invoked!"
        );
    }
}