From cfe5e46f0d75f266b18c12ab5c0c98c196054c9f Mon Sep 17 00:00:00 2001 From: Alex Franchuk Date: Fri, 19 Apr 2024 11:41:45 -0400 Subject: [PATCH] Read ELF SONAMEs directly from process memory when possible. When this fails, it falls back to the previous behavior of trying to open the mapped file name directly. Cloes #79. --- src/bin/test.rs | 8 +- src/linux.rs | 2 +- src/linux/errors.rs | 16 ++- src/linux/maps_reader.rs | 18 +-- .../{build_id_reader.rs => module_reader.rs} | 136 +++++++++++++----- src/linux/ptrace_dumper.rs | 38 ++--- src/linux/sections/mappings.rs | 18 ++- tests/linux_minidump_writer.rs | 5 +- 8 files changed, 164 insertions(+), 77 deletions(-) rename src/linux/{build_id_reader.rs => module_reader.rs} (75%) diff --git a/src/bin/test.rs b/src/bin/test.rs index 77a54081..d8ac8b60 100644 --- a/src/bin/test.rs +++ b/src/bin/test.rs @@ -9,6 +9,7 @@ mod linux { use super::*; use minidump_writer::{ minidump_writer::STOP_TIMEOUT, + module_reader, ptrace_dumper::{PtraceDumper, AT_SYSINFO_EHDR}, LINUX_GATE_LIBRARY_NAME, }; @@ -100,7 +101,7 @@ mod linux { } } let idx = found_exe.unwrap(); - let id = dumper.elf_identifier_for_mapping_index(idx)?; + let module_reader::BuildId(id) = dumper.from_process_memory_for_index(idx)?; dumper.resume_threads()?; assert!(!id.is_empty()); assert!(id.iter().any(|&x| x > 0)); @@ -133,11 +134,12 @@ mod linux { let ppid = getppid().as_raw(); let mut dumper = PtraceDumper::new(ppid, STOP_TIMEOUT)?; let mut found_linux_gate = false; - for mut mapping in dumper.mappings.clone() { + for mapping in dumper.mappings.clone() { if mapping.name == Some(LINUX_GATE_LIBRARY_NAME.into()) { found_linux_gate = true; dumper.suspend_threads()?; - let id = PtraceDumper::elf_identifier_for_mapping(&mut mapping, ppid)?; + let module_reader::BuildId(id) = + dumper.from_process_memory_for_mapping(&mapping)?; test!(!id.is_empty(), "id-vec is empty")?; test!(id.iter().any(|&x| x > 0), "all id elements are 0")?; dumper.resume_threads()?; diff --git a/src/linux.rs b/src/linux.rs index 31e21c76..0b68c125 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -5,13 +5,13 @@ mod android; pub mod app_memory; pub(crate) mod auxv_reader; -pub mod build_id_reader; pub mod crash_context; mod dso_debug; mod dumper_cpu_info; pub mod errors; pub mod maps_reader; pub mod minidump_writer; +pub mod module_reader; pub mod ptrace_dumper; pub(crate) mod sections; pub mod thread_info; diff --git a/src/linux/errors.rs b/src/linux/errors.rs index faf79f4d..1112c13a 100644 --- a/src/linux/errors.rs +++ b/src/linux/errors.rs @@ -26,7 +26,7 @@ pub enum MapsReaderError { #[error("Couldn't parse as ELF file")] ELFParsingFailed(#[from] goblin::error::Error), #[error("No soname found (filename: {})", .0.to_string_lossy())] - NoSoName(OsString), + NoSoName(OsString, #[source] ModuleReaderError), // parse_from_line() #[error("Map entry malformed: No {0} found")] @@ -115,8 +115,8 @@ pub enum DumperError { TryFromSliceError(#[from] std::array::TryFromSliceError), #[error("Couldn't parse as ELF file")] ELFParsingFailed(#[from] goblin::error::Error), - #[error("No build-id found")] - NoBuildIDFound(#[from] BuildIdReaderError), + #[error("Could not read value from module")] + ModuleReaderError(#[from] ModuleReaderError), #[error("Not safe to open mapping: {}", .0.to_string_lossy())] NotSafeToOpenMapping(OsString), #[error("Failed integer conversion")] @@ -250,7 +250,7 @@ pub enum WriterError { } #[derive(Debug, Error)] -pub enum BuildIdReaderError { +pub enum ModuleReaderError { #[error("failed to read module memory: {length} bytes at {offset}: {error}")] ReadModuleMemory { offset: u64, @@ -276,9 +276,15 @@ pub enum BuildIdReaderError { ... from sections: {section}\n\ ... from the text section: {section}" )] - Aggregate { + NoBuildId { program_headers: Box, section: Box, generated: Box, }, + #[error("a string in the strtab did not have a terminating nul byte")] + StrTabNoNulByte, + #[error("no SONAME found in dynamic linking information")] + NoSoNameEntry, + #[error("no dynamic linking information section")] + NoDynamicSection, } diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs index eecabae8..66fa2f46 100644 --- a/src/linux/maps_reader.rs +++ b/src/linux/maps_reader.rs @@ -256,14 +256,13 @@ impl MappingInfo { /// 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)?; + use super::module_reader::{ReadFromModule, SoName}; - let soname = elf_obj.soname.ok_or_else(|| { - MapsReaderError::NoSoName(self.name.clone().unwrap_or_else(|| "None".into())) - })?; - Ok(soname.to_string()) + let mapped_file = MappingInfo::get_mmap(&self.name, self.offset)?; + Ok(SoName::read_from_module(&*mapped_file) + .map_err(|e| MapsReaderError::NoSoName(self.name.clone().unwrap_or_default(), e))? + .0 + .to_string()) } #[inline] @@ -273,6 +272,7 @@ impl MappingInfo { pub fn get_mapping_effective_path_name_and_version( &self, + soname: Option, ) -> Result<(PathBuf, String, Option)> { let mut file_path = PathBuf::from(self.name.clone().unwrap_or_default()); @@ -282,7 +282,7 @@ impl MappingInfo { // filesystem name of the module. // Just use the filesystem name if no SONAME is present. - let Ok(file_name) = self.so_name() else { + let Some(file_name) = soname.or_else(|| self.so_name().ok()) else { // file_path := /path/to/libname.so // file_name := libname.so let file_name = file_path @@ -689,7 +689,7 @@ a4840000-a4873000 rw-p 09021000 08:12 393449 /data/app/org.mozilla.firefox-1 assert_eq!(mappings.len(), 1); let (file_path, file_name, _version) = mappings[0] - .get_mapping_effective_path_name_and_version() + .get_mapping_effective_path_name_and_version(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")); diff --git a/src/linux/build_id_reader.rs b/src/linux/module_reader.rs similarity index 75% rename from src/linux/build_id_reader.rs rename to src/linux/module_reader.rs index 94288b24..c0a5ee1e 100644 --- a/src/linux/build_id_reader.rs +++ b/src/linux/module_reader.rs @@ -1,9 +1,10 @@ -use crate::errors::BuildIdReaderError as Error; +use crate::errors::ModuleReaderError as Error; use crate::minidump_format::GUID; use goblin::{ container::{Container, Ctx, Endian}, elf, }; +use std::ffi::CStr; const NOTE_SECTION_NAME: &[u8] = b".note.gnu.build-id\0"; @@ -61,34 +62,57 @@ fn build_id_from_bytes(data: &[u8]) -> Vec { ) } -pub fn read_build_id(module_memory: impl ModuleMemory) -> Result, Error> { - let reader = ElfBuildIdReader::new(module_memory)?; - let program_headers = match reader.read_from_program_headers() { - Ok(v) => return Ok(v), - Err(e) => Box::new(e), - }; - let section = match reader.read_from_section() { - Ok(v) => return Ok(v), - Err(e) => Box::new(e), - }; - let generated = match reader.generate_from_text() { - Ok(v) => return Ok(v), - Err(e) => Box::new(e), - }; - Err(Error::Aggregate { - program_headers, - section, - generated, - }) +/// Types which can be read from an `impl ModuleMemory`. +pub trait ReadFromModule: Sized { + fn read_from_module(module_memory: impl ModuleMemory) -> Result; } -pub struct ElfBuildIdReader { +/// The module build id. +#[derive(Default, Clone, Debug)] +pub struct BuildId(pub Vec); + +impl ReadFromModule for BuildId { + fn read_from_module(module_memory: impl ModuleMemory) -> Result { + let reader = ModuleReader::new(module_memory)?; + let program_headers = match reader.build_id_from_program_headers() { + Ok(v) => return Ok(BuildId(v)), + Err(e) => Box::new(e), + }; + let section = match reader.build_id_from_section() { + Ok(v) => return Ok(BuildId(v)), + Err(e) => Box::new(e), + }; + let generated = match reader.build_id_generate_from_text() { + Ok(v) => return Ok(BuildId(v)), + Err(e) => Box::new(e), + }; + Err(Error::NoBuildId { + program_headers, + section, + generated, + }) + } +} + +/// The module SONAME. +#[derive(Default, Clone, Debug)] +pub struct SoName(pub String); + +impl ReadFromModule for SoName { + fn read_from_module(module_memory: impl ModuleMemory) -> Result { + ModuleReader::new(module_memory) + .and_then(|r| r.soname()) + .map(SoName) + } +} + +pub struct ModuleReader { module_memory: T, header: elf::Header, context: Ctx, } -impl ElfBuildIdReader { +impl ModuleReader { pub fn new(module_memory: T) -> Result { // We could use `Ctx::default()` (which defaults to the native system), however to be extra // permissive we'll just use a 64-bit ("Big") context which would result in the largest @@ -97,15 +121,57 @@ impl ElfBuildIdReader { let header_data = read(&module_memory, 0, header_size as u64)?; let header = elf::Elf::parse_header(&header_data)?; let context = Ctx::new(header.container()?, header.endianness()?); - Ok(ElfBuildIdReader { + Ok(ModuleReader { module_memory, header, context, }) } + pub fn soname(&self) -> Result { + let section_headers = self.read_section_headers()?; + + let strtab_section_header = section_headers + .get(self.header.e_shstrndx as usize) + .ok_or(Error::NoStrTab)?; + + let dynamic_section_header = section_headers + .iter() + .find(|h| h.sh_type == elf::section_header::SHT_DYNAMIC) + .ok_or(Error::NoDynamicSection)?; + + let dynamic_section: &[u8] = &read( + &self.module_memory, + dynamic_section_header.sh_offset, + dynamic_section_header.sh_size, + )?; + + let mut offset = 0; + loop { + use scroll::Pread; + let dyn_: elf::dynamic::Dyn = dynamic_section.gread_with(&mut offset, self.context)?; + if dyn_.d_tag == elf::dynamic::DT_SONAME { + let strtab_offset = dyn_.d_val; + if strtab_offset < strtab_section_header.sh_size { + let name = read( + &self.module_memory, + strtab_section_header.sh_offset + strtab_offset, + strtab_section_header.sh_size - strtab_offset, + )?; + return CStr::from_bytes_until_nul(&name) + .map(|s| s.to_string_lossy().into_owned()) + .map_err(|_| Error::StrTabNoNulByte); + } + } + if dyn_.d_tag == elf::dynamic::DT_NULL { + break; + } + } + Err(Error::NoSoNameEntry) + } + /// Read the build id from a program header note. - pub fn read_from_program_headers(&self) -> Result, Error> { + pub fn build_id_from_program_headers(&self) -> Result, Error> { if self.header.e_phoff == 0 { return Err(Error::NoProgramHeaderNote); } @@ -134,7 +200,7 @@ impl ElfBuildIdReader { } /// Read the build id from a notes section. - pub fn read_from_section(&self) -> Result, Error> { + pub fn build_id_from_section(&self) -> Result, Error> { let section_headers = self.read_section_headers()?; let strtab_section_header = section_headers @@ -173,7 +239,7 @@ impl ElfBuildIdReader { } /// Generate a build id by hashing the first page of the text section. - pub fn generate_from_text(&self) -> Result, Error> { + pub fn build_id_generate_from_text(&self) -> Result, Error> { let Some(text_header) = self .read_section_headers()? .into_iter() @@ -288,9 +354,9 @@ mod test { ]; #[test] - fn program_headers() { - let reader = ElfBuildIdReader::new(TINY_ELF).unwrap(); - let id = reader.read_from_program_headers().unwrap(); + fn build_id_program_headers() { + let reader = ModuleReader::new(TINY_ELF).unwrap(); + let id = reader.build_id_from_program_headers().unwrap(); assert_eq!( id, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] @@ -298,9 +364,9 @@ mod test { } #[test] - fn section() { - let reader = ElfBuildIdReader::new(TINY_ELF).unwrap(); - let id = reader.read_from_section().unwrap(); + fn build_id_section() { + let reader = ModuleReader::new(TINY_ELF).unwrap(); + let id = reader.build_id_from_section().unwrap(); assert_eq!( id, vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] @@ -308,9 +374,9 @@ mod test { } #[test] - fn text_hash() { - let reader = ElfBuildIdReader::new(TINY_ELF).unwrap(); - let id = reader.generate_from_text().unwrap(); + fn build_id_text_hash() { + let reader = ModuleReader::new(TINY_ELF).unwrap(); + let id = reader.build_id_generate_from_text().unwrap(); assert_eq!( id, vec![0x6a, 0x3c, 0x58, 0x31, 0xff, 0x0f, 0x05, 0, 0, 0, 0, 0, 0, 0, 0, 0] diff --git a/src/linux/ptrace_dumper.rs b/src/linux/ptrace_dumper.rs index bcb0a8e4..d05e53ca 100644 --- a/src/linux/ptrace_dumper.rs +++ b/src/linux/ptrace_dumper.rs @@ -2,9 +2,9 @@ use crate::linux::android::late_process_mappings; use crate::linux::{ auxv_reader::{AuxvType, ProcfsAuxvIter}, - build_id_reader, errors::{DumperError, InitError, ThreadInfoError}, maps_reader::MappingInfo, + module_reader, thread_info::{Pid, ThreadInfo}, }; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -557,28 +557,35 @@ impl PtraceDumper { }) } - pub fn elf_identifier_for_mapping_index(&mut self, idx: usize) -> Result, DumperError> { + pub fn from_process_memory_for_index( + &self, + idx: usize, + ) -> Result { assert!(idx < self.mappings.len()); - Self::elf_identifier_for_mapping(&mut self.mappings[idx], self.pid) + self.from_process_memory_for_mapping(&self.mappings[idx]) } - pub fn elf_identifier_for_mapping( - mapping: &mut MappingInfo, - pid: Pid, - ) -> Result, DumperError> { - let result = if pid == std::process::id().try_into()? { + pub fn from_process_memory_for_mapping( + &self, + mapping: &MappingInfo, + ) -> Result { + if std::process::id() + .try_into() + .map(|v: Pid| v == self.pid) + .unwrap_or(false) + { let mem_slice = unsafe { std::slice::from_raw_parts(mapping.start_address as *const u8, mapping.size) }; - build_id_reader::read_build_id(mem_slice) + T::read_from_module(mem_slice) } else { - struct ProcessReader { + struct ProcessModuleMemory { pid: Pid, start_address: u64, } - impl build_id_reader::ModuleMemory for ProcessReader { + impl module_reader::ModuleMemory for ProcessModuleMemory { type Memory = Vec; fn read_module_memory( @@ -596,12 +603,11 @@ impl PtraceDumper { } } - build_id_reader::read_build_id(ProcessReader { - pid, + T::read_from_module(ProcessModuleMemory { + pid: self.pid, start_address: mapping.start_address as u64, }) - }; - - result.map_err(|e| e.into()) + } + .map_err(|e| e.into()) } } diff --git a/src/linux/sections/mappings.rs b/src/linux/sections/mappings.rs index 9012ae35..2c45d1e4 100644 --- a/src/linux/sections/mappings.rs +++ b/src/linux/sections/mappings.rs @@ -1,5 +1,6 @@ use super::*; use crate::linux::maps_reader::MappingInfo; +use crate::linux::module_reader::{BuildId, SoName}; /// Write information about the mappings in effect. Because we are using the /// minidump format, the information about the mappings is pretty limited. @@ -23,9 +24,8 @@ pub fn write( { continue; } - // Note: elf_identifier_for_mapping_index() can manipulate the |mapping.name|. - let identifier = dumper - .elf_identifier_for_mapping_index(map_idx) + let BuildId(identifier) = dumper + .from_process_memory_for_index(map_idx) .unwrap_or_default(); // If the identifier is all 0, its an uninteresting mapping (bmc#1676109) @@ -33,14 +33,19 @@ pub fn write( continue; } - let module = fill_raw_module(buffer, &dumper.mappings[map_idx], &identifier)?; + let soname = dumper + .from_process_memory_for_index(map_idx) + .ok() + .map(|SoName(n)| n); + + let module = fill_raw_module(buffer, &dumper.mappings[map_idx], &identifier, soname)?; modules.push(module); } // Next write all the mappings provided by the caller for user in &config.user_mapping_list { // GUID was provided by caller. - let module = fill_raw_module(buffer, &user.mapping, &user.identifier)?; + let module = fill_raw_module(buffer, &user.mapping, &user.identifier, None)?; modules.push(module); } @@ -63,6 +68,7 @@ fn fill_raw_module( buffer: &mut DumpBuf, mapping: &MappingInfo, identifier: &[u8], + soname: Option, ) -> Result { let cv_record = if identifier.is_empty() { // Just zeroes @@ -84,7 +90,7 @@ fn fill_raw_module( }; let (file_path, _, so_version) = mapping - .get_mapping_effective_path_name_and_version() + .get_mapping_effective_path_name_and_version(soname) .map_err(|e| errors::SectionMappingsError::GetEffectivePathError(mapping.clone(), e))?; let name_header = write_string_to_location(buffer, file_path.to_string_lossy().as_ref())?; diff --git a/tests/linux_minidump_writer.rs b/tests/linux_minidump_writer.rs index 6cf75e4b..5983e12b 100644 --- a/tests/linux_minidump_writer.rs +++ b/tests/linux_minidump_writer.rs @@ -5,11 +5,11 @@ use minidump::*; use minidump_common::format::{GUID, MINIDUMP_STREAM_TYPE::*}; use minidump_writer::{ app_memory::AppMemory, - build_id_reader::read_build_id, crash_context::CrashContext, errors::*, maps_reader::{MappingEntry, MappingInfo, SystemMappingInfo}, minidump_writer::MinidumpWriter, + module_reader::{BuildId, ReadFromModule}, ptrace_dumper::PtraceDumper, thread_info::Pid, }; @@ -705,7 +705,8 @@ fn with_deleted_binary() { let pid = child.id() as i32; - let mut build_id = read_build_id(mem_slice.as_slice()).expect("Failed to get build_id"); + let BuildId(mut build_id) = + BuildId::read_from_module(mem_slice.as_slice()).expect("Failed to get build_id"); std::fs::remove_file(&binary_copy).expect("Failed to remove binary");