diff --git a/Cargo.lock b/Cargo.lock index f58e7f26..db2cc26a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,7 +181,7 @@ dependencies = [ "circular", "debugid", "futures-util", - "minidump-common", + "minidump-common 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "nom", "range-map", "thiserror", @@ -1191,7 +1191,7 @@ dependencies = [ "debugid", "encoding_rs", "memmap2", - "minidump-common", + "minidump-common 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits", "procfs-core", "range-map", @@ -1202,6 +1202,19 @@ dependencies = [ "uuid", ] +[[package]] +name = "minidump-common" +version = "0.22.0" +dependencies = [ + "bitflags 2.6.0", + "debugid", + "num-derive", + "num-traits", + "range-map", + "scroll 0.12.0", + "smart-default", +] + [[package]] name = "minidump-common" version = "0.22.0" @@ -1228,7 +1241,7 @@ dependencies = [ "debugid", "futures-util", "minidump", - "minidump-common", + "minidump-common 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "minidump-unwind", "scroll 0.12.0", "serde", @@ -1250,7 +1263,7 @@ dependencies = [ "futures-util", "memmap2", "minidump", - "minidump-common", + "minidump-common 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)", "object", "scroll 0.12.0", "tracing", @@ -1275,7 +1288,7 @@ dependencies = [ "memmap2", "memoffset", "minidump", - "minidump-common", + "minidump-common 0.22.0", "minidump-processor", "minidump-unwind", "nix", diff --git a/Cargo.toml b/Cargo.toml index f78be2a7..949f2f73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ cfg-if = "1.0" crash-context = "0.6" log = "0.4" memoffset = "0.9" -minidump-common = "0.22" +minidump-common = { path = "../rust-minidump/minidump-common" } scroll = "0.12" tempfile = "3.8" thiserror = "1.0" diff --git a/src/bin/test.rs b/src/bin/test.rs index 6713852c..64c96649 100644 --- a/src/bin/test.rs +++ b/src/bin/test.rs @@ -26,13 +26,15 @@ mod linux { fn test_setup() -> Result<()> { let ppid = getppid(); - PtraceDumper::new(ppid.as_raw(), STOP_TIMEOUT, Default::default())?; + let (_dumper, _soft_errors) = + PtraceDumper::new_report_soft_errors(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, Default::default())?; + let (dumper, _soft_errors) = + PtraceDumper::new_report_soft_errors(ppid.as_raw(), STOP_TIMEOUT, Default::default())?; test!(!dumper.threads.is_empty(), "No threads"); test!( dumper @@ -59,8 +61,12 @@ mod linux { use minidump_writer::mem_reader::MemReader; let ppid = getppid().as_raw(); - let mut dumper = PtraceDumper::new(ppid, STOP_TIMEOUT, Default::default())?; - dumper.suspend_threads()?; + let (mut dumper, _soft_errors) = + PtraceDumper::new_report_soft_errors(ppid, STOP_TIMEOUT, Default::default())?; + + if let Some(soft_errors) = dumper.suspend_threads().some() { + return Err(soft_errors.into()); + } // We support 3 different methods of reading memory from another // process, ensure they all function and give the same results @@ -113,13 +119,17 @@ mod linux { test!(heap_res == expected_heap, "heap var not correct"); - dumper.resume_threads()?; + if let Some(soft_errors) = dumper.resume_threads().some() { + return Err(soft_errors.into()); + } + Ok(()) } fn test_find_mappings(addr1: usize, addr2: usize) -> Result<()> { let ppid = getppid(); - let dumper = PtraceDumper::new(ppid.as_raw(), STOP_TIMEOUT, Default::default())?; + let (dumper, _soft_errors) = + PtraceDumper::new_report_soft_errors(ppid.as_raw(), STOP_TIMEOUT, Default::default())?; dumper .find_mapping(addr1) .ok_or("No mapping for addr1 found")?; @@ -136,8 +146,12 @@ mod linux { let ppid = getppid().as_raw(); let exe_link = format!("/proc/{ppid}/exe"); let exe_name = std::fs::read_link(exe_link)?.into_os_string(); - let mut dumper = PtraceDumper::new(ppid, STOP_TIMEOUT, Default::default())?; - dumper.suspend_threads()?; + let (mut dumper, _soft_errors) = + PtraceDumper::new_report_soft_errors(ppid, STOP_TIMEOUT, Default::default())?; + if let Some(soft_errors) = dumper.suspend_threads().some() { + return Err(soft_errors.into()); + } + let mut found_exe = None; for (idx, mapping) in dumper.mappings.iter().enumerate() { if mapping.name.as_ref().map(|x| x.into()).as_ref() == Some(&exe_name) { @@ -147,7 +161,9 @@ mod linux { } let idx = found_exe.unwrap(); let module_reader::BuildId(id) = dumper.from_process_memory_for_index(idx)?; - dumper.resume_threads()?; + if let Some(soft_errors) = dumper.resume_threads().some() { + return Err(soft_errors.into()); + } assert!(!id.is_empty()); assert!(id.iter().any(|&x| x > 0)); Ok(()) @@ -155,7 +171,11 @@ 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, Default::default())?; + let (dumper, _soft_errors) = PtraceDumper::new_report_soft_errors( + getppid().as_raw(), + STOP_TIMEOUT, + Default::default(), + )?; let mut mapping_count = 0; for map in &dumper.mappings { if map @@ -177,17 +197,22 @@ mod linux { fn test_linux_gate_mapping_id() -> Result<()> { let ppid = getppid().as_raw(); - let mut dumper = PtraceDumper::new(ppid, STOP_TIMEOUT, Default::default())?; + let (mut dumper, _soft_errors) = + PtraceDumper::new_report_soft_errors(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()) { found_linux_gate = true; - dumper.suspend_threads()?; + if let Some(soft_errors) = dumper.suspend_threads().some() { + return Err(soft_errors.into()); + } let module_reader::BuildId(id) = PtraceDumper::from_process_memory_for_mapping(&mapping, ppid)?; test!(!id.is_empty(), "id-vec is empty"); test!(id.iter().any(|&x| x > 0), "all id elements are 0"); - dumper.resume_threads()?; + if let Some(soft_errors) = dumper.resume_threads().some() { + return Err(soft_errors.into()); + } break; } } @@ -197,7 +222,8 @@ mod linux { fn test_mappings_include_linux_gate() -> Result<()> { let ppid = getppid().as_raw(); - let dumper = PtraceDumper::new(ppid, STOP_TIMEOUT, Default::default())?; + let (dumper, _soft_errors) = + PtraceDumper::new_report_soft_errors(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; diff --git a/src/error_list.rs b/src/error_list.rs new file mode 100644 index 00000000..d10f2426 --- /dev/null +++ b/src/error_list.rs @@ -0,0 +1,101 @@ +//! Handling of "soft errors" while generating the minidump + +/// Encapsulates a list of "soft error"s +/// +/// A "soft error" is an error that is encounted while generating the minidump that doesn't +/// totally prevent the minidump from being useful, but it may have missing or invalid +/// information. +/// +/// It should be returned by a function when the function was able to at-least partially achieve +/// its goals, and when further use of functions in the same API is permissible and can still be +/// at-least partially functional. +/// +/// Admittedly, this concept makes layers of abstraction a bit more difficult, as something that +/// is considered "soft" by one layer may be a deal-breaker for the layer above it, or visa-versa +/// -- an error that a lower layer considers a total failure might just be a nuissance for the layer +/// above it. +/// +/// An example of the former might be the act of suspending all the threads -- The `PTraceDumper`` +/// API will actually work just fine even if none of the threads are suspended, so it only returns +/// a soft error; however, the dumper itself considers it to be a critical failure if not even one +/// thread could be stopped. +/// +/// An example of the latter might trying to stop the process -- Being unable to send SIGSTOP to +/// the process would be considered a critical failure by `stop_process()`, but merely an +/// inconvenience by the code that's calling it. +#[must_use] +pub struct SoftErrorList { + errors: Vec, +} + +impl SoftErrorList { + /// Returns `Some(Self)` if the list contains at least one soft error + pub fn some(self) -> Option { + if !self.is_empty() { + Some(self) + } else { + None + } + } + /// Returns `true` if the list is empty + pub fn is_empty(&self) -> bool { + self.errors.is_empty() + } + /// Add a soft error to the list + pub fn push(&mut self, error: E) { + self.errors.push(error); + } +} + +impl Default for SoftErrorList { + fn default() -> Self { + Self { errors: Vec::new() } + } +} + +impl SoftErrorList { + // Helper function for the Debug and Display traits + fn fmt(&self, f: &mut std::fmt::Formatter<'_>, write_sources: bool) -> std::fmt::Result { + writeln!(f, "one or more soft errors occurred:")?; + writeln!(f)?; + for (i, e) in self.errors.iter().enumerate() { + writeln!(f, " {i}:")?; + + for line in e.to_string().lines() { + writeln!(f, " {line}")?; + } + + writeln!(f)?; + + if write_sources { + let mut source = e.source(); + while let Some(e) = source { + writeln!(f, " caused by:")?; + + for line in e.to_string().lines() { + writeln!(f, " {line}")?; + } + + writeln!(f)?; + + source = e.source(); + } + } + } + Ok(()) + } +} + +impl std::fmt::Debug for SoftErrorList { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.fmt(f, true) + } +} + +impl std::fmt::Display for SoftErrorList { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.fmt(f, false) + } +} + +impl std::error::Error for SoftErrorList {} diff --git a/src/lib.rs b/src/lib.rs index a76291d0..d397d25c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,3 +19,5 @@ pub mod minidump_format; pub mod dir_section; pub mod mem_writer; + +mod error_list; diff --git a/src/linux/auxv/mod.rs b/src/linux/auxv/mod.rs index 403ab114..8834f7fb 100644 --- a/src/linux/auxv/mod.rs +++ b/src/linux/auxv/mod.rs @@ -1,4 +1,6 @@ +use crate::error_list::SoftErrorList; pub use reader::ProcfsAuxvIter; + use { crate::Pid, std::{fs::File, io::BufReader}, @@ -79,17 +81,27 @@ pub struct AuxvDumpInfo { } impl AuxvDumpInfo { - pub fn try_filling_missing_info(&mut self, pid: Pid) -> Result<(), AuxvError> { + pub fn try_filling_missing_info( + &mut self, + pid: Pid, + ) -> Result, AuxvError> { if self.is_complete() { - return Ok(()); + return Ok(SoftErrorList::default()); } 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 mut soft_errors = SoftErrorList::default(); + + for pair_result in ProcfsAuxvIter::new(BufReader::new(auxv_file)) { + let AuxvPair { key, value } = match pair_result { + Ok(pair) => pair, + Err(e) => { + soft_errors.push(e); + continue; + } + }; let dest_field = match key { consts::AT_PHNUM => &mut self.program_header_count, consts::AT_PHDR => &mut self.program_header_address, @@ -102,7 +114,7 @@ impl AuxvDumpInfo { } } - Ok(()) + Ok(soft_errors) } pub fn get_program_header_count(&self) -> Option { self.program_header_count diff --git a/src/linux/errors.rs b/src/linux/errors.rs index 982445a0..9166cf1c 100644 --- a/src/linux/errors.rs +++ b/src/linux/errors.rs @@ -1,26 +1,12 @@ use crate::{ - dir_section::FileWriterError, maps_reader::MappingInfo, mem_writer::MemoryWriterError, Pid, + dir_section::FileWriterError, error_list::SoftErrorList, maps_reader::MappingInfo, + mem_writer::MemoryWriterError, Pid, }; use goblin; -use nix::errno::Errno; use std::ffi::OsString; use thiserror::Error; -#[derive(Debug, Error)] -pub enum InitError { - #[error("failed to read auxv")] - ReadAuxvFailed(crate::auxv::AuxvError), - #[error("IO error for file {0}")] - IOError(String, #[source] std::io::Error), - #[error("crash thread does not reference principal mapping")] - PrincipalMappingNotReferenced, - #[error("Failed Android specific late init")] - AndroidLateInitError(#[from] AndroidError), - #[error("Failed to read the page size")] - PageSizeError(#[from] Errno), - #[error("Ptrace does not function within the same process")] - CannotPtraceSameProcess, -} +use super::ptrace_dumper::InitError; #[derive(Error, Debug)] pub enum MapsReaderError { @@ -110,8 +96,6 @@ pub enum DumperError { CopyFromProcessError(#[from] CopyFromProcessError), #[error("Skipped thread {0} due to it being part of the seccomp sandbox's trusted code")] DetachSkippedThread(Pid), - #[error("No threads left to suspend out of {0}")] - SuspendNoThreadsLeft(usize), #[error("No mapping for stack pointer found")] NoStackPointerMapping, #[error("Failed slice conversion")] @@ -180,6 +164,8 @@ pub enum SectionSystemInfoError { MemoryWriterError(#[from] MemoryWriterError), #[error("Failed to get CPU Info")] CpuInfoError(#[from] CpuInfoError), + #[error("Failed trying to write CPU information")] + WriteCpuInformationFailed(#[source] CpuInfoError), } #[derive(Debug, Error)] @@ -250,6 +236,38 @@ pub enum WriterError { FileWriterError(#[from] FileWriterError), #[error("Failed to get current timestamp when writing header of minidump")] SystemTimeError(#[from] std::time::SystemTimeError), + #[error("Errors occurred while initializing PTraceDumper")] + InitErrors(#[source] SoftErrorList), + #[error("Errors occurred while suspending threads")] + SuspendThreadsErrors(#[source] SoftErrorList), + #[error("Crash thread does not reference principal mapping")] + PrincipalMappingNotReferenced, + #[error("Errors occurred while writing system info")] + WriteSystemInfoErrors(#[source] SoftErrorList), + #[error("Failed writing cpuinfo")] + WriteCpuInfoFailed(#[source] MemoryWriterError), + #[error("Failed writing thread proc status")] + WriteThreadProcStatusFailed(#[source] MemoryWriterError), + #[error("Failed writing OS Release Information")] + WriteOsReleaseInfoFailed(#[source] MemoryWriterError), + #[error("Failed writing process command line")] + WriteCommandLineFailed(#[source] MemoryWriterError), + #[error("Writing process environment failed")] + WriteEnvironmentFailed(#[source] MemoryWriterError), + #[error("Failed to write auxv file")] + WriteAuxvFailed(#[source] MemoryWriterError), + #[error("Failed to write maps file")] + WriteMapsFailed(#[source] MemoryWriterError), + #[error("Failed writing DSO Debug Stream")] + WriteDSODebugStreamFailed(#[source] SectionDsoDebugError), + #[error("Failed writing limits file")] + WriteLimitsFailed(#[source] MemoryWriterError), + #[error("Failed writing handle data stream")] + WriteHandleDataStreamFailed(#[source] SectionHandleDataStreamError), + #[error("Failed writing handle data stream direction entry")] + WriteHandleDataStreamDirentFailed(#[source] FileWriterError), + #[error("No threads left to suspend out of {0}")] + SuspendNoThreadsLeft(usize), } #[derive(Debug, Error)] diff --git a/src/linux/minidump_writer.rs b/src/linux/minidump_writer.rs index ba6b82ec..72d6800c 100644 --- a/src/linux/minidump_writer.rs +++ b/src/linux/minidump_writer.rs @@ -2,11 +2,12 @@ pub use crate::linux::auxv::{AuxvType, DirectAuxvDumpInfo}; use crate::{ auxv::AuxvDumpInfo, dir_section::{DirSection, DumpBuf}, + error_list::SoftErrorList, linux::{ app_memory::AppMemoryList, crash_context::CrashContext, dso_debug, - errors::{InitError, WriterError}, + errors::WriterError, maps_reader::{MappingInfo, MappingList}, ptrace_dumper::PtraceDumper, sections::*, @@ -144,8 +145,27 @@ impl MinidumpWriter { .clone() .map(AuxvDumpInfo::from) .unwrap_or_default(); - let mut dumper = PtraceDumper::new(self.process_id, self.stop_timeout, auxv)?; - dumper.suspend_threads()?; + + let mut soft_errors = SoftErrorList::default(); + + let (mut dumper, init_soft_errors) = + PtraceDumper::new_report_soft_errors(self.process_id, self.stop_timeout, auxv)?; + + if !init_soft_errors.is_empty() { + soft_errors.push(WriterError::InitErrors(init_soft_errors)); + } + + let threads_count = dumper.threads.len(); + + if let Some(suspend_soft_errors) = dumper.suspend_threads().some() { + if dumper.threads.is_empty() { + // TBH I'm not sure this even needs to be a hard error. Is a minidump without any + // thread info still at-least a little useful? + return Err(WriterError::SuspendNoThreadsLeft(threads_count)); + } + soft_errors.push(WriterError::SuspendThreadsErrors(suspend_soft_errors)); + } + dumper.late_init()?; if self.skip_stacks_if_mapping_unreferenced { @@ -154,16 +174,15 @@ impl MinidumpWriter { } if !self.crash_thread_references_principal_mapping(&dumper) { - return Err(InitError::PrincipalMappingNotReferenced.into()); + soft_errors.push(WriterError::PrincipalMappingNotReferenced); } } let mut buffer = Buffer::with_capacity(0); - self.generate_dump(&mut buffer, &mut dumper, destination)?; + self.generate_dump(&mut buffer, &mut dumper, soft_errors, destination)?; - // dumper would resume threads in drop() automatically, - // but in case there is an error, we want to catch it - dumper.resume_threads()?; + // TODO - Record these errors? Or maybe we don't care? + let _resume_soft_errors = dumper.resume_threads(); Ok(buffer.into()) } @@ -226,11 +245,12 @@ impl MinidumpWriter { &mut self, buffer: &mut DumpBuf, dumper: &mut PtraceDumper, + mut soft_errors: SoftErrorList, destination: &mut (impl Write + Seek), ) -> Result<()> { // A minidump file contains a number of tagged streams. This is the number // of streams which we write. - let num_writers = 17u32; + let num_writers = 19u32; let mut header_section = MemoryWriter::::alloc(buffer)?; @@ -270,9 +290,13 @@ impl MinidumpWriter { let dirent = exception_stream::write(self, buffer)?; dir_section.write_to_file(buffer, Some(dirent))?; - let dirent = systeminfo_stream::write(buffer)?; + let (dirent, systeminfo_soft_errors) = systeminfo_stream::write(buffer)?; dir_section.write_to_file(buffer, Some(dirent))?; + if !systeminfo_soft_errors.is_empty() { + soft_errors.push(WriterError::WriteSystemInfoErrors(systeminfo_soft_errors)); + } + let dirent = memory_info_list_stream::write(self, buffer)?; dir_section.write_to_file(buffer, Some(dirent))?; @@ -281,7 +305,10 @@ impl MinidumpWriter { stream_type: MDStreamType::LinuxCpuInfo as u32, location, }, - Err(_) => Default::default(), + Err(e) => { + soft_errors.push(WriterError::WriteCpuInfoFailed(e)); + Default::default() + } }; dir_section.write_to_file(buffer, Some(dirent))?; @@ -291,7 +318,10 @@ impl MinidumpWriter { stream_type: MDStreamType::LinuxProcStatus as u32, location, }, - Err(_) => Default::default(), + Err(e) => { + soft_errors.push(WriterError::WriteThreadProcStatusFailed(e)); + Default::default() + } }; dir_section.write_to_file(buffer, Some(dirent))?; @@ -303,7 +333,10 @@ impl MinidumpWriter { stream_type: MDStreamType::LinuxLsbRelease as u32, location, }, - Err(_) => Default::default(), + Err(e) => { + soft_errors.push(WriterError::WriteOsReleaseInfoFailed(e)); + Default::default() + } }; dir_section.write_to_file(buffer, Some(dirent))?; @@ -313,7 +346,10 @@ impl MinidumpWriter { stream_type: MDStreamType::LinuxCmdLine as u32, location, }, - Err(_) => Default::default(), + Err(e) => { + soft_errors.push(WriterError::WriteCommandLineFailed(e)); + Default::default() + } }; dir_section.write_to_file(buffer, Some(dirent))?; @@ -323,7 +359,10 @@ impl MinidumpWriter { stream_type: MDStreamType::LinuxEnviron as u32, location, }, - Err(_) => Default::default(), + Err(e) => { + soft_errors.push(WriterError::WriteEnvironmentFailed(e)); + Default::default() + } }; dir_section.write_to_file(buffer, Some(dirent))?; @@ -332,7 +371,10 @@ impl MinidumpWriter { stream_type: MDStreamType::LinuxAuxv as u32, location, }, - Err(_) => Default::default(), + Err(e) => { + soft_errors.push(WriterError::WriteAuxvFailed(e)); + Default::default() + } }; dir_section.write_to_file(buffer, Some(dirent))?; @@ -341,12 +383,21 @@ impl MinidumpWriter { stream_type: MDStreamType::LinuxMaps as u32, location, }, - Err(_) => Default::default(), + Err(e) => { + soft_errors.push(WriterError::WriteMapsFailed(e)); + Default::default() + } }; dir_section.write_to_file(buffer, Some(dirent))?; - let dirent = dso_debug::write_dso_debug_stream(buffer, self.process_id, &dumper.auxv) - .unwrap_or_default(); + let dirent = match dso_debug::write_dso_debug_stream(buffer, self.process_id, &dumper.auxv) + { + Ok(dirent) => dirent, + Err(e) => { + soft_errors.push(WriterError::WriteDSODebugStreamFailed(e)); + Default::default() + } + }; dir_section.write_to_file(buffer, Some(dirent))?; let dirent = match self.write_file(buffer, &format!("/proc/{}/limits", self.blamed_thread)) @@ -355,17 +406,33 @@ impl MinidumpWriter { stream_type: MDStreamType::MozLinuxLimits as u32, location, }, - Err(_) => Default::default(), + Err(e) => { + soft_errors.push(WriterError::WriteLimitsFailed(e)); + Default::default() + } }; dir_section.write_to_file(buffer, Some(dirent))?; let dirent = thread_names_stream::write(buffer, dumper)?; dir_section.write_to_file(buffer, Some(dirent))?; - // This section is optional, so we ignore errors when writing it - if let Ok(dirent) = handle_data_stream::write(self, buffer) { - let _ = dir_section.write_to_file(buffer, Some(dirent)); - } + let dirent = match handle_data_stream::write(self, buffer) { + Ok(dirent) => dirent, + Err(e) => { + soft_errors.push(WriterError::WriteHandleDataStreamFailed(e)); + Default::default() + } + }; + dir_section.write_to_file(buffer, Some(dirent))?; + + // If this fails, there's really nothing we can do about that (other than ignore it). + let dirent = write_soft_errors(buffer, soft_errors) + .map(|location| MDRawDirectory { + stream_type: MDStreamType::MozSoftErrors as u32, + location, + }) + .unwrap_or_default(); + dir_section.write_to_file(buffer, Some(dirent))?; // If you add more directory entries, don't forget to update num_writers, above. Ok(()) @@ -383,3 +450,13 @@ impl MinidumpWriter { Ok(section.location()) } } + +fn write_soft_errors( + buffer: &mut DumpBuf, + soft_errors: SoftErrorList, +) -> Result { + let soft_error_list_str = format!("{soft_errors:?}"); + + let section = MemoryArrayWriter::write_bytes(buffer, soft_error_list_str.as_bytes()); + Ok(section.location()) +} diff --git a/src/linux/ptrace_dumper.rs b/src/linux/ptrace_dumper.rs index 66f36564..28379063 100644 --- a/src/linux/ptrace_dumper.rs +++ b/src/linux/ptrace_dumper.rs @@ -1,15 +1,18 @@ #[cfg(target_os = "android")] use crate::linux::android::late_process_mappings; -use crate::linux::{ - auxv::AuxvDumpInfo, - errors::{DumperError, InitError, ThreadInfoError}, - maps_reader::MappingInfo, - module_reader, - thread_info::ThreadInfo, - Pid, -}; #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] use crate::thread_info; +use crate::{ + error_list::SoftErrorList, + linux::{ + auxv::AuxvDumpInfo, + errors::{DumperError, ThreadInfoError}, + maps_reader::MappingInfo, + module_reader, + thread_info::ThreadInfo, + Pid, + }, +}; use nix::{ errno::Errno, sys::{ptrace, signal, wait}, @@ -19,11 +22,17 @@ use procfs_core::{ FromRead, ProcError, }; use std::{ + ffi::OsString, path, result::Result, time::{Duration, Instant}, }; +use super::{ + auxv::AuxvError, + errors::{AndroidError, MapsReaderError}, +}; + #[derive(Debug, Clone)] pub struct Thread { pub tid: Pid, @@ -55,7 +64,45 @@ impl Drop for PtraceDumper { } #[derive(Debug, thiserror::Error)] -enum StopProcessError { +pub enum InitError { + #[error("failed to read auxv")] + ReadAuxvFailed(#[source] crate::auxv::AuxvError), + #[error("IO error for file {0}")] + IOError(String, #[source] std::io::Error), + #[error("Failed Android specific late init")] + AndroidLateInitError(#[from] AndroidError), + #[error("Failed to read the page size")] + PageSizeError(#[from] Errno), + #[error("Ptrace does not function within the same process")] + CannotPtraceSameProcess, + #[error("Failed to stop the target process")] + StopProcessFailed(#[source] StopProcessError), + #[error("Errors occurred while filling missing Auxv info")] + FillMissingAuxvInfoErrors(#[source] SoftErrorList), + #[error("Failed filling missing Auxv info")] + FillMissingAuxvInfoFailed(#[source] AuxvError), + #[error("Failed reading proc/pid/task entry for process")] + ReadProcessThreadEntryFailed(#[source] std::io::Error), + #[error("Process task entry `{0:?}` could not be parsed as a TID")] + ProcessTaskEntryNotTid(OsString), + #[error("Failed to read thread name")] + ReadThreadNameFailed(#[source] std::io::Error), + #[error("Proc task directory `{0:?}` is not a directory")] + ProcPidTaskNotDirectory(String), + #[error("Errors while enumerating threads")] + EnumerateThreadsErrors(#[source] SoftErrorList), + #[error("Failed to enumerate threads")] + EnumerateThreadsFailed(#[source] Box), + #[error("Failed to read process map file")] + ReadProcessMapFileFailed(#[source] ProcError), + #[error("Failed to aggregate process mappings")] + AggregateMappingsFailed(#[source] MapsReaderError), + #[error("Failed to enumerate process mappings")] + EnumerateMappingsFailed(#[source] Box), +} + +#[derive(Debug, thiserror::Error)] +pub enum StopProcessError { #[error("Failed to stop the process")] Stop(#[from] Errno), #[error("Failed to get the process state")] @@ -65,7 +112,7 @@ enum StopProcessError { } #[derive(Debug, thiserror::Error)] -enum ContinueProcessError { +pub enum ContinueProcessError { #[error("Failed to continue the process")] Continue(#[from] Errno), } @@ -88,7 +135,11 @@ fn ptrace_detach(child: Pid) -> Result<(), DumperError> { impl PtraceDumper { /// Constructs a dumper for extracting information from the specified process id - pub fn new(pid: Pid, stop_timeout: Duration, auxv: AuxvDumpInfo) -> Result { + pub fn new_report_soft_errors( + pid: Pid, + stop_timeout: Duration, + auxv: AuxvDumpInfo, + ) -> Result<(Self, SoftErrorList), InitError> { if pid == std::process::id() as _ { return Err(InitError::CannotPtraceSameProcess); } @@ -101,28 +152,56 @@ impl PtraceDumper { mappings: Vec::new(), page_size: 0, }; - dumper.init(stop_timeout)?; - Ok(dumper) + let soft_errors = dumper.init(stop_timeout)?; + Ok((dumper, soft_errors)) } // TODO: late_init for chromeos and android - pub fn init(&mut self, stop_timeout: Duration) -> Result<(), InitError> { + pub fn init(&mut self, stop_timeout: Duration) -> Result, InitError> { + let mut soft_errors = SoftErrorList::default(); + // Stopping the process is best-effort. if let Err(e) = self.stop_process(stop_timeout) { - log::warn!("failed to stop process {}: {e}", self.pid); + soft_errors.push(InitError::StopProcessFailed(e)); } - if let Err(e) = self.auxv.try_filling_missing_info(self.pid) { - log::warn!("failed trying to fill in missing auxv info: {e}"); + // Even if we completely fail to fill in any additional Auxv info, we can still press + // forward. + match self.auxv.try_filling_missing_info(self.pid) { + Ok(auxv_soft_errors) if !auxv_soft_errors.is_empty() => { + soft_errors.push(InitError::FillMissingAuxvInfoErrors(auxv_soft_errors)); + } + Err(e) => { + soft_errors.push(InitError::FillMissingAuxvInfoFailed(e)); + } + _ => (), + } + + // If we completely fail to enumerate any threads... Some information is still better than + // no information! + match self.enumerate_threads() { + Ok(enumerate_soft_errors) if !enumerate_soft_errors.is_empty() => { + soft_errors.push(InitError::EnumerateThreadsErrors(enumerate_soft_errors)); + } + Err(e) => { + soft_errors.push(InitError::EnumerateThreadsFailed(Box::new(e))); + } + _ => (), + } + + // Same with mappings -- Some information is still better than no information! + match self.enumerate_mappings() { + Ok(()) => (), + Err(e) => { + soft_errors.push(InitError::EnumerateMappingsFailed(Box::new(e))); + } } - self.enumerate_threads()?; - self.enumerate_mappings()?; self.page_size = nix::unistd::sysconf(nix::unistd::SysconfVar::PAGE_SIZE)? .expect("page size apparently unlimited: doesn't make sense.") as usize; - Ok(()) + Ok(soft_errors) } #[cfg_attr(not(target_os = "android"), allow(clippy::unused_self))] @@ -207,36 +286,39 @@ impl PtraceDumper { ptrace_detach(child) } - pub fn suspend_threads(&mut self) -> Result<(), DumperError> { - let threads_count = self.threads.len(); + pub fn suspend_threads(&mut self) -> SoftErrorList { + let mut soft_errors = SoftErrorList::default(); + // Iterate over all threads and try to suspend them. // If the thread either disappeared before we could attach to it, or if // it was part of the seccomp sandbox's trusted code, it is OK to // silently drop it from the minidump. - self.threads.retain(|x| Self::suspend_thread(x.tid).is_ok()); + self.threads.retain(|x| match Self::suspend_thread(x.tid) { + Ok(()) => true, + Err(e) => { + soft_errors.push(e); + false + } + }); - if self.threads.is_empty() { - Err(DumperError::SuspendNoThreadsLeft(threads_count)) - } else { - self.threads_suspended = true; - Ok(()) - } + self.threads_suspended = true; + soft_errors } - pub fn resume_threads(&mut self) -> Result<(), DumperError> { - let mut result = Ok(()); + pub fn resume_threads(&mut self) -> SoftErrorList { + let mut soft_errors = SoftErrorList::default(); if self.threads_suspended { for thread in &self.threads { match Self::resume_thread(thread.tid) { - Ok(_) => {} - x => { - result = x; + Ok(()) => (), + Err(e) => { + soft_errors.push(e); } } } } self.threads_suspended = false; - result + soft_errors } /// Send SIGSTOP to the process so that we can get a consistent state. @@ -273,31 +355,46 @@ impl PtraceDumper { /// Parse /proc/$pid/task to list all the threads of the process identified by /// pid. - fn enumerate_threads(&mut self) -> Result<(), InitError> { + fn enumerate_threads(&mut self) -> Result, InitError> { + let mut soft_errors = SoftErrorList::default(); + let pid = self.pid; let filename = format!("/proc/{}/task", pid); let task_path = path::PathBuf::from(&filename); - if task_path.is_dir() { - std::fs::read_dir(task_path) - .map_err(|e| InitError::IOError(filename, e))? - .filter_map(|entry| entry.ok()) // Filter out bad entries - .filter_map(|entry| { - entry - .file_name() // Parse name to Pid, filter out those that are unparsable - .to_str() - .and_then(|name| name.parse::().ok()) - }) - .map(|tid| { - // Read the thread-name (if there is any) - let name = std::fs::read_to_string(format!("/proc/{}/task/{}/comm", pid, tid)) - // NOTE: This is a bit wasteful as it does two allocations in order to trim, but leaving it for now - .map(|s| s.trim_end().to_string()) - .ok(); - (tid, name) - }) - .for_each(|(tid, name)| self.threads.push(Thread { tid, name })); + if !task_path.is_dir() { + return Err(InitError::ProcPidTaskNotDirectory(filename)); } - Ok(()) + + for entry in std::fs::read_dir(task_path).map_err(|e| InitError::IOError(filename, e))? { + let entry = match entry { + Ok(entry) => entry, + Err(e) => { + soft_errors.push(InitError::ReadProcessThreadEntryFailed(e)); + continue; + } + }; + let file_name = entry.file_name(); + let tid = match file_name.to_str().and_then(|name| name.parse::().ok()) { + Some(tid) => tid, + None => { + soft_errors.push(InitError::ProcessTaskEntryNotTid(file_name)); + continue; + } + }; + + // Read the thread-name (if there is any) + let name = match std::fs::read_to_string(format!("/proc/{}/task/{}/comm", pid, tid)) { + Ok(name) => Some(name.trim_end().to_string()), + Err(e) => { + soft_errors.push(InitError::ReadThreadNameFailed(e)); + None + } + }; + + self.threads.push(Thread { tid, name }); + } + + Ok(soft_errors) } fn enumerate_mappings(&mut self) -> Result<(), InitError> { @@ -309,22 +406,21 @@ impl PtraceDumper { // See http://www.trilithium.com/johan/2005/08/linux-gate/ for more // information. 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 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); - let maps_file = std::fs::File::open(maps_path).map_err(errmap)?; + let maps_path = format!("/proc/{}/maps", self.pid); + let maps_file = + std::fs::File::open(&maps_path).map_err(|e| InitError::IOError(maps_path, e))?; use procfs_core::FromRead; - self.mappings = procfs_core::process::MemoryMaps::from_read(maps_file) - .ok() - .and_then(|maps| MappingInfo::aggregate(maps, linux_gate_loc).ok()) - .unwrap_or_default(); + let maps = procfs_core::process::MemoryMaps::from_read(maps_file) + .map_err(InitError::ReadProcessMapFileFailed)?; + + self.mappings = MappingInfo::aggregate(maps, linux_gate_loc) + .map_err(InitError::AggregateMappingsFailed)?; - if entry_point_loc != 0 { + // 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() { let mut swap_idx = None; for (idx, module) in self.mappings.iter().enumerate() { // If this module contains the entry-point, and it's not already the first diff --git a/src/linux/sections/systeminfo_stream.rs b/src/linux/sections/systeminfo_stream.rs index a298c00d..5e48b170 100644 --- a/src/linux/sections/systeminfo_stream.rs +++ b/src/linux/sections/systeminfo_stream.rs @@ -1,7 +1,10 @@ use super::*; -use crate::linux::dumper_cpu_info as dci; +use crate::{error_list::SoftErrorList, linux::dumper_cpu_info as dci}; +use errors::SectionSystemInfoError; -pub fn write(buffer: &mut DumpBuf) -> Result { +pub fn write( + buffer: &mut DumpBuf, +) -> Result<(MDRawDirectory, SoftErrorList), SectionSystemInfoError> { let mut info_section = MemoryWriter::::alloc(buffer)?; let dirent = MDRawDirectory { stream_type: MDStreamType::SystemInfoStream as u32, @@ -16,8 +19,11 @@ pub fn write(buffer: &mut DumpBuf) -> Result()], defaced); - dumper.resume_threads().expect("Failed to resume threads"); + assert!( + dumper.resume_threads().is_empty(), + "Failed to resume threads" + ); child.kill().expect("Failed to kill process"); // Reap child