Skip to main content

minidump_writer/linux/
maps_reader.rs

1use {
2    super::{
3        auxv::AuxvType,
4        module_reader::ModuleReaderError,
5        process_inspection::{self, ProcessInspector},
6        serializers::*,
7    },
8    crate::serializers::*,
9    byteorder::{NativeEndian, ReadBytesExt},
10    procfs_core::{
11        FromRead,
12        process::{MMPermissions, MMapPath, MemoryMaps},
13    },
14    std::{
15        ffi::{OsStr, OsString},
16        mem::size_of,
17        os::unix::ffi::{OsStrExt, OsStringExt},
18        path::{Path, PathBuf},
19    },
20};
21
22pub const LINUX_GATE_LIBRARY_NAME: &str = "linux-gate.so";
23pub const DELETED_SUFFIX: &[u8] = b" (deleted)";
24
25type Result<T> = std::result::Result<T, MapsReaderError>;
26
27#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)]
28pub struct SystemMappingInfo {
29    pub start_address: usize,
30    pub end_address: usize,
31}
32
33// One of these is produced for each mapping in the process (i.e. line in
34// /proc/$x/maps).
35#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)]
36pub struct MappingInfo {
37    // On Android, relocation packing can mean that the reported start
38    // address of the mapping must be adjusted by a bias in order to
39    // compensate for the compression of the relocation section. The
40    // following two members hold (after LateInit) the adjusted mapping
41    // range. See crbug.com/606972 for more information.
42    pub start_address: usize,
43    pub size: usize,
44    // When Android relocation packing causes |start_addr| and |size| to
45    // be modified with a load bias, we need to remember the unbiased
46    // address range. The following structure holds the original mapping
47    // address range as reported by the operating system.
48    pub system_mapping_info: SystemMappingInfo,
49    pub offset: usize,              // offset into the backed file.
50    pub permissions: MMPermissions, // read, write and execute permissions.
51    pub name: Option<OsString>,
52    // pub elf_obj: Option<elf::Elf>,
53}
54
55#[derive(Debug)]
56pub struct MappingEntry {
57    pub mapping: MappingInfo,
58    pub identifier: Vec<u8>,
59}
60
61// A list of <MappingInfo, GUID>
62pub type MappingList = Vec<MappingEntry>;
63
64#[derive(thiserror::Error, Debug, serde::Serialize)]
65pub enum MapsReaderError {
66    #[error("failed to read /proc/<pid>/maps")]
67    ReadFileFailed(#[source] process_inspection::Error),
68    #[error("Couldn't parse as ELF file")]
69    ELFParsingFailed(
70        #[from]
71        #[serde(serialize_with = "serialize_goblin_error")]
72        goblin::error::Error,
73    ),
74    #[error("error reading soname from file")]
75    ReadSoNameFromFileFailed(#[source] ModuleReaderError),
76    #[error("failed to memory map file")]
77    MemoryMapFileFailed(#[source] ModuleReaderError),
78    #[error("No soname found (filename: {})", .0.to_string_lossy())]
79    NoSoName(OsString, #[source] ModuleReaderError),
80
81    // parse_from_line()
82    #[error("Map entry malformed: No {0} found")]
83    MapEntryMalformed(&'static str),
84    #[error("Couldn't parse address")]
85    UnparsableInteger(
86        #[from]
87        #[serde(skip)]
88        std::num::ParseIntError,
89    ),
90    #[error("Linux gate location doesn't fit in the required integer type")]
91    LinuxGateNotConvertable(
92        #[from]
93        #[serde(skip)]
94        std::num::TryFromIntError,
95    ),
96
97    // get_mmap()
98    #[error("Not safe to open mapping {}", .0.to_string_lossy())]
99    NotSafeToOpenMapping(OsString),
100    #[error("IO Error")]
101    FileError(
102        #[from]
103        #[serde(serialize_with = "serialize_io_error")]
104        std::io::Error,
105    ),
106    #[error("Mmapped file empty or not an ELF file")]
107    MmapSanityCheckFailed,
108    #[error("Symlink does not match ({0} vs. {1})")]
109    SymlinkError(std::path::PathBuf, std::path::PathBuf),
110    #[error("Failed to parse memory maps file")]
111    ParsingError(
112        #[from]
113        #[serde(serialize_with = "serialize_proc_error")]
114        procfs_core::ProcError,
115    ),
116}
117
118fn is_mapping_a_path(pathname: Option<&OsStr>) -> bool {
119    match pathname {
120        Some(x) => x.as_bytes().contains(&b'/'),
121        None => false,
122    }
123}
124
125/// Sanitize mapped paths.
126///
127/// This removes a ` (deleted)` suffix, if present.
128fn sanitize_path(pathname: OsString) -> OsString {
129    if let Some(bytes) = pathname.as_bytes().strip_suffix(DELETED_SUFFIX) {
130        OsString::from_vec(bytes.to_owned())
131    } else {
132        pathname
133    }
134}
135
136impl MappingInfo {
137    /// Get the mappings for the given process.
138    pub fn for_pid(
139        process_inspector: &ProcessInspector,
140        pid: i32,
141        linux_gate_loc: Option<AuxvType>,
142    ) -> Result<Vec<Self>> {
143        let maps_path = format!("/proc/{}/maps", pid);
144        let maps_file = process_inspector
145            .read_file(&maps_path)
146            .map_err(MapsReaderError::ReadFileFailed)?;
147        let maps = MemoryMaps::from_read(maps_file)?;
148        Self::aggregate(maps, linux_gate_loc)
149    }
150
151    /// Return whether the `name` field is a path (contains a `/`).
152    pub fn name_is_path(&self) -> bool {
153        is_mapping_a_path(self.name.as_deref())
154    }
155
156    pub fn is_empty_page(&self) -> bool {
157        (self.offset == 0) && (self.permissions == MMPermissions::PRIVATE) && self.name.is_none()
158    }
159
160    pub fn end_address(&self) -> usize {
161        self.start_address + self.size
162    }
163
164    pub fn aggregate(
165        memory_maps: MemoryMaps,
166        linux_gate_loc: Option<AuxvType>,
167    ) -> Result<Vec<Self>> {
168        let mut infos = Vec::<Self>::new();
169
170        for mm in memory_maps {
171            let start_address: usize = mm.address.0.try_into()?;
172            let end_address: usize = mm.address.1.try_into()?;
173            let mut offset: usize = mm.offset.try_into()?;
174
175            let mut pathname: Option<OsString> = match mm.pathname {
176                MMapPath::Path(p) => Some(sanitize_path(p.into())),
177                MMapPath::Heap => Some("[heap]".into()),
178                MMapPath::Stack => Some("[stack]".into()),
179                MMapPath::TStack(i) => Some(format!("[stack:{i}]").into()),
180                MMapPath::Vdso => Some("[vdso]".into()),
181                MMapPath::Vvar => Some("[vvar]".into()),
182                MMapPath::Vsyscall => Some("[vsyscall]".into()),
183                MMapPath::Rollup => Some("[rollup]".into()),
184                MMapPath::Vsys(i) => Some(format!("/SYSV{i:x}").into()),
185                MMapPath::Other(n) => Some(format!("[{n}]").into()),
186                MMapPath::Anonymous => None,
187            };
188
189            let is_path = is_mapping_a_path(pathname.as_deref());
190
191            if let Some(linux_gate_loc) = linux_gate_loc.map(|u| usize::try_from(u).unwrap())
192                && (!is_path && (start_address == linux_gate_loc))
193            {
194                pathname = Some(LINUX_GATE_LIBRARY_NAME.into());
195                offset = 0;
196            }
197
198            if let Some(prev_module) = infos.last_mut() {
199                if (start_address == prev_module.end_address())
200                    && pathname.is_some()
201                    && (pathname == prev_module.name)
202                {
203                    // Merge adjacent mappings into one module, assuming they're a single
204                    // library mapped by the dynamic linker.
205                    prev_module.system_mapping_info.end_address = end_address;
206                    prev_module.size = end_address - prev_module.start_address;
207                    prev_module.permissions |= mm.perms;
208                    continue;
209                } else if (start_address == prev_module.end_address())
210                    && prev_module.is_executable()
211                    && prev_module.name_is_path()
212                    && ((offset == 0) || (offset == prev_module.end_address()))
213                    && (mm.perms == MMPermissions::PRIVATE)
214                {
215                    // Also merge mappings that result from address ranges that the
216                    // linker reserved but which a loaded library did not use. These
217                    // appear as an anonymous private mapping with no access flags set
218                    // and which directly follow an executable mapping.
219                    prev_module.size = end_address - prev_module.start_address;
220                    continue;
221                }
222            }
223
224            // Sometimes the unused ranges reserved but the linker appear within the library.
225            // If we detect an empty page that is adjacent to two mappings of the same library
226            // we fold the three mappings together.
227            if let Some(previous_modules) = infos.rchunks_exact_mut(2).next() {
228                let empty_page = if let Some(prev_module) = previous_modules.last() {
229                    let prev_prev_module = previous_modules.first().unwrap();
230                    prev_prev_module.name_is_path()
231                        && (prev_prev_module.end_address() == prev_module.start_address)
232                        && prev_module.is_empty_page()
233                        && (prev_module.end_address() == start_address)
234                } else {
235                    false
236                };
237
238                if empty_page {
239                    let prev_prev_module = previous_modules.first_mut().unwrap();
240
241                    if pathname == prev_prev_module.name {
242                        prev_prev_module.system_mapping_info.end_address = end_address;
243                        prev_prev_module.size = end_address - prev_prev_module.start_address;
244                        prev_prev_module.permissions |= mm.perms;
245                        infos.pop();
246                        continue;
247                    }
248                }
249            }
250
251            infos.push(MappingInfo {
252                start_address,
253                size: end_address - start_address,
254                system_mapping_info: SystemMappingInfo {
255                    start_address,
256                    end_address,
257                },
258                offset,
259                permissions: mm.perms,
260                name: pathname,
261            });
262        }
263        Ok(infos)
264    }
265
266    pub fn stack_has_pointer_to_mapping(&self, stack_copy: &[u8], sp_offset: usize) -> bool {
267        // Loop over all stack words that would have been on the stack in
268        // the target process (i.e. are word aligned, and at addresses >=
269        // the stack pointer).  Regardless of the alignment of |stack_copy|,
270        // the memory starting at |stack_copy| + |offset| represents an
271        // aligned word in the target process.
272        let low_addr = self.system_mapping_info.start_address;
273        let high_addr = self.system_mapping_info.end_address;
274        let mut offset = (sp_offset + size_of::<usize>() - 1) & !(size_of::<usize>() - 1);
275        while offset <= stack_copy.len() - size_of::<usize>() {
276            let addr = match std::mem::size_of::<usize>() {
277                4 => stack_copy[offset..]
278                    .as_ref()
279                    .read_u32::<NativeEndian>()
280                    .map(|u| u as usize),
281                8 => stack_copy[offset..]
282                    .as_ref()
283                    .read_u64::<NativeEndian>()
284                    .map(|u| u as usize),
285                x => panic!("Unexpected type width: {x}"),
286            };
287            if let Ok(addr) = addr {
288                if low_addr <= addr && addr <= high_addr {
289                    return true;
290                }
291                offset += size_of::<usize>();
292            } else {
293                break;
294            }
295        }
296        false
297    }
298
299    /// Find the shared object name (SONAME) by examining the ELF information
300    /// for the mapping.
301    fn so_name(&self, process_inspector: &ProcessInspector) -> Result<String> {
302        let path = Path::new(self.name.as_deref().unwrap_or_default());
303        super::module_reader::read_soname_from_file(process_inspector, path, self.offset)
304            .map_err(MapsReaderError::ReadSoNameFromFileFailed)
305    }
306
307    #[inline]
308    fn so_version(&self) -> Option<SoVersion> {
309        SoVersion::parse(self.name.as_deref()?)
310    }
311
312    pub fn get_mapping_effective_path_name_and_version(
313        &self,
314        process_inspector: &ProcessInspector,
315        soname: Option<String>,
316    ) -> Result<(PathBuf, String, Option<SoVersion>)> {
317        let mut file_path = PathBuf::from(self.name.clone().unwrap_or_default());
318
319        // Tools such as minidump_stackwalk use the name of the module to look up
320        // symbols produced by dump_syms. dump_syms will prefer to use a module's
321        // DT_SONAME as the module name, if one exists, and will fall back to the
322        // filesystem name of the module.
323
324        // Just use the filesystem name if no SONAME is present.
325        let Some(file_name) = soname.or_else(|| self.so_name(process_inspector).ok()) else {
326            //   file_path := /path/to/libname.so
327            //   file_name := libname.so
328            let file_name = file_path
329                .file_name()
330                .map(|s| s.to_string_lossy().into_owned())
331                .unwrap_or_default();
332
333            return Ok((file_path, file_name, self.so_version()));
334        };
335
336        if self.is_executable() && self.offset != 0 {
337            // If an executable is mapped from a non-zero offset, this is likely because
338            // the executable was loaded directly from inside an archive file (e.g., an
339            // apk on Android).
340            // In this case, we append the file_name to the mapped archive path:
341            //   file_name := libname.so
342            //   file_path := /path/to/ARCHIVE.APK/libname.so
343            file_path.push(&file_name);
344        } else {
345            // Otherwise, replace the basename with the SONAME.
346            file_path.set_file_name(&file_name);
347        }
348
349        Ok((file_path, file_name, self.so_version()))
350    }
351
352    pub fn is_contained_in(&self, user_mapping_list: &MappingList) -> bool {
353        for user in user_mapping_list {
354            // Ignore any mappings that are wholly contained within
355            // mappings in the mapping_info_ list.
356            if self.start_address >= user.mapping.start_address
357                && (self.start_address + self.size)
358                    <= (user.mapping.start_address + user.mapping.size)
359            {
360                return true;
361            }
362        }
363        false
364    }
365
366    pub fn is_interesting(&self) -> bool {
367        // only want modules with filenames.
368        self.name.is_some() &&
369        // Only want to include one mapping per shared lib.
370        // Avoid filtering executable mappings.
371        (self.offset == 0 || self.is_executable()) &&
372        // big enough to get a signature for.
373        self.size >= 4096
374    }
375
376    pub fn contains_address(&self, address: usize) -> bool {
377        self.system_mapping_info.start_address <= address
378            && address < self.system_mapping_info.end_address
379    }
380
381    pub fn is_executable(&self) -> bool {
382        self.permissions.contains(MMPermissions::EXECUTE)
383    }
384
385    pub fn is_readable(&self) -> bool {
386        self.permissions.contains(MMPermissions::READ)
387    }
388
389    pub fn is_writable(&self) -> bool {
390        self.permissions.contains(MMPermissions::WRITE)
391    }
392}
393
394/// Version metadata retrieved from an .so filename
395///
396/// There is no standard for .so version numbers so this implementation just
397/// does a best effort to pull as much data as it can based on real .so schemes
398/// seen
399///
400/// That being said, the [libtool](https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html)
401/// versioning scheme is fairly common
402#[cfg_attr(test, derive(Debug))]
403pub struct SoVersion {
404    /// Might be non-zero if there is at least one non-zero numeric component after .so.
405    ///
406    /// Equivalent to `current` in libtool versions
407    pub major: u32,
408    /// The numeric component after the major version, if any
409    ///
410    /// Equivalent to `revision` in libtool versions
411    pub minor: u32,
412    /// The numeric component after the minor version, if any
413    ///
414    /// Equivalent to `age` in libtool versions
415    pub patch: u32,
416    /// The patch component may contain additional non-numeric metadata similar
417    /// to a semver prelease, this is any numeric data that suffixes that prerelease
418    /// string
419    pub prerelease: u32,
420}
421
422impl SoVersion {
423    /// Attempts to retrieve the .so version of the elf path via its filename
424    fn parse(so_path: &OsStr) -> Option<Self> {
425        let filename = std::path::Path::new(so_path).file_name()?;
426
427        // Avoid an allocation unless the string contains non-utf8
428        let filename = filename.to_string_lossy();
429
430        let (_, version) = filename.split_once(".so.")?;
431
432        let mut sov = Self {
433            major: 0,
434            minor: 0,
435            patch: 0,
436            prerelease: 0,
437        };
438
439        let comps = [
440            &mut sov.major,
441            &mut sov.minor,
442            &mut sov.patch,
443            &mut sov.prerelease,
444        ];
445
446        for (i, comp) in version.split('.').enumerate() {
447            if i <= 1 {
448                *comps[i] = comp.parse().unwrap_or_default();
449            } else if i >= 4 {
450                break;
451            } else {
452                // In some cases the release/patch version is alphanumeric (eg. '2rc5'),
453                // so try to parse either a single or two numbers
454                if let Some(pend) = comp.find(|c: char| !c.is_ascii_digit()) {
455                    if let Ok(patch) = comp[..pend].parse() {
456                        *comps[i] = patch;
457                    }
458
459                    if i >= comps.len() - 1 {
460                        break;
461                    }
462                    if let Some(pre) = comp.rfind(|c: char| !c.is_ascii_digit())
463                        && let Ok(pre) = comp[pre + 1..].parse()
464                    {
465                        *comps[i + 1] = pre;
466                        break;
467                    }
468                } else {
469                    *comps[i] = comp.parse().unwrap_or_default();
470                }
471            }
472        }
473
474        Some(sov)
475    }
476}
477
478#[cfg(test)]
479impl PartialEq<(u32, u32, u32, u32)> for SoVersion {
480    fn eq(&self, o: &(u32, u32, u32, u32)) -> bool {
481        self.major == o.0 && self.minor == o.1 && self.patch == o.2 && self.prerelease == o.3
482    }
483}
484
485#[cfg(test)]
486#[cfg(target_pointer_width = "64")] // All addresses are 64 bit and I'm currently too lazy to adjust it to work for both
487mod tests {
488    use super::*;
489    use procfs_core::FromRead;
490
491    fn get_mappings_for(map: &str, linux_gate_loc: u64) -> Vec<MappingInfo> {
492        MappingInfo::aggregate(
493            MemoryMaps::from_read(map.as_bytes()).expect("failed to read mapping info"),
494            Some(linux_gate_loc),
495        )
496        .unwrap_or_default()
497    }
498
499    const LINES: &str = "\
5005597483fc000-5597483fe000 r--p 00000000 00:31 4750073                    /usr/bin/cat
5015597483fe000-559748402000 r-xp 00002000 00:31 4750073                    /usr/bin/cat
502559748402000-559748404000 r--p 00006000 00:31 4750073                    /usr/bin/cat
503559748404000-559748405000 r--p 00007000 00:31 4750073                    /usr/bin/cat
504559748405000-559748406000 rw-p 00008000 00:31 4750073                    /usr/bin/cat
505559749b0e000-559749b2f000 rw-p 00000000 00:00 0                          [heap]
5067efd968d3000-7efd968f5000 rw-p 00000000 00:00 0 
5077efd968f5000-7efd9694a000 r--p 00000000 00:31 5004638                    /usr/lib/locale/en_US.utf8/LC_CTYPE
5087efd9694a000-7efd96bc2000 r--p 00000000 00:31 5004373                    /usr/lib/locale/en_US.utf8/LC_COLLATE
5097efd96bc2000-7efd96bc4000 rw-p 00000000 00:00 0 
5107efd96bc4000-7efd96bea000 r--p 00000000 00:31 4996104                    /lib64/libc-2.32.so
5117efd96bea000-7efd96d39000 r-xp 00026000 00:31 4996104                    /lib64/libc-2.32.so
5127efd96d39000-7efd96d85000 r--p 00175000 00:31 4996104                    /lib64/libc-2.32.so
5137efd96d85000-7efd96d86000 ---p 001c1000 00:31 4996104                    /lib64/libc-2.32.so
5147efd96d86000-7efd96d89000 r--p 001c1000 00:31 4996104                    /lib64/libc-2.32.so
5157efd96d89000-7efd96d8c000 rw-p 001c4000 00:31 4996104                    /lib64/libc-2.32.so
5167efd96d8c000-7efd96d92000 ---p 00000000 00:00 0 
5177efd96da0000-7efd96da1000 r--p 00000000 00:31 5004379                    /usr/lib/locale/en_US.utf8/LC_NUMERIC
5187efd96da1000-7efd96da2000 r--p 00000000 00:31 5004382                    /usr/lib/locale/en_US.utf8/LC_TIME
5197efd96da2000-7efd96da3000 r--p 00000000 00:31 5004377                    /usr/lib/locale/en_US.utf8/LC_MONETARY
5207efd96da3000-7efd96da4000 r--p 00000000 00:31 5004376                    /usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES
5217efd96da4000-7efd96da5000 r--p 00000000 00:31 5004380                    /usr/lib/locale/en_US.utf8/LC_PAPER
5227efd96da5000-7efd96da6000 r--p 00000000 00:31 5004378                    /usr/lib/locale/en_US.utf8/LC_NAME
5237efd96da6000-7efd96da7000 r--p 00000000 00:31 5004372                    /usr/lib/locale/en_US.utf8/LC_ADDRESS
5247efd96da7000-7efd96da8000 r--p 00000000 00:31 5004381                    /usr/lib/locale/en_US.utf8/LC_TELEPHONE
5257efd96da8000-7efd96da9000 r--p 00000000 00:31 5004375                    /usr/lib/locale/en_US.utf8/LC_MEASUREMENT
5267efd96da9000-7efd96db0000 r--s 00000000 00:31 5004639                    /usr/lib64/gconv/gconv-modules.cache
5277efd96db0000-7efd96db1000 r--p 00000000 00:31 5004374                    /usr/lib/locale/en_US.utf8/LC_IDENTIFICATION
5287efd96db1000-7efd96db2000 r--p 00000000 00:31 4996100                    /lib64/ld-2.32.so
5297efd96db2000-7efd96dd3000 r-xp 00001000 00:31 4996100                    /lib64/ld-2.32.so
5307efd96dd3000-7efd96ddc000 r--p 00022000 00:31 4996100                    /lib64/ld-2.32.so
5317efd96ddc000-7efd96ddd000 r--p 0002a000 00:31 4996100                    /lib64/ld-2.32.so
5327efd96ddd000-7efd96ddf000 rw-p 0002b000 00:31 4996100                    /lib64/ld-2.32.so
5337ffc6dfda000-7ffc6dffb000 rw-p 00000000 00:00 0                          [stack]
5347ffc6e0f3000-7ffc6e0f7000 r--p 00000000 00:00 0                          [vvar]
5357ffc6e0f7000-7ffc6e0f9000 r-xp 00000000 00:00 0                          [vdso]
536ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]";
537    const LINUX_GATE_LOC: u64 = 0x7ffc6e0f7000;
538
539    fn get_all_mappings() -> Vec<MappingInfo> {
540        get_mappings_for(LINES, LINUX_GATE_LOC)
541    }
542
543    #[test]
544    fn test_merged() {
545        // Only /usr/bin/cat and [heap]
546        let mappings = get_mappings_for(
547            "\
5485597483fc000-5597483fe000 r--p 00000000 00:31 4750073                    /usr/bin/cat
5495597483fe000-559748402000 r-xp 00002000 00:31 4750073                    /usr/bin/cat
550559748402000-559748404000 r--p 00006000 00:31 4750073                    /usr/bin/cat
551559748404000-559748405000 r--p 00007000 00:31 4750073                    /usr/bin/cat
552559748405000-559748406000 rw-p 00008000 00:31 4750073                    /usr/bin/cat
553559749b0e000-559749b2f000 rw-p 00000000 00:00 0                          [heap]
5547efd968d3000-7efd968f5000 rw-p 00000000 00:00 0 ",
555            0x7ffc6e0f7000,
556        );
557
558        assert_eq!(mappings.len(), 3);
559        let cat_map = MappingInfo {
560            start_address: 0x5597483fc000,
561            size: 40960,
562            system_mapping_info: SystemMappingInfo {
563                start_address: 0x5597483fc000,
564                end_address: 0x559748406000,
565            },
566            offset: 0,
567            permissions: MMPermissions::READ
568                | MMPermissions::WRITE
569                | MMPermissions::EXECUTE
570                | MMPermissions::PRIVATE,
571            name: Some("/usr/bin/cat".into()),
572        };
573
574        assert_eq!(mappings[0], cat_map);
575
576        let heap_map = MappingInfo {
577            start_address: 0x559749b0e000,
578            size: 135168,
579            system_mapping_info: SystemMappingInfo {
580                start_address: 0x559749b0e000,
581                end_address: 0x559749b2f000,
582            },
583            offset: 0,
584            permissions: MMPermissions::READ | MMPermissions::WRITE | MMPermissions::PRIVATE,
585            name: Some("[heap]".into()),
586        };
587
588        assert_eq!(mappings[1], heap_map);
589
590        let empty_map = MappingInfo {
591            start_address: 0x7efd968d3000,
592            size: 139264,
593            system_mapping_info: SystemMappingInfo {
594                start_address: 0x7efd968d3000,
595                end_address: 0x7efd968f5000,
596            },
597            offset: 0,
598            permissions: MMPermissions::READ | MMPermissions::WRITE | MMPermissions::PRIVATE,
599            name: None,
600        };
601
602        assert_eq!(mappings[2], empty_map);
603    }
604
605    #[test]
606    fn test_linux_gate_parsing() {
607        let mappings = get_all_mappings();
608
609        let gate_map = MappingInfo {
610            start_address: 0x7ffc6e0f7000,
611            size: 8192,
612            system_mapping_info: SystemMappingInfo {
613                start_address: 0x7ffc6e0f7000,
614                end_address: 0x7ffc6e0f9000,
615            },
616            offset: 0,
617            permissions: MMPermissions::READ | MMPermissions::EXECUTE | MMPermissions::PRIVATE,
618            name: Some("linux-gate.so".into()),
619        };
620
621        assert_eq!(mappings[21], gate_map);
622    }
623
624    #[test]
625    fn test_reading_all() {
626        let mappings = get_all_mappings();
627
628        let found_items: Vec<Option<OsString>> = vec![
629            Some("/usr/bin/cat".into()),
630            Some("[heap]".into()),
631            None,
632            Some("/usr/lib/locale/en_US.utf8/LC_CTYPE".into()),
633            Some("/usr/lib/locale/en_US.utf8/LC_COLLATE".into()),
634            None,
635            Some("/lib64/libc-2.32.so".into()),
636            // The original shows a None here, but this is an address ranges that the
637            // linker reserved but which a loaded library did not use. These
638            // appear as an anonymous private mapping with no access flags set
639            // and which directly follow an executable mapping.
640            Some("/usr/lib/locale/en_US.utf8/LC_NUMERIC".into()),
641            Some("/usr/lib/locale/en_US.utf8/LC_TIME".into()),
642            Some("/usr/lib/locale/en_US.utf8/LC_MONETARY".into()),
643            Some("/usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES".into()),
644            Some("/usr/lib/locale/en_US.utf8/LC_PAPER".into()),
645            Some("/usr/lib/locale/en_US.utf8/LC_NAME".into()),
646            Some("/usr/lib/locale/en_US.utf8/LC_ADDRESS".into()),
647            Some("/usr/lib/locale/en_US.utf8/LC_TELEPHONE".into()),
648            Some("/usr/lib/locale/en_US.utf8/LC_MEASUREMENT".into()),
649            Some("/usr/lib64/gconv/gconv-modules.cache".into()),
650            Some("/usr/lib/locale/en_US.utf8/LC_IDENTIFICATION".into()),
651            Some("/lib64/ld-2.32.so".into()),
652            Some("[stack]".into()),
653            Some("[vvar]".into()),
654            // This is rewritten from [vdso] to linux-gate.so
655            Some("linux-gate.so".into()),
656            Some("[vsyscall]".into()),
657        ];
658
659        assert_eq!(
660            mappings.iter().map(|x| x.name.clone()).collect::<Vec<_>>(),
661            found_items
662        );
663    }
664
665    #[test]
666    fn test_merged_reserved_mappings() {
667        let mappings = get_all_mappings();
668
669        let gate_map = MappingInfo {
670            start_address: 0x7efd96bc4000,
671            size: 1892352, // Merged the anonymous area after in this mapping, so its bigger..
672            system_mapping_info: SystemMappingInfo {
673                start_address: 0x7efd96bc4000,
674                end_address: 0x7efd96d8c000, // ..but this is not visible here
675            },
676            offset: 0,
677            permissions: MMPermissions::READ
678                | MMPermissions::WRITE
679                | MMPermissions::EXECUTE
680                | MMPermissions::PRIVATE,
681            name: Some("/lib64/libc-2.32.so".into()),
682        };
683
684        assert_eq!(mappings[6], gate_map);
685    }
686
687    #[test]
688    fn test_merged_reserved_mappings_within_module() {
689        let mappings = get_mappings_for(
690            "\
6919b4a0000-9b931000 r--p 00000000 08:12 393449     /data/app/org.mozilla.firefox-1/lib/x86/libxul.so
6929b931000-9bcae000 ---p 00000000 00:00 0 
6939bcae000-a116b000 r-xp 00490000 08:12 393449     /data/app/org.mozilla.firefox-1/lib/x86/libxul.so
694a116b000-a4562000 r--p 0594d000 08:12 393449     /data/app/org.mozilla.firefox-1/lib/x86/libxul.so
695a4562000-a4563000 ---p 00000000 00:00 0 
696a4563000-a4840000 r--p 08d44000 08:12 393449     /data/app/org.mozilla.firefox-1/lib/x86/libxul.so
697a4840000-a4873000 rw-p 09021000 08:12 393449     /data/app/org.mozilla.firefox-1/lib/x86/libxul.so",
698            0xa4876000,
699        );
700
701        let gate_map = MappingInfo {
702            start_address: 0x9b4a0000,
703            size: 155004928, // Merged the anonymous area after in this mapping, so its bigger..
704            system_mapping_info: SystemMappingInfo {
705                start_address: 0x9b4a0000,
706                end_address: 0xa4873000,
707            },
708            offset: 0,
709            permissions: MMPermissions::READ
710                | MMPermissions::WRITE
711                | MMPermissions::EXECUTE
712                | MMPermissions::PRIVATE,
713            name: Some("/data/app/org.mozilla.firefox-1/lib/x86/libxul.so".into()),
714        };
715
716        assert_eq!(mappings[0], gate_map);
717    }
718
719    #[test]
720    fn test_get_mapping_effective_name() {
721        let mappings = get_mappings_for(
722            "\
7237f0b97b6f000-7f0b97b70000 r--p 00000000 00:3e 27136458                   /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so
7247f0b97b70000-7f0b97b71000 r-xp 00000000 00:3e 27136458                   /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so
7257f0b97b71000-7f0b97b73000 r--p 00000000 00:3e 27136458                   /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so
7267f0b97b73000-7f0b97b74000 rw-p 00001000 00:3e 27136458                   /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so",
727            0x7ffe091bf000,
728        );
729        assert_eq!(mappings.len(), 1);
730
731        let process_inspector = ProcessInspector::local(0);
732
733        let (file_path, file_name, _version) = mappings[0]
734            .get_mapping_effective_path_name_and_version(&process_inspector, None)
735            .expect("Couldn't get effective name for mapping");
736        assert_eq!(file_name, "libmozgtk.so");
737        assert_eq!(
738            file_path,
739            PathBuf::from(
740                "/home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so"
741            )
742        );
743    }
744
745    #[test]
746    fn test_elf_file_so_version() {
747        #[rustfmt::skip]
748        let test_cases = [
749            ("/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.32", (6, 0, 32, 0)),
750            ("/usr/lib/x86_64-linux-gnu/libcairo-gobject.so.2.11800.0", (2, 11800, 0, 0)),
751            ("/usr/lib/x86_64-linux-gnu/libm.so.6", (6, 0, 0, 0)),
752            ("/usr/lib/x86_64-linux-gnu/libpthread.so.0", (0, 0, 0, 0)),
753            ("/usr/lib/x86_64-linux-gnu/libgmodule-2.0.so.0.7800.0", (0, 7800, 0, 0)),
754            ("/usr/lib/x86_64-linux-gnu/libabsl_time_zone.so.20220623.0.0", (20220623, 0, 0, 0)),
755            ("/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.2rc5", (3, 34, 2, 5)),
756            ("/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.2rc", (3, 34, 2, 0)),
757            ("/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.rc5", (3, 34, 0, 5)),
758            ("/usr/lib/x86_64-linux-gnu/libtoto.so.AAA", (0, 0, 0, 0)),
759            ("/usr/lib/x86_64-linux-gnu/libsemver-1.so.1.2.alpha.1", (1, 2, 0, 1)),
760            ("/usr/lib/x86_64-linux-gnu/libboop.so.1.2.3.4.5", (1, 2, 3, 4)),
761            ("/usr/lib/x86_64-linux-gnu/libboop.so.1.2.3pre4.5", (1, 2, 3, 4)),
762        ];
763
764        assert!(SoVersion::parse(OsStr::new("/home/alex/bin/firefox/libmozsandbox.so")).is_none());
765
766        for (path, expected) in test_cases {
767            let actual = SoVersion::parse(OsStr::new(path)).unwrap();
768            assert_eq!(actual, expected);
769        }
770    }
771
772    #[test]
773    fn test_whitespaces_in_name() {
774        let mappings = get_mappings_for(
775            "\
77610000000-20000000 r--p 00000000 00:3e 27136458                   libmoz    gtk.so
77730000000-40000000 r--p 00000000 00:3e 27136458                   \"libmoz     gtk.so (deleted)\"
77830000000-40000000 r--p 00000000 00:3e 27136458                   ",
779            0x7ffe091bf000,
780        );
781
782        assert_eq!(mappings.len(), 3);
783        assert_eq!(mappings[0].name, Some("libmoz    gtk.so".into()));
784        assert_eq!(
785            mappings[1].name,
786            Some("\"libmoz     gtk.so (deleted)\"".into())
787        );
788        assert_eq!(mappings[2].name, None);
789    }
790}