diff --git a/Cargo.lock b/Cargo.lock index 4f8719c7..04757f8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -160,6 +160,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] [[package]] name = "block-buffer" @@ -600,6 +603,24 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "error-graph" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b920e777967421aa5f9bf34f842c0ab6ba19b3bdb4a082946093860f5858879" +dependencies = [ + "serde", +] + +[[package]] +name = "failspot" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c942e64b20ecd39933d5ff938ca4fdb6ef0d298cc3855b231179a5ef0b24948d" +dependencies = [ + "flagset", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -618,6 +639,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "flagset" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3ea1ec5f8307826a5b71094dd91fc04d4ae75d5709b20ad351c7fb4815c86ec" + [[package]] name = "flate2" version = "1.0.35" @@ -1335,7 +1362,7 @@ dependencies = [ "memmap2", "minidump-common", "num-traits", - "procfs-core 0.17.0", + "procfs-core", "range-map", "scroll 0.12.0", "thiserror 1.0.69", @@ -1389,6 +1416,8 @@ dependencies = [ "crash-context", "current_platform", "dump_syms", + "error-graph", + "failspot", "futures", "goblin", "libc", @@ -1400,8 +1429,10 @@ dependencies = [ "minidump-common", "minidump-unwind", "nix", - "procfs-core 0.16.0", + "procfs-core", "scroll 0.12.0", + "serde", + "serde_json", "similar-asserts", "tempfile", "thiserror 1.0.69", @@ -1698,16 +1729,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "procfs-core" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" -dependencies = [ - "bitflags 2.6.0", - "hex", -] - [[package]] name = "procfs-core" version = "0.17.0" @@ -1716,6 +1737,7 @@ checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" dependencies = [ "bitflags 2.6.0", "hex", + "serde", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ae3afe34..e87d1caa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,14 @@ bitflags = "2.4" byteorder = "1.4" cfg-if = "1.0" crash-context = "0.6" +error-graph = { version = "0.1.1", features = ["serde"] } +failspot = "0.2.0" log = "0.4" memoffset = "0.9" minidump-common = "0.24" scroll = "0.12" +serde = { version = "1.0.208", features = ["derive"] } +serde_json = "1.0.116" tempfile = "3.8" thiserror = "1.0" @@ -36,7 +40,7 @@ nix = { version = "0.29", default-features = false, features = [ ] } # Used for parsing procfs info. # default-features is disabled since it pulls in chrono -procfs-core = { version = "0.16", default-features = false } +procfs-core = { version = "0.17", default-features = false, features = ["serde1"] } [target.'cfg(target_os = "windows")'.dependencies] bitflags = "2.4" @@ -49,6 +53,7 @@ mach2 = "0.4" # We auto-detect what the test binary that is spawned for most tests should be # compiled for current_platform = "0.2" +failspot = { version = "0.2.0", features = ["enabled"] } # Minidump-processor is async so we need an executor futures = { version = "0.3", features = ["executor"] } minidump = "0.24" diff --git a/src/bin/test.rs b/src/bin/test.rs index 6713852c..9ab42f13 100644 --- a/src/bin/test.rs +++ b/src/bin/test.rs @@ -6,14 +6,17 @@ pub type Result = std::result::Result; #[cfg(any(target_os = "linux", target_os = "android"))] mod linux { - use super::*; - use minidump_writer::{ - minidump_writer::STOP_TIMEOUT, module_reader, ptrace_dumper::PtraceDumper, - LINUX_GATE_LIBRARY_NAME, - }; - use nix::{ - sys::mman::{mmap_anonymous, MapFlags, ProtFlags}, - unistd::getppid, + use { + super::*, + error_graph::ErrorList, + minidump_writer::{ + minidump_writer::STOP_TIMEOUT, module_reader, ptrace_dumper::PtraceDumper, + LINUX_GATE_LIBRARY_NAME, + }, + nix::{ + sys::mman::{mmap_anonymous, MapFlags, ProtFlags}, + unistd::getppid, + }, }; macro_rules! test { @@ -24,15 +27,40 @@ mod linux { }; } + macro_rules! fail_on_soft_error(($n: ident, $e: expr) => {{ + let mut $n = ErrorList::default(); + let __result = $e; + if !$n.is_empty() { + return Err($n.into()); + } + __result + }}); + fn test_setup() -> Result<()> { let ppid = getppid(); - PtraceDumper::new(ppid.as_raw(), STOP_TIMEOUT, Default::default())?; + fail_on_soft_error!( + soft_errors, + PtraceDumper::new_report_soft_errors( + ppid.as_raw(), + STOP_TIMEOUT, + Default::default(), + &mut soft_errors, + )? + ); Ok(()) } fn test_thread_list() -> Result<()> { let ppid = getppid(); - let dumper = PtraceDumper::new(ppid.as_raw(), STOP_TIMEOUT, Default::default())?; + let dumper = fail_on_soft_error!( + soft_errors, + PtraceDumper::new_report_soft_errors( + ppid.as_raw(), + STOP_TIMEOUT, + Default::default(), + &mut soft_errors, + )? + ); test!(!dumper.threads.is_empty(), "No threads"); test!( dumper @@ -59,8 +87,17 @@ 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 = fail_on_soft_error!( + soft_errors, + PtraceDumper::new_report_soft_errors( + ppid, + STOP_TIMEOUT, + Default::default(), + &mut soft_errors + )? + ); + + fail_on_soft_error!(soft_errors, dumper.suspend_threads(&mut soft_errors)); // We support 3 different methods of reading memory from another // process, ensure they all function and give the same results @@ -113,13 +150,22 @@ mod linux { test!(heap_res == expected_heap, "heap var not correct"); - dumper.resume_threads()?; + fail_on_soft_error!(soft_errors, dumper.resume_threads(&mut soft_errors)); + 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 = fail_on_soft_error!( + soft_errors, + PtraceDumper::new_report_soft_errors( + ppid.as_raw(), + STOP_TIMEOUT, + Default::default(), + &mut soft_errors, + )? + ); dumper .find_mapping(addr1) .ok_or("No mapping for addr1 found")?; @@ -136,8 +182,19 @@ 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 = fail_on_soft_error!( + soft_errors, + PtraceDumper::new_report_soft_errors( + ppid, + STOP_TIMEOUT, + Default::default(), + &mut soft_errors + )? + ); + + fail_on_soft_error!(soft_errors, dumper.suspend_threads(&mut soft_errors)); + 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 +204,9 @@ mod linux { } let idx = found_exe.unwrap(); let module_reader::BuildId(id) = dumper.from_process_memory_for_index(idx)?; - dumper.resume_threads()?; + + fail_on_soft_error!(soft_errors, dumper.resume_threads(&mut soft_errors)); + assert!(!id.is_empty()); assert!(id.iter().any(|&x| x > 0)); Ok(()) @@ -155,13 +214,21 @@ 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 = fail_on_soft_error!( + soft_errors, + PtraceDumper::new_report_soft_errors( + getppid().as_raw(), + STOP_TIMEOUT, + Default::default(), + &mut soft_errors, + )? + ); let mut mapping_count = 0; for map in &dumper.mappings { if map .name .as_ref() - .map_or(false, |name| name.to_string_lossy().starts_with(&path)) + .is_some_and(|name| name.to_string_lossy().starts_with(&path)) { mapping_count += 1; // This mapping should encompass the entire original mapped @@ -177,17 +244,29 @@ 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 = fail_on_soft_error!( + soft_errors, + PtraceDumper::new_report_soft_errors( + ppid, + STOP_TIMEOUT, + Default::default(), + &mut soft_errors + )? + ); 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()?; + + fail_on_soft_error!(soft_errors, dumper.suspend_threads(&mut soft_errors)); + 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()?; + + fail_on_soft_error!(soft_errors, dumper.resume_threads(&mut soft_errors)); + break; } } @@ -197,7 +276,15 @@ 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 = fail_on_soft_error!( + soft_errors, + PtraceDumper::new_report_soft_errors( + ppid, + STOP_TIMEOUT, + Default::default(), + &mut soft_errors + )? + ); 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; @@ -205,7 +292,7 @@ mod linux { if mapping.name == Some(LINUX_GATE_LIBRARY_NAME.into()) { found_linux_gate = true; test!( - linux_gate_loc == mapping.start_address.try_into()?, + usize::try_from(linux_gate_loc)? == mapping.start_address, "linux_gate_loc != start_address" ); diff --git a/src/dir_section.rs b/src/dir_section.rs index ced5a2d1..2aaed65a 100644 --- a/src/dir_section.rs +++ b/src/dir_section.rs @@ -1,15 +1,22 @@ -use crate::{ - mem_writer::{Buffer, MemoryArrayWriter, MemoryWriterError}, - minidump_format::MDRawDirectory, +use { + crate::{ + mem_writer::{Buffer, MemoryArrayWriter, MemoryWriterError}, + minidump_format::MDRawDirectory, + serializers::*, + }, + std::io::{Error, Seek, Write}, }; -use std::io::{Error, Seek, Write}; pub type DumpBuf = Buffer; -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, serde::Serialize)] pub enum FileWriterError { #[error("IO error")] - IOError(#[from] Error), + IOError( + #[from] + #[serde(serialize_with = "serialize_io_error")] + Error, + ), #[error("Failed to write to memory")] MemoryWriterError(#[from] MemoryWriterError), } diff --git a/src/lib.rs b/src/lib.rs index a76291d0..67181b4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,8 +14,19 @@ cfg_if::cfg_if! { } } +pub mod dir_section; +pub mod mem_writer; pub mod minidump_cpu; pub mod minidump_format; -pub mod dir_section; -pub mod mem_writer; +mod serializers; + +failspot::failspot_name! { + pub enum FailSpotName { + StopProcess, + FillMissingAuxvInfo, + ThreadName, + SuspendThreads, + CpuInfoFileOpen, + } +} diff --git a/src/linux.rs b/src/linux.rs index febdeabe..dc605084 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -15,6 +15,7 @@ pub mod minidump_writer; pub mod module_reader; pub mod ptrace_dumper; pub(crate) mod sections; +mod serializers; pub mod thread_info; pub use maps_reader::LINUX_GATE_LIBRARY_NAME; diff --git a/src/linux/auxv/mod.rs b/src/linux/auxv/mod.rs index 403ab114..527fc45b 100644 --- a/src/linux/auxv/mod.rs +++ b/src/linux/auxv/mod.rs @@ -1,6 +1,8 @@ -pub use reader::ProcfsAuxvIter; use { - crate::Pid, + self::reader::ProcfsAuxvIter, + crate::{serializers::*, Pid}, + error_graph::WriteErrorList, + failspot::failspot, std::{fs::File, io::BufReader}, thiserror::Error, }; @@ -79,7 +81,11 @@ 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, + mut soft_errors: impl WriteErrorList, + ) -> Result<(), AuxvError> { if self.is_complete() { return Ok(()); } @@ -87,9 +93,14 @@ impl AuxvDumpInfo { 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) - { + 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,6 +113,8 @@ impl AuxvDumpInfo { } } + failspot!(FillMissingAuxvInfo soft_errors.push(AuxvError::InvalidFormat)); + Ok(()) } pub fn get_program_header_count(&self) -> Option { @@ -124,14 +137,23 @@ impl AuxvDumpInfo { } } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] pub enum AuxvError { #[error("Failed to open file {0}")] - OpenError(String, #[source] std::io::Error), + OpenError( + String, + #[source] + #[serde(serialize_with = "serialize_io_error")] + 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), + IOError( + #[from] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), } diff --git a/src/linux/dumper_cpu_info/x86_mips.rs b/src/linux/dumper_cpu_info/x86_mips.rs index 0c03d8e5..025a97be 100644 --- a/src/linux/dumper_cpu_info/x86_mips.rs +++ b/src/linux/dumper_cpu_info/x86_mips.rs @@ -1,7 +1,11 @@ -use crate::errors::CpuInfoError; -use crate::minidump_format::*; -use std::io::{BufRead, BufReader}; -use std::path; +use { + crate::{errors::CpuInfoError, minidump_format::*}, + failspot::failspot, + std::{ + io::{BufRead, BufReader}, + path, + }, +}; type Result = std::result::Result; @@ -44,6 +48,11 @@ pub fn write_cpu_information(sys_info: &mut MDRawSystemInfo) -> Result<()> { MDCPUArchitecture::PROCESSOR_ARCHITECTURE_AMD64 } as u16; + failspot!( + CpuInfoFileOpen + bail(std::io::Error::other("test requested cpuinfo file failure")) + ); + let cpuinfo_file = std::fs::File::open(path::PathBuf::from("/proc/cpuinfo"))?; let mut vendor_id = String::new(); diff --git a/src/linux/errors.rs b/src/linux/errors.rs index 982445a0..95322f10 100644 --- a/src/linux/errors.rs +++ b/src/linux/errors.rs @@ -1,31 +1,22 @@ -use crate::{ - dir_section::FileWriterError, maps_reader::MappingInfo, mem_writer::MemoryWriterError, Pid, +use { + super::{ptrace_dumper::InitError, serializers::*}, + crate::{ + dir_section::FileWriterError, maps_reader::MappingInfo, mem_writer::MemoryWriterError, + serializers::*, Pid, + }, + error_graph::ErrorList, + std::ffi::OsString, + thiserror::Error, }; -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, -} -#[derive(Error, Debug)] +#[derive(Error, Debug, serde::Serialize)] pub enum MapsReaderError { #[error("Couldn't parse as ELF file")] - ELFParsingFailed(#[from] goblin::error::Error), + ELFParsingFailed( + #[from] + #[serde(serialize_with = "serialize_goblin_error")] + goblin::error::Error, + ), #[error("No soname found (filename: {})", .0.to_string_lossy())] NoSoName(OsString, #[source] ModuleReaderError), @@ -33,102 +24,168 @@ pub enum MapsReaderError { #[error("Map entry malformed: No {0} found")] MapEntryMalformed(&'static str), #[error("Couldn't parse address")] - UnparsableInteger(#[from] std::num::ParseIntError), + UnparsableInteger( + #[from] + #[serde(skip)] + std::num::ParseIntError, + ), #[error("Linux gate location doesn't fit in the required integer type")] - LinuxGateNotConvertable(#[from] std::num::TryFromIntError), + LinuxGateNotConvertable( + #[from] + #[serde(skip)] + std::num::TryFromIntError, + ), // get_mmap() #[error("Not safe to open mapping {}", .0.to_string_lossy())] NotSafeToOpenMapping(OsString), #[error("IO Error")] - FileError(#[from] std::io::Error), + FileError( + #[from] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), #[error("Mmapped file empty or not an ELF file")] MmapSanityCheckFailed, #[error("Symlink does not match ({0} vs. {1})")] SymlinkError(std::path::PathBuf, std::path::PathBuf), } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] pub enum CpuInfoError { #[error("IO error for file /proc/cpuinfo")] - IOError(#[from] std::io::Error), + IOError( + #[from] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), #[error("Not all entries of /proc/cpuinfo found!")] NotAllProcEntriesFound, #[error("Couldn't parse core from file")] - UnparsableInteger(#[from] std::num::ParseIntError), + UnparsableInteger( + #[from] + #[serde(skip)] + std::num::ParseIntError, + ), #[error("Couldn't parse cores: {0}")] UnparsableCores(String), } -#[derive(Error, Debug)] +#[derive(Error, Debug, serde::Serialize)] pub enum ThreadInfoError { #[error("Index out of bounds: Got {0}, only have {1}")] IndexOutOfBounds(usize, usize), #[error("Either ppid ({1}) or tgid ({2}) not found in {0}")] InvalidPid(String, Pid, Pid), #[error("IO error")] - IOError(#[from] std::io::Error), + IOError( + #[from] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), #[error("Couldn't parse address")] - UnparsableInteger(#[from] std::num::ParseIntError), + UnparsableInteger( + #[from] + #[serde(skip)] + std::num::ParseIntError, + ), #[error("nix::ptrace() error")] - PtraceError(#[from] nix::Error), + PtraceError( + #[from] + #[serde(serialize_with = "serialize_nix_error")] + nix::Error, + ), #[error("Invalid line in /proc/{0}/status: {1}")] InvalidProcStatusFile(Pid, String), } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] pub enum AndroidError { #[error("Failed to copy memory from process")] CopyFromProcessError(#[from] DumperError), #[error("Failed slice conversion")] - TryFromSliceError(#[from] std::array::TryFromSliceError), + TryFromSliceError( + #[from] + #[serde(skip)] + std::array::TryFromSliceError, + ), #[error("No Android rel found")] NoRelFound, } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] #[error("Copy from process {child} failed (source {src}, offset: {offset}, length: {length})")] pub struct CopyFromProcessError { pub child: Pid, pub src: usize, pub offset: usize, pub length: usize, + #[serde(serialize_with = "serialize_nix_error")] pub source: nix::Error, } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] pub enum DumperError { #[error("Failed to get PAGE_SIZE from system")] - SysConfError(#[from] nix::Error), + SysConfError( + #[from] + #[serde(serialize_with = "serialize_nix_error")] + nix::Error, + ), #[error("wait::waitpid(Pid={0}) failed")] - WaitPidError(Pid, #[source] nix::Error), + WaitPidError( + Pid, + #[source] + #[serde(serialize_with = "serialize_nix_error")] + nix::Error, + ), #[error("nix::ptrace::attach(Pid={0}) failed")] - PtraceAttachError(Pid, #[source] nix::Error), + PtraceAttachError( + Pid, + #[source] + #[serde(serialize_with = "serialize_nix_error")] + nix::Error, + ), #[error("nix::ptrace::detach(Pid={0}) failed")] - PtraceDetachError(Pid, #[source] nix::Error), + PtraceDetachError( + Pid, + #[source] + #[serde(serialize_with = "serialize_nix_error")] + nix::Error, + ), #[error(transparent)] 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")] - TryFromSliceError(#[from] std::array::TryFromSliceError), + TryFromSliceError( + #[from] + #[serde(skip)] + std::array::TryFromSliceError, + ), #[error("Couldn't parse as ELF file")] - ELFParsingFailed(#[from] goblin::error::Error), + ELFParsingFailed( + #[from] + #[serde(serialize_with = "serialize_goblin_error")] + goblin::error::Error, + ), #[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")] - TryFromIntError(#[from] std::num::TryFromIntError), + TryFromIntError( + #[from] + #[serde(skip)] + std::num::TryFromIntError, + ), #[error("Maps reader error")] MapsReaderError(#[from] MapsReaderError), } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] pub enum SectionAppMemoryError { #[error("Failed to copy memory from process")] CopyFromProcessError(#[from] DumperError), @@ -136,23 +193,31 @@ pub enum SectionAppMemoryError { MemoryWriterError(#[from] MemoryWriterError), } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] pub enum SectionExceptionStreamError { #[error("Failed to write to memory")] MemoryWriterError(#[from] MemoryWriterError), } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] pub enum SectionHandleDataStreamError { #[error("Failed to access file")] - IOError(#[from] std::io::Error), + IOError( + #[from] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), #[error("Failed to write to memory")] MemoryWriterError(#[from] MemoryWriterError), #[error("Failed integer conversion")] - TryFromIntError(#[from] std::num::TryFromIntError), + TryFromIntError( + #[from] + #[serde(skip)] + std::num::TryFromIntError, + ), } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] pub enum SectionMappingsError { #[error("Failed to write to memory")] MemoryWriterError(#[from] MemoryWriterError), @@ -160,53 +225,75 @@ pub enum SectionMappingsError { GetEffectivePathError(MappingInfo, #[source] MapsReaderError), } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] pub enum SectionMemInfoListError { #[error("Failed to write to memory")] MemoryWriterError(#[from] MemoryWriterError), #[error("Failed to read from procfs")] - ProcfsError(#[from] procfs_core::ProcError), + ProcfsError( + #[from] + #[serde(serialize_with = "serialize_proc_error")] + procfs_core::ProcError, + ), } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] pub enum SectionMemListError { #[error("Failed to write to memory")] MemoryWriterError(#[from] MemoryWriterError), } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] pub enum SectionSystemInfoError { #[error("Failed to write to memory")] 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)] +#[derive(Debug, Error, serde::Serialize)] pub enum SectionThreadListError { #[error("Failed to write to memory")] MemoryWriterError(#[from] MemoryWriterError), #[error("Failed integer conversion")] - TryFromIntError(#[from] std::num::TryFromIntError), + TryFromIntError( + #[from] + #[serde(skip)] + std::num::TryFromIntError, + ), #[error("Failed to copy memory from process")] CopyFromProcessError(#[from] DumperError), #[error("Failed to get thread info")] ThreadInfoError(#[from] ThreadInfoError), #[error("Failed to write to memory buffer")] - IOError(#[from] std::io::Error), + IOError( + #[from] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] pub enum SectionThreadNamesError { #[error("Failed integer conversion")] - TryFromIntError(#[from] std::num::TryFromIntError), + TryFromIntError( + #[from] + #[serde(skip)] + std::num::TryFromIntError, + ), #[error("Failed to write to memory")] MemoryWriterError(#[from] MemoryWriterError), #[error("Failed to write to memory buffer")] - IOError(#[from] std::io::Error), + IOError( + #[from] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] pub enum SectionDsoDebugError { #[error("Failed to write to memory")] MemoryWriterError(#[from] MemoryWriterError), @@ -215,10 +302,14 @@ pub enum SectionDsoDebugError { #[error("Failed to copy memory from process")] CopyFromProcessError(#[from] DumperError), #[error("Failed to copy memory from process")] - FromUTF8Error(#[from] std::string::FromUtf8Error), + FromUTF8Error( + #[from] + #[serde(serialize_with = "serialize_from_utf8_error")] + std::string::FromUtf8Error, + ), } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] pub enum WriterError { #[error("Error during init phase")] InitError(#[from] InitError), @@ -249,15 +340,60 @@ pub enum WriterError { #[error("Failed to write to file")] FileWriterError(#[from] FileWriterError), #[error("Failed to get current timestamp when writing header of minidump")] - SystemTimeError(#[from] std::time::SystemTimeError), + SystemTimeError( + #[from] + #[serde(serialize_with = "serialize_system_time_error")] + std::time::SystemTimeError, + ), + #[error("Errors occurred while initializing PTraceDumper")] + InitErrors(#[source] ErrorList), + #[error("Errors occurred while suspending threads")] + SuspendThreadsErrors(#[source] ErrorList), + #[error("Errors occurred while resuming threads")] + ResumeThreadsErrors(#[source] ErrorList), + #[error("Crash thread does not reference principal mapping")] + PrincipalMappingNotReferenced, + #[error("Errors occurred while writing system info")] + WriteSystemInfoErrors(#[source] ErrorList), + #[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), + #[error("Failed to convert soft error list to JSON")] + ConvertToJsonFailed( + #[source] + #[serde(skip)] + serde_json::Error, + ), } -#[derive(Debug, Error)] +#[derive(Debug, Error, serde::Serialize)] pub enum ModuleReaderError { #[error("failed to read module file ({path}): {error}")] MapFile { path: std::path::PathBuf, #[source] + #[serde(serialize_with = "serialize_io_error")] error: std::io::Error, }, #[error("failed to read module memory: {length} bytes at {offset}{}: {error}", .start_address.map(|addr| format!(" (start address: {addr})")).unwrap_or_default())] @@ -266,10 +402,15 @@ pub enum ModuleReaderError { length: u64, start_address: Option, #[source] + #[serde(serialize_with = "serialize_nix_error")] error: nix::Error, }, #[error("failed to parse ELF memory: {0}")] - Parsing(#[from] goblin::error::Error), + Parsing( + #[from] + #[serde(serialize_with = "serialize_goblin_error")] + goblin::error::Error, + ), #[error("no build id notes in program headers")] NoProgramHeaderNote, #[error("no string table available to locate note sections")] diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs index e023a21a..3113d35f 100644 --- a/src/linux/maps_reader.rs +++ b/src/linux/maps_reader.rs @@ -1,19 +1,24 @@ -use crate::auxv::AuxvType; -use crate::errors::MapsReaderError; -use byteorder::{NativeEndian, ReadBytesExt}; -use goblin::elf; -use memmap2::{Mmap, MmapOptions}; -use procfs_core::process::{MMPermissions, MMapPath, MemoryMaps}; -use std::ffi::{OsStr, OsString}; -use std::os::unix::ffi::{OsStrExt, OsStringExt}; -use std::{fs::File, mem::size_of, path::PathBuf}; +use { + crate::{auxv::AuxvType, errors::MapsReaderError}, + byteorder::{NativeEndian, ReadBytesExt}, + goblin::elf, + memmap2::{Mmap, MmapOptions}, + procfs_core::process::{MMPermissions, MMapPath, MemoryMaps}, + std::{ + ffi::{OsStr, OsString}, + fs::File, + mem::size_of, + os::unix::ffi::{OsStrExt, OsStringExt}, + path::PathBuf, + }, +}; pub const LINUX_GATE_LIBRARY_NAME: &str = "linux-gate.so"; pub const DELETED_SUFFIX: &[u8] = b" (deleted)"; type Result = std::result::Result; -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize)] pub struct SystemMappingInfo { pub start_address: usize, pub end_address: usize, @@ -21,7 +26,7 @@ pub struct SystemMappingInfo { // One of these is produced for each mapping in the process (i.e. line in // /proc/$x/maps). -#[derive(Debug, PartialEq, Eq, Clone)] +#[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 @@ -88,7 +93,10 @@ impl MappingInfo { self.start_address + self.size } - pub fn aggregate(memory_maps: MemoryMaps, linux_gate_loc: AuxvType) -> Result> { + pub fn aggregate( + memory_maps: MemoryMaps, + linux_gate_loc: Option, + ) -> Result> { let mut infos = Vec::::new(); for mm in memory_maps { @@ -112,9 +120,11 @@ impl MappingInfo { let is_path = is_mapping_a_path(pathname.as_deref()); - if !is_path && linux_gate_loc != 0 && start_address == linux_gate_loc.try_into()? { - pathname = Some(LINUX_GATE_LIBRARY_NAME.into()); - offset = 0; + if let Some(linux_gate_loc) = linux_gate_loc.map(|u| usize::try_from(u).unwrap()) { + if !is_path && start_address == linux_gate_loc { + pathname = Some(LINUX_GATE_LIBRARY_NAME.into()); + offset = 0; + } } if let Some(prev_module) = infos.last_mut() { @@ -451,7 +461,7 @@ mod tests { 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"), - linux_gate_loc, + Some(linux_gate_loc), ) .unwrap_or_default() } diff --git a/src/linux/minidump_writer.rs b/src/linux/minidump_writer.rs index ba6b82ec..0c6e385a 100644 --- a/src/linux/minidump_writer.rs +++ b/src/linux/minidump_writer.rs @@ -1,23 +1,26 @@ pub use crate::linux::auxv::{AuxvType, DirectAuxvDumpInfo}; -use crate::{ - auxv::AuxvDumpInfo, - dir_section::{DirSection, DumpBuf}, - linux::{ - app_memory::AppMemoryList, - crash_context::CrashContext, - dso_debug, - errors::{InitError, WriterError}, - maps_reader::{MappingInfo, MappingList}, - ptrace_dumper::PtraceDumper, - sections::*, +use { + crate::{ + auxv::AuxvDumpInfo, + dir_section::{DirSection, DumpBuf}, + linux::{ + app_memory::AppMemoryList, + crash_context::CrashContext, + dso_debug, + errors::WriterError, + maps_reader::{MappingInfo, MappingList}, + ptrace_dumper::PtraceDumper, + sections::*, + }, + mem_writer::{Buffer, MemoryArrayWriter, MemoryWriter, MemoryWriterError}, + minidump_format::*, + Pid, + }, + error_graph::{ErrorList, WriteErrorList}, + std::{ + io::{Seek, Write}, + time::Duration, }, - mem_writer::{Buffer, MemoryArrayWriter, MemoryWriter, MemoryWriterError}, - minidump_format::*, - Pid, -}; -use std::{ - io::{Seek, Write}, - time::Duration, }; pub enum CrashingThreadContext { @@ -144,8 +147,24 @@ 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 = ErrorList::default(); + + let mut dumper = PtraceDumper::new_report_soft_errors( + self.process_id, + self.stop_timeout, + auxv, + soft_errors.subwriter(WriterError::InitErrors), + )?; + + let threads_count = dumper.threads.len(); + + dumper.suspend_threads(soft_errors.subwriter(WriterError::SuspendThreadsErrors)); + + if dumper.threads.is_empty() { + soft_errors.push(WriterError::SuspendNoThreadsLeft(threads_count)); + } + dumper.late_init()?; if self.skip_stacks_if_mapping_unreferenced { @@ -154,16 +173,12 @@ 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)?; - - // dumper would resume threads in drop() automatically, - // but in case there is an error, we want to catch it - dumper.resume_threads()?; + self.generate_dump(&mut buffer, &mut dumper, soft_errors, destination)?; Ok(buffer.into()) } @@ -226,11 +241,12 @@ impl MinidumpWriter { &mut self, buffer: &mut DumpBuf, dumper: &mut PtraceDumper, + mut soft_errors: ErrorList, 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 = 18u32; let mut header_section = MemoryWriter::::alloc(buffer)?; @@ -270,7 +286,10 @@ 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_stream::write( + buffer, + soft_errors.subwriter(WriterError::WriteSystemInfoErrors), + )?; dir_section.write_to_file(buffer, Some(dirent))?; let dirent = memory_info_list_stream::write(self, buffer)?; @@ -281,7 +300,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 +313,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 +328,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 +341,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 +354,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 +366,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 +378,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 +401,43 @@ 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))?; + + // ======================================================================================== + // + // PAST THIS BANNER, THE THREADS ARE RUNNING IN THE TARGET PROCESS AGAIN. IF YOU NEED TO + // ADD NEW ENTRIES THAT ACCESS THE TARGET MEMORY, DO IT BEFORE HERE! + // + // ======================================================================================== + + // Collect any last-minute soft errors when trying to restart threads + dumper.resume_threads(soft_errors.subwriter(WriterError::ResumeThreadsErrors)); + + // 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 +455,13 @@ impl MinidumpWriter { Ok(section.location()) } } + +fn write_soft_errors( + buffer: &mut DumpBuf, + soft_errors: ErrorList, +) -> Result { + let soft_errors_json_str = + serde_json::to_string_pretty(&soft_errors).map_err(WriterError::ConvertToJsonFailed)?; + let section = MemoryArrayWriter::write_bytes(buffer, soft_errors_json_str.as_bytes()); + Ok(section.location()) +} diff --git a/src/linux/ptrace_dumper.rs b/src/linux/ptrace_dumper.rs index 66f36564..7b04a2f5 100644 --- a/src/linux/ptrace_dumper.rs +++ b/src/linux/ptrace_dumper.rs @@ -1,28 +1,43 @@ +use { + super::{ + auxv::AuxvError, + errors::{AndroidError, MapsReaderError}, + serializers::*, + }, + crate::{ + linux::{ + auxv::AuxvDumpInfo, + errors::{DumperError, ThreadInfoError}, + maps_reader::MappingInfo, + module_reader, + thread_info::ThreadInfo, + Pid, + }, + serializers::*, + }, + error_graph::{ErrorList, WriteErrorList}, + failspot::failspot, + nix::{ + errno::Errno, + sys::{ptrace, signal, wait}, + }, + procfs_core::{ + process::{MMPermissions, ProcState, Stat}, + FromRead, ProcError, + }, + std::{ + ffi::OsString, + path, + result::Result, + time::{Duration, Instant}, + }, + thiserror::Error, +}; + #[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 nix::{ - errno::Errno, - sys::{ptrace, signal, wait}, -}; -use procfs_core::{ - process::{MMPermissions, ProcState, Stat}, - FromRead, ProcError, -}; -use std::{ - path, - result::Result, - time::{Duration, Instant}, -}; #[derive(Debug, Clone)] pub struct Thread { @@ -48,24 +63,91 @@ pub const AT_SYSINFO_EHDR: u64 = 33; impl Drop for PtraceDumper { fn drop(&mut self) { // Always try to resume all threads (e.g. in case of error) - let _ = self.resume_threads(); + self.resume_threads(error_graph::strategy::DontCare); // Always allow the process to continue. let _ = self.continue_process(); } } -#[derive(Debug, thiserror::Error)] -enum StopProcessError { +#[derive(Debug, Error, serde::Serialize)] +pub enum InitError { + #[error("failed to read auxv")] + ReadAuxvFailed(#[source] crate::auxv::AuxvError), + #[error("IO error for file {0}")] + IOError( + String, + #[source] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), + #[error("Failed Android specific late init")] + AndroidLateInitError(#[from] AndroidError), + #[error("Failed to read the page size")] + PageSizeError( + #[from] + #[serde(serialize_with = "serialize_nix_error")] + nix::Error, + ), + #[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] ErrorList), + #[error("Failed filling missing Auxv info")] + FillMissingAuxvInfoFailed(#[source] AuxvError), + #[error("Failed reading proc/pid/task entry for process")] + ReadProcessThreadEntryFailed( + #[source] + #[serde(serialize_with = "serialize_io_error")] + 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] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), + #[error("Proc task directory `{0:?}` is not a directory")] + ProcPidTaskNotDirectory(String), + #[error("Errors while enumerating threads")] + EnumerateThreadsErrors(#[source] ErrorList), + #[error("Failed to enumerate threads")] + EnumerateThreadsFailed(#[source] Box), + #[error("Failed to read process map file")] + ReadProcessMapFileFailed( + #[source] + #[serde(serialize_with = "serialize_proc_error")] + ProcError, + ), + #[error("Failed to aggregate process mappings")] + AggregateMappingsFailed(#[source] MapsReaderError), + #[error("Failed to enumerate process mappings")] + EnumerateMappingsFailed(#[source] Box), +} + +#[derive(Debug, thiserror::Error, serde::Serialize)] +pub enum StopProcessError { #[error("Failed to stop the process")] - Stop(#[from] Errno), + Stop( + #[from] + #[serde(serialize_with = "serialize_nix_error")] + nix::Error, + ), #[error("Failed to get the process state")] - State(#[from] ProcError), + State( + #[from] + #[serde(serialize_with = "serialize_proc_error")] + ProcError, + ), #[error("Timeout waiting for process to stop")] Timeout, } #[derive(Debug, thiserror::Error)] -enum ContinueProcessError { +pub enum ContinueProcessError { #[error("Failed to continue the process")] Continue(#[from] Errno), } @@ -88,8 +170,13 @@ 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 { - if pid == std::process::id() as _ { + pub fn new_report_soft_errors( + pid: Pid, + stop_timeout: Duration, + auxv: AuxvDumpInfo, + soft_errors: impl WriteErrorList, + ) -> Result { + if pid == std::process::id() as i32 { return Err(InitError::CannotPtraceSameProcess); } @@ -101,23 +188,43 @@ impl PtraceDumper { mappings: Vec::new(), page_size: 0, }; - dumper.init(stop_timeout)?; + dumper.init(stop_timeout, soft_errors)?; Ok(dumper) } // 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, + mut soft_errors: impl WriteErrorList, + ) -> Result<(), InitError> { // 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. + if let Err(e) = self.auxv.try_filling_missing_info( + self.pid, + soft_errors.subwriter(InitError::FillMissingAuxvInfoErrors), + ) { + soft_errors.push(InitError::FillMissingAuxvInfoFailed(e)); + } + + // If we completely fail to enumerate any threads... Some information is still better than + // no information! + if let Err(e) = + self.enumerate_threads(soft_errors.subwriter(InitError::EnumerateThreadsErrors)) + { + soft_errors.push(InitError::EnumerateThreadsFailed(Box::new(e))); + } + + // Same with mappings -- Some information is still better than no information! + if let Err(e) = self.enumerate_mappings() { + 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; @@ -207,42 +314,44 @@ 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, mut soft_errors: impl WriteErrorList) { // 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; + + failspot::failspot!(::SuspendThreads soft_errors.push(DumperError::PtraceAttachError(1234, nix::Error::EPERM))) } - pub fn resume_threads(&mut self) -> Result<(), DumperError> { - let mut result = Ok(()); + pub fn resume_threads(&mut self, mut soft_errors: impl WriteErrorList) { 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 } /// Send SIGSTOP to the process so that we can get a consistent state. /// /// This will block waiting for the process to stop until `timeout` has passed. fn stop_process(&mut self, timeout: Duration) -> Result<(), StopProcessError> { + failspot!(StopProcess bail(nix::Error::EPERM)); + signal::kill(nix::unistd::Pid::from_raw(self.pid), Some(signal::SIGSTOP))?; // Something like waitpid for non-child processes would be better, but we have no such @@ -273,30 +382,54 @@ 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, + mut soft_errors: impl WriteErrorList, + ) -> Result<(), InitError> { 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)); + } + + 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_result = failspot!(if ThreadName { + Err(std::io::Error::other( + "testing requested failure reading thread name", + )) + } else { + std::fs::read_to_string(format!("/proc/{}/task/{}/comm", pid, tid)) + }); + + let name = match name_result { + Ok(name) => Some(name.trim_end().to_string()), + Err(e) => { + soft_errors.push(InitError::ReadThreadNameFailed(e)); + None + } + }; + + self.threads.push(Thread { tid, name }); } + Ok(()) } @@ -308,39 +441,34 @@ 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_linux_gate_address().unwrap_or_default(); + 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))?; + + let maps = procfs_core::process::MemoryMaps::from_read(maps_file) + .map_err(InitError::ReadProcessMapFileFailed)?; + + self.mappings = MappingInfo::aggregate(maps, self.auxv.get_linux_gate_address()) + .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. - 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)?; - - 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(); - - if entry_point_loc != 0 { - 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 - // 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 entry_point_loc >= module.start_address.try_into().unwrap() - && entry_point_loc < (module.start_address + module.size).try_into().unwrap() - { - swap_idx = Some(idx); - break; - } - } - if let Some(idx) = swap_idx { - self.mappings.swap(0, idx); + 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(()) diff --git a/src/linux/sections/systeminfo_stream.rs b/src/linux/sections/systeminfo_stream.rs index a298c00d..62ca332c 100644 --- a/src/linux/sections/systeminfo_stream.rs +++ b/src/linux/sections/systeminfo_stream.rs @@ -1,7 +1,12 @@ -use super::*; -use crate::linux::dumper_cpu_info as dci; +use { + super::*, crate::linux::dumper_cpu_info as dci, error_graph::WriteErrorList, + errors::SectionSystemInfoError, +}; -pub fn write(buffer: &mut DumpBuf) -> Result { +pub fn write( + buffer: &mut DumpBuf, + mut soft_errors: impl WriteErrorList, +) -> Result { let mut info_section = MemoryWriter::::alloc(buffer)?; let dirent = MDRawDirectory { stream_type: MDStreamType::SystemInfoStream as u32, @@ -16,7 +21,9 @@ pub fn write(buffer: &mut DumpBuf) -> Result( + error: &goblin::error::Error, + serializer: S, +) -> Result { + serialize_generic_error(error, serializer) +} +/// Serialize [nix::Error] +pub fn serialize_nix_error( + error: &nix::Error, + serializer: S, +) -> Result { + serialize_generic_error(error, serializer) +} +/// Serialize [procfs_core::ProcError] +pub fn serialize_proc_error( + error: &procfs_core::ProcError, + serializer: S, +) -> Result { + serialize_generic_error(error, serializer) +} +/// Serialize [std::string::FromUtf8Error] +pub fn serialize_from_utf8_error( + error: &std::string::FromUtf8Error, + serializer: S, +) -> Result { + serialize_generic_error(error, serializer) +} +/// Serialize [std::time::SystemTimeError] +pub fn serialize_system_time_error( + error: &std::time::SystemTimeError, + serializer: S, +) -> Result { + serialize_generic_error(error, serializer) +} diff --git a/src/mem_writer.rs b/src/mem_writer.rs index a703d2b1..ae38c79b 100644 --- a/src/mem_writer.rs +++ b/src/mem_writer.rs @@ -1,14 +1,31 @@ -use crate::minidump_format::{MDLocationDescriptor, MDRVA}; -use scroll::ctx::{SizeWith, TryIntoCtx}; - -#[derive(Debug, thiserror::Error)] +use { + crate::{ + minidump_format::{MDLocationDescriptor, MDRVA}, + serializers::*, + }, + scroll::ctx::{SizeWith, TryIntoCtx}, +}; + +#[derive(Debug, thiserror::Error, serde::Serialize)] pub enum MemoryWriterError { #[error("IO error when writing to DumpBuf")] - IOError(#[from] std::io::Error), + IOError( + #[from] + #[serde(serialize_with = "serialize_io_error")] + std::io::Error, + ), #[error("Failed integer conversion")] - TryFromIntError(#[from] std::num::TryFromIntError), + TryFromIntError( + #[from] + #[serde(skip)] + std::num::TryFromIntError, + ), #[error("Failed to write to buffer")] - Scroll(#[from] scroll::Error), + Scroll( + #[from] + #[serde(serialize_with = "serialize_scroll_error")] + scroll::Error, + ), } type WriteResult = std::result::Result; diff --git a/src/serializers.rs b/src/serializers.rs new file mode 100644 index 00000000..a42313da --- /dev/null +++ b/src/serializers.rs @@ -0,0 +1,30 @@ +//! Functions used by Serde to serialize types that we don't own (and thus can't implement +//! [Serialize] for) + +use serde::Serializer; +/// Useful for types that implement [Error][std::error::Error] and don't need any special +/// treatment. +pub fn serialize_generic_error( + error: &E, + serializer: S, +) -> Result { + // I guess we'll have to see if it's more useful to store the debug representation of a + // foreign error type or something else (like maybe iterating its error chain into a + // list?) + let dbg = format!("{error:#?}"); + serializer.serialize_str(&dbg) +} +/// Serialize [std::io::Error] +pub fn serialize_io_error( + error: &std::io::Error, + serializer: S, +) -> Result { + serialize_generic_error(error, serializer) +} +/// Serialize [scroll::Error] +pub fn serialize_scroll_error( + error: &scroll::Error, + serializer: S, +) -> Result { + serialize_generic_error(error, serializer) +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index cf258819..2b5499ef 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -94,3 +94,38 @@ pub fn start_child_and_return(args: &[&str]) -> Child { .spawn() .expect("failed to execute child") } + +#[allow(unused)] +pub fn read_minidump_soft_errors_or_panic<'a, T>( + dump: &minidump::Minidump<'a, T>, +) -> serde_json::Value +where + T: std::ops::Deref + 'a, +{ + let contents = std::str::from_utf8( + dump.get_raw_stream(minidump_common::format::MINIDUMP_STREAM_TYPE::MozSoftErrors.into()) + .expect("missing soft error stream"), + ) + .expect("expected utf-8 stream"); + + serde_json::from_str(contents).expect("expected json") +} + +#[allow(unused)] +pub fn assert_soft_errors_in_minidump<'a, 'b, T, I>( + dump: &minidump::Minidump<'a, T>, + expected_errors: I, +) where + T: std::ops::Deref + 'a, + I: IntoIterator, +{ + let actual_json = read_minidump_soft_errors_or_panic(dump); + let actual_errors = actual_json.as_array().unwrap(); + + // Ensure that every error we expect is in the actual list somewhere + for expected_error in expected_errors { + assert!(actual_errors + .iter() + .any(|actual_error| actual_error == expected_error)); + } +} diff --git a/tests/linux_minidump_writer.rs b/tests/linux_minidump_writer.rs index 77986d34..d62f56ff 100644 --- a/tests/linux_minidump_writer.rs +++ b/tests/linux_minidump_writer.rs @@ -1,30 +1,32 @@ #![cfg(any(target_os = "linux", target_os = "android"))] #![allow(unused_imports, unused_variables)] -use minidump::*; -use minidump_common::format::{GUID, MINIDUMP_STREAM_TYPE::*}; -use minidump_writer::{ - app_memory::AppMemory, - crash_context::CrashContext, - errors::*, - maps_reader::{MappingEntry, MappingInfo, SystemMappingInfo}, - minidump_writer::MinidumpWriter, - module_reader::{BuildId, ReadFromModule}, - ptrace_dumper::PtraceDumper, - Pid, -}; -use nix::{errno::Errno, sys::signal::Signal}; -use procfs_core::process::MMPermissions; -use std::collections::HashSet; - -use std::{ - io::{BufRead, BufReader}, - os::unix::process::ExitStatusExt, - process::{Command, Stdio}, +use { + common::*, + minidump::*, + minidump_common::format::{GUID, MINIDUMP_STREAM_TYPE::*}, + minidump_writer::{ + app_memory::AppMemory, + crash_context::CrashContext, + errors::*, + maps_reader::{MappingEntry, MappingInfo, SystemMappingInfo}, + minidump_writer::MinidumpWriter, + module_reader::{BuildId, ReadFromModule}, + ptrace_dumper::PtraceDumper, + Pid, + }, + nix::{errno::Errno, sys::signal::Signal}, + procfs_core::process::MMPermissions, + serde_json::json, + std::{ + collections::HashSet, + io::{BufRead, BufReader}, + os::unix::process::ExitStatusExt, + process::{Command, Stdio}, + }, }; mod common; -use common::*; #[derive(Debug, PartialEq)] enum Context { @@ -299,6 +301,10 @@ contextual_test! { contextual_test! { fn skip_if_requested(context: Context) { + let expected_errors = vec![ + json!("PrincipalMappingNotReferenced"), + ]; + let num_of_threads = 1; let mut child = start_child_and_wait_for_threads(num_of_threads); let pid = child.id() as i32; @@ -331,7 +337,9 @@ contextual_test! { assert_eq!(waitres.code(), None); assert_eq!(status, Signal::SIGKILL as i32); - assert!(res.is_err()); + // Ensure the MozSoftErrors stream contains the expected errors + let dump = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); + assert_soft_errors_in_minidump(&dump, &expected_errors); } } @@ -790,7 +798,7 @@ fn memory_info_list_stream() { .dump(&mut tmpfile) .expect("cound not write minidump"); child.kill().expect("Failed to kill process"); - let _ = child.wait(); + child.wait().expect("Failed to wait on killed process"); // Ensure the minidump has a MemoryInfoListStream present and has at least one entry. let dump = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); diff --git a/tests/linux_minidump_writer_soft_error.rs b/tests/linux_minidump_writer_soft_error.rs new file mode 100644 index 00000000..5c5df212 --- /dev/null +++ b/tests/linux_minidump_writer_soft_error.rs @@ -0,0 +1,92 @@ +#![cfg(any(target_os = "linux", target_os = "android"))] + +use { + common::*, + minidump::Minidump, + minidump_writer::{minidump_writer::MinidumpWriter, FailSpotName}, + serde_json::json, +}; + +mod common; + +#[test] +fn soft_error_stream() { + let mut child = start_child_and_wait_for_threads(1); + let pid = child.id() as i32; + + let mut tmpfile = tempfile::Builder::new() + .prefix("soft_error_stream") + .tempfile() + .unwrap(); + + let mut fail_client = FailSpotName::testing_client(); + fail_client.set_enabled(FailSpotName::StopProcess, true); + + // Write a minidump + MinidumpWriter::new(pid, pid) + .dump(&mut tmpfile) + .expect("cound not write minidump"); + child.kill().expect("Failed to kill process"); + child.wait().expect("Failed to wait on killed process"); + + // Ensure the minidump has a MozSoftErrors present + let dump = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); + read_minidump_soft_errors_or_panic(&dump); +} + +#[test] +fn soft_error_stream_content() { + let expected_errors = vec![ + json!({"InitErrors": [ + {"StopProcessFailed": {"Stop": "EPERM"}}, + {"FillMissingAuxvInfoErrors": ["InvalidFormat"]}, + {"EnumerateThreadsErrors": [ + {"ReadThreadNameFailed": "\ + Custom {\n \ + kind: Other,\n \ + error: \"testing requested failure reading thread name\",\n\ + }" + } + ]} + ]}), + json!({"SuspendThreadsErrors": [{"PtraceAttachError": [1234, "EPERM"]}]}), + json!({"WriteSystemInfoErrors": [ + {"WriteCpuInformationFailed": {"IOError": "\ + Custom {\n \ + kind: Other,\n \ + error: \"test requested cpuinfo file failure\",\n\ + }" + }} + ]}), + ]; + + let mut child = start_child_and_wait_for_threads(1); + let pid = child.id() as i32; + + let mut tmpfile = tempfile::Builder::new() + .prefix("soft_error_stream_content") + .tempfile() + .unwrap(); + + let mut fail_client = FailSpotName::testing_client(); + for name in [ + FailSpotName::StopProcess, + FailSpotName::FillMissingAuxvInfo, + FailSpotName::ThreadName, + FailSpotName::SuspendThreads, + FailSpotName::CpuInfoFileOpen, + ] { + fail_client.set_enabled(name, true); + } + + // Write a minidump + MinidumpWriter::new(pid, pid) + .dump(&mut tmpfile) + .expect("cound not write minidump"); + child.kill().expect("Failed to kill process"); + child.wait().expect("Failed to wait on killed process"); + + // Ensure the MozSoftErrors stream contains the expected errors + let dump = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); + assert_soft_errors_in_minidump(&dump, &expected_errors); +} diff --git a/tests/ptrace_dumper.rs b/tests/ptrace_dumper.rs index 48c4fa27..b166c256 100644 --- a/tests/ptrace_dumper.rs +++ b/tests/ptrace_dumper.rs @@ -1,13 +1,20 @@ //! All of these tests are specific to ptrace #![cfg(any(target_os = "linux", target_os = "android"))] -use minidump_writer::ptrace_dumper::PtraceDumper; -use nix::sys::mman::{mmap, MapFlags, ProtFlags}; -use nix::sys::signal::Signal; -use std::convert::TryInto; -use std::io::{BufRead, BufReader}; -use std::mem::size_of; -use std::os::unix::process::ExitStatusExt; +use { + error_graph::ErrorList, + minidump_writer::ptrace_dumper::PtraceDumper, + nix::{ + sys::mman::{mmap, MapFlags, ProtFlags}, + sys::signal::Signal, + }, + std::{ + convert::TryInto, + io::{BufRead, BufReader}, + mem::size_of, + os::unix::process::ExitStatusExt, + }, +}; mod common; use common::*; @@ -23,6 +30,13 @@ macro_rules! disabled_on_ci_and_android { }; } +macro_rules! assert_no_soft_errors(($n: ident, $e: expr) => {{ + let mut $n = ErrorList::default(); + let __result = $e; + assert!($n.is_empty(), "{:?}", $n); + __result +}}); + #[test] fn test_setup() { spawn_child("setup", &[]); @@ -91,14 +105,21 @@ 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, - Default::default(), + + let mut dumper = assert_no_soft_errors!( + soft_errors, + PtraceDumper::new_report_soft_errors( + pid, + minidump_writer::minidump_writer::STOP_TIMEOUT, + Default::default(), + &mut soft_errors, + ) ) .expect("Couldn't init dumper"); + assert_eq!(dumper.threads.len(), num_of_threads); - dumper.suspend_threads().expect("Could not suspend threads"); + + assert_no_soft_errors!(soft_errors, dumper.suspend_threads(&mut soft_errors)); // let mut matching_threads = 0; for (idx, curr_thread) in dumper.threads.iter().enumerate() { @@ -146,7 +167,7 @@ fn test_thread_list_from_parent() { 0 }; */ } - dumper.resume_threads().expect("Failed to resume threads"); + assert_no_soft_errors!(soft_errors, dumper.resume_threads(&mut soft_errors)); child.kill().expect("Failed to kill process"); // Reap child @@ -272,14 +293,20 @@ 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, - Default::default(), + let mut dumper = assert_no_soft_errors!( + soft_errors, + PtraceDumper::new_report_soft_errors( + pid, + minidump_writer::minidump_writer::STOP_TIMEOUT, + Default::default(), + &mut soft_errors, + ) ) .expect("Couldn't init dumper"); assert_eq!(dumper.threads.len(), num_of_threads); - dumper.suspend_threads().expect("Could not suspend threads"); + + assert_no_soft_errors!(soft_errors, dumper.suspend_threads(&mut soft_errors)); + let thread_info = dumper .get_thread_info_by_index(0) .expect("Couldn't find thread_info"); @@ -374,7 +401,8 @@ fn test_sanitize_stack_copy() { assert_eq!(simulated_stack[0..size_of::()], defaced); - dumper.resume_threads().expect("Failed to resume threads"); + assert_no_soft_errors!(soft_errors, dumper.resume_threads(&mut soft_errors)); + child.kill().expect("Failed to kill process"); // Reap child