shadow_rs/utility/
proc_maps.rs

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/// Whether a region of memory is shared.
10#[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/// The "path" of where a region is mapped from.
30#[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                // Experimentally there other labels here that are undocumented in proc(5). Some
65                // examples include `vvar` and `vsyscall`. Rather than failing to parse, we just
66                // save the label.
67                _ => MappingPath::OtherSpecial(s.to_string()),
68            });
69        }
70        Err(format!("Couldn't parse '{}'", s).into())
71    }
72}
73
74/// Represents a single line in /proc/\[pid\]/maps.
75#[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
91// Parses the given field with the given function, decorating errors with the field name and value.
92fn 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            // Undocumented whether this is actually base 10; change to 16 if we find
159            // counter-examples.
160            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
181/// Parses the contents of a /proc/\[pid\]/maps file
182pub 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
190/// Reads and parses the contents of a /proc/\[pid\]/maps file
191pub 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        // Private mapping of part of a file. Taken from proc(5).
208        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        // Heap. Taken from proc(5).
229        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        // Anonymous. Taken from proc(5).
250        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        // Thread stack. Taken from proc(5).
271        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        // Initial stack. Taken from proc(5).
292        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        // vdso. Taken from proc(5).
313        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        // vsyscall. Undocumented in proc(5), but found experimentally.
334        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        // Hexadecimal device major and minor numbers. Base is unspecified in proc(5), but
343        // experimentally they're hex.
344        {
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        // Undocumented '(deleted)' trailer, indicating that the mapped file has been removed from
353        // the file system.
354        {
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        // A large 64-bit inode value.
363        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        // These should return errors but *not* panic.
387
388        // Empty
389        assert!("".parse::<Mapping>().is_err());
390
391        // Non-match
392        assert!("garbage".parse::<Mapping>().is_err());
393
394        // Validate parseable template as baseline, before testing different mutations of it below.
395        "7fffb2d48000-7fffb2d49000 ---p 00000000 00:00 0   [vdso]"
396            .parse::<Mapping>()
397            .unwrap();
398
399        // Bad begin
400        assert!(
401            "7fffb2d4800q-7fffb2d49000 ---p 00000000 00:00 0   [vdso]"
402                .parse::<Mapping>()
403                .is_err()
404        );
405
406        // Bad end
407        assert!(
408            "7fffb2d48000-7fffb2d4900q ---p 00000000 00:00 0   [vdso]"
409                .parse::<Mapping>()
410                .is_err()
411        );
412
413        // Bad r
414        assert!(
415            "7fffb2d48000-7fffb2d49000 z--p 00000000 00:00 0   [vdso]"
416                .parse::<Mapping>()
417                .is_err()
418        );
419
420        // Bad w
421        assert!(
422            "7fffb2d48000-7fffb2d49000 -z-p 00000000 00:00 0   [vdso]"
423                .parse::<Mapping>()
424                .is_err()
425        );
426
427        // Bad x
428        assert!(
429            "7fffb2d48000-7fffb2d49000 --zp 00000000 00:00 0   [vdso]"
430                .parse::<Mapping>()
431                .is_err()
432        );
433
434        // Bad sharing
435        assert!(
436            "7fffb2d48000-7fffb2d49000 ---- 00000000 00:00 0   [vdso]"
437                .parse::<Mapping>()
438                .is_err()
439        );
440
441        // Bad offset
442        assert!(
443            "7fffb2d48000-7fffb2d49000 ---p 0000000z 00:00 0   [vdso]"
444                .parse::<Mapping>()
445                .is_err()
446        );
447
448        // Bad device high
449        assert!(
450            "7fffb2d48000-7fffb2d49000 ---p 00000000 0z:00 0   [vdso]"
451                .parse::<Mapping>()
452                .is_err()
453        );
454
455        // Bad device low
456        assert!(
457            "7fffb2d48000-7fffb2d49000 ---p 00000000 00:0z 0   [vdso]"
458                .parse::<Mapping>()
459                .is_err()
460        );
461
462        // Bad inode
463        assert!(
464            "7fffb2d48000-7fffb2d49000 ---p 00000000 00:00 z   [vdso]"
465                .parse::<Mapping>()
466                .is_err()
467        );
468
469        // Bad path
470        assert!(
471            "7fffb2d48000-7fffb2d49000 ---p 00000000 00:00 0   z"
472                .parse::<Mapping>()
473                .is_err()
474        );
475
476        // Leading garbage
477        assert!(
478            "z 7fffb2d48000-7fffb2d49000 ---p 00000000 00:00 0   [vdso]"
479                .parse::<Mapping>()
480                .is_err()
481        );
482
483        // Trailing garbage after path
484        assert!(
485            "7fffb2d48000-7fffb2d49000 ---p 00000000 00:00 0   [vdso] z"
486                .parse::<Mapping>()
487                .is_err()
488        );
489
490        // Trailing garbage after (deleted)
491        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        // Multiple lines.
501        #[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        // Empty.
539        assert_eq!(parse_file_contents("").unwrap(), Vec::<Mapping>::new());
540    }
541
542    #[test]
543    // Hangs in miri. Not sure why, but also not surpring in general that this
544    // test would be incompatible.
545    #[cfg_attr(miri, ignore)]
546    fn test_mappings_for_pid() {
547        // Difficult to write a precise test here; just try to read our own mappings and validate
548        // that it parses and is non-empty.
549        let pid = unsafe { libc::getpid() };
550        let mappings = mappings_for_pid(pid).unwrap();
551        assert!(!mappings.is_empty());
552    }
553}