1use anyhow::Result;
2use std::fs::{self, File};
3use std::io::{BufRead, BufReader};
4use std::ops::ControlFlow;
5use std::os::unix::fs::MetadataExt;
6use std::path::Path;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct ProcMapEntry<'a> {
10 pub start: u64,
11 pub end: u64,
12 pub perms: &'a str,
13 pub offset: u64,
14 pub dev_major: u64,
15 pub dev_minor: u64,
16 pub inode: u64,
17 path: Option<&'a str>,
18}
19
20impl<'a> ProcMapEntry<'a> {
21 pub fn path(&self) -> Option<&'a str> {
22 self.path
23 }
24
25 pub fn normalized_path(&self) -> Option<&'a str> {
26 self.path.map(normalize_mapped_module_path)
27 }
28
29 pub fn executable(&self) -> bool {
30 self.perms.contains('x')
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct OwnedProcMapEntry {
36 pub start: u64,
37 pub end: u64,
38 pub perms: String,
39 pub offset: u64,
40 pub dev_major: u64,
41 pub dev_minor: u64,
42 pub inode: u64,
43 path: Option<String>,
44}
45
46impl OwnedProcMapEntry {
47 pub fn path(&self) -> Option<&str> {
48 self.path.as_deref()
49 }
50
51 pub fn normalized_path(&self) -> Option<&str> {
52 self.path().map(normalize_mapped_module_path)
53 }
54
55 pub fn executable(&self) -> bool {
56 self.perms.contains('x')
57 }
58}
59
60impl From<ProcMapEntry<'_>> for OwnedProcMapEntry {
61 fn from(entry: ProcMapEntry<'_>) -> Self {
62 Self {
63 start: entry.start,
64 end: entry.end,
65 perms: entry.perms.to_owned(),
66 offset: entry.offset,
67 dev_major: entry.dev_major,
68 dev_minor: entry.dev_minor,
69 inode: entry.inode,
70 path: entry.path().map(str::to_owned),
71 }
72 }
73}
74
75#[derive(Debug, Clone)]
76pub struct ModuleIdentity {
77 dev_major: Option<u64>,
78 dev_minor: Option<u64>,
79 inode: Option<u64>,
80 normalized_path: String,
81}
82
83impl ModuleIdentity {
84 pub fn from_path(path: &Path) -> Self {
85 let path_str = path.to_string_lossy();
86 let normalized_path = normalize_mapped_module_path(&path_str).replace("/./", "/");
90 let (dev_major, dev_minor, inode) = fs::metadata(path)
91 .map(|meta| {
92 let dev = meta.dev() as libc::dev_t;
93 (
94 Some(libc::major(dev) as u64),
95 Some(libc::minor(dev) as u64),
96 Some(meta.ino()),
97 )
98 })
99 .unwrap_or((None, None, None));
100
101 Self {
102 dev_major,
103 dev_minor,
104 inode,
105 normalized_path,
106 }
107 }
108
109 pub fn normalized_path(&self) -> &str {
110 &self.normalized_path
111 }
112
113 pub fn matches(&self, entry: &ProcMapEntry<'_>) -> bool {
114 if let (Some(maj), Some(min), Some(ino)) = (self.dev_major, self.dev_minor, self.inode) {
115 if entry.dev_major == maj && entry.dev_minor == min && entry.inode == ino {
116 return true;
117 }
118
119 return entry.inode == ino && entry.normalized_path() == Some(self.normalized_path());
123 }
124
125 entry.normalized_path() == Some(self.normalized_path())
126 }
127}
128
129pub fn parse_maps_line(line: &str) -> Option<ProcMapEntry<'_>> {
130 let (range, rest) = take_field(line)?;
131 let (perms, rest) = take_field(rest)?;
132 let (offset, rest) = take_field(rest)?;
133 let (dev, rest) = take_field(rest)?;
134 let (inode, rest) = take_field(rest)?;
135
136 let (start_s, end_s) = range.split_once('-')?;
137 let (dev_major_s, dev_minor_s) = dev.split_once(':')?;
138 let path = rest.trim_start();
139
140 Some(ProcMapEntry {
141 start: u64::from_str_radix(start_s, 16).ok()?,
142 end: u64::from_str_radix(end_s, 16).ok()?,
143 perms,
144 offset: u64::from_str_radix(offset, 16).ok()?,
145 dev_major: u64::from_str_radix(dev_major_s, 16).ok()?,
146 dev_minor: u64::from_str_radix(dev_minor_s, 16).ok()?,
147 inode: inode.parse::<u64>().ok()?,
148 path: (!path.is_empty()).then_some(path),
149 })
150}
151
152pub fn visit_proc_maps<F>(pid: u32, visitor: F) -> Result<()>
153where
154 F: FnMut(ProcMapEntry<'_>) -> ControlFlow<()>,
155{
156 let maps_path = format!("/proc/{pid}/maps");
157 let file = File::open(&maps_path)?;
158 visit_maps_reader(BufReader::new(file), visitor)
159}
160
161pub fn read_proc_maps(pid: u32) -> Result<Vec<OwnedProcMapEntry>> {
162 let maps_path = format!("/proc/{pid}/maps");
163 let file = File::open(&maps_path)?;
164 read_maps_reader(BufReader::new(file))
165}
166
167fn visit_maps_reader<R, F>(mut reader: R, mut visitor: F) -> Result<()>
168where
169 R: BufRead,
170 F: FnMut(ProcMapEntry<'_>) -> ControlFlow<()>,
171{
172 let mut line = String::new();
173
174 loop {
175 line.clear();
176 if reader.read_line(&mut line)? == 0 {
177 break;
178 }
179
180 let line = line.trim_end_matches(['\n', '\r']);
181 if let Some(entry) = parse_maps_line(line) {
182 if matches!(visitor(entry), ControlFlow::Break(())) {
183 break;
184 }
185 }
186 }
187
188 Ok(())
189}
190
191fn read_maps_reader<R>(reader: R) -> Result<Vec<OwnedProcMapEntry>>
192where
193 R: BufRead,
194{
195 let mut entries = Vec::new();
196 visit_maps_reader(reader, |entry| {
197 entries.push(entry.into());
198 ControlFlow::Continue(())
199 })?;
200 Ok(entries)
201}
202
203pub fn normalize_mapped_module_path(path: &str) -> &str {
204 if let Some(idx) = path.find(" (deleted)") {
205 &path[..idx]
206 } else {
207 path
208 }
209}
210
211pub fn is_filtered_module_prefix(path: &str) -> bool {
212 matches!(path, "/proc" | "/sys") || path.starts_with("/proc/") || path.starts_with("/sys/")
213}
214
215pub fn should_skip_mapped_module_path(path: &str) -> bool {
216 let path = normalize_mapped_module_path(path);
217 path.starts_with('[')
218 || is_filtered_module_prefix(path)
219 || matches!(fs::metadata(path), Ok(meta) if !meta.file_type().is_file())
220}
221
222fn take_field(input: &str) -> Option<(&str, &str)> {
223 let input = input.trim_start();
224 if input.is_empty() {
225 return None;
226 }
227
228 let split_at = input.find(char::is_whitespace).unwrap_or(input.len());
229 Some((&input[..split_at], &input[split_at..]))
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use std::path::PathBuf;
236 use std::time::{SystemTime, UNIX_EPOCH};
237
238 fn dev_pair_differs_from(meta: &std::fs::Metadata, salt: u64) -> (u64, u64) {
239 let dev = meta.dev() as libc::dev_t;
240 let actual_major = libc::major(dev) as u64;
241 let actual_minor = libc::minor(dev) as u64;
242 let major = actual_major ^ (0x40 + salt);
243 let minor = actual_minor ^ (0x80 + salt);
244 if major == actual_major && minor == actual_minor {
245 (actual_major + 1, actual_minor)
246 } else {
247 (major, minor)
248 }
249 }
250
251 #[test]
252 fn parse_maps_line_handles_deleted_paths_and_spaces() {
253 let line = "7f1234500000-7f1234510000 r-xp 00000000 08:02 12345 /tmp/lib demo.so (deleted)";
254 let entry = parse_maps_line(line).unwrap();
255
256 assert_eq!(entry.start, 0x7f1234500000);
257 assert_eq!(entry.end, 0x7f1234510000);
258 assert_eq!(entry.offset, 0);
259 assert_eq!(entry.dev_major, 0x08);
260 assert_eq!(entry.dev_minor, 0x02);
261 assert_eq!(entry.inode, 12345);
262 assert_eq!(entry.normalized_path(), Some("/tmp/lib demo.so"));
263 }
264
265 #[test]
266 fn skips_virtual_and_pseudo_filesystem_mappings() {
267 assert!(should_skip_mapped_module_path("[heap]"));
268 assert!(should_skip_mapped_module_path("/dev/null"));
269 assert!(should_skip_mapped_module_path("/sys/kernel/tracing"));
270 assert!(should_skip_mapped_module_path("/proc/123/maps"));
271 assert!(!is_filtered_module_prefix("/dev/shm/ghostscope-module.so"));
272 assert!(!should_skip_mapped_module_path(
273 "/dev/shm/ghostscope-module.so"
274 ));
275 assert!(!should_skip_mapped_module_path("/usr/lib/libc.so.6"));
276 }
277
278 #[test]
279 fn module_identity_matches_by_path_when_metadata_is_missing() {
280 let missing = PathBuf::from("/tmp/./ghostscope-missing-lib.so");
281 let identity = ModuleIdentity::from_path(&missing);
282 let entry = parse_maps_line(
283 "7f1234500000-7f1234510000 r-xp 00000000 00:00 0 /tmp/ghostscope-missing-lib.so (deleted)",
284 )
285 .unwrap();
286
287 assert!(identity.matches(&entry));
288 }
289
290 #[test]
291 fn module_identity_falls_back_to_inode_and_path_when_dev_differs() {
292 let suffix = SystemTime::now()
293 .duration_since(UNIX_EPOCH)
294 .unwrap()
295 .as_nanos();
296 let path = std::env::temp_dir().join(format!("ghostscope-overlayfs-{suffix}.so"));
297 std::fs::write(&path, b"current").unwrap();
298
299 let meta = std::fs::metadata(&path).unwrap();
300 let inode = meta.ino();
301 let path_str = path.to_string_lossy().to_string();
302 let identity = ModuleIdentity::from_path(&path);
303 let (dev_major, dev_minor) = dev_pair_differs_from(&meta, 1);
304 let line = format!(
305 "7f1234500000-7f1234510000 r-xp 00000000 {dev_major:02x}:{dev_minor:02x} {inode} {path_str}"
306 );
307 let entry = parse_maps_line(&line).unwrap();
308
309 assert!(identity.matches(&entry));
310
311 let _ = std::fs::remove_file(path);
312 }
313
314 #[test]
315 fn module_identity_does_not_fallback_without_path_match() {
316 let suffix = SystemTime::now()
317 .duration_since(UNIX_EPOCH)
318 .unwrap()
319 .as_nanos();
320 let path = std::env::temp_dir().join(format!("ghostscope-overlayfs-{suffix}.so"));
321 std::fs::write(&path, b"current").unwrap();
322
323 let meta = std::fs::metadata(&path).unwrap();
324 let inode = meta.ino();
325 let identity = ModuleIdentity::from_path(&path);
326 let (dev_major, dev_minor) = dev_pair_differs_from(&meta, 2);
327 let line = format!(
328 "7f1234500000-7f1234510000 r-xp 00000000 {dev_major:02x}:{dev_minor:02x} {inode} /tmp/other-{suffix}.so"
329 );
330 let entry = parse_maps_line(&line).unwrap();
331
332 assert!(!identity.matches(&entry));
333
334 let _ = std::fs::remove_file(path);
335 }
336
337 #[test]
338 fn visit_proc_maps_stops_when_visitor_breaks() {
339 use std::cell::Cell;
340
341 let lines =
342 b"7f1-7f2 r-xp 00000000 08:02 1 /tmp/a.so\n7f2-7f3 r-xp 00000000 08:02 2 /tmp/b.so\n";
343 let mut reader = BufReader::new(&lines[..]);
344 let seen = Cell::new(0usize);
345
346 visit_maps_reader(&mut reader, |entry| {
347 seen.set(seen.get() + 1);
348 if entry.inode == 1 {
349 return ControlFlow::Break(());
350 }
351 ControlFlow::Continue(())
352 })
353 .unwrap();
354
355 assert_eq!(seen.get(), 1);
356 }
357
358 #[test]
359 fn read_maps_reader_collects_owned_entries() {
360 let lines =
361 b"7f1-7f2 r-xp 00000000 08:02 1 /tmp/a.so\n7f2-7f3 rw-p 00001000 08:02 1 /tmp/a.so\n";
362 let reader = BufReader::new(&lines[..]);
363
364 let entries = read_maps_reader(reader).unwrap();
365
366 assert_eq!(entries.len(), 2);
367 assert_eq!(entries[0].path(), Some("/tmp/a.so"));
368 assert!(entries[0].executable());
369 assert_eq!(entries[1].offset, 0x1000);
370 }
371}