diff --git a/src/linux/android.rs b/src/linux/android.rs index 8b795bce..c212689f 100644 --- a/src/linux/android.rs +++ b/src/linux/android.rs @@ -1,7 +1,7 @@ use { super::{ - maps_reader::MappingInfo, minidump_writer::MinidumpWriter, - process_inspection::ProcessInspector, process_reader::CopyFromProcessError, + minidump_writer::MinidumpWriter, process_inspection::ProcessInspector, + process_reader::CopyFromProcessError, }, goblin::elf, }; @@ -130,37 +130,26 @@ fn parse_loaded_elf_program_headers( } } -pub fn late_process_mappings( +/// Compute the effective load base of the ELF library pointed at by `elf_header_addr`. +/// In most cases it would be the same as that address, but libraries that use +/// Android packed relocations might need some tweaking. +pub fn effective_load_base( process_inspector: &dyn ProcessInspector, - mappings: &mut [MappingInfo], -) -> Result<()> { - // Only consider exec mappings that indicate a file path was mapped, and - // where the ELF header indicates a mapped shared library. - for map in mappings - .iter_mut() - .filter(|m| m.is_executable() && m.name_is_path()) - { - let ehdr_opt = MinidumpWriter::copy_from_process( - process_inspector, - map.start_address, - elf_header::SIZEOF_EHDR, - ) - .ok() - .and_then(|x| elf_header::Header::parse(&x).ok()); - - if let Some(ehdr) = ehdr_opt - && ehdr.e_type == elf_header::ET_DYN - { - // Compute the effective load bias for this mapped library, and update - // the mapping to hold that rather than |start_addr|, at the same time - // adjusting |size| to account for the change in |start_addr|. Where - // the library does not contain Android packed relocations, - // GetEffectiveLoadBias() returns |start_addr| and the mapping entry - // is not changed. - let load_bias = get_effective_load_bias(process_inspector, &ehdr, map.start_address); - map.size += map.start_address - load_bias; - map.start_address = load_bias; + elf_header_addr: usize, +) -> usize { + let ehdr = MinidumpWriter::copy_from_process( + process_inspector, + elf_header_addr, + elf_header::SIZEOF_EHDR, + ) + .ok() + .and_then(|v| elf_header::Header::parse(&v).ok()); + + match ehdr { + // Only a relocatable object can have been packed. + Some(ehdr) if ehdr.e_type == elf_header::ET_DYN => { + get_effective_load_bias(process_inspector, &ehdr, elf_header_addr) } + _ => elf_header_addr, } - Ok(()) } diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs index 9e8ca80d..655a181e 100644 --- a/src/linux/maps_reader.rs +++ b/src/linux/maps_reader.rs @@ -15,7 +15,6 @@ use { ffi::{OsStr, OsString}, mem::size_of, os::unix::ffi::{OsStrExt, OsStringExt}, - path::{Path, PathBuf}, }, }; @@ -24,28 +23,12 @@ pub const DELETED_SUFFIX: &[u8] = b" (deleted)"; type Result = std::result::Result; -#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] -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, serde::Serialize)] 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, @@ -71,8 +54,6 @@ pub enum MapsReaderError { #[serde(serialize_with = "serialize_goblin_error")] goblin::error::Error, ), - #[error("error reading soname from file")] - ReadSoNameFromFileFailed(#[source] ModuleReaderError), #[error("failed to memory map file")] MemoryMapFileFailed(#[source] ModuleReaderError), #[error("No soname found (filename: {})", .0.to_string_lossy())] @@ -115,13 +96,25 @@ pub enum MapsReaderError { ), } -fn is_mapping_a_path(pathname: Option<&OsStr>) -> bool { +/// Return whether a `/proc//maps` pathname is a path (contains a `/`). +pub(crate) fn name_is_path(pathname: Option<&OsStr>) -> bool { match pathname { Some(x) => x.as_bytes().contains(&b'/'), None => false, } } +/// Quick helper to convert a `/proc//maps` excerpt into mapping data. +#[cfg(test)] +#[cfg(target_pointer_width = "64")] // All tests are 64-bit +pub(crate) 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"), + Some(linux_gate_loc), + ) + .unwrap_or_default() +} + /// Sanitize mapped paths. /// /// This removes a ` (deleted)` suffix, if present. @@ -150,7 +143,7 @@ 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()) + name_is_path(self.name.as_deref()) } pub fn is_empty_page(&self) -> bool { @@ -186,7 +179,7 @@ impl MappingInfo { MMapPath::Anonymous => None, }; - let is_path = is_mapping_a_path(pathname.as_deref()); + let is_path = name_is_path(pathname.as_deref()); if let Some(linux_gate_loc) = linux_gate_loc.map(|u| usize::try_from(u).unwrap()) && (!is_path && (start_address == linux_gate_loc)) @@ -202,7 +195,6 @@ impl MappingInfo { { // 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; @@ -239,7 +231,6 @@ impl MappingInfo { 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(); @@ -251,10 +242,6 @@ impl MappingInfo { infos.push(MappingInfo { start_address, size: end_address - start_address, - system_mapping_info: SystemMappingInfo { - start_address, - end_address, - }, offset, permissions: mm.perms, name: pathname, @@ -269,8 +256,8 @@ impl MappingInfo { // 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 low_addr = self.start_address; + let high_addr = self.start_address + self.size; 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::() { @@ -296,73 +283,6 @@ impl MappingInfo { false } - /// Find the shared object name (SONAME) by examining the ELF information - /// for the mapping. - fn so_name(&self, process_inspector: &dyn ProcessInspector) -> Result { - let path = Path::new(self.name.as_deref().unwrap_or_default()); - super::module_reader::read_soname_from_file(process_inspector, path, self.offset) - .map_err(MapsReaderError::ReadSoNameFromFileFailed) - } - - #[inline] - fn so_version(&self) -> Option { - SoVersion::parse(self.name.as_deref()?) - } - - pub fn get_mapping_effective_path_name_and_version( - &self, - process_inspector: &dyn ProcessInspector, - soname: Option, - ) -> 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 Some(file_name) = soname.or_else(|| self.so_name(process_inspector).ok()) 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() && @@ -374,8 +294,7 @@ impl MappingInfo { } pub fn contains_address(&self, address: usize) -> bool { - self.system_mapping_info.start_address <= address - && address < self.system_mapping_info.end_address + self.start_address <= address && address < self.start_address + self.size } pub fn is_executable(&self) -> bool { @@ -391,110 +310,10 @@ impl MappingInfo { } } -/// 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()) - && 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"), - Some(linux_gate_loc), - ) - .unwrap_or_default() - } const LINES: &str = "\ 5597483fc000-5597483fe000 r--p 00000000 00:31 4750073 /usr/bin/cat @@ -559,10 +378,6 @@ ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsysca 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 @@ -576,10 +391,6 @@ ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsysca 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()), @@ -590,10 +401,6 @@ ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsysca 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, @@ -609,10 +416,6 @@ ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsysca 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()), @@ -669,10 +472,6 @@ ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsysca 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 @@ -701,10 +500,6 @@ a4840000-a4873000 rw-p 09021000 08:12 393449 /data/app/org.mozilla.firefox-1 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 @@ -716,59 +511,6 @@ a4840000-a4873000 rw-p 09021000 08:12 393449 /data/app/org.mozilla.firefox-1 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 process_inspector = process_inspection::local(0); - - let (file_path, file_name, _version) = mappings[0] - .get_mapping_effective_path_name_and_version(process_inspector.as_ref(), None) - .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( diff --git a/src/linux/minidump_writer/errors.rs b/src/linux/minidump_writer/errors.rs index 0b8f49bc..8c2784ec 100644 --- a/src/linux/minidump_writer/errors.rs +++ b/src/linux/minidump_writer/errors.rs @@ -12,6 +12,7 @@ use { thread_list_stream::SectionThreadListError, thread_names_stream::SectionThreadNamesError, }, + module_list::ModuleResolveError, module_reader::ModuleReaderError, process_inspection, serializers::*, @@ -71,6 +72,8 @@ pub enum WriterError { WriteSystemInfoErrors(#[source] ErrorList), #[error("Errors occurred while writing the thread list")] WriteThreadListErrors(#[source] ErrorList), + #[error("Errors occurred while writing the module list")] + WriteModuleListErrors(#[source] ErrorList), #[error("Failed writing cpuinfo")] WriteCpuInfoFailed(#[source] MemoryWriterError), #[error("Failed writing thread proc status")] @@ -172,6 +175,10 @@ pub enum InitError { AggregateMappingsFailed(#[source] MapsReaderError), #[error("Failed to enumerate process mappings")] EnumerateMappingsFailed(#[source] Box), + #[error("Failed to enumerate the process' loaded modules")] + EnumerateModulesFailed(#[source] Box), + #[error("Errors occurred while resolving the module list")] + ResolveModuleListErrors(#[source] ErrorList), #[error("Errors occurred while suspending threads")] SuspendThreadsErrors(#[source] ErrorList), #[error("No threads left to suspend out of {0}")] diff --git a/src/linux/minidump_writer/mappings.rs b/src/linux/minidump_writer/mappings.rs index 678fe361..db65ea41 100644 --- a/src/linux/minidump_writer/mappings.rs +++ b/src/linux/minidump_writer/mappings.rs @@ -1,107 +1,121 @@ -use super::{super::maps_reader::MappingInfo, *}; +use { + super::*, + crate::module_list::{ModuleListError, ModuleSource}, + std::ffi::OsString, +}; #[derive(Debug, Error, serde::Serialize)] pub enum SectionMappingsError { #[error("Failed to write to memory")] MemoryWriterError(#[from] MemoryWriterError), - #[error("Failed to get effective path of mapping ({0:?})")] - GetEffectivePathError(MappingInfo, #[source] MapsReaderError), + #[error("Errors occurred while inspecting module {}", .name.to_string_lossy())] + ModuleErrors { + name: OsString, + #[source] + errors: ErrorList, + }, } impl MinidumpWriter { - /// Write information about the mappings in effect. Because we are using the - /// minidump format, the information about the mappings is pretty limited. - /// Because of this, we also include the full, unparsed, /proc/$x/maps file in - /// another stream in the file. + /// Write information about the modules loaded into the process. Because we + /// are using the minidump format, the information about the modules is + /// pretty limited. Because of this, we also include the full, unparsed, + /// /proc/$x/maps file in another stream in the file. pub fn write_mappings( - &mut self, + &self, buffer: &mut DumpBuf, + mut soft_errors: impl WriteErrorList, ) -> Result { - let mut modules = Vec::new(); + let mut raw_modules = Vec::new(); - // First write all the mappings from the dumper - for mapping in &self.mappings { - // If the mapping is uninteresting, or if - // there is caller-provided information about this mapping - // in the user_mapping_list list, skip it + for module in &self.modules { + let module_name = module.name.clone().unwrap_or_default(); - if !mapping.is_interesting() || mapping.is_contained_in(&self.user_mapping_list) { + let Some((identifier, soname)) = self.get_mod_id_and_soname(module) else { continue; - } - log::debug!("retrieving build id for {:?}", mapping); - let identifier = self - .build_id_from_process_memory(mapping.start_address) - .or_else(|e| { - // If the mapping has an associated name that is a file, try to read the build id - // from the file. If there is no note segment with the build id in - // the program headers, we can't get to the note section if the section header - // table isn't loaded. - let Some(path) = &mapping.name else { - return Err(e); - }; - - log::debug!("failed to get build id from process memory ({e}), attempting to retrieve from {}", path.display()); - - module_reader::read_build_id_from_file(self.process_inspector.as_ref(), path.as_ref()).map_err(errors::WriterError::ModuleReaderError) - }) - .unwrap_or_else(|e| { - log::warn!("failed to get build id for mapping: {e}"); - Vec::new() - }); - - // If the identifier is all 0, its an uninteresting mapping (bmc#1676109) - if identifier.is_empty() || identifier.iter().all(|&x| x == 0) { - continue; - } - - // SONAME should always be accessible through program headers alone, so we don't really - // need to fall back to trying to read from the mapping file. - let soname = self.soname_from_process_memory(mapping.start_address).ok(); + }; let module = fill_raw_module( self.process_inspector.as_ref(), buffer, - mapping, + module, &identifier, soname, + soft_errors.subwriter(|errors| SectionMappingsError::ModuleErrors { + name: module_name, + errors, + }), )?; - modules.push(module); - } - - // Next write all the mappings provided by the caller - for user in &self.user_mapping_list { - // GUID was provided by caller. - let module = fill_raw_module( - self.process_inspector.as_ref(), - buffer, - &user.mapping, - &user.identifier, - None, - )?; - modules.push(module); + raw_modules.push(module); } - let list_header = MemoryWriter::::alloc_with_val(buffer, modules.len() as u32)?; + let list_header = MemoryWriter::::alloc_with_val(buffer, raw_modules.len() as u32)?; let mut dirent = MDRawDirectory { stream_type: MDStreamType::ModuleListStream as u32, location: list_header.location(), }; - if !modules.is_empty() { - let mapping_list = MemoryArrayWriter::::alloc_from_iter(buffer, modules)?; - dirent.location.data_size += mapping_list.location().data_size; + if !raw_modules.is_empty() { + let module_list = + MemoryArrayWriter::::alloc_from_iter(buffer, raw_modules)?; + dirent.location.data_size += module_list.location().data_size; } Ok(dirent) } + + /// What it says on the tin. Returns None if the module doesn't have identifying data. + fn get_mod_id_and_soname(&self, module: &ModuleInfo) -> Option<(Vec, Option)> { + // User-provided data is presumed to be correct. + if let ModuleSource::User { identifier } = &module.source { + return Some((identifier.clone(), None)); + } + + log::debug!("retrieving build id for {module:?}"); + let identifier = self + .build_id_from_process_memory(module.base_address) + .or_else(|e| { + // If the module has an associated name that is a file, try to read the build id + // from the file. If there is no note segment with the build id in + // the program headers, we can't get to the note section if the section header + // table isn't loaded. + let Some(path) = &module.name else { + return Err(e); + }; + + log::debug!( + "failed to get build id from process memory ({e}), attempting to retrieve from {}", + path.display() + ); + + module_reader::read_build_id_from_file(self.process_inspector.as_ref(), path.as_ref()) + .map_err(errors::WriterError::ModuleReaderError) + }) + .unwrap_or_else(|e| { + log::warn!("failed to get build id for module: {e}"); + Vec::new() + }); + + // If the identifier is all 0, its an uninteresting module (bmc#1676109) + if identifier.is_empty() || identifier.iter().all(|&x| x == 0) { + return None; + } + + // SONAME should always be accessible through program headers alone, so we don't really + // need to fall back to trying to read from the module file. + let soname = self.soname_from_process_memory(module.base_address).ok(); + + Some((identifier, soname)) + } } fn fill_raw_module( process_inspector: &dyn ProcessInspector, buffer: &mut DumpBuf, - mapping: &MappingInfo, + module: &ModuleInfo, identifier: &[u8], soname: Option, + soft_errors: impl WriteErrorList, ) -> Result { let cv_record = if identifier.is_empty() { // Just zeroes @@ -122,9 +136,8 @@ fn fill_raw_module( sig_section.location() }; - let (file_path, _, so_version) = mapping - .get_mapping_effective_path_name_and_version(process_inspector, soname) - .map_err(|e| SectionMappingsError::GetEffectivePathError(mapping.clone(), e))?; + let (file_path, _, so_version) = + module.effective_path_name_and_version(process_inspector, soname, soft_errors); let name_header = write_string_to_location(buffer, file_path.to_string_lossy().as_ref())?; let version_info = so_version.map_or(Default::default(), |sov| format::VS_FIXEDFILEINFO { @@ -138,8 +151,8 @@ fn fill_raw_module( }); let raw_module = MDRawModule { - base_of_image: mapping.start_address as u64, - size_of_image: mapping.size as u32, + base_of_image: module.base_address as u64, + size_of_image: module.size as u32, cv_record, module_name_rva: name_header.rva, version_info, diff --git a/src/linux/minidump_writer/mod.rs b/src/linux/minidump_writer/mod.rs index 8b85e258..1330fb1f 100644 --- a/src/linux/minidump_writer/mod.rs +++ b/src/linux/minidump_writer/mod.rs @@ -1,3 +1,5 @@ +use crate::module_list::ModuleInfo; + use { super::{ Pid, @@ -6,7 +8,8 @@ use { crash_context_ext::CrashContextExt, dso_debug, dumper_cpu_info::CpuInfoError, - maps_reader::{MappingInfo, MappingList, MapsReaderError}, + maps_reader::{MappingInfo, MappingList}, + module_list, process_inspection::{self, ProcessInspector, process_reader::CopyFromProcessError}, serializers::*, thread_info::{ThreadInfo, ThreadInfoError}, @@ -35,9 +38,6 @@ use { thiserror::Error, }; -#[cfg(target_os = "android")] -use super::android::late_process_mappings; - pub use super::auxv::{AuxvType, DirectAuxvDumpInfo}; pub mod app_memory; @@ -92,6 +92,7 @@ pub struct MinidumpWriter { pub threads: Vec, pub auxv: AuxvDumpInfo, pub mappings: Vec, + pub modules: Vec, pub page_size: usize, pub sanitize_stack: bool, pub minidump_size_limit: Option, @@ -234,6 +235,7 @@ impl MinidumpWriterConfig { threads: Default::default(), auxv, mappings: Default::default(), + modules: Default::default(), page_size: Default::default(), sanitize_stack: self.sanitize_stack, minidump_size_limit: self.minidump_size_limit, @@ -301,14 +303,14 @@ impl MinidumpWriter { soft_errors.push(InitError::SuspendNoThreadsLeft(threads_count)); } - #[cfg(target_os = "android")] - { - late_process_mappings(self.process_inspector.as_ref(), &mut self.mappings)?; + // The module list is derived from mappings. + if let Err(e) = self.enumerate_modules(&mut soft_errors) { + soft_errors.push(InitError::EnumerateModulesFailed(Box::new(e))); } if self.skip_stacks_if_mapping_unreferenced { if let Some(address) = self.principal_mapping_address { - self.principal_mapping = self.find_mapping_no_bias(address).cloned(); + self.principal_mapping = self.find_mapping(address).cloned(); } if !self.crash_thread_references_principal_mapping() { @@ -359,7 +361,10 @@ impl MinidumpWriter { )?; dir_section.write_to_file(buffer, Some(dirent))?; - let dirent = self.write_mappings(buffer)?; + let dirent = self.write_mappings( + buffer, + soft_errors.subwriter(WriterError::WriteModuleListErrors), + )?; dir_section.write_to_file(buffer, Some(dirent))?; self.write_app_memory(buffer) @@ -478,34 +483,17 @@ impl MinidumpWriter { } fn crash_thread_references_principal_mapping(&self) -> bool { - if self.crash_context.is_none() || self.principal_mapping.is_none() { + let (Some(crash_context), Some(mapping)) = + (self.crash_context.as_ref(), self.principal_mapping.as_ref()) + else { return false; - } - - let low_addr = self - .principal_mapping - .as_ref() - .unwrap() - .system_mapping_info - .start_address; - let high_addr = self - .principal_mapping - .as_ref() - .unwrap() - .system_mapping_info - .end_address; - - let pc = self - .crash_context - .as_ref() - .unwrap() - .get_instruction_pointer(); - let stack_pointer = self.crash_context.as_ref().unwrap().get_stack_pointer(); + }; - if pc >= low_addr && pc < high_addr { + if mapping.contains_address(crash_context.get_instruction_pointer()) { return true; } + let stack_pointer = crash_context.get_stack_pointer(); let (valid_stack_pointer, stack_len) = match self.get_stack_info(stack_pointer) { Ok(x) => x, Err(_) => { @@ -732,26 +720,31 @@ impl MinidumpWriter { ) .map_err(InitError::AggregateMappingsFailed)?; - // Although the initial executable is usually the first mapping, it's not - // guaranteed (see http://crosbug.com/25355); therefore, try to use the - // actual entry point to find the mapping. - if let Some(entry_point_loc) = self - .auxv - .get_entry_address() - .map(|u| usize::try_from(u).unwrap()) - { - // If this module contains the entry-point, and it's not already the first - // one, then we need to make it be first. This is because the minidump - // format assumes the first module is the one that corresponds to the main - // executable (as codified in - // processor/minidump.cc:MinidumpModuleList::GetMainModule()). - if let Some(entry_mapping_idx) = self.mappings.iter().position(|mapping| { - (mapping.start_address..mapping.start_address + mapping.size) - .contains(&entry_point_loc) - }) { - self.mappings.swap(0, entry_mapping_idx); - } - } + Ok(()) + } + + /// Builds the list of loaded modules written to the `ModuleListStream`. + /// Typically, those would be ELF objects loaded by the dynamic linker, but + /// the user can also provide additional modules to be included through + /// the configuration object. + fn enumerate_modules( + &mut self, + mut soft_errors: impl WriteErrorList, + ) -> Result<(), InitError> { + let mut candidates = + module_list::from_mappings(self.process_inspector.as_ref(), &self.mappings); + candidates.append(&mut module_list::from_user_mappings( + &self.user_mapping_list, + )); + + self.modules = module_list::resolve( + candidates, + self.auxv + .get_entry_address() + .map(|u| usize::try_from(u).unwrap()), + soft_errors.subwriter(InitError::ResolveModuleListErrors), + ); + Ok(()) } @@ -850,7 +843,7 @@ impl MinidumpWriter { let shift = 32 - 11; // let MappingInfo* last_hit_mapping = nullptr; // let MappingInfo* hit_mapping = nullptr; - let stack_mapping = self.find_mapping_no_bias(stack_pointer); + let stack_mapping = self.find_mapping(stack_pointer); let mut last_hit_mapping: Option<&MappingInfo> = None; // The magnitude below which integers are considered to be to be // 'small', and not constitute a PII risk. These are included to @@ -908,7 +901,7 @@ impl MinidumpWriter { let test = addr >> shift; if (could_hit_mapping[(test >> 3) & array_mask] & (1 << (test & 7)) != 0) - && let Some(hit_mapping) = self.find_mapping_no_bias(addr) + && let Some(hit_mapping) = self.find_mapping(addr) && hit_mapping.is_executable() { last_hit_mapping = Some(hit_mapping); @@ -931,16 +924,6 @@ impl MinidumpWriter { .find(|map| address >= map.start_address && address - map.start_address < map.size) } - // Find the mapping which the given memory address falls in. Uses the - // unadjusted mapping address range from the kernel, rather than the - // biased range. - pub fn find_mapping_no_bias(&self, address: usize) -> Option<&MappingInfo> { - self.mappings.iter().find(|map| { - address >= map.system_mapping_info.start_address - && address < map.system_mapping_info.end_address - }) - } - /// Reads the build ID out of the ELF object loaded at `start_address`. pub fn build_id_from_process_memory( &self, diff --git a/src/linux/minidump_writer/thread_list_stream.rs b/src/linux/minidump_writer/thread_list_stream.rs index c312d436..4bed4134 100644 --- a/src/linux/minidump_writer/thread_list_stream.rs +++ b/src/linux/minidump_writer/thread_list_stream.rs @@ -266,9 +266,7 @@ impl MinidumpWriter { let stack_pointer_offset = stack_ptr.saturating_sub(valid_stack_ptr); if self.skip_stacks_if_mapping_unreferenced { if let Some(principal_mapping) = &self.principal_mapping { - let low_addr = principal_mapping.system_mapping_info.start_address; - let high_addr = principal_mapping.system_mapping_info.end_address; - if (instruction_ptr < low_addr || instruction_ptr > high_addr) + if !principal_mapping.contains_address(instruction_ptr) && !principal_mapping .stack_has_pointer_to_mapping(&stack_bytes, stack_pointer_offset) { diff --git a/src/linux/mod.rs b/src/linux/mod.rs index 4141d2a7..ad9470f7 100644 --- a/src/linux/mod.rs +++ b/src/linux/mod.rs @@ -10,6 +10,7 @@ pub mod app_memory; mod crash_context_ext; pub mod maps_reader; pub mod minidump_writer; +pub mod module_list; pub mod module_reader; pub mod thread_info; diff --git a/src/linux/module_list.rs b/src/linux/module_list.rs new file mode 100644 index 00000000..bbf7cdb0 --- /dev/null +++ b/src/linux/module_list.rs @@ -0,0 +1,492 @@ +//! Enumeration of the ELF modules loaded into the target process. + +use std::{ + ffi::{OsStr, OsString}, + path::{Path, PathBuf}, +}; + +use crate::{ + linux::process_inspection::ProcessInspector, + maps_reader::{MappingEntry, MappingList}, + module_reader::ModuleReaderError, +}; +use error_graph::WriteErrorList; + +use super::maps_reader::MappingInfo; + +#[derive(thiserror::Error, Debug, serde::Serialize)] +pub enum ModuleListError { + #[error("error reading soname from file")] + ReadSoNameFromFileFailed(#[source] ModuleReaderError), +} + +/// Errors that only cost us the accuracy of a single entry of the module list. +#[derive(thiserror::Error, Debug, serde::Serialize)] +pub enum ModuleResolveError { + #[error( + "user-supplied module `{}` starts at {base_address:#x}, where another \ + one already does, so only one of them can be written", + name.to_string_lossy() + )] + DuplicateUserModule { name: OsString, base_address: usize }, +} + +/// Where a module's information came from. +#[derive(Debug)] +pub enum ModuleSource { + /// Read out of the process, be it from its memory map or from the dynamic + /// linker's rendez-vous. + Process, + /// Supplied by the user, and taken at face value: they know things about it + /// that we cannot derive, which is why it was supplied in the first place. + User { identifier: Vec }, +} + +impl ModuleSource { + fn is_user(&self) -> bool { + matches!(self, Self::User { .. }) + } +} + +/// An entry of the module list written to the `ModuleListStream`. +#[derive(Debug)] +pub struct ModuleInfo { + /// Where the ELF object was loaded — the minidump's `base_of_image`. + pub base_address: usize, + pub size: usize, + pub name: Option, + /// Offset of the object within its backing file; typically 0, but might + /// not be, e.g. if loaded from an APK. + pub file_offset: usize, + /// Whether the object has an executable segment. + /// It *is* possible to load data-only ELF objects! + pub executable: bool, + pub source: ModuleSource, +} + +/// A module we may write, before its extent is final. +#[derive(Debug)] +pub struct ModuleCandidate { + base_address: usize, + end_address: usize, + /// End of the object's last executable segment, or `base_address` if none. + code_end: usize, + name: Option, + file_offset: usize, + source: ModuleSource, +} + +impl ModuleCandidate { + /// Builds a module from user-supplied information. + fn from_user_entry(entry: &MappingEntry) -> Self { + Self { + base_address: entry.mapping.start_address, + end_address: entry.mapping.start_address + entry.mapping.size, + // Let's assume it's all executable. + code_end: entry.mapping.start_address + entry.mapping.size, + name: entry.mapping.name.clone(), + file_offset: entry.mapping.offset, + source: ModuleSource::User { + identifier: entry.identifier.clone(), + }, + } + } + + /// Whether `address` falls within this module's extent. + fn contains_address(&self, address: usize) -> bool { + (self.base_address..self.end_address).contains(&address) + } + + fn into_module(self) -> ModuleInfo { + ModuleInfo { + base_address: self.base_address, + size: self.end_address - self.base_address, + name: self.name, + file_offset: self.file_offset, + executable: self.code_end > self.base_address, + source: self.source, + } + } +} + +/// Any detected module that starts within a user-provided module must be dropped, as just keeping +/// the tail would make its addresses unresolvable anyway. +fn filter_out_user_overlap( + process_candidates: &mut Vec, + user_candidates: &[ModuleCandidate], +) { + process_candidates.retain(|c| { + !user_candidates + .iter() + .any(|uc| uc.contains_address(c.base_address)) + }); +} + +/// If a module contains the entry-point, and it's not already the first +/// one, then we need to make it be first. This is because the minidump +/// format assumes the first module is the one that corresponds to the main +/// executable (as codified in processor/minidump.cc:MinidumpModuleList::GetMainModule()). +fn ensure_entrypoint_is_first(candidates: &mut [ModuleCandidate], entry_point: Option) { + if let Some(entry_point) = entry_point + && let Some(index) = candidates + .iter() + .position(|c| c.contains_address(entry_point)) + { + candidates.swap(0, index); + } +} + +/// Turns the candidates into the entries written to the `ModuleListStream`. +pub(crate) fn resolve( + candidates: Vec, + entry_point: Option, + mut soft_errors: impl WriteErrorList, +) -> Vec { + let (user_candidates, mut process_candidates): (Vec<_>, Vec<_>) = + candidates.into_iter().partition(|c| c.source.is_user()); + + // Sanity check on the user-provided modules: if they happen to start at the same place, + // log the error and keep them both, as we don't have enough data to prioritize one. + for (index, candidate) in user_candidates.iter().enumerate() { + if user_candidates[..index] + .iter() + .any(|earlier| earlier.base_address == candidate.base_address) + { + soft_errors.push(ModuleResolveError::DuplicateUserModule { + name: candidate.name.clone().unwrap_or_default(), + base_address: candidate.base_address, + }); + } + } + + filter_out_user_overlap(&mut process_candidates, &user_candidates); + ensure_entrypoint_is_first(&mut process_candidates, entry_point); + + process_candidates + .into_iter() + .chain(user_candidates) + .map(ModuleCandidate::into_module) + .collect() +} + +/// Where the object behind `mapping` was loaded. +/// +/// `/proc//maps` reports where a mapping *starts*, which for an Android +/// object with packed relocations is `min_vaddr` above where the object was +/// actually loaded. Everywhere else the two are the same address. +#[cfg(target_os = "android")] +fn loaded_at(process_inspector: &dyn ProcessInspector, mapping: &MappingInfo) -> usize { + // Filter out unlikely candidates for libraries + if mapping.is_executable() && mapping.name_is_path() { + super::android::effective_load_base(process_inspector, mapping.start_address) + } else { + mapping.start_address + } +} + +#[cfg(not(target_os = "android"))] +fn loaded_at(_process_inspector: &dyn ProcessInspector, mapping: &MappingInfo) -> usize { + mapping.start_address +} + +impl ModuleCandidate { + /// Builds a module from a memory mapping. + fn from_mapping(process_inspector: &dyn ProcessInspector, mapping: &MappingInfo) -> Self { + // Adjust the base address if needed. + let base_address = loaded_at(process_inspector, mapping); + + Self { + base_address, + end_address: mapping.start_address + mapping.size, + code_end: if mapping.is_executable() { + mapping.start_address + mapping.size + } else { + mapping.start_address + }, + name: mapping.name.clone(), + file_offset: mapping.offset, + source: ModuleSource::Process, + } + } +} + +impl ModuleInfo { + pub fn effective_path_name_and_version( + &self, + process_inspector: &dyn ProcessInspector, + soname: Option, + soft_errors: impl WriteErrorList, + ) -> (PathBuf, String, Option) { + let mut file_path = PathBuf::from(self.name.clone().unwrap_or_default()); + + // Just use the filesystem name if no SONAME is present. + let Some(file_name) = self.find_soname(soname, process_inspector, soft_errors) 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 (file_path, file_name, self.so_version()); + }; + + self.fix_filename(&mut file_path, &file_name); + (file_path, file_name, self.so_version()) + } + + /// 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. For this reason, we try different approaches + /// before giving up. + /// + /// We prefer the SONAME if it's available rather than parse the path. + fn find_soname( + &self, + provided_soname: Option, + process_inspector: &dyn ProcessInspector, + mut soft_errors: impl WriteErrorList, + ) -> Option { + provided_soname.or_else(|| { + // User supplied the filename, let's use that directly. + if self.source.is_user() { + return None; + } + match self.elf_so_name(process_inspector) { + Ok(soname) => soname, + Err(e) => { + // Log error and move on to fallback path. + soft_errors.push(e); + None + } + } + }) + } + + /// Look in the module ELF metadata to find its soname if it has any. + /// Typically, executables don't have one. + fn elf_so_name( + &self, + process_inspector: &dyn ProcessInspector, + ) -> Result, ModuleListError> { + let path = Path::new(self.name.as_deref().unwrap_or_default()); + match super::module_reader::read_soname_from_file(process_inspector, path, self.file_offset) + { + Ok(soname) => Ok(Some(soname)), + Err(ModuleReaderError::NoSoName { .. }) => Ok(None), + Err(e) => Err(ModuleListError::ReadSoNameFromFileFailed(e)), + } + } + + #[inline] + fn so_version(&self) -> Option { + SoVersion::parse(self.name.as_deref()?) + } + + /// Handle edge-cases on filename, e.g. archive files. + fn fix_filename(&self, path: &mut PathBuf, file_name: &str) { + if self.executable && self.file_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 + path.push(file_name); + } else { + // Otherwise, replace the basename with the SONAME. + path.set_file_name(file_name); + } + } +} + +/// Build module information out of kernel mapping data +/// +/// Only requires access to /proc/pid/maps, but is ultimately just an approximation based on +/// observed behaviour of linkers. +pub(crate) fn from_mappings( + process_inspector: &dyn ProcessInspector, + mappings: &[MappingInfo], +) -> Vec { + mappings + .iter() + .filter(|mapping| mapping.is_interesting()) + .map(|mapping| ModuleCandidate::from_mapping(process_inspector, mapping)) + .collect() +} + +/// Builds the modules the user told us about. +pub(crate) fn from_user_mappings(user_mapping_list: &MappingList) -> Vec { + user_mapping_list + .iter() + .map(ModuleCandidate::from_user_entry) + .collect() +} + +/// 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()) + && 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 error_graph::ErrorList; + use std::path::PathBuf; + + use crate::linux::{maps_reader::get_mappings_for, process_inspection}; + + // All addresses below are 64 bit, and `MappingInfo::aggregate` drops any + // mapping it can't fit into a `usize`. + #[cfg(target_pointer_width = "64")] + #[test] + fn test_get_module_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 process_inspector = process_inspection::local(0); + let modules = resolve( + from_mappings(process_inspector.as_ref(), &mappings), + None, + &mut ErrorList::default(), + ); + + // The path doesn't exist, so the SONAME read fails and we fall back to + // the filesystem name -- which is the behaviour under test. + let mut soft_errors = ErrorList::default(); + let (file_path, file_name, _version) = modules[0].effective_path_name_and_version( + process_inspector.as_ref(), + None, + &mut soft_errors, + ); + assert!(!soft_errors.is_empty()); + 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); + } + } +} diff --git a/tests/linux_minidump_writer.rs b/tests/linux_minidump_writer.rs index 03104c50..71fbec90 100644 --- a/tests/linux_minidump_writer.rs +++ b/tests/linux_minidump_writer.rs @@ -8,7 +8,7 @@ use { minidump_writer::{ CrashContextExt, Pid, app_memory::AppMemory, - maps_reader::{MappingEntry, MappingInfo, SystemMappingInfo}, + maps_reader::{MappingEntry, MappingInfo}, minidump_writer::{MinidumpWriter, MinidumpWriterConfig, errors::WriterError}, module_reader::{self}, }, @@ -127,10 +127,6 @@ contextual_test! { offset: 0, permissions: MMPermissions::READ | MMPermissions::WRITE, name: Some("a fake mapping".into()), - system_mapping_info: SystemMappingInfo { - start_address: mmap_addr, - end_address: mmap_addr + memory_size, - }, }; let identifier = vec![ diff --git a/tests/ptrace_dumper.rs b/tests/ptrace_dumper.rs index cdbb6f79..299837fe 100644 --- a/tests/ptrace_dumper.rs +++ b/tests/ptrace_dumper.rs @@ -338,7 +338,7 @@ fn sanitizes_stack_copies() { // The instruction pointer definitely should point into an executable mapping. let instr_ptr = thread_info.get_instruction_pointer(); let mapping_info = dumper - .find_mapping_no_bias(instr_ptr) + .find_mapping(instr_ptr) .expect("Failed to find mapping info"); assert!(mapping_info.is_executable());