proc_maps/
lib.rs

1//! Get virtual memory maps from another process
2//!
3//! This crate provides a function—[`get_process_maps`](linux_maps/fn.get_process_maps.html)
4//! that returns a Vec of [`MapRange`](linux_maps/struct.MapRange.html) structs.
5//!
6//! This code works on Linux, macOS, and Windows. Each operating system has a different
7//! implementation, but the functions and structs for all OSes share the same
8//! interface - so this can be used generically across operating systems.
9//!
10//! Note: on macOS this requires root access, and even with root will still not
11//! work on processes that have System Integrity Protection enabled
12//! (anything in /usr/bin for example).
13//!
14//! # Example
15//!
16//! ```rust,no_run
17//! use proc_maps::{get_process_maps, MapRange, Pid};
18//!
19//! let maps = get_process_maps(123456 as Pid).unwrap();
20//! for map in maps {
21//!    println!("Filename {:?} Address {} Size {}", map.filename(), map.start(), map.size());
22//! }
23//! ```
24
25extern crate libc;
26
27#[cfg(target_os = "macos")]
28extern crate anyhow;
29#[cfg(target_os = "macos")]
30extern crate libproc;
31#[cfg(target_os = "macos")]
32extern crate mach2;
33#[cfg(windows)]
34extern crate winapi;
35
36#[cfg(target_os = "macos")]
37pub mod mac_maps;
38#[cfg(target_os = "macos")]
39pub use mac_maps::{get_process_maps, MapRange, Pid};
40
41#[cfg(any(target_os = "linux", target_os = "android"))]
42pub mod linux_maps;
43#[cfg(any(target_os = "linux", target_os = "android"))]
44pub use linux_maps::{get_process_maps, MapRange, Pid};
45
46#[cfg(windows)]
47pub mod win_maps;
48#[cfg(windows)]
49pub use win_maps::{get_process_maps, MapRange, Pid};
50
51#[cfg(target_os = "freebsd")]
52pub mod freebsd_maps;
53#[cfg(target_os = "freebsd")]
54pub use freebsd_maps::{get_process_maps, MapRange, Pid};
55
56/// Trait to implement on MapRange, to provide an implementation.
57///
58/// By using a private trait, and providing an inherent implementation, we ensure the provided methods
59/// are the same for all supported OSes.
60trait MapRangeImpl {
61    /// Returns the size of this MapRange in bytes
62    fn size(&self) -> usize;
63    /// Returns the address this MapRange starts at
64    fn start(&self) -> usize;
65    /// Returns the filename of the loaded module
66    fn filename(&self) -> Option<&std::path::Path>;
67    /// Returns whether this range contains executable code
68    fn is_exec(&self) -> bool;
69    /// Returns whether this range contains writeable memory
70    fn is_write(&self) -> bool;
71    /// Returns whether this range contains readable memory
72    fn is_read(&self) -> bool;
73}
74
75impl MapRange {
76    /// Returns the size of this MapRange in bytes
77    #[inline]
78    pub fn size(&self) -> usize {
79        MapRangeImpl::size(self)
80    }
81    /// Returns the address this MapRange starts at
82    #[inline]
83    pub fn start(&self) -> usize {
84        MapRangeImpl::start(self)
85    }
86    /// Returns the filename of the loaded module
87    #[inline]
88    pub fn filename(&self) -> Option<&std::path::Path> {
89        MapRangeImpl::filename(self)
90    }
91    /// Returns whether this range contains executable code
92    #[inline]
93    pub fn is_exec(&self) -> bool {
94        MapRangeImpl::is_exec(self)
95    }
96    /// Returns whether this range contains writeable memory
97    #[inline]
98    pub fn is_write(&self) -> bool {
99        MapRangeImpl::is_write(self)
100    }
101    /// Returns whether this range contains readable memory
102    #[inline]
103    pub fn is_read(&self) -> bool {
104        MapRangeImpl::is_read(self)
105    }
106}
107
108fn map_contain_addr(map: &MapRange, addr: usize) -> bool {
109    let start = map.start();
110    (addr >= start) && (addr < (start + map.size()))
111}
112
113/// Returns whether or not any MapRange contains the given address
114/// Note: this will only work correctly on macOS and Linux.
115pub fn maps_contain_addr(addr: usize, maps: &[MapRange]) -> bool {
116    maps.iter().any(|map| map_contain_addr(map, addr))
117}
118
119/// Returns whether or not any MapRange contains the given address range.
120/// Note: this will only work correctly on macOS and Linux.
121pub fn maps_contain_addr_range(mut addr: usize, mut size: usize, maps: &[MapRange]) -> bool {
122    if size == 0 || addr.checked_add(size).is_none() {
123        return false;
124    }
125
126    while size > 0 {
127        match maps.iter().find(|map| map_contain_addr(map, addr)) {
128            None => return false,
129            Some(map) => {
130                let end = map.start() + map.size();
131                if addr + size <= end {
132                    return true;
133                } else {
134                    size -= end - addr;
135                    addr = end;
136                }
137            }
138        }
139    }
140
141    true
142}
143
144#[cfg(test)]
145mod tests {
146    use crate::get_process_maps;
147    use crate::Pid;
148
149    #[cfg(not(target_os = "windows"))]
150    fn test_process_path() -> Option<std::path::PathBuf> {
151        std::env::current_exe().ok().and_then(|p| {
152            p.parent().map(|p| {
153                p.with_file_name("test")
154                    .with_extension(std::env::consts::EXE_EXTENSION)
155            })
156        })
157    }
158
159    #[cfg(not(target_os = "freebsd"))]
160    #[test]
161    fn test_map_from_test_binary_present() -> () {
162        let maps = get_process_maps(std::process::id() as Pid).unwrap();
163
164        let region = maps.iter().find(|map| {
165            if let Some(filename) = map.filename() {
166                filename.to_string_lossy().contains("proc_maps")
167            } else {
168                false
169            }
170        });
171
172        assert!(
173            region.is_some(),
174            "We should have a map for the current test process"
175        );
176    }
177
178    #[cfg(not(target_os = "windows"))]
179    #[test]
180    fn test_map_from_invoked_binary_present() -> () {
181        let path = test_process_path().unwrap();
182        if !path.exists() {
183            println!("Skipping test because the 'test' binary hasn't been built");
184            return;
185        }
186
187        let mut have_expected_map = false;
188        // The maps aren't populated immediately on Linux, so retry a few times if needed
189        for _ in 1..10 {
190            let mut child = std::process::Command::new(&path)
191                .spawn()
192                .expect("failed to execute test process");
193
194            let maps = get_process_maps(child.id() as Pid).unwrap();
195
196            child.kill().expect("failed to kill test process");
197
198            let region = maps.iter().find(|map| {
199                if let Some(filename) = map.filename() {
200                    filename.to_string_lossy().contains("/test")
201                } else {
202                    false
203                }
204            });
205
206            if region.is_some() {
207                have_expected_map = true;
208                break;
209            } else {
210                std::thread::sleep(std::time::Duration::from_millis(100));
211            }
212        }
213
214        assert!(
215            have_expected_map,
216            "We should have a map from the binary we invoked!"
217        );
218    }
219}