use crate::auxv_reader::AuxvType; use crate::errors::MapsReaderError; use crate::thread_info::Pid; use byteorder::{NativeEndian, ReadBytesExt}; use goblin::elf; use memmap2::{Mmap, MmapOptions}; use procfs_core::process::{MMPermissions, MMapPath, MemoryMaps}; use std::ffi::{OsStr, OsString}; use std::os::unix::ffi::OsStrExt; use std::{fs::File, mem::size_of, path::PathBuf}; pub const LINUX_GATE_LIBRARY_NAME: &str = "linux-gate.so"; pub const DELETED_SUFFIX: &[u8] = b" (deleted)"; type Result = std::result::Result; #[derive(Debug, PartialEq, Eq, Clone)] pub struct SystemMappingInfo { pub start_address: usize, pub end_address: usize, } // One of these is produced for each mapping in the process (i.e. line in // /proc/$x/maps). #[derive(Debug, PartialEq, Eq, Clone)] pub struct MappingInfo { // On Android, relocation packing can mean that the reported start // address of the mapping must be adjusted by a bias in order to // compensate for the compression of the relocation section. The // following two members hold (after LateInit) the adjusted mapping // range. See crbug.com/606972 for more information. pub start_address: usize, pub size: usize, // When Android relocation packing causes |start_addr| and |size| to // be modified with a load bias, we need to remember the unbiased // address range. The following structure holds the original mapping // address range as reported by the operating system. pub system_mapping_info: SystemMappingInfo, pub offset: usize, // offset into the backed file. pub permissions: MMPermissions, // read, write and execute permissions. pub name: Option, // pub elf_obj: Option, } #[derive(Debug)] pub struct MappingEntry { pub mapping: MappingInfo, pub identifier: Vec, } // A list of pub type MappingList = Vec; #[derive(Debug)] pub enum MappingInfoParsingResult { SkipLine, Success(MappingInfo), } fn is_mapping_a_path(pathname: Option<&OsStr>) -> bool { match pathname { Some(x) => x.as_bytes().contains(&b'/'), None => false, } } impl MappingInfo { /// Return whether the `name` field is a path (contains a `/`). pub fn name_is_path(&self) -> bool { is_mapping_a_path(self.name.as_deref()) } pub fn is_empty_page(&self) -> bool { (self.offset == 0) && (self.permissions == MMPermissions::PRIVATE) && self.name.is_none() } pub fn end_address(&self) -> usize { self.start_address + self.size } pub fn aggregate(memory_maps: MemoryMaps, linux_gate_loc: AuxvType) -> Result> { let mut infos = Vec::::new(); for mm in memory_maps { let start_address: usize = mm.address.0.try_into()?; let end_address: usize = mm.address.1.try_into()?; let mut offset: usize = mm.offset.try_into()?; let mut pathname: Option = match mm.pathname { MMapPath::Path(p) => Some(p.into()), MMapPath::Heap => Some("[heap]".into()), MMapPath::Stack => Some("[stack]".into()), MMapPath::TStack(i) => Some(format!("[stack:{i}]").into()), MMapPath::Vdso => Some("[vdso]".into()), MMapPath::Vvar => Some("[vvar]".into()), MMapPath::Vsyscall => Some("[vsyscall]".into()), MMapPath::Rollup => Some("[rollup]".into()), MMapPath::Vsys(i) => Some(format!("/SYSV{i:x}").into()), MMapPath::Other(n) => Some(format!("[{n}]").into()), MMapPath::Anonymous => None, }; let is_path = is_mapping_a_path(pathname.as_deref()); if !is_path && linux_gate_loc != 0 && start_address == linux_gate_loc.try_into()? { pathname = Some(LINUX_GATE_LIBRARY_NAME.into()); offset = 0; } if let Some(prev_module) = infos.last_mut() { if (start_address == prev_module.end_address()) && pathname.is_some() && (pathname == prev_module.name) { // Merge adjacent mappings into one module, assuming they're a single // library mapped by the dynamic linker. prev_module.system_mapping_info.end_address = end_address; prev_module.size = end_address - prev_module.start_address; prev_module.permissions |= mm.perms; continue; } else if (start_address == prev_module.end_address()) && prev_module.is_executable() && prev_module.name_is_path() && ((offset == 0) || (offset == prev_module.end_address())) && (mm.perms == MMPermissions::PRIVATE) { // Also merge mappings that result from address ranges that the // linker reserved but which a loaded library did not use. These // appear as an anonymous private mapping with no access flags set // and which directly follow an executable mapping. prev_module.size = end_address - prev_module.start_address; continue; } } // Sometimes the unused ranges reserved but the linker appear within the library. // If we detect an empty page that is adjacent to two mappings of the same library // we fold the three mappings together. if let Some(previous_modules) = infos.rchunks_exact_mut(2).next() { let empty_page = if let Some(prev_module) = previous_modules.last() { let prev_prev_module = previous_modules.first().unwrap(); prev_prev_module.name_is_path() && (prev_prev_module.end_address() == prev_module.start_address) && prev_module.is_empty_page() && (prev_module.end_address() == start_address) } else { false }; if empty_page { let prev_prev_module = previous_modules.first_mut().unwrap(); if pathname == prev_prev_module.name { prev_prev_module.system_mapping_info.end_address = end_address; prev_prev_module.size = end_address - prev_prev_module.start_address; prev_prev_module.permissions |= mm.perms; infos.pop(); continue; } } } infos.push(MappingInfo { start_address, size: end_address - start_address, system_mapping_info: SystemMappingInfo { start_address, end_address, }, offset, permissions: mm.perms, name: pathname, }); } Ok(infos) } pub fn get_mmap(name: &Option, offset: usize) -> Result { if !MappingInfo::is_mapped_file_safe_to_open(name) { return Err(MapsReaderError::NotSafeToOpenMapping( name.clone().unwrap_or_default(), )); } // Not doing this as root_prefix is always "" at the moment // if (!dumper.GetMappingAbsolutePath(mapping, filename)) let filename = name.clone().unwrap_or_default(); let mapped_file = unsafe { MmapOptions::new() .offset(offset.try_into()?) // try_into() to work for both 32 and 64 bit .map(&File::open(filename)?)? }; if mapped_file.is_empty() || mapped_file.len() < elf::header::SELFMAG { return Err(MapsReaderError::MmapSanityCheckFailed); } Ok(mapped_file) } /// Check whether the mapping refers to a deleted file, and if so try to find the file /// elsewhere and return that path. /// /// Currently this only supports fixing a deleted file that was the main exe of the given /// `pid`. /// /// Returns a tuple, where the first element is the file path (which is possibly different than /// `self.name`), and the second element is the original file path if a different path was /// used. If no mapping name exists, returns an error. pub fn fixup_deleted_file(&self, pid: Pid) -> Result<(OsString, Option<&OsStr>)> { // Check for ' (deleted)' in |path|. // |path| has to be at least as long as "/x (deleted)". let Some(path) = &self.name else { return Err(MapsReaderError::AnonymousMapping); }; let Some(old_path) = path.as_bytes().strip_suffix(DELETED_SUFFIX) else { return Ok((path.clone(), None)); }; // Check |path| against the /proc/pid/exe 'symlink'. let exe_link = format!("/proc/{}/exe", pid); let link_path = std::fs::read_link(&exe_link)?; // This is a no-op for now (until we want to support root_prefix for chroot-envs) // if (!GetMappingAbsolutePath(new_mapping, new_path)) // return false; if &link_path != path { return Err(MapsReaderError::SymlinkError( PathBuf::from(path), link_path, )); } // Check to see if someone actually named their executable 'foo (deleted)'. // This makes currently no sense, as exe_link == new_path // if let (Some(exe_stat), Some(new_path_stat)) = (nix::stat::stat(exe_link), nix::stat::stat(new_path)) { // if exe_stat.st_dev == new_path_stat.st_dev && exe_stat.st_ino == new_path_stat.st_ino { // return Err("".into()); // } // } Ok((exe_link.into(), Some(OsStr::from_bytes(old_path)))) } pub fn stack_has_pointer_to_mapping(&self, stack_copy: &[u8], sp_offset: usize) -> bool { // Loop over all stack words that would have been on the stack in // the target process (i.e. are word aligned, and at addresses >= // the stack pointer). Regardless of the alignment of |stack_copy|, // the memory starting at |stack_copy| + |offset| represents an // aligned word in the target process. let low_addr = self.system_mapping_info.start_address; let high_addr = self.system_mapping_info.end_address; let mut offset = (sp_offset + size_of::() - 1) & !(size_of::() - 1); while offset <= stack_copy.len() - size_of::() { let addr = match std::mem::size_of::() { 4 => stack_copy[offset..] .as_ref() .read_u32::() .map(|u| u as usize), 8 => stack_copy[offset..] .as_ref() .read_u64::() .map(|u| u as usize), x => panic!("Unexpected type width: {}", x), }; if let Ok(addr) = addr { if low_addr <= addr && addr <= high_addr { return true; } offset += size_of::(); } else { break; } } false } pub fn is_mapped_file_safe_to_open(name: &Option) -> bool { // It is unsafe to attempt to open a mapped file that lives under /dev, // because the semantics of the open may be driver-specific so we'd risk // hanging the crash dumper. And a file in /dev/ almost certainly has no // ELF file identifier anyways. if let Some(name) = name { if name.as_bytes().starts_with(b"/dev/") { return false; } } true } /// Find the shared object name (SONAME) by examining the ELF information /// for the mapping. fn so_name(&self) -> Result { let mapped_file = MappingInfo::get_mmap(&self.name, self.offset)?; let elf_obj = elf::Elf::parse(&mapped_file)?; let soname = elf_obj.soname.ok_or_else(|| { MapsReaderError::NoSoName(self.name.clone().unwrap_or_else(|| "None".into())) })?; Ok(soname.to_string()) } #[inline] fn so_version(&self) -> Option { SoVersion::parse(self.name.as_deref()?) } pub fn get_mapping_effective_path_name_and_version( &self, ) -> Result<(PathBuf, String, Option)> { let mut file_path = PathBuf::from(self.name.clone().unwrap_or_default()); // Tools such as minidump_stackwalk use the name of the module to look up // symbols produced by dump_syms. dump_syms will prefer to use a module's // DT_SONAME as the module name, if one exists, and will fall back to the // filesystem name of the module. // Just use the filesystem name if no SONAME is present. let Ok(file_name) = self.so_name() else { // file_path := /path/to/libname.so // file_name := libname.so let file_name = file_path .file_name() .map(|s| s.to_string_lossy().into_owned()) .unwrap_or_default(); return Ok((file_path, file_name, self.so_version())); }; if self.is_executable() && self.offset != 0 { // If an executable is mapped from a non-zero offset, this is likely because // the executable was loaded directly from inside an archive file (e.g., an // apk on Android). // In this case, we append the file_name to the mapped archive path: // file_name := libname.so // file_path := /path/to/ARCHIVE.APK/libname.so file_path.push(&file_name); } else { // Otherwise, replace the basename with the SONAME. file_path.set_file_name(&file_name); } Ok((file_path, file_name, self.so_version())) } pub fn is_contained_in(&self, user_mapping_list: &MappingList) -> bool { for user in user_mapping_list { // Ignore any mappings that are wholly contained within // mappings in the mapping_info_ list. if self.start_address >= user.mapping.start_address && (self.start_address + self.size) <= (user.mapping.start_address + user.mapping.size) { return true; } } false } pub fn is_interesting(&self) -> bool { // only want modules with filenames. self.name.is_some() && // Only want to include one mapping per shared lib. // Avoid filtering executable mappings. (self.offset == 0 || self.is_executable()) && // big enough to get a signature for. self.size >= 4096 } pub fn contains_address(&self, address: usize) -> bool { self.system_mapping_info.start_address <= address && address < self.system_mapping_info.end_address } pub fn is_executable(&self) -> bool { self.permissions.contains(MMPermissions::EXECUTE) } pub fn is_readable(&self) -> bool { self.permissions.contains(MMPermissions::READ) } pub fn is_writable(&self) -> bool { self.permissions.contains(MMPermissions::WRITE) } } /// Version metadata retrieved from an .so filename /// /// There is no standard for .so version numbers so this implementation just /// does a best effort to pull as much data as it can based on real .so schemes /// seen /// /// That being said, the [libtool](https://www.gnu.org/software/libtool/manual/html_node/Libtool-versioning.html) /// versioning scheme is fairly common #[cfg_attr(test, derive(Debug))] pub struct SoVersion { /// Might be non-zero if there is at least one non-zero numeric component after .so. /// /// Equivalent to `current` in libtool versions pub major: u32, /// The numeric component after the major version, if any /// /// Equivalent to `revision` in libtool versions pub minor: u32, /// The numeric component after the minor version, if any /// /// Equivalent to `age` in libtool versions pub patch: u32, /// The patch component may contain additional non-numeric metadata similar /// to a semver prelease, this is any numeric data that suffixes that prerelease /// string pub prerelease: u32, } impl SoVersion { /// Attempts to retrieve the .so version of the elf path via its filename fn parse(so_path: &OsStr) -> Option { let filename = std::path::Path::new(so_path).file_name()?; // Avoid an allocation unless the string contains non-utf8 let filename = filename.to_string_lossy(); let (_, version) = filename.split_once(".so.")?; let mut sov = Self { major: 0, minor: 0, patch: 0, prerelease: 0, }; let comps = [ &mut sov.major, &mut sov.minor, &mut sov.patch, &mut sov.prerelease, ]; for (i, comp) in version.split('.').enumerate() { if i <= 1 { *comps[i] = comp.parse().unwrap_or_default(); } else if i >= 4 { break; } else { // In some cases the release/patch version is alphanumeric (eg. '2rc5'), // so try to parse either a single or two numbers if let Some(pend) = comp.find(|c: char| !c.is_ascii_digit()) { if let Ok(patch) = comp[..pend].parse() { *comps[i] = patch; } if i >= comps.len() - 1 { break; } if let Some(pre) = comp.rfind(|c: char| !c.is_ascii_digit()) { if let Ok(pre) = comp[pre + 1..].parse() { *comps[i + 1] = pre; break; } } } else { *comps[i] = comp.parse().unwrap_or_default(); } } } Some(sov) } } #[cfg(test)] impl PartialEq<(u32, u32, u32, u32)> for SoVersion { fn eq(&self, o: &(u32, u32, u32, u32)) -> bool { self.major == o.0 && self.minor == o.1 && self.patch == o.2 && self.prerelease == o.3 } } #[cfg(test)] #[cfg(target_pointer_width = "64")] // All addresses are 64 bit and I'm currently too lazy to adjust it to work for both mod tests { use super::*; use procfs_core::FromRead; fn get_mappings_for(map: &str, linux_gate_loc: u64) -> Vec { MappingInfo::aggregate( MemoryMaps::from_read(map.as_bytes()).expect("failed to read mapping info"), linux_gate_loc, ) .unwrap_or_default() } const LINES: &str = "\ 5597483fc000-5597483fe000 r--p 00000000 00:31 4750073 /usr/bin/cat 5597483fe000-559748402000 r-xp 00002000 00:31 4750073 /usr/bin/cat 559748402000-559748404000 r--p 00006000 00:31 4750073 /usr/bin/cat 559748404000-559748405000 r--p 00007000 00:31 4750073 /usr/bin/cat 559748405000-559748406000 rw-p 00008000 00:31 4750073 /usr/bin/cat 559749b0e000-559749b2f000 rw-p 00000000 00:00 0 [heap] 7efd968d3000-7efd968f5000 rw-p 00000000 00:00 0 7efd968f5000-7efd9694a000 r--p 00000000 00:31 5004638 /usr/lib/locale/en_US.utf8/LC_CTYPE 7efd9694a000-7efd96bc2000 r--p 00000000 00:31 5004373 /usr/lib/locale/en_US.utf8/LC_COLLATE 7efd96bc2000-7efd96bc4000 rw-p 00000000 00:00 0 7efd96bc4000-7efd96bea000 r--p 00000000 00:31 4996104 /lib64/libc-2.32.so 7efd96bea000-7efd96d39000 r-xp 00026000 00:31 4996104 /lib64/libc-2.32.so 7efd96d39000-7efd96d85000 r--p 00175000 00:31 4996104 /lib64/libc-2.32.so 7efd96d85000-7efd96d86000 ---p 001c1000 00:31 4996104 /lib64/libc-2.32.so 7efd96d86000-7efd96d89000 r--p 001c1000 00:31 4996104 /lib64/libc-2.32.so 7efd96d89000-7efd96d8c000 rw-p 001c4000 00:31 4996104 /lib64/libc-2.32.so 7efd96d8c000-7efd96d92000 ---p 00000000 00:00 0 7efd96da0000-7efd96da1000 r--p 00000000 00:31 5004379 /usr/lib/locale/en_US.utf8/LC_NUMERIC 7efd96da1000-7efd96da2000 r--p 00000000 00:31 5004382 /usr/lib/locale/en_US.utf8/LC_TIME 7efd96da2000-7efd96da3000 r--p 00000000 00:31 5004377 /usr/lib/locale/en_US.utf8/LC_MONETARY 7efd96da3000-7efd96da4000 r--p 00000000 00:31 5004376 /usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES 7efd96da4000-7efd96da5000 r--p 00000000 00:31 5004380 /usr/lib/locale/en_US.utf8/LC_PAPER 7efd96da5000-7efd96da6000 r--p 00000000 00:31 5004378 /usr/lib/locale/en_US.utf8/LC_NAME 7efd96da6000-7efd96da7000 r--p 00000000 00:31 5004372 /usr/lib/locale/en_US.utf8/LC_ADDRESS 7efd96da7000-7efd96da8000 r--p 00000000 00:31 5004381 /usr/lib/locale/en_US.utf8/LC_TELEPHONE 7efd96da8000-7efd96da9000 r--p 00000000 00:31 5004375 /usr/lib/locale/en_US.utf8/LC_MEASUREMENT 7efd96da9000-7efd96db0000 r--s 00000000 00:31 5004639 /usr/lib64/gconv/gconv-modules.cache 7efd96db0000-7efd96db1000 r--p 00000000 00:31 5004374 /usr/lib/locale/en_US.utf8/LC_IDENTIFICATION 7efd96db1000-7efd96db2000 r--p 00000000 00:31 4996100 /lib64/ld-2.32.so 7efd96db2000-7efd96dd3000 r-xp 00001000 00:31 4996100 /lib64/ld-2.32.so 7efd96dd3000-7efd96ddc000 r--p 00022000 00:31 4996100 /lib64/ld-2.32.so 7efd96ddc000-7efd96ddd000 r--p 0002a000 00:31 4996100 /lib64/ld-2.32.so 7efd96ddd000-7efd96ddf000 rw-p 0002b000 00:31 4996100 /lib64/ld-2.32.so 7ffc6dfda000-7ffc6dffb000 rw-p 00000000 00:00 0 [stack] 7ffc6e0f3000-7ffc6e0f7000 r--p 00000000 00:00 0 [vvar] 7ffc6e0f7000-7ffc6e0f9000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]"; const LINUX_GATE_LOC: u64 = 0x7ffc6e0f7000; fn get_all_mappings() -> Vec { get_mappings_for(LINES, LINUX_GATE_LOC) } #[test] fn test_merged() { // Only /usr/bin/cat and [heap] let mappings = get_mappings_for( "\ 5597483fc000-5597483fe000 r--p 00000000 00:31 4750073 /usr/bin/cat 5597483fe000-559748402000 r-xp 00002000 00:31 4750073 /usr/bin/cat 559748402000-559748404000 r--p 00006000 00:31 4750073 /usr/bin/cat 559748404000-559748405000 r--p 00007000 00:31 4750073 /usr/bin/cat 559748405000-559748406000 rw-p 00008000 00:31 4750073 /usr/bin/cat 559749b0e000-559749b2f000 rw-p 00000000 00:00 0 [heap] 7efd968d3000-7efd968f5000 rw-p 00000000 00:00 0 ", 0x7ffc6e0f7000, ); assert_eq!(mappings.len(), 3); let cat_map = MappingInfo { start_address: 0x5597483fc000, size: 40960, system_mapping_info: SystemMappingInfo { start_address: 0x5597483fc000, end_address: 0x559748406000, }, offset: 0, permissions: MMPermissions::READ | MMPermissions::WRITE | MMPermissions::EXECUTE | MMPermissions::PRIVATE, name: Some("/usr/bin/cat".into()), }; assert_eq!(mappings[0], cat_map); let heap_map = MappingInfo { start_address: 0x559749b0e000, size: 135168, system_mapping_info: SystemMappingInfo { start_address: 0x559749b0e000, end_address: 0x559749b2f000, }, offset: 0, permissions: MMPermissions::READ | MMPermissions::WRITE | MMPermissions::PRIVATE, name: Some("[heap]".into()), }; assert_eq!(mappings[1], heap_map); let empty_map = MappingInfo { start_address: 0x7efd968d3000, size: 139264, system_mapping_info: SystemMappingInfo { start_address: 0x7efd968d3000, end_address: 0x7efd968f5000, }, offset: 0, permissions: MMPermissions::READ | MMPermissions::WRITE | MMPermissions::PRIVATE, name: None, }; assert_eq!(mappings[2], empty_map); } #[test] fn test_linux_gate_parsing() { let mappings = get_all_mappings(); let gate_map = MappingInfo { start_address: 0x7ffc6e0f7000, size: 8192, system_mapping_info: SystemMappingInfo { start_address: 0x7ffc6e0f7000, end_address: 0x7ffc6e0f9000, }, offset: 0, permissions: MMPermissions::READ | MMPermissions::EXECUTE | MMPermissions::PRIVATE, name: Some("linux-gate.so".into()), }; assert_eq!(mappings[21], gate_map); } #[test] fn test_reading_all() { let mappings = get_all_mappings(); let found_items: Vec> = vec![ Some("/usr/bin/cat".into()), Some("[heap]".into()), None, Some("/usr/lib/locale/en_US.utf8/LC_CTYPE".into()), Some("/usr/lib/locale/en_US.utf8/LC_COLLATE".into()), None, Some("/lib64/libc-2.32.so".into()), // The original shows a None here, but this is an address ranges that the // linker reserved but which a loaded library did not use. These // appear as an anonymous private mapping with no access flags set // and which directly follow an executable mapping. Some("/usr/lib/locale/en_US.utf8/LC_NUMERIC".into()), Some("/usr/lib/locale/en_US.utf8/LC_TIME".into()), Some("/usr/lib/locale/en_US.utf8/LC_MONETARY".into()), Some("/usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES".into()), Some("/usr/lib/locale/en_US.utf8/LC_PAPER".into()), Some("/usr/lib/locale/en_US.utf8/LC_NAME".into()), Some("/usr/lib/locale/en_US.utf8/LC_ADDRESS".into()), Some("/usr/lib/locale/en_US.utf8/LC_TELEPHONE".into()), Some("/usr/lib/locale/en_US.utf8/LC_MEASUREMENT".into()), Some("/usr/lib64/gconv/gconv-modules.cache".into()), Some("/usr/lib/locale/en_US.utf8/LC_IDENTIFICATION".into()), Some("/lib64/ld-2.32.so".into()), Some("[stack]".into()), Some("[vvar]".into()), // This is rewritten from [vdso] to linux-gate.so Some("linux-gate.so".into()), Some("[vsyscall]".into()), ]; assert_eq!( mappings.iter().map(|x| x.name.clone()).collect::>(), found_items ); } #[test] fn test_merged_reserved_mappings() { let mappings = get_all_mappings(); let gate_map = MappingInfo { start_address: 0x7efd96bc4000, size: 1892352, // Merged the anonymous area after in this mapping, so its bigger.. system_mapping_info: SystemMappingInfo { start_address: 0x7efd96bc4000, end_address: 0x7efd96d8c000, // ..but this is not visible here }, offset: 0, permissions: MMPermissions::READ | MMPermissions::WRITE | MMPermissions::EXECUTE | MMPermissions::PRIVATE, name: Some("/lib64/libc-2.32.so".into()), }; assert_eq!(mappings[6], gate_map); } #[test] fn test_merged_reserved_mappings_within_module() { let mappings = get_mappings_for( "\ 9b4a0000-9b931000 r--p 00000000 08:12 393449 /data/app/org.mozilla.firefox-1/lib/x86/libxul.so 9b931000-9bcae000 ---p 00000000 00:00 0 9bcae000-a116b000 r-xp 00490000 08:12 393449 /data/app/org.mozilla.firefox-1/lib/x86/libxul.so a116b000-a4562000 r--p 0594d000 08:12 393449 /data/app/org.mozilla.firefox-1/lib/x86/libxul.so a4562000-a4563000 ---p 00000000 00:00 0 a4563000-a4840000 r--p 08d44000 08:12 393449 /data/app/org.mozilla.firefox-1/lib/x86/libxul.so a4840000-a4873000 rw-p 09021000 08:12 393449 /data/app/org.mozilla.firefox-1/lib/x86/libxul.so", 0xa4876000, ); let gate_map = MappingInfo { start_address: 0x9b4a0000, size: 155004928, // Merged the anonymous area after in this mapping, so its bigger.. system_mapping_info: SystemMappingInfo { start_address: 0x9b4a0000, end_address: 0xa4873000, }, offset: 0, permissions: MMPermissions::READ | MMPermissions::WRITE | MMPermissions::EXECUTE | MMPermissions::PRIVATE, name: Some("/data/app/org.mozilla.firefox-1/lib/x86/libxul.so".into()), }; assert_eq!(mappings[0], gate_map); } #[test] fn test_get_mapping_effective_name() { let mappings = get_mappings_for( "\ 7f0b97b6f000-7f0b97b70000 r--p 00000000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so 7f0b97b70000-7f0b97b71000 r-xp 00000000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so 7f0b97b71000-7f0b97b73000 r--p 00000000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so 7f0b97b73000-7f0b97b74000 rw-p 00001000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so", 0x7ffe091bf000, ); assert_eq!(mappings.len(), 1); let (file_path, file_name, _version) = mappings[0] .get_mapping_effective_path_name_and_version() .expect("Couldn't get effective name for mapping"); assert_eq!(file_name, "libmozgtk.so"); assert_eq!(file_path, PathBuf::from("/home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so")); } #[test] fn test_elf_file_so_version() { #[rustfmt::skip] let test_cases = [ ("/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.32", (6, 0, 32, 0)), ("/usr/lib/x86_64-linux-gnu/libcairo-gobject.so.2.11800.0", (2, 11800, 0, 0)), ("/usr/lib/x86_64-linux-gnu/libm.so.6", (6, 0, 0, 0)), ("/usr/lib/x86_64-linux-gnu/libpthread.so.0", (0, 0, 0, 0)), ("/usr/lib/x86_64-linux-gnu/libgmodule-2.0.so.0.7800.0", (0, 7800, 0, 0)), ("/usr/lib/x86_64-linux-gnu/libabsl_time_zone.so.20220623.0.0", (20220623, 0, 0, 0)), ("/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.2rc5", (3, 34, 2, 5)), ("/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.2rc", (3, 34, 2, 0)), ("/usr/lib/x86_64-linux-gnu/libdbus-1.so.3.34.rc5", (3, 34, 0, 5)), ("/usr/lib/x86_64-linux-gnu/libtoto.so.AAA", (0, 0, 0, 0)), ("/usr/lib/x86_64-linux-gnu/libsemver-1.so.1.2.alpha.1", (1, 2, 0, 1)), ("/usr/lib/x86_64-linux-gnu/libboop.so.1.2.3.4.5", (1, 2, 3, 4)), ("/usr/lib/x86_64-linux-gnu/libboop.so.1.2.3pre4.5", (1, 2, 3, 4)), ]; assert!(SoVersion::parse(OsStr::new("/home/alex/bin/firefox/libmozsandbox.so")).is_none()); for (path, expected) in test_cases { let actual = SoVersion::parse(OsStr::new(path)).unwrap(); assert_eq!(actual, expected); } } #[test] fn test_whitespaces_in_name() { let mappings = get_mappings_for( "\ 10000000-20000000 r--p 00000000 00:3e 27136458 libmoz gtk.so 20000000-30000000 r--p 00000000 00:3e 27136458 libmozgtk.so (deleted) 30000000-40000000 r--p 00000000 00:3e 27136458 \"libmoz gtk.so (deleted)\" 30000000-40000000 r--p 00000000 00:3e 27136458 ", 0x7ffe091bf000, ); assert_eq!(mappings.len(), 4); assert_eq!(mappings[0].name, Some("libmoz gtk.so".into())); assert_eq!(mappings[1].name, Some("libmozgtk.so (deleted)".into())); assert_eq!( mappings[2].name, Some("\"libmoz gtk.so (deleted)\"".into()) ); assert_eq!(mappings[3].name, None); } }