1use std::error::Error;
2use std::fmt::Display;
3use std::path::PathBuf;
4use std::str::FromStr;
5
6use once_cell::sync::Lazy;
7use regex::Regex;
8
9#[derive(PartialEq, Eq, Debug, Copy, Clone)]
11pub enum Sharing {
12 Private,
13 Shared,
14}
15
16impl FromStr for Sharing {
17 type Err = Box<dyn Error>;
18 fn from_str(s: &str) -> Result<Self, Self::Err> {
19 if s == "p" {
20 Ok(Sharing::Private)
21 } else if s == "s" {
22 Ok(Sharing::Shared)
23 } else {
24 Err(format!("Bad sharing specifier {}", s).into())
25 }
26 }
27}
28
29#[derive(PartialEq, Eq, Debug, Clone)]
31pub enum MappingPath {
32 InitialStack,
33 ThreadStack(i32),
34 Vdso,
35 Heap,
36 OtherSpecial(String),
37 Path(PathBuf),
38}
39
40impl FromStr for MappingPath {
41 type Err = Box<dyn Error>;
42 fn from_str(s: &str) -> Result<Self, Self::Err> {
43 static SPECIAL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^\[(\S+)\]$").unwrap());
44 static THREAD_STACK_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"^stack:(\d+)$").unwrap());
45
46 if s.starts_with('/') {
47 return Ok(MappingPath::Path(PathBuf::from(s)));
48 }
49 if let Some(caps) = SPECIAL_RE.captures(s) {
50 let s = caps.get(1).unwrap().as_str();
51 if let Some(caps) = THREAD_STACK_RE.captures(s) {
52 return Ok(MappingPath::ThreadStack(
53 caps.get(1)
54 .unwrap()
55 .as_str()
56 .parse::<i32>()
57 .map_err(|e| format!("Parsing thread id: {}", e))?,
58 ));
59 }
60 return Ok(match s {
61 "stack" => MappingPath::InitialStack,
62 "vdso" => MappingPath::Vdso,
63 "heap" => MappingPath::Heap,
64 _ => MappingPath::OtherSpecial(s.to_string()),
68 });
69 }
70 Err(format!("Couldn't parse '{}'", s).into())
71 }
72}
73
74#[derive(PartialEq, Eq, Debug, Clone)]
76pub struct Mapping {
77 pub begin: usize,
78 pub end: usize,
79 pub read: bool,
80 pub write: bool,
81 pub execute: bool,
82 pub sharing: Sharing,
83 pub offset: usize,
84 pub device_major: i32,
85 pub device_minor: i32,
86 pub inode: u64,
87 pub path: Option<MappingPath>,
88 pub deleted: bool,
89}
90
91fn parse_field<T, F, U>(field: &str, field_name: &str, parse_fn: F) -> Result<T, String>
93where
94 F: FnOnce(&str) -> Result<T, U>,
95 U: Display,
96{
97 match parse_fn(field) {
98 Ok(res) => Ok(res),
99 Err(err) => Err(format!("Parsing {} '{}': {}", field_name, field, err)),
100 }
101}
102
103impl FromStr for Mapping {
104 type Err = Box<dyn Error>;
105 fn from_str(line: &str) -> Result<Self, Self::Err> {
106 static RE: Lazy<Regex> = Lazy::new(|| {
107 Regex::new(
108 r"^(\S+)-(\S+)\s+(\S)(\S)(\S)(\S)\s+(\S+)\s+(\S+):(\S+)\s+(\S+)\s*(\S*)\s*(\S*)$",
109 )
110 .unwrap()
111 });
112
113 let caps = RE
114 .captures(line)
115 .ok_or_else(|| format!("Didn't match regex: {}", line))?;
116
117 Ok(Mapping {
118 begin: parse_field(caps.get(1).unwrap().as_str(), "begin", |s| {
119 usize::from_str_radix(s, 16)
120 })?,
121 end: parse_field(caps.get(2).unwrap().as_str(), "end", |s| {
122 usize::from_str_radix(s, 16)
123 })?,
124 read: {
125 let s = caps.get(3).unwrap().as_str();
126 match s {
127 "r" => true,
128 "-" => false,
129 _ => return Err(format!("Couldn't parse read bit {}", s).into()),
130 }
131 },
132 write: {
133 let s = caps.get(4).unwrap().as_str();
134 match s {
135 "w" => true,
136 "-" => false,
137 _ => return Err(format!("Couldn't parse write bit {}", s).into()),
138 }
139 },
140 execute: {
141 let s = caps.get(5).unwrap().as_str();
142 match s {
143 "x" => true,
144 "-" => false,
145 _ => return Err(format!("Couldn't parse execute bit {}", s).into()),
146 }
147 },
148 sharing: caps.get(6).unwrap().as_str().parse::<Sharing>()?,
149 offset: parse_field(caps.get(7).unwrap().as_str(), "offset", |s| {
150 usize::from_str_radix(s, 16)
151 })?,
152 device_major: parse_field(caps.get(8).unwrap().as_str(), "device_major", |s| {
153 i32::from_str_radix(s, 16)
154 })?,
155 device_minor: parse_field(caps.get(9).unwrap().as_str(), "device_minor", |s| {
156 i32::from_str_radix(s, 16)
157 })?,
158 inode: parse_field(caps.get(10).unwrap().as_str(), "inode", |s| s.parse())?,
161 path: parse_field::<_, _, Box<dyn Error>>(
162 caps.get(11).unwrap().as_str(),
163 "path",
164 |s| match s {
165 "" => Ok(None),
166 s => Ok(Some(s.parse::<MappingPath>()?)),
167 },
168 )?,
169 deleted: {
170 let s = caps.get(12).unwrap().as_str();
171 match s {
172 "" => false,
173 "(deleted)" => true,
174 _ => return Err(format!("Couldn't parse trailing field '{}'", s).into()),
175 }
176 },
177 })
178 }
179}
180
181pub fn parse_file_contents(mappings: &str) -> Result<Vec<Mapping>, Box<dyn Error>> {
183 let res: Result<Vec<_>, String> = mappings
184 .lines()
185 .map(|line| Mapping::from_str(line).map_err(|e| format!("Parsing line: {}\n{}", line, e)))
186 .collect();
187 Ok(res?)
188}
189
190pub fn mappings_for_pid(pid: libc::pid_t) -> Result<Vec<Mapping>, Box<dyn Error>> {
192 use std::fs::File;
193 use std::io::Read;
194
195 let mut file = File::open(format!("/proc/{}/maps", pid))?;
196 let mut contents = String::new();
197 file.read_to_string(&mut contents)?;
198 parse_file_contents(&contents)
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_mapping_from_str() {
207 assert_eq!(
209 "00400000-00452000 r-xp 00000000 08:02 173521 /usr/bin/dbus-daemon"
210 .parse::<Mapping>()
211 .unwrap(),
212 Mapping {
213 begin: 0x00400000,
214 end: 0x00452000,
215 read: true,
216 write: false,
217 execute: true,
218 sharing: Sharing::Private,
219 offset: 0,
220 device_major: 8,
221 device_minor: 2,
222 inode: 173521,
223 path: Some(MappingPath::Path(PathBuf::from("/usr/bin/dbus-daemon"))),
224 deleted: false,
225 }
226 );
227
228 assert_eq!(
230 "00e03000-00e24000 rw-p 00000000 00:00 0 [heap]"
231 .parse::<Mapping>()
232 .unwrap(),
233 Mapping {
234 begin: 0x00e03000,
235 end: 0x00e24000,
236 read: true,
237 write: true,
238 execute: false,
239 sharing: Sharing::Private,
240 offset: 0,
241 device_major: 0,
242 device_minor: 0,
243 inode: 0,
244 path: Some(MappingPath::Heap),
245 deleted: false,
246 }
247 );
248
249 assert_eq!(
251 "35b1a21000-35b1a22000 rw-p 00000000 00:00 0"
252 .parse::<Mapping>()
253 .unwrap(),
254 Mapping {
255 begin: 0x35b1a21000,
256 end: 0x35b1a22000,
257 read: true,
258 write: true,
259 execute: false,
260 sharing: Sharing::Private,
261 offset: 0,
262 device_major: 0,
263 device_minor: 0,
264 inode: 0,
265 path: None,
266 deleted: false,
267 }
268 );
269
270 assert_eq!(
272 "f2c6ff8c000-7f2c7078c000 rw-p 00000000 00:00 0 [stack:986]"
273 .parse::<Mapping>()
274 .unwrap(),
275 Mapping {
276 begin: 0xf2c6ff8c000,
277 end: 0x7f2c7078c000,
278 read: true,
279 write: true,
280 execute: false,
281 sharing: Sharing::Private,
282 offset: 0,
283 device_major: 0,
284 device_minor: 0,
285 inode: 0,
286 path: Some(MappingPath::ThreadStack(986)),
287 deleted: false,
288 }
289 );
290
291 assert_eq!(
293 "7fffb2c0d000-7fffb2c2e000 rw-p 00000000 00:00 0 [stack]"
294 .parse::<Mapping>()
295 .unwrap(),
296 Mapping {
297 begin: 0x7fffb2c0d000,
298 end: 0x7fffb2c2e000,
299 read: true,
300 write: true,
301 execute: false,
302 sharing: Sharing::Private,
303 offset: 0,
304 device_major: 0,
305 device_minor: 0,
306 inode: 0,
307 path: Some(MappingPath::InitialStack),
308 deleted: false,
309 }
310 );
311
312 assert_eq!(
314 "7fffb2d48000-7fffb2d49000 r-xp 00000000 00:00 0 [vdso]"
315 .parse::<Mapping>()
316 .unwrap(),
317 Mapping {
318 begin: 0x7fffb2d48000,
319 end: 0x7fffb2d49000,
320 read: true,
321 write: false,
322 execute: true,
323 sharing: Sharing::Private,
324 offset: 0,
325 device_major: 0,
326 device_minor: 0,
327 inode: 0,
328 path: Some(MappingPath::Vdso),
329 deleted: false,
330 }
331 );
332
333 assert_eq!(
335 "7fffb2d48000-7fffb2d49000 r-xp 00000000 00:00 0 [vsyscall]"
336 .parse::<Mapping>()
337 .unwrap()
338 .path,
339 Some(MappingPath::OtherSpecial("vsyscall".to_string()))
340 );
341
342 {
345 let mapping = "00400000-00452000 r-xp 00000000 bb:cc 173521 /usr/bin/dbus-daemon"
346 .parse::<Mapping>()
347 .unwrap();
348 assert_eq!(mapping.device_major, 0xbb);
349 assert_eq!(mapping.device_minor, 0xcc);
350 }
351
352 {
355 let mapping =
356 "00400000-00452000 r-xp 00000000 bb:cc 173521 /usr/bin/dbus-daemon (deleted)"
357 .parse::<Mapping>()
358 .unwrap();
359 assert!(mapping.deleted);
360 }
361
362 assert_eq!(
364 "00400000-00452000 r-xp 00000000 08:02 18446744073709551615 /usr/bin/dbus-daemon"
365 .parse::<Mapping>()
366 .unwrap(),
367 Mapping {
368 begin: 0x00400000,
369 end: 0x00452000,
370 read: true,
371 write: false,
372 execute: true,
373 sharing: Sharing::Private,
374 offset: 0,
375 device_major: 8,
376 device_minor: 2,
377 inode: 18446744073709551615,
378 path: Some(MappingPath::Path(PathBuf::from("/usr/bin/dbus-daemon"))),
379 deleted: false,
380 }
381 );
382 }
383
384 #[test]
385 fn test_bad_inputs() {
386 assert!("".parse::<Mapping>().is_err());
390
391 assert!("garbage".parse::<Mapping>().is_err());
393
394 "7fffb2d48000-7fffb2d49000 ---p 00000000 00:00 0 [vdso]"
396 .parse::<Mapping>()
397 .unwrap();
398
399 assert!(
401 "7fffb2d4800q-7fffb2d49000 ---p 00000000 00:00 0 [vdso]"
402 .parse::<Mapping>()
403 .is_err()
404 );
405
406 assert!(
408 "7fffb2d48000-7fffb2d4900q ---p 00000000 00:00 0 [vdso]"
409 .parse::<Mapping>()
410 .is_err()
411 );
412
413 assert!(
415 "7fffb2d48000-7fffb2d49000 z--p 00000000 00:00 0 [vdso]"
416 .parse::<Mapping>()
417 .is_err()
418 );
419
420 assert!(
422 "7fffb2d48000-7fffb2d49000 -z-p 00000000 00:00 0 [vdso]"
423 .parse::<Mapping>()
424 .is_err()
425 );
426
427 assert!(
429 "7fffb2d48000-7fffb2d49000 --zp 00000000 00:00 0 [vdso]"
430 .parse::<Mapping>()
431 .is_err()
432 );
433
434 assert!(
436 "7fffb2d48000-7fffb2d49000 ---- 00000000 00:00 0 [vdso]"
437 .parse::<Mapping>()
438 .is_err()
439 );
440
441 assert!(
443 "7fffb2d48000-7fffb2d49000 ---p 0000000z 00:00 0 [vdso]"
444 .parse::<Mapping>()
445 .is_err()
446 );
447
448 assert!(
450 "7fffb2d48000-7fffb2d49000 ---p 00000000 0z:00 0 [vdso]"
451 .parse::<Mapping>()
452 .is_err()
453 );
454
455 assert!(
457 "7fffb2d48000-7fffb2d49000 ---p 00000000 00:0z 0 [vdso]"
458 .parse::<Mapping>()
459 .is_err()
460 );
461
462 assert!(
464 "7fffb2d48000-7fffb2d49000 ---p 00000000 00:00 z [vdso]"
465 .parse::<Mapping>()
466 .is_err()
467 );
468
469 assert!(
471 "7fffb2d48000-7fffb2d49000 ---p 00000000 00:00 0 z"
472 .parse::<Mapping>()
473 .is_err()
474 );
475
476 assert!(
478 "z 7fffb2d48000-7fffb2d49000 ---p 00000000 00:00 0 [vdso]"
479 .parse::<Mapping>()
480 .is_err()
481 );
482
483 assert!(
485 "7fffb2d48000-7fffb2d49000 ---p 00000000 00:00 0 [vdso] z"
486 .parse::<Mapping>()
487 .is_err()
488 );
489
490 assert!(
492 "7fffb2d48000-7fffb2d49000 ---p 00000000 00:00 0 [vdso] (deleted) z"
493 .parse::<Mapping>()
494 .is_err()
495 );
496 }
497
498 #[test]
499 fn test_parse_file_contents() {
500 #[rustfmt::skip]
502 let s = "00400000-00452000 r-xp 00000000 08:02 173521 /usr/bin/dbus-daemon\n\
503 7fffb2c0d000-7fffb2c2e000 rw-p 00000000 00:00 0 [stack]\n";
504 assert_eq!(
505 parse_file_contents(s).unwrap(),
506 vec![
507 Mapping {
508 begin: 0x00400000,
509 end: 0x00452000,
510 read: true,
511 write: false,
512 execute: true,
513 sharing: Sharing::Private,
514 offset: 0,
515 device_major: 8,
516 device_minor: 2,
517 inode: 173521,
518 path: Some(MappingPath::Path(PathBuf::from("/usr/bin/dbus-daemon"))),
519 deleted: false,
520 },
521 Mapping {
522 begin: 0x7fffb2c0d000,
523 end: 0x7fffb2c2e000,
524 read: true,
525 write: true,
526 execute: false,
527 sharing: Sharing::Private,
528 offset: 0,
529 device_major: 0,
530 device_minor: 0,
531 inode: 0,
532 path: Some(MappingPath::InitialStack),
533 deleted: false,
534 }
535 ]
536 );
537
538 assert_eq!(parse_file_contents("").unwrap(), Vec::<Mapping>::new());
540 }
541
542 #[test]
543 #[cfg_attr(miri, ignore)]
546 fn test_mappings_for_pid() {
547 let pid = unsafe { libc::getpid() };
550 let mappings = mappings_for_pid(pid).unwrap();
551 assert!(!mappings.is_empty());
552 }
553}