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 std::{fs::File, mem::size_of, path::PathBuf}; pub const LINUX_GATE_LIBRARY_NAME: &str = "linux-gate.so"; pub const DELETED_SUFFIX: &str = " (deleted)"; pub const RESERVED_FLAGS: &str = "---p"; 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 executable: bool, // true if the mapping has the execute bit set. 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<&str>) -> bool { match pathname { Some(x) => x.contains('/'), None => false, } } impl MappingInfo { pub fn parse_from_line( line: &str, linux_gate_loc: AuxvType, last_mapping: Option<&mut MappingInfo>, ) -> Result { let mut last_whitespace = false; // There is no `line.splitn_whitespace(6)`, so we have to do it somewhat manually // Split at the first whitespace, trim of the rest. let mut splits = line .trim() .splitn(6, |c: char| { if c.is_whitespace() { if last_whitespace { return false; } last_whitespace = true; true } else { last_whitespace = false; false } }) .map(str::trim); let address = splits .next() .ok_or(MapsReaderError::MapEntryMalformed("address"))?; let perms = splits .next() .ok_or(MapsReaderError::MapEntryMalformed("permissions"))?; let mut offset = usize::from_str_radix( splits .next() .ok_or(MapsReaderError::MapEntryMalformed("offset"))?, 16, )?; let _dev = splits .next() .ok_or(MapsReaderError::MapEntryMalformed("dev"))?; let _inode = splits .next() .ok_or(MapsReaderError::MapEntryMalformed("inode"))?; let mut pathname = splits.next(); // Optional // Due to our ugly `splitn_whitespace()` hack from above, we might have // only trailing whitespaces as the name, so we it might still be "Some()" if let Some(x) = pathname { if x.is_empty() { pathname = None; } } let mut addresses = address.split('-'); let start_address = usize::from_str_radix(addresses.next().unwrap(), 16)?; let end_address = usize::from_str_radix(addresses.next().unwrap(), 16)?; let executable = perms.contains('x'); // Only copy name if the name is a valid path name, or if // it's the VDSO image. let is_path = is_mapping_a_path(pathname); if !is_path && linux_gate_loc != 0 && start_address == linux_gate_loc.try_into()? { pathname = Some(LINUX_GATE_LIBRARY_NAME); offset = 0; } match (pathname, last_mapping) { (Some(_name), Some(module)) => { // Merge adjacent mappings into one module, assuming they're a single // library mapped by the dynamic linker. if (start_address == module.start_address + module.size) && (pathname == module.name.as_deref()) { module.system_mapping_info.end_address = end_address; module.size = end_address - module.start_address; module.executable |= executable; return Ok(MappingInfoParsingResult::SkipLine); } } (None, Some(module)) => { // 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. let module_end_address = module.start_address + module.size; if (start_address == module_end_address) && module.executable && is_mapping_a_path(module.name.as_deref()) && (offset == 0 || offset == module_end_address) && perms == RESERVED_FLAGS { module.size = end_address - module.start_address; return Ok(MappingInfoParsingResult::SkipLine); } } _ => (), } let name = pathname.map(ToOwned::to_owned); let info = MappingInfo { start_address, size: end_address - start_address, system_mapping_info: SystemMappingInfo { start_address, end_address, }, offset, executable, name, // elf_obj, }; Ok(MappingInfoParsingResult::Success(info)) } 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) } pub fn handle_deleted_file_in_mapping(path: &str, pid: Pid) -> Result { // Check for ' (deleted)' in |path|. // |path| has to be at least as long as "/x (deleted)". if !path.ends_with(DELETED_SUFFIX) { return Ok(path.to_string()); } // 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 != PathBuf::from(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) } 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.starts_with("/dev/") { return false; } } true } fn elf_file_so_name(&self) -> Result { // Find the shared object name (SONAME) by examining the ELF information // for |mapping|. If the SONAME is found copy it into the passed buffer // |soname| and return true. The size of the buffer is |soname_size|. 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".to_string())) })?; Ok(soname.to_string()) } pub fn get_mapping_effective_name_and_path(&self) -> Result<(String, String)> { let mut file_path = 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 file_name = if let Ok(name) = self.elf_file_so_name() { name } else { // file_path := /path/to/libname.so // file_name := libname.so // SAFETY: The unwrap is safe as rsplit always returns at least one item let file_name = file_path.rsplit('/').next().unwrap().to_owned(); return Ok((file_path, file_name)); }; if self.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 = format!("{}/{}", file_path, file_name); } else { // Otherwise, replace the basename with the SONAME. let split: Vec<_> = file_path.rsplitn(2, '/').collect(); if split.len() == 2 { // NOTE: rsplitn reverses the order, so the remainder is the last item file_path = format!("{}/{}", split[1], file_name); } else { file_path = file_name.clone(); } } Ok((file_path, file_name)) } 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.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 } } #[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::*; fn get_lines_and_loc() -> (Vec<&'static str>, u64) { (vec![ "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]" ], 0x7ffc6e0f7000) } fn get_all_mappings() -> Vec { let mut mappings: Vec = Vec::new(); let (lines, linux_gate_loc) = get_lines_and_loc(); // Only /usr/bin/cat and [heap] for line in lines { match MappingInfo::parse_from_line(line, linux_gate_loc, mappings.last_mut()) .expect("failed to read mapping info") { MappingInfoParsingResult::Success(map) => mappings.push(map), MappingInfoParsingResult::SkipLine => continue, } } assert_eq!(mappings.len(), 23); mappings } #[test] fn test_merged() { let mut mappings: Vec = Vec::new(); let (lines, linux_gate_loc) = get_lines_and_loc(); // Only /usr/bin/cat and [heap] for line in lines[0..=6].iter() { match MappingInfo::parse_from_line(line, linux_gate_loc, mappings.last_mut()) .expect("failed to read mapping info") { MappingInfoParsingResult::Success(map) => mappings.push(map), MappingInfoParsingResult::SkipLine => continue, } } 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, executable: true, name: Some("/usr/bin/cat".to_string()), }; 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, executable: false, name: Some("[heap]".to_string()), }; 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, executable: false, 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, executable: true, name: Some("linux-gate.so".to_string()), }; assert_eq!(mappings[21], gate_map); } #[test] fn test_reading_all() { let mappings = get_all_mappings(); let found_items = vec![ Some("/usr/bin/cat".to_string()), Some("[heap]".to_string()), None, Some("/usr/lib/locale/en_US.utf8/LC_CTYPE".to_string()), Some("/usr/lib/locale/en_US.utf8/LC_COLLATE".to_string()), None, Some("/lib64/libc-2.32.so".to_string()), // 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".to_string()), Some("/usr/lib/locale/en_US.utf8/LC_TIME".to_string()), Some("/usr/lib/locale/en_US.utf8/LC_MONETARY".to_string()), Some("/usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES".to_string()), Some("/usr/lib/locale/en_US.utf8/LC_PAPER".to_string()), Some("/usr/lib/locale/en_US.utf8/LC_NAME".to_string()), Some("/usr/lib/locale/en_US.utf8/LC_ADDRESS".to_string()), Some("/usr/lib/locale/en_US.utf8/LC_TELEPHONE".to_string()), Some("/usr/lib/locale/en_US.utf8/LC_MEASUREMENT".to_string()), Some("/usr/lib64/gconv/gconv-modules.cache".to_string()), Some("/usr/lib/locale/en_US.utf8/LC_IDENTIFICATION".to_string()), Some("/lib64/ld-2.32.so".to_string()), Some("[stack]".to_string()), Some("[vvar]".to_string()), // This is rewritten from [vdso] to linux-gate.so Some("linux-gate.so".to_string()), Some("[vsyscall]".to_string()), ]; 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, executable: true, name: Some("/lib64/libc-2.32.so".to_string()), }; assert_eq!(mappings[6], gate_map); } #[test] fn test_get_mapping_effective_name() { let lines = vec![ "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", ]; let linux_gate_loc = 0x7ffe091bf000; let mut mappings: Vec = Vec::new(); for line in lines { match MappingInfo::parse_from_line(line, linux_gate_loc, mappings.last_mut()) .expect("failed to read mapping info") { MappingInfoParsingResult::Success(map) => mappings.push(map), MappingInfoParsingResult::SkipLine => continue, } } assert_eq!(mappings.len(), 1); let (file_path, file_name) = mappings[0] .get_mapping_effective_name_and_path() .expect("Couldn't get effective name for mapping"); assert_eq!(file_name, "libmozgtk.so"); assert_eq!(file_path, "/home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so"); } #[test] fn test_whitespaces_in_maps() { let lines = vec![ " 7f0b97b6f000-7f0b97b70000 r--p 00000000 00:3e 27136458 libmozgtk.so", "7f0b97b70000-7f0b97b71000 r-xp 00000000 00:3e 27136458 libmozgtk.so ", "7f0b97b71000-7f0b97b73000 r--p 00000000 00:3e 27136458\t\t\tlibmozgtk.so", ]; let linux_gate_loc = 0x7ffe091bf000; let mut mappings: Vec = Vec::new(); for line in lines { match MappingInfo::parse_from_line(line, linux_gate_loc, mappings.last_mut()) .expect("failed to read mapping info") { MappingInfoParsingResult::Success(map) => mappings.push(map), MappingInfoParsingResult::SkipLine => continue, } } assert_eq!(mappings.len(), 1); let expected_map = MappingInfo { start_address: 0x7f0b97b6f000, size: 16384, system_mapping_info: SystemMappingInfo { start_address: 0x7f0b97b6f000, end_address: 0x7f0b97b73000, }, offset: 0, executable: true, name: Some("libmozgtk.so".to_string()), }; assert_eq!(expected_map, mappings[0]); } #[test] fn test_whitespaces_in_name() { let lines = vec![ "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 ", ]; let linux_gate_loc = 0x7ffe091bf000; let mut mappings: Vec = Vec::new(); for line in lines { match MappingInfo::parse_from_line(line, linux_gate_loc, mappings.last_mut()) .expect("failed to read mapping info") { MappingInfoParsingResult::Success(map) => mappings.push(map), MappingInfoParsingResult::SkipLine => continue, } } assert_eq!(mappings.len(), 4); assert_eq!(mappings[0].name, Some("libmoz gtk.so".to_string())); assert_eq!(mappings[1].name, Some("libmozgtk.so (deleted)".to_string())); assert_eq!( mappings[2].name, Some("\"libmoz gtk.so (deleted)\"".to_string()) ); assert_eq!(mappings[3].name, None); } }