diff --git a/src/bin/test.rs b/src/bin/test.rs index d8ac8b60..82c31d31 100644 --- a/src/bin/test.rs +++ b/src/bin/test.rs @@ -8,9 +8,7 @@ pub type Result = std::result::Result; mod linux { use super::*; use minidump_writer::{ - minidump_writer::STOP_TIMEOUT, - module_reader, - ptrace_dumper::{PtraceDumper, AT_SYSINFO_EHDR}, + minidump_writer::STOP_TIMEOUT, module_reader, ptrace_dumper::PtraceDumper, LINUX_GATE_LIBRARY_NAME, }; use nix::{ @@ -30,13 +28,13 @@ mod linux { fn test_setup() -> Result<()> { let ppid = getppid(); - PtraceDumper::new(ppid.as_raw(), STOP_TIMEOUT)?; + PtraceDumper::new(ppid.as_raw(), STOP_TIMEOUT, Default::default())?; Ok(()) } fn test_thread_list() -> Result<()> { let ppid = getppid(); - let dumper = PtraceDumper::new(ppid.as_raw(), STOP_TIMEOUT)?; + let dumper = PtraceDumper::new(ppid.as_raw(), STOP_TIMEOUT, Default::default())?; test!(!dumper.threads.is_empty(), "No threads")?; test!( dumper @@ -52,7 +50,7 @@ mod linux { fn test_copy_from_process(stack_var: usize, heap_var: usize) -> Result<()> { let ppid = getppid().as_raw(); - let mut dumper = PtraceDumper::new(ppid, STOP_TIMEOUT)?; + let mut dumper = PtraceDumper::new(ppid, STOP_TIMEOUT, Default::default())?; dumper.suspend_threads()?; let stack_res = PtraceDumper::copy_from_process(ppid, stack_var as *mut libc::c_void, 1)?; @@ -74,7 +72,7 @@ mod linux { fn test_find_mappings(addr1: usize, addr2: usize) -> Result<()> { let ppid = getppid(); - let dumper = PtraceDumper::new(ppid.as_raw(), STOP_TIMEOUT)?; + let dumper = PtraceDumper::new(ppid.as_raw(), STOP_TIMEOUT, Default::default())?; dumper .find_mapping(addr1) .ok_or("No mapping for addr1 found")?; @@ -91,7 +89,7 @@ mod linux { let ppid = getppid().as_raw(); let exe_link = format!("/proc/{}/exe", ppid); let exe_name = std::fs::read_link(exe_link)?.into_os_string(); - let mut dumper = PtraceDumper::new(ppid, STOP_TIMEOUT)?; + let mut dumper = PtraceDumper::new(ppid, STOP_TIMEOUT, Default::default())?; dumper.suspend_threads()?; let mut found_exe = None; for (idx, mapping) in dumper.mappings.iter().enumerate() { @@ -110,7 +108,7 @@ mod linux { fn test_merged_mappings(path: String, mapped_mem: usize, mem_size: usize) -> Result<()> { // Now check that PtraceDumper interpreted the mappings properly. - let dumper = PtraceDumper::new(getppid().as_raw(), STOP_TIMEOUT)?; + let dumper = PtraceDumper::new(getppid().as_raw(), STOP_TIMEOUT, Default::default())?; let mut mapping_count = 0; for map in &dumper.mappings { if map @@ -132,7 +130,7 @@ mod linux { fn test_linux_gate_mapping_id() -> Result<()> { let ppid = getppid().as_raw(); - let mut dumper = PtraceDumper::new(ppid, STOP_TIMEOUT)?; + let mut dumper = PtraceDumper::new(ppid, STOP_TIMEOUT, Default::default())?; let mut found_linux_gate = false; for mapping in dumper.mappings.clone() { if mapping.name == Some(LINUX_GATE_LIBRARY_NAME.into()) { @@ -152,8 +150,8 @@ mod linux { fn test_mappings_include_linux_gate() -> Result<()> { let ppid = getppid().as_raw(); - let dumper = PtraceDumper::new(ppid, STOP_TIMEOUT)?; - let linux_gate_loc = dumper.auxv[&AT_SYSINFO_EHDR]; + let dumper = PtraceDumper::new(ppid, STOP_TIMEOUT, Default::default())?; + let linux_gate_loc = dumper.auxv.get_linux_gate_address().unwrap(); test!(linux_gate_loc != 0, "linux_gate_loc == 0")?; let mut found_linux_gate = false; for mapping in &dumper.mappings { diff --git a/src/linux.rs b/src/linux.rs index 0b68c125..d1f666d9 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -4,7 +4,7 @@ #[cfg(target_os = "android")] mod android; pub mod app_memory; -pub(crate) mod auxv_reader; +pub(crate) mod auxv; pub mod crash_context; mod dso_debug; mod dumper_cpu_info; diff --git a/src/linux/auxv/mod.rs b/src/linux/auxv/mod.rs new file mode 100644 index 00000000..c8ee248a --- /dev/null +++ b/src/linux/auxv/mod.rs @@ -0,0 +1,137 @@ +pub use reader::ProcfsAuxvIter; +use { + crate::linux::thread_info::Pid, + std::{fs::File, io::BufReader}, + thiserror::Error, +}; + +mod reader; + +/// The type used in auxv keys and values. +#[cfg(target_pointer_width = "32")] +pub type AuxvType = u32; +/// The type used in auxv keys and values. +#[cfg(target_pointer_width = "64")] +pub type AuxvType = u64; + +#[cfg(target_os = "android")] +mod consts { + use super::AuxvType; + pub const AT_PHDR: AuxvType = 3; + pub const AT_PHNUM: AuxvType = 5; + pub const AT_ENTRY: AuxvType = 9; + pub const AT_SYSINFO_EHDR: AuxvType = 33; +} +#[cfg(not(target_os = "android"))] +mod consts { + use super::AuxvType; + pub const AT_PHDR: AuxvType = libc::AT_PHDR; + pub const AT_PHNUM: AuxvType = libc::AT_PHNUM; + pub const AT_ENTRY: AuxvType = libc::AT_ENTRY; + pub const AT_SYSINFO_EHDR: AuxvType = libc::AT_SYSINFO_EHDR; +} + +/// An auxv key-value pair. +#[derive(Debug, PartialEq, Eq)] +pub struct AuxvPair { + pub key: AuxvType, + pub value: AuxvType, +} + +/// Auxv info that can be passed from crashing process +/// +/// Since `/proc/{pid}/auxv` can sometimes be inaccessible, the calling process should prefer to transfer this +/// information directly using the Linux `getauxval()` call (if possible). +/// +/// Any field that is set to `0` will be considered unset. In that case, minidump-writer might try other techniques +/// to obtain it (like reading `/proc/{pid}/auxv`). +#[repr(C)] +#[derive(Clone, Debug, Default)] +pub struct DirectAuxvDumpInfo { + /// The value of `getauxval(AT_PHNUM)` + pub program_header_count: AuxvType, + /// The value of `getauxval(AT_PHDR)` + pub program_header_address: AuxvType, + /// The value of `getauxval(AT_SYSINFO_EHDR)` + pub linux_gate_address: AuxvType, + /// The value of `getauxval(AT_ENTRY)` + pub entry_address: AuxvType, +} + +impl From for AuxvDumpInfo { + fn from(f: DirectAuxvDumpInfo) -> AuxvDumpInfo { + AuxvDumpInfo { + program_header_count: (f.program_header_count > 0).then_some(f.program_header_count), + program_header_address: (f.program_header_address > 0) + .then_some(f.program_header_address), + linux_gate_address: (f.linux_gate_address > 0).then_some(f.linux_gate_address), + entry_address: (f.entry_address > 0).then_some(f.entry_address), + } + } +} + +#[derive(Debug, Default)] +pub struct AuxvDumpInfo { + program_header_count: Option, + program_header_address: Option, + linux_gate_address: Option, + entry_address: Option, +} + +impl AuxvDumpInfo { + pub fn try_filling_missing_info(&mut self, pid: Pid) -> Result<(), AuxvError> { + if self.is_complete() { + return Ok(()); + } + + let auxv_path = format!("/proc/{pid}/auxv"); + let auxv_file = File::open(&auxv_path).map_err(|e| AuxvError::OpenError(auxv_path, e))?; + + for AuxvPair { key, value } in + ProcfsAuxvIter::new(BufReader::new(auxv_file)).filter_map(Result::ok) + { + let dest_field = match key { + consts::AT_PHNUM => &mut self.program_header_count, + consts::AT_PHDR => &mut self.program_header_address, + consts::AT_SYSINFO_EHDR => &mut self.linux_gate_address, + consts::AT_ENTRY => &mut self.entry_address, + _ => continue, + }; + if dest_field.is_none() { + *dest_field = Some(value); + } + } + + Ok(()) + } + pub fn get_program_header_count(&self) -> Option { + self.program_header_count + } + pub fn get_program_header_address(&self) -> Option { + self.program_header_address + } + pub fn get_linux_gate_address(&self) -> Option { + self.linux_gate_address + } + pub fn get_entry_address(&self) -> Option { + self.entry_address + } + pub fn is_complete(&self) -> bool { + self.program_header_count.is_some() + && self.program_header_address.is_some() + && self.linux_gate_address.is_some() + && self.entry_address.is_some() + } +} + +#[derive(Debug, Error)] +pub enum AuxvError { + #[error("Failed to open file {0}")] + OpenError(String, #[source] std::io::Error), + #[error("No auxv entry found for PID {0}")] + NoAuxvEntryFound(Pid), + #[error("Invalid auxv format (should not hit EOF before AT_NULL)")] + InvalidFormat, + #[error("IO Error")] + IOError(#[from] std::io::Error), +} diff --git a/src/linux/auxv_reader.rs b/src/linux/auxv/reader.rs similarity index 84% rename from src/linux/auxv_reader.rs rename to src/linux/auxv/reader.rs index 3ed09e5a..dadb9979 100644 --- a/src/linux/auxv_reader.rs +++ b/src/linux/auxv/reader.rs @@ -4,26 +4,15 @@ // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be in…substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -use crate::errors::AuxvReaderError; -use byteorder::{NativeEndian, ReadBytesExt}; -use std::fs::File; -use std::io::{BufReader, Read}; -pub type Result = std::result::Result; - -/// The type used in auxv keys and values. -#[cfg(target_pointer_width = "32")] -pub type AuxvType = u32; -/// The type used in auxv keys and values. -#[cfg(target_pointer_width = "64")] -pub type AuxvType = u64; - -/// An auxv key-value pair. -#[derive(Debug, PartialEq, Eq)] -pub struct AuxvPair { - pub key: AuxvType, - pub value: AuxvType, -} +use { + super::{AuxvError, AuxvPair, AuxvType}, + byteorder::{NativeEndian, ReadBytesExt}, + std::{ + fs::File, + io::{BufReader, Read}, + }, +}; /// An iterator across auxv pairs from procfs. pub struct ProcfsAuxvIter { @@ -48,7 +37,7 @@ impl ProcfsAuxvIter { } impl Iterator for ProcfsAuxvIter { - type Item = Result; + type Item = Result; fn next(&mut self) -> Option { if !self.keep_going { return None; @@ -65,7 +54,7 @@ impl Iterator for ProcfsAuxvIter { Ok(n) => { if n == 0 { // should not hit EOF before AT_NULL - return Some(Err(AuxvReaderError::InvalidFormat)); + return Some(Err(AuxvError::InvalidFormat)); } read_bytes += n; diff --git a/src/linux/dso_debug.rs b/src/linux/dso_debug.rs index 01c0a735..c2f873b0 100644 --- a/src/linux/dso_debug.rs +++ b/src/linux/dso_debug.rs @@ -1,9 +1,8 @@ use crate::{ - linux::{auxv_reader::AuxvType, errors::SectionDsoDebugError, ptrace_dumper::PtraceDumper}, + linux::{auxv::AuxvDumpInfo, errors::SectionDsoDebugError, ptrace_dumper::PtraceDumper}, mem_writer::{write_string_to_location, Buffer, MemoryArrayWriter, MemoryWriter}, minidump_format::*, }; -use std::collections::HashMap; type Result = std::result::Result; @@ -77,26 +76,13 @@ pub struct RDebug { pub fn write_dso_debug_stream( buffer: &mut Buffer, blamed_thread: i32, - auxv: &HashMap, + auxv: &AuxvDumpInfo, ) -> Result { - let at_phnum; - let at_phdr; - #[cfg(any(target_arch = "arm", all(target_os = "android", target_arch = "x86")))] - { - at_phdr = 3; - at_phnum = 5; - } - #[cfg(not(any(target_arch = "arm", all(target_os = "android", target_arch = "x86"))))] - { - at_phdr = libc::AT_PHDR; - at_phnum = libc::AT_PHNUM; - } - let phnum_max = *auxv - .get(&at_phnum) - .ok_or(SectionDsoDebugError::CouldNotFind("AT_PHNUM in auxv"))? - as usize; - let phdr = *auxv - .get(&at_phdr) + let phnum_max = + auxv.get_program_header_count() + .ok_or(SectionDsoDebugError::CouldNotFind("AT_PHNUM in auxv"))? as usize; + let phdr = auxv + .get_program_header_address() .ok_or(SectionDsoDebugError::CouldNotFind("AT_PHDR in auxv"))? as usize; let ph = PtraceDumper::copy_from_process( diff --git a/src/linux/errors.rs b/src/linux/errors.rs index 29486e47..e94c0cce 100644 --- a/src/linux/errors.rs +++ b/src/linux/errors.rs @@ -1,3 +1,4 @@ +use crate::auxv::AuxvError; use crate::dir_section::FileWriterError; use crate::maps_reader::MappingInfo; use crate::mem_writer::MemoryWriterError; @@ -9,10 +10,10 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum InitError { + #[error("failed to read auxv")] + ReadAuxvFailed(AuxvError), #[error("IO error for file {0}")] IOError(String, #[source] std::io::Error), - #[error("No auxv entry found for PID {0}")] - NoAuxvEntryFound(Pid), #[error("crash thread does not reference principal mapping")] PrincipalMappingNotReferenced, #[error("Failed Android specific late init")] @@ -47,14 +48,6 @@ pub enum MapsReaderError { SymlinkError(std::path::PathBuf, std::path::PathBuf), } -#[derive(Debug, Error)] -pub enum AuxvReaderError { - #[error("Invalid auxv format (should not hit EOF before AT_NULL)")] - InvalidFormat, - #[error("IO Error")] - IOError(#[from] std::io::Error), -} - #[derive(Debug, Error)] pub enum CpuInfoError { #[error("IO error for file /proc/cpuinfo")] diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs index 66fa2f46..8b89e873 100644 --- a/src/linux/maps_reader.rs +++ b/src/linux/maps_reader.rs @@ -1,4 +1,4 @@ -use crate::auxv_reader::AuxvType; +use crate::auxv::AuxvType; use crate::errors::MapsReaderError; use byteorder::{NativeEndian, ReadBytesExt}; use goblin::elf; diff --git a/src/linux/minidump_writer.rs b/src/linux/minidump_writer.rs index c83308f5..aff337b6 100644 --- a/src/linux/minidump_writer.rs +++ b/src/linux/minidump_writer.rs @@ -1,4 +1,6 @@ +pub use crate::linux::auxv::{AuxvType, DirectAuxvDumpInfo}; use crate::{ + auxv::AuxvDumpInfo, dir_section::{DirSection, DumpBuf}, linux::{ app_memory::AppMemoryList, @@ -42,6 +44,7 @@ pub struct MinidumpWriter { pub crash_context: Option, pub crashing_thread_context: CrashingThreadContext, pub stop_timeout: Duration, + pub direct_auxv_dump_info: Option, } // This doesn't work yet: @@ -71,6 +74,7 @@ impl MinidumpWriter { crash_context: None, crashing_thread_context: CrashingThreadContext::None, stop_timeout: STOP_TIMEOUT, + direct_auxv_dump_info: None, } } @@ -117,10 +121,30 @@ impl MinidumpWriter { self } + /// Directly set important Auxv info determined by the crashing process + /// + /// Since `/proc/{pid}/auxv` can sometimes be inaccessible, the calling process should prefer to transfer this + /// information directly using the Linux `getauxval()` call (if possible). + /// + /// Any field that is set to `0` will be considered unset. In that case, minidump-writer might try other techniques + /// to obtain it (like reading `/proc/{pid}/auxv`). + pub fn set_direct_auxv_dump_info( + &mut self, + direct_auxv_dump_info: DirectAuxvDumpInfo, + ) -> &mut Self { + self.direct_auxv_dump_info = Some(direct_auxv_dump_info); + self + } + /// Generates a minidump and writes to the destination provided. Returns the in-memory /// version of the minidump as well. pub fn dump(&mut self, destination: &mut (impl Write + Seek)) -> Result> { - let mut dumper = PtraceDumper::new(self.process_id, self.stop_timeout)?; + let auxv = self + .direct_auxv_dump_info + .clone() + .map(AuxvDumpInfo::from) + .unwrap_or_default(); + let mut dumper = PtraceDumper::new(self.process_id, self.stop_timeout, auxv)?; dumper.suspend_threads()?; dumper.late_init()?; diff --git a/src/linux/ptrace_dumper.rs b/src/linux/ptrace_dumper.rs index 4db81d3d..e709d06a 100644 --- a/src/linux/ptrace_dumper.rs +++ b/src/linux/ptrace_dumper.rs @@ -1,7 +1,7 @@ #[cfg(target_os = "android")] use crate::linux::android::late_process_mappings; use crate::linux::{ - auxv_reader::{AuxvType, ProcfsAuxvIter}, + auxv::AuxvDumpInfo, errors::{DumperError, InitError, ThreadInfoError}, maps_reader::MappingInfo, module_reader, @@ -18,9 +18,7 @@ use procfs_core::{ FromRead, ProcError, }; use std::{ - collections::HashMap, ffi::c_void, - io::BufReader, path, result::Result, time::{Duration, Instant}, @@ -37,7 +35,7 @@ pub struct PtraceDumper { pub pid: Pid, threads_suspended: bool, pub threads: Vec, - pub auxv: HashMap, + pub auxv: AuxvDumpInfo, pub mappings: Vec, pub page_size: usize, } @@ -91,12 +89,12 @@ fn ptrace_detach(child: Pid) -> Result<(), DumperError> { impl PtraceDumper { /// Constructs a dumper for extracting information of a given process /// with a process ID of |pid|. - pub fn new(pid: Pid, stop_timeout: Duration) -> Result { + pub fn new(pid: Pid, stop_timeout: Duration, auxv: AuxvDumpInfo) -> Result { let mut dumper = PtraceDumper { pid, threads_suspended: false, threads: Vec::new(), - auxv: HashMap::new(), + auxv, mappings: Vec::new(), page_size: 0, }; @@ -110,7 +108,11 @@ impl PtraceDumper { if let Err(e) = self.stop_process(stop_timeout) { log::warn!("failed to stop process {}: {e}", self.pid); } - self.read_auxv()?; + + if let Err(e) = self.auxv.try_filling_missing_info(self.pid) { + log::warn!("failed trying to fill in missing auxv info: {e}"); + } + self.enumerate_threads()?; self.enumerate_mappings()?; self.page_size = nix::unistd::sysconf(nix::unistd::SysconfVar::PAGE_SIZE)? @@ -295,25 +297,6 @@ impl PtraceDumper { Ok(()) } - fn read_auxv(&mut self) -> Result<(), InitError> { - let filename = format!("/proc/{}/auxv", self.pid); - let auxv_path = path::PathBuf::from(&filename); - let auxv_file = - std::fs::File::open(auxv_path).map_err(|e| InitError::IOError(filename, e))?; - let input = BufReader::new(auxv_file); - let reader = ProcfsAuxvIter::new(input); - self.auxv = reader - .filter_map(Result::ok) - .map(|x| (x.key, x.value)) - .collect(); - - if self.auxv.is_empty() { - Err(InitError::NoAuxvEntryFound(self.pid)) - } else { - Ok(()) - } - } - fn enumerate_mappings(&mut self) -> Result<(), InitError> { // linux_gate_loc is the beginning of the kernel's mapping of // linux-gate.so in the process. It doesn't actually show up in the @@ -322,21 +305,11 @@ impl PtraceDumper { // case its entry when creating the list of mappings. // See http://www.trilithium.com/johan/2005/08/linux-gate/ for more // information. - let linux_gate_loc = *self.auxv.get(&AT_SYSINFO_EHDR).unwrap_or(&0); + let linux_gate_loc = self.auxv.get_linux_gate_address().unwrap_or_default(); // 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. - let at_entry; - #[cfg(any(target_arch = "arm", all(target_os = "android", target_arch = "x86")))] - { - at_entry = 9; - } - #[cfg(not(any(target_arch = "arm", all(target_os = "android", target_arch = "x86"))))] - { - at_entry = libc::AT_ENTRY; - } - - let entry_point_loc = *self.auxv.get(&at_entry).unwrap_or(&0); + let entry_point_loc = self.auxv.get_entry_address().unwrap_or_default(); let filename = format!("/proc/{}/maps", self.pid); let errmap = |e| InitError::IOError(filename.clone(), e); let maps_path = path::PathBuf::from(&filename); diff --git a/tests/ptrace_dumper.rs b/tests/ptrace_dumper.rs index 089d6542..dfc2976f 100644 --- a/tests/ptrace_dumper.rs +++ b/tests/ptrace_dumper.rs @@ -39,8 +39,12 @@ fn test_thread_list_from_parent() { let num_of_threads = 5; let mut child = start_child_and_wait_for_threads(num_of_threads); let pid = child.id() as i32; - let mut dumper = PtraceDumper::new(pid, minidump_writer::minidump_writer::STOP_TIMEOUT) - .expect("Couldn't init dumper"); + let mut dumper = PtraceDumper::new( + pid, + minidump_writer::minidump_writer::STOP_TIMEOUT, + Default::default(), + ) + .expect("Couldn't init dumper"); assert_eq!(dumper.threads.len(), num_of_threads); dumper.suspend_threads().expect("Could not suspend threads"); @@ -216,8 +220,12 @@ fn test_sanitize_stack_copy() { let heap_addr = usize::from_str_radix(output.next().unwrap().trim_start_matches("0x"), 16) .expect("unable to parse mmap_addr"); - let mut dumper = PtraceDumper::new(pid, minidump_writer::minidump_writer::STOP_TIMEOUT) - .expect("Couldn't init dumper"); + let mut dumper = PtraceDumper::new( + pid, + minidump_writer::minidump_writer::STOP_TIMEOUT, + Default::default(), + ) + .expect("Couldn't init dumper"); assert_eq!(dumper.threads.len(), num_of_threads); dumper.suspend_threads().expect("Could not suspend threads"); let thread_info = dumper