diff --git a/Cargo.toml b/Cargo.toml index ee85b34f..3815457e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ byteorder = "1.3.2" cfg-if = "1.0" crash-context = "0.5" memoffset = "0.8" -minidump-common = "0.15" +minidump-common = "0.16.0" scroll = "0.11" tempfile = "3.1.0" thiserror = "1.0.21" @@ -30,6 +30,8 @@ nix = { version = "0.26", default-features = false, features = [ "ptrace", "user", ] } +# Used for parsing procfs info. +procfs = "0.15.1" [target.'cfg(target_os = "macos")'.dependencies] # Binds some additional mac specifics not in libc @@ -44,13 +46,13 @@ features = ["handleapi", "minwindef", "processthreadsapi", "winnt"] [dev-dependencies] # Minidump-processor is async so we need an executor futures = { version = "0.3", features = ["executor"] } -minidump = "0.15" +minidump = "0.16.0" memmap2 = "0.5" [target.'cfg(target_os = "macos")'.dev-dependencies] # We dump symbols for the `test` executable so that we can validate that minidumps # created by this crate can be processed by minidump-processor dump_syms = { version = "2.0.0", default-features = false } -minidump-processor = { version = "0.15", default-features = false } +minidump-processor = { version = "0.16.0", default-features = false } similar-asserts = "1.2" uuid = "1.0" diff --git a/src/bin/test.rs b/src/bin/test.rs index accbc998..a94086c6 100644 --- a/src/bin/test.rs +++ b/src/bin/test.rs @@ -109,7 +109,7 @@ mod linux { let dumper = PtraceDumper::new(getppid().as_raw())?; let mut mapping_count = 0; for map in &dumper.mappings { - if map.name == Some(path.clone()) { + if map.name == Some(path.clone().into()) { mapping_count += 1; // This mapping should encompass the entire original mapped // range. @@ -127,7 +127,7 @@ mod linux { let mut dumper = PtraceDumper::new(ppid)?; let mut found_linux_gate = false; for mut mapping in dumper.mappings.clone() { - if mapping.name.as_deref() == Some(LINUX_GATE_LIBRARY_NAME) { + if mapping.name == Some(LINUX_GATE_LIBRARY_NAME.into()) { found_linux_gate = true; dumper.suspend_threads()?; let id = PtraceDumper::elf_identifier_for_mapping(&mut mapping, ppid)?; @@ -148,7 +148,7 @@ mod linux { test!(linux_gate_loc != 0, "linux_gate_loc == 0")?; let mut found_linux_gate = false; for mapping in &dumper.mappings { - if mapping.name.as_deref() == Some(LINUX_GATE_LIBRARY_NAME) { + if mapping.name == Some(LINUX_GATE_LIBRARY_NAME.into()) { found_linux_gate = true; test!( linux_gate_loc == mapping.start_address.try_into()?, diff --git a/src/linux/android.rs b/src/linux/android.rs index 0c8195cc..465e93c9 100644 --- a/src/linux/android.rs +++ b/src/linux/android.rs @@ -118,7 +118,7 @@ pub fn late_process_mappings(pid: Pid, mappings: &mut [MappingInfo]) -> Result<( // where the ELF header indicates a mapped shared library. for mut map in mappings .iter_mut() - .filter(|m| m.executable && m.name.as_ref().map_or(false, |n| n.starts_with("/"))) + .filter(|m| m.executable && m.name_is_path()) { let ehdr_opt = PtraceDumper::copy_from_process( pid, diff --git a/src/linux/errors.rs b/src/linux/errors.rs index e423389f..d7b1c586 100644 --- a/src/linux/errors.rs +++ b/src/linux/errors.rs @@ -3,6 +3,7 @@ use crate::maps_reader::MappingInfo; use crate::mem_writer::MemoryWriterError; use crate::thread_info::Pid; use goblin; +use std::ffi::OsString; use thiserror::Error; #[derive(Debug, Error)] @@ -28,20 +29,22 @@ pub enum MapsReaderError { LinuxGateNotConvertable(#[from] std::num::TryFromIntError), // get_mmap() - #[error("Not safe to open mapping {0}")] - NotSafeToOpenMapping(String), + #[error("Not safe to open mapping {}", .0.to_string_lossy())] + NotSafeToOpenMapping(OsString), #[error("IO Error")] FileError(#[from] std::io::Error), #[error("Mmapped file empty or not an ELF file")] MmapSanityCheckFailed, - #[error("Symlink does not match ({0} vs. {1}")] + #[error("Symlink does not match ({0} vs. {1})")] SymlinkError(std::path::PathBuf, std::path::PathBuf), - // handle_deleted_file_in_mapping() + // fixup_deleted_file() #[error("Couldn't parse as ELF file")] ELFParsingFailed(#[from] goblin::error::Error), - #[error("No soname found (filename: {0}")] - NoSoName(String), + #[error("An anonymous mapping has no associated file")] + AnonymousMapping, + #[error("No soname found (filename: {})", .0.to_string_lossy())] + NoSoName(OsString), } #[derive(Debug, Error)] @@ -114,8 +117,8 @@ pub enum DumperError { ELFParsingFailed(#[from] goblin::error::Error), #[error("No build-id found")] NoBuildIDFound, - #[error("Not safe to open mapping: {0}")] - NotSafeToOpenMapping(String), + #[error("Not safe to open mapping: {}", .0.to_string_lossy())] + NotSafeToOpenMapping(OsString), #[error("Failed integer conversion")] TryFromIntError(#[from] std::num::TryFromIntError), #[error("Maps reader error")] @@ -144,6 +147,14 @@ pub enum SectionMappingsError { GetEffectivePathError(MappingInfo, #[source] MapsReaderError), } +#[derive(Debug, Error)] +pub enum SectionMemInfoListError { + #[error("Failed to write to memory")] + MemoryWriterError(#[from] MemoryWriterError), + #[error("Failed to read from procfs")] + ProcfsError(#[from] procfs::ProcError), +} + #[derive(Debug, Error)] pub enum SectionMemListError { #[error("Failed to write to memory")] @@ -210,6 +221,8 @@ pub enum WriterError { SectionMemListError(#[from] SectionMemListError), #[error("Failed when writing section SystemInfo")] SectionSystemInfoError(#[from] SectionSystemInfoError), + #[error("Failed when writing section MemoryInfoList")] + SectionMemoryInfoListError(#[from] SectionMemInfoListError), #[error("Failed when writing section ThreadList")] SectionThreadListError(#[from] SectionThreadListError), #[error("Failed when writing section ThreadNameList")] diff --git a/src/linux/maps_reader.rs b/src/linux/maps_reader.rs index 8050387a..249f63b3 100644 --- a/src/linux/maps_reader.rs +++ b/src/linux/maps_reader.rs @@ -4,11 +4,13 @@ use crate::thread_info::Pid; use byteorder::{NativeEndian, ReadBytesExt}; use goblin::elf; use memmap2::{Mmap, MmapOptions}; +use procfs::process::{MMPermissions, MMapPath, MemoryMaps}; +use std::ffi::{OsStr, OsString}; +use std::os::unix::ffi::OsStrExt; use std::{fs::File, mem::size_of, path::PathBuf}; pub const LINUX_GATE_LIBRARY_NAME: &str = "linux-gate.so"; -pub const DELETED_SUFFIX: &str = " (deleted)"; -pub const RESERVED_FLAGS: &str = "---p"; +pub const DELETED_SUFFIX: &[u8] = b" (deleted)"; type Result = std::result::Result; @@ -36,7 +38,7 @@ pub struct MappingInfo { pub system_mapping_info: SystemMappingInfo, pub offset: usize, // offset into the backed file. pub executable: bool, // true if the mapping has the execute bit set. - pub name: Option, + pub name: Option, // pub elf_obj: Option, } @@ -55,133 +57,96 @@ pub enum MappingInfoParsingResult { Success(MappingInfo), } -fn is_mapping_a_path(pathname: Option<&str>) -> bool { +fn is_mapping_a_path(pathname: Option<&OsStr>) -> bool { match pathname { - Some(x) => x.contains('/'), + Some(x) => x.as_bytes().contains(&b'/'), None => false, } } impl MappingInfo { - pub fn parse_from_line( - line: &str, - linux_gate_loc: AuxvType, - last_mapping: Option<&mut MappingInfo>, - ) -> Result { - let mut last_whitespace = false; - - // There is no `line.splitn_whitespace(6)`, so we have to do it somewhat manually - // Split at the first whitespace, trim of the rest. - let mut splits = line - .trim() - .splitn(6, |c: char| { - if c.is_whitespace() { - if last_whitespace { - return false; - } - last_whitespace = true; - true - } else { - last_whitespace = false; - false - } - }) - .map(str::trim); - - let address = splits - .next() - .ok_or(MapsReaderError::MapEntryMalformed("address"))?; - let perms = splits - .next() - .ok_or(MapsReaderError::MapEntryMalformed("permissions"))?; - let mut offset = usize::from_str_radix( - splits - .next() - .ok_or(MapsReaderError::MapEntryMalformed("offset"))?, - 16, - )?; - let _dev = splits - .next() - .ok_or(MapsReaderError::MapEntryMalformed("dev"))?; - let _inode = splits - .next() - .ok_or(MapsReaderError::MapEntryMalformed("inode"))?; - let mut pathname = splits.next(); // Optional - - // Due to our ugly `splitn_whitespace()` hack from above, we might have - // only trailing whitespaces as the name, so we it might still be "Some()" - if let Some(x) = pathname { - if x.is_empty() { - pathname = None; - } - } - - let mut addresses = address.split('-'); - let start_address = usize::from_str_radix(addresses.next().unwrap(), 16)?; - let end_address = usize::from_str_radix(addresses.next().unwrap(), 16)?; - - let executable = perms.contains('x'); + /// Return whether the `name` field is a path (contains a `/`). + pub fn name_is_path(&self) -> bool { + is_mapping_a_path(self.name.as_deref()) + } - // Only copy name if the name is a valid path name, or if - // it's the VDSO image. - let is_path = is_mapping_a_path(pathname); + pub fn aggregate(memory_maps: MemoryMaps, linux_gate_loc: AuxvType) -> Result> { + let mut infos = Vec::::new(); + + for mm in memory_maps.memory_maps { + let start_address: usize = mm.address.0.try_into()?; + let end_address: usize = mm.address.1.try_into()?; + let mut offset: usize = mm.offset.try_into()?; + + let executable = mm.perms.contains(MMPermissions::EXECUTE); + + let mut pathname: Option = match mm.pathname { + MMapPath::Path(p) => Some(p.into()), + MMapPath::Heap => Some("[heap]".into()), + MMapPath::Stack => Some("[stack]".into()), + MMapPath::TStack(i) => Some(format!("[stack:{i}]").into()), + MMapPath::Vdso => Some("[vdso]".into()), + MMapPath::Vvar => Some("[vvar]".into()), + MMapPath::Vsyscall => Some("[vsyscall]".into()), + MMapPath::Rollup => Some("[rollup]".into()), + MMapPath::Vsys(i) => Some(format!("/SYSV{i:x}").into()), + MMapPath::Other(n) => Some(format!("[{n}]").into()), + MMapPath::Anonymous => None, + }; - if !is_path && linux_gate_loc != 0 && start_address == linux_gate_loc.try_into()? { - pathname = Some(LINUX_GATE_LIBRARY_NAME); - offset = 0; - } + let is_path = is_mapping_a_path(pathname.as_deref()); - match (pathname, last_mapping) { - (Some(_name), Some(module)) => { - // Merge adjacent mappings into one module, assuming they're a single - // library mapped by the dynamic linker. - if (start_address == module.start_address + module.size) - && (pathname == module.name.as_deref()) - { - module.system_mapping_info.end_address = end_address; - module.size = end_address - module.start_address; - module.executable |= executable; - return Ok(MappingInfoParsingResult::SkipLine); - } + if !is_path && linux_gate_loc != 0 && start_address == linux_gate_loc.try_into()? { + pathname = Some(LINUX_GATE_LIBRARY_NAME.into()); + offset = 0; } - (None, Some(module)) => { - // Also merge mappings that result from address ranges that the - // linker reserved but which a loaded library did not use. These - // appear as an anonymous private mapping with no access flags set - // and which directly follow an executable mapping. - let module_end_address = module.start_address + module.size; - if (start_address == module_end_address) - && module.executable - && is_mapping_a_path(module.name.as_deref()) - && (offset == 0 || offset == module_end_address) - && perms == RESERVED_FLAGS - { - module.size = end_address - module.start_address; - return Ok(MappingInfoParsingResult::SkipLine); + + // Merge adjacent mappings into one module, assuming they're a single + // library mapped by the dynamic linker. + if let Some(module) = infos.last_mut() { + if pathname.is_some() { + if (start_address == module.start_address + module.size) + && (pathname == module.name) + { + module.system_mapping_info.end_address = end_address; + module.size = end_address - module.start_address; + module.executable |= executable; + continue; + } + } else { + // Also merge mappings that result from address ranges that the + // linker reserved but which a loaded library did not use. These + // appear as an anonymous private mapping with no access flags set + // and which directly follow an executable mapping. + let module_end_address = module.start_address + module.size; + if (start_address == module_end_address) + && module.executable + && is_mapping_a_path(module.name.as_deref()) + && (offset == 0 || offset == module_end_address) + && mm.perms == MMPermissions::PRIVATE + { + module.size = end_address - module.start_address; + continue; + } } } - _ => (), - } - let name = pathname.map(ToOwned::to_owned); - - let info = MappingInfo { - start_address, - size: end_address - start_address, - system_mapping_info: SystemMappingInfo { + infos.push(MappingInfo { start_address, - end_address, - }, - offset, - executable, - name, - // elf_obj, - }; - - Ok(MappingInfoParsingResult::Success(info)) + size: end_address - start_address, + system_mapping_info: SystemMappingInfo { + start_address, + end_address, + }, + offset, + executable, + name: pathname, + }); + } + Ok(infos) } - pub fn get_mmap(name: &Option, offset: usize) -> Result { + pub fn get_mmap(name: &Option, offset: usize) -> Result { if !MappingInfo::is_mapped_file_safe_to_open(name) { return Err(MapsReaderError::NotSafeToOpenMapping( name.clone().unwrap_or_default(), @@ -203,12 +168,25 @@ impl MappingInfo { Ok(mapped_file) } - pub fn handle_deleted_file_in_mapping(path: &str, pid: Pid) -> Result { + /// Check whether the mapping refers to a deleted file, and if so try to find the file + /// elsewhere and return that path. + /// + /// Currently this only supports fixing a deleted file that was the main exe of the given + /// `pid`. + /// + /// Returns a tuple, where the first element is the file path (which is possibly different than + /// `self.name`), and the second element is the original file path if a different path was + /// used. If no mapping name exists, returns an error. + pub fn fixup_deleted_file(&self, pid: Pid) -> Result<(OsString, Option<&OsStr>)> { // Check for ' (deleted)' in |path|. // |path| has to be at least as long as "/x (deleted)". - if !path.ends_with(DELETED_SUFFIX) { - return Ok(path.to_string()); - } + let Some(path) = &self.name else { + return Err(MapsReaderError::AnonymousMapping); + }; + + let Some(old_path) = path.as_bytes().strip_suffix(DELETED_SUFFIX) else { + return Ok((path.clone(), None)); + }; // Check |path| against the /proc/pid/exe 'symlink'. let exe_link = format!("/proc/{}/exe", pid); @@ -218,7 +196,7 @@ impl MappingInfo { // if (!GetMappingAbsolutePath(new_mapping, new_path)) // return false; - if link_path != PathBuf::from(path) { + if &link_path != path { return Err(MapsReaderError::SymlinkError( PathBuf::from(path), link_path, @@ -233,7 +211,7 @@ impl MappingInfo { // return Err("".into()); // } // } - Ok(exe_link) + Ok((exe_link.into(), Some(OsStr::from_bytes(old_path)))) } pub fn stack_has_pointer_to_mapping(&self, stack_copy: &[u8], sp_offset: usize) -> bool { @@ -269,13 +247,13 @@ impl MappingInfo { false } - pub fn is_mapped_file_safe_to_open(name: &Option) -> bool { + pub fn is_mapped_file_safe_to_open(name: &Option) -> bool { // It is unsafe to attempt to open a mapped file that lives under /dev, // because the semantics of the open may be driver-specific so we'd risk // hanging the crash dumper. And a file in /dev/ almost certainly has no // ELF file identifier anyways. if let Some(name) = name { - if name.starts_with("/dev/") { + if name.as_bytes().starts_with(b"/dev/") { return false; } } @@ -291,13 +269,13 @@ impl MappingInfo { let elf_obj = elf::Elf::parse(&mapped_file)?; let soname = elf_obj.soname.ok_or_else(|| { - MapsReaderError::NoSoName(self.name.clone().unwrap_or_else(|| "None".to_string())) + MapsReaderError::NoSoName(self.name.clone().unwrap_or_else(|| "None".into())) })?; Ok(soname.to_string()) } - pub fn get_mapping_effective_name_and_path(&self) -> Result<(String, String)> { - let mut file_path = self.name.clone().unwrap_or_default(); + pub fn get_mapping_effective_path_and_name(&self) -> Result<(PathBuf, String)> { + let mut file_path = PathBuf::from(self.name.clone().unwrap_or_default()); // Tools such as minidump_stackwalk use the name of the module to look up // symbols produced by dump_syms. dump_syms will prefer to use a module's @@ -310,8 +288,10 @@ impl MappingInfo { } else { // file_path := /path/to/libname.so // file_name := libname.so - // SAFETY: The unwrap is safe as rsplit always returns at least one item - let file_name = file_path.rsplit('/').next().unwrap().to_owned(); + let file_name = file_path + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_default(); return Ok((file_path, file_name)); }; @@ -322,16 +302,10 @@ impl MappingInfo { // In this case, we append the file_name to the mapped archive path: // file_name := libname.so // file_path := /path/to/ARCHIVE.APK/libname.so - file_path = format!("{}/{}", file_path, file_name); + file_path.push(&file_name); } else { // Otherwise, replace the basename with the SONAME. - let split: Vec<_> = file_path.rsplitn(2, '/').collect(); - if split.len() == 2 { - // NOTE: rsplitn reverses the order, so the remainder is the last item - file_path = format!("{}/{}", split[1], file_name); - } else { - file_path = file_name.clone(); - } + file_path.set_file_name(&file_name); } Ok((file_path, file_name)) @@ -372,77 +346,72 @@ impl MappingInfo { mod tests { use super::*; - fn get_lines_and_loc() -> (Vec<&'static str>, u64) { - (vec![ -"5597483fc000-5597483fe000 r--p 00000000 00:31 4750073 /usr/bin/cat", -"5597483fe000-559748402000 r-xp 00002000 00:31 4750073 /usr/bin/cat", -"559748402000-559748404000 r--p 00006000 00:31 4750073 /usr/bin/cat", -"559748404000-559748405000 r--p 00007000 00:31 4750073 /usr/bin/cat", -"559748405000-559748406000 rw-p 00008000 00:31 4750073 /usr/bin/cat", -"559749b0e000-559749b2f000 rw-p 00000000 00:00 0 [heap]", -"7efd968d3000-7efd968f5000 rw-p 00000000 00:00 0", -"7efd968f5000-7efd9694a000 r--p 00000000 00:31 5004638 /usr/lib/locale/en_US.utf8/LC_CTYPE", -"7efd9694a000-7efd96bc2000 r--p 00000000 00:31 5004373 /usr/lib/locale/en_US.utf8/LC_COLLATE", -"7efd96bc2000-7efd96bc4000 rw-p 00000000 00:00 0", -"7efd96bc4000-7efd96bea000 r--p 00000000 00:31 4996104 /lib64/libc-2.32.so", -"7efd96bea000-7efd96d39000 r-xp 00026000 00:31 4996104 /lib64/libc-2.32.so", -"7efd96d39000-7efd96d85000 r--p 00175000 00:31 4996104 /lib64/libc-2.32.so", -"7efd96d85000-7efd96d86000 ---p 001c1000 00:31 4996104 /lib64/libc-2.32.so", -"7efd96d86000-7efd96d89000 r--p 001c1000 00:31 4996104 /lib64/libc-2.32.so", -"7efd96d89000-7efd96d8c000 rw-p 001c4000 00:31 4996104 /lib64/libc-2.32.so", -"7efd96d8c000-7efd96d92000 ---p 00000000 00:00 0", -"7efd96da0000-7efd96da1000 r--p 00000000 00:31 5004379 /usr/lib/locale/en_US.utf8/LC_NUMERIC", -"7efd96da1000-7efd96da2000 r--p 00000000 00:31 5004382 /usr/lib/locale/en_US.utf8/LC_TIME", -"7efd96da2000-7efd96da3000 r--p 00000000 00:31 5004377 /usr/lib/locale/en_US.utf8/LC_MONETARY", -"7efd96da3000-7efd96da4000 r--p 00000000 00:31 5004376 /usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES", -"7efd96da4000-7efd96da5000 r--p 00000000 00:31 5004380 /usr/lib/locale/en_US.utf8/LC_PAPER", -"7efd96da5000-7efd96da6000 r--p 00000000 00:31 5004378 /usr/lib/locale/en_US.utf8/LC_NAME", -"7efd96da6000-7efd96da7000 r--p 00000000 00:31 5004372 /usr/lib/locale/en_US.utf8/LC_ADDRESS", -"7efd96da7000-7efd96da8000 r--p 00000000 00:31 5004381 /usr/lib/locale/en_US.utf8/LC_TELEPHONE", -"7efd96da8000-7efd96da9000 r--p 00000000 00:31 5004375 /usr/lib/locale/en_US.utf8/LC_MEASUREMENT", -"7efd96da9000-7efd96db0000 r--s 00000000 00:31 5004639 /usr/lib64/gconv/gconv-modules.cache", -"7efd96db0000-7efd96db1000 r--p 00000000 00:31 5004374 /usr/lib/locale/en_US.utf8/LC_IDENTIFICATION", -"7efd96db1000-7efd96db2000 r--p 00000000 00:31 4996100 /lib64/ld-2.32.so", -"7efd96db2000-7efd96dd3000 r-xp 00001000 00:31 4996100 /lib64/ld-2.32.so", -"7efd96dd3000-7efd96ddc000 r--p 00022000 00:31 4996100 /lib64/ld-2.32.so", -"7efd96ddc000-7efd96ddd000 r--p 0002a000 00:31 4996100 /lib64/ld-2.32.so", -"7efd96ddd000-7efd96ddf000 rw-p 0002b000 00:31 4996100 /lib64/ld-2.32.so", -"7ffc6dfda000-7ffc6dffb000 rw-p 00000000 00:00 0 [stack]", -"7ffc6e0f3000-7ffc6e0f7000 r--p 00000000 00:00 0 [vvar]", -"7ffc6e0f7000-7ffc6e0f9000 r-xp 00000000 00:00 0 [vdso]", -"ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]" - ], 0x7ffc6e0f7000) + fn get_mappings_for(map: &str, linux_gate_loc: u64) -> Vec { + MappingInfo::aggregate( + MemoryMaps::from_reader(map.as_bytes()).expect("failed to read mapping info"), + linux_gate_loc, + ) + .unwrap_or_default() } + const LINES: &str = "\ +5597483fc000-5597483fe000 r--p 00000000 00:31 4750073 /usr/bin/cat +5597483fe000-559748402000 r-xp 00002000 00:31 4750073 /usr/bin/cat +559748402000-559748404000 r--p 00006000 00:31 4750073 /usr/bin/cat +559748404000-559748405000 r--p 00007000 00:31 4750073 /usr/bin/cat +559748405000-559748406000 rw-p 00008000 00:31 4750073 /usr/bin/cat +559749b0e000-559749b2f000 rw-p 00000000 00:00 0 [heap] +7efd968d3000-7efd968f5000 rw-p 00000000 00:00 0 +7efd968f5000-7efd9694a000 r--p 00000000 00:31 5004638 /usr/lib/locale/en_US.utf8/LC_CTYPE +7efd9694a000-7efd96bc2000 r--p 00000000 00:31 5004373 /usr/lib/locale/en_US.utf8/LC_COLLATE +7efd96bc2000-7efd96bc4000 rw-p 00000000 00:00 0 +7efd96bc4000-7efd96bea000 r--p 00000000 00:31 4996104 /lib64/libc-2.32.so +7efd96bea000-7efd96d39000 r-xp 00026000 00:31 4996104 /lib64/libc-2.32.so +7efd96d39000-7efd96d85000 r--p 00175000 00:31 4996104 /lib64/libc-2.32.so +7efd96d85000-7efd96d86000 ---p 001c1000 00:31 4996104 /lib64/libc-2.32.so +7efd96d86000-7efd96d89000 r--p 001c1000 00:31 4996104 /lib64/libc-2.32.so +7efd96d89000-7efd96d8c000 rw-p 001c4000 00:31 4996104 /lib64/libc-2.32.so +7efd96d8c000-7efd96d92000 ---p 00000000 00:00 0 +7efd96da0000-7efd96da1000 r--p 00000000 00:31 5004379 /usr/lib/locale/en_US.utf8/LC_NUMERIC +7efd96da1000-7efd96da2000 r--p 00000000 00:31 5004382 /usr/lib/locale/en_US.utf8/LC_TIME +7efd96da2000-7efd96da3000 r--p 00000000 00:31 5004377 /usr/lib/locale/en_US.utf8/LC_MONETARY +7efd96da3000-7efd96da4000 r--p 00000000 00:31 5004376 /usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES +7efd96da4000-7efd96da5000 r--p 00000000 00:31 5004380 /usr/lib/locale/en_US.utf8/LC_PAPER +7efd96da5000-7efd96da6000 r--p 00000000 00:31 5004378 /usr/lib/locale/en_US.utf8/LC_NAME +7efd96da6000-7efd96da7000 r--p 00000000 00:31 5004372 /usr/lib/locale/en_US.utf8/LC_ADDRESS +7efd96da7000-7efd96da8000 r--p 00000000 00:31 5004381 /usr/lib/locale/en_US.utf8/LC_TELEPHONE +7efd96da8000-7efd96da9000 r--p 00000000 00:31 5004375 /usr/lib/locale/en_US.utf8/LC_MEASUREMENT +7efd96da9000-7efd96db0000 r--s 00000000 00:31 5004639 /usr/lib64/gconv/gconv-modules.cache +7efd96db0000-7efd96db1000 r--p 00000000 00:31 5004374 /usr/lib/locale/en_US.utf8/LC_IDENTIFICATION +7efd96db1000-7efd96db2000 r--p 00000000 00:31 4996100 /lib64/ld-2.32.so +7efd96db2000-7efd96dd3000 r-xp 00001000 00:31 4996100 /lib64/ld-2.32.so +7efd96dd3000-7efd96ddc000 r--p 00022000 00:31 4996100 /lib64/ld-2.32.so +7efd96ddc000-7efd96ddd000 r--p 0002a000 00:31 4996100 /lib64/ld-2.32.so +7efd96ddd000-7efd96ddf000 rw-p 0002b000 00:31 4996100 /lib64/ld-2.32.so +7ffc6dfda000-7ffc6dffb000 rw-p 00000000 00:00 0 [stack] +7ffc6e0f3000-7ffc6e0f7000 r--p 00000000 00:00 0 [vvar] +7ffc6e0f7000-7ffc6e0f9000 r-xp 00000000 00:00 0 [vdso] +ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]"; + const LINUX_GATE_LOC: u64 = 0x7ffc6e0f7000; + fn get_all_mappings() -> Vec { - let mut mappings: Vec = Vec::new(); - let (lines, linux_gate_loc) = get_lines_and_loc(); - // Only /usr/bin/cat and [heap] - for line in lines { - match MappingInfo::parse_from_line(line, linux_gate_loc, mappings.last_mut()) - .expect("failed to read mapping info") - { - MappingInfoParsingResult::Success(map) => mappings.push(map), - MappingInfoParsingResult::SkipLine => continue, - } - } - assert_eq!(mappings.len(), 23); - mappings + get_mappings_for(LINES, LINUX_GATE_LOC) } #[test] fn test_merged() { - let mut mappings: Vec = Vec::new(); - let (lines, linux_gate_loc) = get_lines_and_loc(); // Only /usr/bin/cat and [heap] - for line in lines[0..=6].iter() { - match MappingInfo::parse_from_line(line, linux_gate_loc, mappings.last_mut()) - .expect("failed to read mapping info") - { - MappingInfoParsingResult::Success(map) => mappings.push(map), - MappingInfoParsingResult::SkipLine => continue, - } - } + let mappings = get_mappings_for( + "\ +5597483fc000-5597483fe000 r--p 00000000 00:31 4750073 /usr/bin/cat +5597483fe000-559748402000 r-xp 00002000 00:31 4750073 /usr/bin/cat +559748402000-559748404000 r--p 00006000 00:31 4750073 /usr/bin/cat +559748404000-559748405000 r--p 00007000 00:31 4750073 /usr/bin/cat +559748405000-559748406000 rw-p 00008000 00:31 4750073 /usr/bin/cat +559749b0e000-559749b2f000 rw-p 00000000 00:00 0 [heap] +7efd968d3000-7efd968f5000 rw-p 00000000 00:00 0 ", + 0x7ffc6e0f7000, + ); assert_eq!(mappings.len(), 3); let cat_map = MappingInfo { @@ -454,7 +423,7 @@ mod tests { }, offset: 0, executable: true, - name: Some("/usr/bin/cat".to_string()), + name: Some("/usr/bin/cat".into()), }; assert_eq!(mappings[0], cat_map); @@ -468,7 +437,7 @@ mod tests { }, offset: 0, executable: false, - name: Some("[heap]".to_string()), + name: Some("[heap]".into()), }; assert_eq!(mappings[1], heap_map); @@ -501,7 +470,7 @@ mod tests { }, offset: 0, executable: true, - name: Some("linux-gate.so".to_string()), + name: Some("linux-gate.so".into()), }; assert_eq!(mappings[21], gate_map); @@ -511,35 +480,35 @@ mod tests { fn test_reading_all() { let mappings = get_all_mappings(); - let found_items = vec![ - Some("/usr/bin/cat".to_string()), - Some("[heap]".to_string()), + let found_items: Vec> = vec![ + Some("/usr/bin/cat".into()), + Some("[heap]".into()), None, - Some("/usr/lib/locale/en_US.utf8/LC_CTYPE".to_string()), - Some("/usr/lib/locale/en_US.utf8/LC_COLLATE".to_string()), + Some("/usr/lib/locale/en_US.utf8/LC_CTYPE".into()), + Some("/usr/lib/locale/en_US.utf8/LC_COLLATE".into()), None, - Some("/lib64/libc-2.32.so".to_string()), + Some("/lib64/libc-2.32.so".into()), // The original shows a None here, but this is an address ranges that the // linker reserved but which a loaded library did not use. These // appear as an anonymous private mapping with no access flags set // and which directly follow an executable mapping. - Some("/usr/lib/locale/en_US.utf8/LC_NUMERIC".to_string()), - Some("/usr/lib/locale/en_US.utf8/LC_TIME".to_string()), - Some("/usr/lib/locale/en_US.utf8/LC_MONETARY".to_string()), - Some("/usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES".to_string()), - Some("/usr/lib/locale/en_US.utf8/LC_PAPER".to_string()), - Some("/usr/lib/locale/en_US.utf8/LC_NAME".to_string()), - Some("/usr/lib/locale/en_US.utf8/LC_ADDRESS".to_string()), - Some("/usr/lib/locale/en_US.utf8/LC_TELEPHONE".to_string()), - Some("/usr/lib/locale/en_US.utf8/LC_MEASUREMENT".to_string()), - Some("/usr/lib64/gconv/gconv-modules.cache".to_string()), - Some("/usr/lib/locale/en_US.utf8/LC_IDENTIFICATION".to_string()), - Some("/lib64/ld-2.32.so".to_string()), - Some("[stack]".to_string()), - Some("[vvar]".to_string()), + Some("/usr/lib/locale/en_US.utf8/LC_NUMERIC".into()), + Some("/usr/lib/locale/en_US.utf8/LC_TIME".into()), + Some("/usr/lib/locale/en_US.utf8/LC_MONETARY".into()), + Some("/usr/lib/locale/en_US.utf8/LC_MESSAGES/SYS_LC_MESSAGES".into()), + Some("/usr/lib/locale/en_US.utf8/LC_PAPER".into()), + Some("/usr/lib/locale/en_US.utf8/LC_NAME".into()), + Some("/usr/lib/locale/en_US.utf8/LC_ADDRESS".into()), + Some("/usr/lib/locale/en_US.utf8/LC_TELEPHONE".into()), + Some("/usr/lib/locale/en_US.utf8/LC_MEASUREMENT".into()), + Some("/usr/lib64/gconv/gconv-modules.cache".into()), + Some("/usr/lib/locale/en_US.utf8/LC_IDENTIFICATION".into()), + Some("/lib64/ld-2.32.so".into()), + Some("[stack]".into()), + Some("[vvar]".into()), // This is rewritten from [vdso] to linux-gate.so - Some("linux-gate.so".to_string()), - Some("[vsyscall]".to_string()), + Some("linux-gate.so".into()), + Some("[vsyscall]".into()), ]; assert_eq!( @@ -561,7 +530,7 @@ mod tests { }, offset: 0, executable: true, - name: Some("/lib64/libc-2.32.so".to_string()), + name: Some("/lib64/libc-2.32.so".into()), }; assert_eq!(mappings[6], gate_map); @@ -569,89 +538,40 @@ mod tests { #[test] fn test_get_mapping_effective_name() { - let lines = vec![ -"7f0b97b6f000-7f0b97b70000 r--p 00000000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so", -"7f0b97b70000-7f0b97b71000 r-xp 00000000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so", -"7f0b97b71000-7f0b97b73000 r--p 00000000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so", -"7f0b97b73000-7f0b97b74000 rw-p 00001000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so", - ]; - let linux_gate_loc = 0x7ffe091bf000; - let mut mappings: Vec = Vec::new(); - for line in lines { - match MappingInfo::parse_from_line(line, linux_gate_loc, mappings.last_mut()) - .expect("failed to read mapping info") - { - MappingInfoParsingResult::Success(map) => mappings.push(map), - MappingInfoParsingResult::SkipLine => continue, - } - } + let mappings = get_mappings_for( + "\ +7f0b97b6f000-7f0b97b70000 r--p 00000000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so +7f0b97b70000-7f0b97b71000 r-xp 00000000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so +7f0b97b71000-7f0b97b73000 r--p 00000000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so +7f0b97b73000-7f0b97b74000 rw-p 00001000 00:3e 27136458 /home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so", + 0x7ffe091bf000, + ); assert_eq!(mappings.len(), 1); let (file_path, file_name) = mappings[0] - .get_mapping_effective_name_and_path() + .get_mapping_effective_path_and_name() .expect("Couldn't get effective name for mapping"); assert_eq!(file_name, "libmozgtk.so"); - assert_eq!(file_path, "/home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so"); - } - - #[test] - fn test_whitespaces_in_maps() { - let lines = vec![ -" 7f0b97b6f000-7f0b97b70000 r--p 00000000 00:3e 27136458 libmozgtk.so", -"7f0b97b70000-7f0b97b71000 r-xp 00000000 00:3e 27136458 libmozgtk.so ", -"7f0b97b71000-7f0b97b73000 r--p 00000000 00:3e 27136458\t\t\tlibmozgtk.so", - ]; - let linux_gate_loc = 0x7ffe091bf000; - let mut mappings: Vec = Vec::new(); - for line in lines { - match MappingInfo::parse_from_line(line, linux_gate_loc, mappings.last_mut()) - .expect("failed to read mapping info") - { - MappingInfoParsingResult::Success(map) => mappings.push(map), - MappingInfoParsingResult::SkipLine => continue, - } - } - assert_eq!(mappings.len(), 1); - - let expected_map = MappingInfo { - start_address: 0x7f0b97b6f000, - size: 16384, - system_mapping_info: SystemMappingInfo { - start_address: 0x7f0b97b6f000, - end_address: 0x7f0b97b73000, - }, - offset: 0, - executable: true, - name: Some("libmozgtk.so".to_string()), - }; - - assert_eq!(expected_map, mappings[0]); + assert_eq!(file_path, PathBuf::from("/home/martin/Documents/mozilla/devel/mozilla-central/obj/widget/gtk/mozgtk/gtk3/libmozgtk.so")); } #[test] fn test_whitespaces_in_name() { - let lines = vec![ -"10000000-20000000 r--p 00000000 00:3e 27136458 libmoz gtk.so", -"20000000-30000000 r--p 00000000 00:3e 27136458 libmozgtk.so (deleted)", -"30000000-40000000 r--p 00000000 00:3e 27136458 \"libmoz gtk.so (deleted)\"", -"30000000-40000000 r--p 00000000 00:3e 27136458 ", - ]; - let linux_gate_loc = 0x7ffe091bf000; - let mut mappings: Vec = Vec::new(); - for line in lines { - match MappingInfo::parse_from_line(line, linux_gate_loc, mappings.last_mut()) - .expect("failed to read mapping info") - { - MappingInfoParsingResult::Success(map) => mappings.push(map), - MappingInfoParsingResult::SkipLine => continue, - } - } + let mappings = get_mappings_for( + "\ +10000000-20000000 r--p 00000000 00:3e 27136458 libmoz gtk.so +20000000-30000000 r--p 00000000 00:3e 27136458 libmozgtk.so (deleted) +30000000-40000000 r--p 00000000 00:3e 27136458 \"libmoz gtk.so (deleted)\" +30000000-40000000 r--p 00000000 00:3e 27136458 ", + 0x7ffe091bf000, + ); + assert_eq!(mappings.len(), 4); - assert_eq!(mappings[0].name, Some("libmoz gtk.so".to_string())); - assert_eq!(mappings[1].name, Some("libmozgtk.so (deleted)".to_string())); + assert_eq!(mappings[0].name, Some("libmoz gtk.so".into())); + assert_eq!(mappings[1].name, Some("libmozgtk.so (deleted)".into())); assert_eq!( mappings[2].name, - Some("\"libmoz gtk.so (deleted)\"".to_string()) + Some("\"libmoz gtk.so (deleted)\"".into()) ); assert_eq!(mappings[3].name, None); } diff --git a/src/linux/minidump_writer.rs b/src/linux/minidump_writer.rs index 606a8590..f864269c 100644 --- a/src/linux/minidump_writer.rs +++ b/src/linux/minidump_writer.rs @@ -187,8 +187,8 @@ impl MinidumpWriter { destination: &mut (impl Write + Seek), ) -> Result<()> { // A minidump file contains a number of tagged streams. This is the number - // of stream which we write. - let num_writers = 14u32; + // of streams which we write. + let num_writers = 15u32; let mut header_section = MemoryWriter::::alloc(buffer)?; @@ -237,6 +237,10 @@ impl MinidumpWriter { // Write section to file dir_section.write_to_file(buffer, Some(dirent))?; + let dirent = memory_info_list_stream::write(self, buffer)?; + // Write section to file + dir_section.write_to_file(buffer, Some(dirent))?; + let dirent = match self.write_file(buffer, "/proc/cpuinfo") { Ok(location) => MDRawDirectory { stream_type: MDStreamType::LinuxCpuInfo as u32, @@ -322,8 +326,7 @@ impl MinidumpWriter { // Write section to file dir_section.write_to_file(buffer, Some(dirent))?; - // If you add more directory entries, don't forget to update kNumWriters, - // above. + // If you add more directory entries, don't forget to update num_writers, above. Ok(()) } diff --git a/src/linux/ptrace_dumper.rs b/src/linux/ptrace_dumper.rs index 2380afb0..1dad9c31 100644 --- a/src/linux/ptrace_dumper.rs +++ b/src/linux/ptrace_dumper.rs @@ -4,7 +4,7 @@ use crate::{ linux::{ auxv_reader::{AuxvType, ProcfsAuxvIter}, errors::{DumperError, InitError, ThreadInfoError}, - maps_reader::{MappingInfo, MappingInfoParsingResult, DELETED_SUFFIX}, + maps_reader::MappingInfo, thread_info::{Pid, ThreadInfo}, LINUX_GATE_LIBRARY_NAME, }, @@ -15,13 +15,7 @@ use nix::{ errno::Errno, sys::{ptrace, wait}, }; -use std::{ - collections::HashMap, - ffi::c_void, - io::{BufRead, BufReader}, - path, - result::Result, -}; +use std::{collections::HashMap, ffi::c_void, io::BufReader, path, result::Result}; #[derive(Debug, Clone)] pub struct Thread { @@ -280,15 +274,10 @@ impl PtraceDumper { let maps_path = path::PathBuf::from(&filename); let maps_file = std::fs::File::open(maps_path).map_err(errmap)?; - for line in BufReader::new(maps_file).lines() { - // /proc//maps looks like this - // 7fe34a863000-7fe34a864000 rw-p 00009000 00:31 4746408 /usr/lib64/libogg.so.0.8.4 - let line = line.map_err(errmap)?; - match MappingInfo::parse_from_line(&line, linux_gate_loc, self.mappings.last_mut()) { - Ok(MappingInfoParsingResult::Success(map)) => self.mappings.push(map), - Ok(MappingInfoParsingResult::SkipLine) | Err(_) => continue, - } - } + self.mappings = procfs::process::MemoryMaps::from_reader(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; @@ -374,7 +363,7 @@ impl PtraceDumper { // the bitfield length is 2^test_bits long. let test_bits = 11; // byte length of the corresponding array. - let array_size = 1 << (test_bits - 3); + let array_size: usize = 1 << (test_bits - 3); let array_mask = array_size - 1; // The amount to right shift pointers by. This captures the top bits // on 32 bit architectures. On 64 bit architectures this would be @@ -553,7 +542,7 @@ impl PtraceDumper { } // Special-case linux-gate because it's not a real file. - if mapping.name.as_deref() == Some(LINUX_GATE_LIBRARY_NAME) { + if mapping.name.as_deref() == Some(LINUX_GATE_LIBRARY_NAME.as_ref()) { if pid == std::process::id().try_into()? { let mem_slice = unsafe { std::slice::from_raw_parts(mapping.start_address as *const u8, mapping.size) @@ -568,26 +557,16 @@ impl PtraceDumper { return Self::elf_file_identifier_from_mapped_file(&mem_slice); } } - let new_name = MappingInfo::handle_deleted_file_in_mapping( - mapping.name.as_deref().unwrap_or_default(), - pid, - )?; - let mem_slice = MappingInfo::get_mmap(&Some(new_name.clone()), mapping.offset)?; + let (filename, old_name) = mapping.fixup_deleted_file(pid)?; + + let mem_slice = MappingInfo::get_mmap(&Some(filename), mapping.offset)?; let build_id = Self::elf_file_identifier_from_mapped_file(&mem_slice)?; - // This means we switched from "/my/binary" to "/proc/1234/exe", because /my/binary - // was deleted and thus has a "/my/binary (deleted)" entry. We found the mapping anyway - // so we remove the "(deleted)". - if let Some(old_name) = &mapping.name { - if &new_name != old_name { - mapping.name = Some( - old_name - .trim_end_matches(DELETED_SUFFIX) - .trim_end() - .to_string(), - ); - } + // This means we switched from "/my/binary" to "/proc/1234/exe", change the mapping to + // remove the " (deleted)" portion. + if let Some(old_name) = old_name { + mapping.name = Some(old_name.into()); } Ok(build_id) } diff --git a/src/linux/sections.rs b/src/linux/sections.rs index c7c4172c..3f7d3004 100644 --- a/src/linux/sections.rs +++ b/src/linux/sections.rs @@ -1,6 +1,7 @@ pub mod app_memory; pub mod exception_stream; pub mod mappings; +pub mod memory_info_list_stream; pub mod memory_list_stream; pub mod systeminfo_stream; pub mod thread_list_stream; diff --git a/src/linux/sections/mappings.rs b/src/linux/sections/mappings.rs index 7eb002d0..de19c540 100644 --- a/src/linux/sections/mappings.rs +++ b/src/linux/sections/mappings.rs @@ -84,9 +84,9 @@ fn fill_raw_module( }; let (file_path, _) = mapping - .get_mapping_effective_name_and_path() + .get_mapping_effective_path_and_name() .map_err(|e| errors::SectionMappingsError::GetEffectivePathError(mapping.clone(), e))?; - let name_header = write_string_to_location(buffer, &file_path)?; + let name_header = write_string_to_location(buffer, file_path.to_string_lossy().as_ref())?; Ok(MDRawModule { base_of_image: mapping.start_address as u64, diff --git a/src/linux/sections/memory_info_list_stream.rs b/src/linux/sections/memory_info_list_stream.rs new file mode 100644 index 00000000..01d2001e --- /dev/null +++ b/src/linux/sections/memory_info_list_stream.rs @@ -0,0 +1,66 @@ +use super::*; +use minidump_common::format::{MemoryProtection, MemoryState, MemoryType}; +use procfs::process::MMPermissions; + +/// Write a MemoryInfoListStream using information from procfs. +pub fn write( + config: &mut MinidumpWriter, + buffer: &mut DumpBuf, +) -> Result { + let process = procfs::process::Process::new(config.blamed_thread)?; + let maps = process.maps()?; + + let list_header = MemoryWriter::alloc_with_val( + buffer, + MDMemoryInfoList { + size_of_header: std::mem::size_of::() as u32, + size_of_entry: std::mem::size_of::() as u32, + number_of_entries: maps.memory_maps.len() as u64, + }, + )?; + + let mut dirent = MDRawDirectory { + stream_type: MDStreamType::MemoryInfoListStream as u32, + location: list_header.location(), + }; + + let block_list = MemoryArrayWriter::::alloc_from_iter( + buffer, + maps.memory_maps.iter().map(|mm| MDMemoryInfo { + base_address: mm.address.0, + allocation_base: mm.address.0, + allocation_protection: get_memory_protection(mm.perms).bits(), + __alignment1: 0, + region_size: mm.address.1 - mm.address.0, + state: MemoryState::MEM_COMMIT.bits(), + protection: get_memory_protection(mm.perms).bits(), + _type: if mm.perms.contains(MMPermissions::PRIVATE) { + MemoryType::MEM_PRIVATE + } else { + MemoryType::MEM_MAPPED + } + .bits(), + __alignment2: 0, + }), + )?; + + dirent.location.data_size += block_list.location().data_size; + + Ok(dirent) +} + +fn get_memory_protection(permissions: MMPermissions) -> MemoryProtection { + let read = permissions.contains(MMPermissions::READ); + let write = permissions.contains(MMPermissions::WRITE); + let exec = permissions.contains(MMPermissions::EXECUTE); + match (read, write, exec) { + (false, false, false) => MemoryProtection::PAGE_NOACCESS, + (false, false, true) => MemoryProtection::PAGE_EXECUTE, + (true, false, false) => MemoryProtection::PAGE_READONLY, + (true, false, true) => MemoryProtection::PAGE_EXECUTE_READ, + // No support for write-only + (true | false, true, false) => MemoryProtection::PAGE_READWRITE, + // No support for execute+write-only + (true | false, true, true) => MemoryProtection::PAGE_EXECUTE_READWRITE, + } +} diff --git a/src/minidump_format.rs b/src/minidump_format.rs index 8ffa40fc..9ced2b65 100644 --- a/src/minidump_format.rs +++ b/src/minidump_format.rs @@ -3,7 +3,8 @@ pub use minidump_common::format::{ ProcessorArchitecture as MDCPUArchitecture, GUID, MINIDUMP_DIRECTORY as MDRawDirectory, MINIDUMP_EXCEPTION as MDException, MINIDUMP_EXCEPTION_STREAM as MDRawExceptionStream, MINIDUMP_HEADER as MDRawHeader, MINIDUMP_LOCATION_DESCRIPTOR as MDLocationDescriptor, - MINIDUMP_MEMORY_DESCRIPTOR as MDMemoryDescriptor, MINIDUMP_MODULE as MDRawModule, + MINIDUMP_MEMORY_DESCRIPTOR as MDMemoryDescriptor, MINIDUMP_MEMORY_INFO as MDMemoryInfo, + MINIDUMP_MEMORY_INFO_LIST as MDMemoryInfoList, MINIDUMP_MODULE as MDRawModule, MINIDUMP_SIGNATURE as MD_HEADER_SIGNATURE, MINIDUMP_STREAM_TYPE as MDStreamType, MINIDUMP_SYSTEM_INFO as MDRawSystemInfo, MINIDUMP_THREAD as MDRawThread, MINIDUMP_THREAD_NAME as MDRawThreadName, MINIDUMP_VERSION as MD_HEADER_VERSION, diff --git a/tests/linux_minidump_writer.rs b/tests/linux_minidump_writer.rs index cd2ca737..e7a230e3 100644 --- a/tests/linux_minidump_writer.rs +++ b/tests/linux_minidump_writer.rs @@ -26,11 +26,22 @@ use common::*; #[derive(Debug, PartialEq)] enum Context { - #[cfg(not(any(target_arch = "mips", target_arch = "arm")))] With, Without, } +impl Context { + pub fn minidump_writer(&self, pid: Pid) -> MinidumpWriter { + let mut mw = MinidumpWriter::new(pid, pid); + #[cfg(not(any(target_arch = "mips", target_arch = "arm")))] + if self == &Context::With { + let crash_context = get_crash_context(pid); + mw.set_crash_context(crash_context); + } + mw + } +} + #[cfg(not(any(target_arch = "mips", target_arch = "arm")))] fn get_ucontext() -> Result { let mut context = std::mem::MaybeUninit::uninit(); @@ -58,243 +69,408 @@ fn get_crash_context(tid: Pid) -> CrashContext { } } -fn test_write_dump_helper(context: Context) { - let num_of_threads = 3; - let mut child = start_child_and_wait_for_threads(num_of_threads); - let pid = child.id() as i32; +macro_rules! contextual_tests { + () => {}; + ( fn $name:ident ($ctx:ident : Context) $body:block $($rest:tt)* ) => { + mod $name { + use super::*; - let mut tmpfile = tempfile::Builder::new() - .prefix("write_dump") - .tempfile() - .unwrap(); + fn test($ctx: Context) $body + + #[test] + fn run() { + test(Context::Without) + } - let mut tmp = MinidumpWriter::new(pid, pid); - #[cfg(not(any(target_arch = "mips", target_arch = "arm")))] - if context == Context::With { - let crash_context = get_crash_context(pid); - tmp.set_crash_context(crash_context); + #[cfg(not(any(target_arch = "mips", target_arch = "arm")))] + #[test] + fn run_with_context() { + test(Context::With) + } + } + contextual_tests! { $($rest)* } } - let in_memory_buffer = tmp.dump(&mut tmpfile).expect("Could not write minidump"); - child.kill().expect("Failed to kill process"); +} - // Reap child - let waitres = child.wait().expect("Failed to wait for child"); - let status = waitres.signal().expect("Child did not die due to signal"); - assert_eq!(waitres.code(), None); - assert_eq!(status, Signal::SIGKILL as i32); +contextual_tests! { + fn test_write_dump(context: Context) { + let num_of_threads = 3; + let mut child = start_child_and_wait_for_threads(num_of_threads); + let pid = child.id() as i32; - let meta = std::fs::metadata(tmpfile.path()).expect("Couldn't get metadata for tempfile"); - assert!(meta.len() > 0); + let mut tmpfile = tempfile::Builder::new() + .prefix("write_dump") + .tempfile() + .unwrap(); - let mem_slice = std::fs::read(tmpfile.path()).expect("Failed to minidump"); - assert_eq!(mem_slice.len(), in_memory_buffer.len()); - assert_eq!(mem_slice, in_memory_buffer); -} + let mut tmp = context.minidump_writer(pid); + let in_memory_buffer = tmp.dump(&mut tmpfile).expect("Could not write minidump"); + child.kill().expect("Failed to kill process"); -#[test] -fn test_write_dump() { - test_write_dump_helper(Context::Without) -} + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); -#[cfg(not(any(target_arch = "mips", target_arch = "arm")))] -#[test] -fn test_write_dump_with_context() { - test_write_dump_helper(Context::With) -} + let meta = std::fs::metadata(tmpfile.path()).expect("Couldn't get metadata for tempfile"); + assert!(meta.len() > 0); -fn test_write_and_read_dump_from_parent_helper(context: Context) { - let mut child = start_child_and_return(&["spawn_mmap_wait"]); - let pid = child.id() as i32; + let mem_slice = std::fs::read(tmpfile.path()).expect("Failed to minidump"); + assert_eq!(mem_slice.len(), in_memory_buffer.len()); + assert_eq!(mem_slice, in_memory_buffer); + } - let mut tmpfile = tempfile::Builder::new() - .prefix("write_and_read_dump") - .tempfile() - .unwrap(); + fn test_write_and_read_dump_from_parent(context: Context) { + let mut child = start_child_and_return(&["spawn_mmap_wait"]); + let pid = child.id() as i32; + + let mut tmpfile = tempfile::Builder::new() + .prefix("write_and_read_dump") + .tempfile() + .unwrap(); - let mut f = BufReader::new(child.stdout.as_mut().expect("Can't open stdout")); - let mut buf = String::new(); - let _ = f - .read_line(&mut buf) - .expect("Couldn't read address provided by child"); - let mut output = buf.split_whitespace(); - let mmap_addr = output - .next() - .unwrap() - .parse() - .expect("unable to parse mmap_addr"); - let memory_size = output - .next() - .unwrap() - .parse() - .expect("unable to parse memory_size"); - // Add information about the mapped memory. - let mapping = MappingInfo { - start_address: mmap_addr, - size: memory_size, - offset: 0, - executable: false, - name: Some("a fake mapping".to_string()), - system_mapping_info: SystemMappingInfo { + let mut f = BufReader::new(child.stdout.as_mut().expect("Can't open stdout")); + let mut buf = String::new(); + let _ = f + .read_line(&mut buf) + .expect("Couldn't read address provided by child"); + let mut output = buf.split_whitespace(); + let mmap_addr = output + .next() + .unwrap() + .parse() + .expect("unable to parse mmap_addr"); + let memory_size = output + .next() + .unwrap() + .parse() + .expect("unable to parse memory_size"); + // Add information about the mapped memory. + let mapping = MappingInfo { start_address: mmap_addr, - end_address: mmap_addr + memory_size, - }, - }; + size: memory_size, + offset: 0, + executable: false, + name: Some("a fake mapping".into()), + system_mapping_info: SystemMappingInfo { + start_address: mmap_addr, + end_address: mmap_addr + memory_size, + }, + }; + + let identifier = vec![ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, + 0xFF, + ]; + let entry = MappingEntry { + mapping, + identifier, + }; + + let mut tmp = context.minidump_writer(pid); + + tmp.set_user_mapping_list(vec![entry]) + .dump(&mut tmpfile) + .expect("Could not write minidump"); - let identifier = vec![ - 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, - 0xFF, - ]; - let entry = MappingEntry { - mapping, - identifier, - }; + child.kill().expect("Failed to kill process"); + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); - let mut tmp = MinidumpWriter::new(pid, pid); - #[cfg(not(any(target_arch = "mips", target_arch = "arm")))] - if context == Context::With { - let crash_context = get_crash_context(pid); - tmp.set_crash_context(crash_context); + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + let module_list: MinidumpModuleList = dump + .get_stream() + .expect("Couldn't find stream MinidumpModuleList"); + let module = module_list + .module_at_address(mmap_addr as u64) + .expect("Couldn't find user mapping module"); + assert_eq!(module.base_address(), mmap_addr as u64); + assert_eq!(module.size(), memory_size as u64); + assert_eq!(module.code_file(), "a fake mapping"); + assert_eq!( + module.debug_identifier(), + Some("33221100554477668899AABBCCDDEEFF0".parse().unwrap()) + ); + + let _: MinidumpException = dump.get_stream().expect("Couldn't find MinidumpException"); + let _: MinidumpThreadList = dump.get_stream().expect("Couldn't find MinidumpThreadList"); + let _: MinidumpMemoryList = dump.get_stream().expect("Couldn't find MinidumpMemoryList"); + let _: MinidumpSystemInfo = dump.get_stream().expect("Couldn't find MinidumpSystemInfo"); + let _ = dump + .get_raw_stream(LinuxCpuInfo as u32) + .expect("Couldn't find LinuxCpuInfo"); + let _ = dump + .get_raw_stream(LinuxProcStatus as u32) + .expect("Couldn't find LinuxProcStatus"); + let _ = dump + .get_raw_stream(LinuxCmdLine as u32) + .expect("Couldn't find LinuxCmdLine"); + let _ = dump + .get_raw_stream(LinuxEnviron as u32) + .expect("Couldn't find LinuxEnviron"); + let _ = dump + .get_raw_stream(LinuxAuxv as u32) + .expect("Couldn't find LinuxAuxv"); + let _ = dump + .get_raw_stream(LinuxMaps as u32) + .expect("Couldn't find LinuxMaps"); + let _ = dump + .get_raw_stream(LinuxDsoDebug as u32) + .expect("Couldn't find LinuxDsoDebug"); } - tmp.set_user_mapping_list(vec![entry]) - .dump(&mut tmpfile) - .expect("Could not write minidump"); + fn test_write_with_additional_memory(context: Context) { + let mut child = start_child_and_return(&["spawn_alloc_wait"]); + let pid = child.id() as i32; - child.kill().expect("Failed to kill process"); - // Reap child - let waitres = child.wait().expect("Failed to wait for child"); - let status = waitres.signal().expect("Child did not die due to signal"); - assert_eq!(waitres.code(), None); - assert_eq!(status, Signal::SIGKILL as i32); + let mut tmpfile = tempfile::Builder::new() + .prefix("additional_memory") + .tempfile() + .unwrap(); - let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); - let module_list: MinidumpModuleList = dump - .get_stream() - .expect("Couldn't find stream MinidumpModuleList"); - let module = module_list - .module_at_address(mmap_addr as u64) - .expect("Couldn't find user mapping module"); - assert_eq!(module.base_address(), mmap_addr as u64); - assert_eq!(module.size(), memory_size as u64); - assert_eq!(module.code_file(), "a fake mapping"); - assert_eq!( - module.debug_identifier(), - Some("33221100554477668899AABBCCDDEEFF0".parse().unwrap()) - ); + let mut f = BufReader::new(child.stdout.as_mut().expect("Can't open stdout")); + let mut buf = String::new(); + let _ = f + .read_line(&mut buf) + .expect("Couldn't read address provided by child"); + let mut output = buf.split_whitespace(); + let memory_addr = usize::from_str_radix(output.next().unwrap().trim_start_matches("0x"), 16) + .expect("unable to parse mmap_addr"); + let memory_size = output + .next() + .unwrap() + .parse() + .expect("unable to parse memory_size"); + + let app_memory = AppMemory { + ptr: memory_addr, + length: memory_size, + }; + + let mut tmp = context.minidump_writer(pid); + + tmp.set_app_memory(vec![app_memory]) + .dump(&mut tmpfile) + .expect("Could not write minidump"); - let _: MinidumpException = dump.get_stream().expect("Couldn't find MinidumpException"); - let _: MinidumpThreadList = dump.get_stream().expect("Couldn't find MinidumpThreadList"); - let _: MinidumpMemoryList = dump.get_stream().expect("Couldn't find MinidumpMemoryList"); - let _: MinidumpSystemInfo = dump.get_stream().expect("Couldn't find MinidumpSystemInfo"); - let _ = dump - .get_raw_stream(LinuxCpuInfo as u32) - .expect("Couldn't find LinuxCpuInfo"); - let _ = dump - .get_raw_stream(LinuxProcStatus as u32) - .expect("Couldn't find LinuxProcStatus"); - let _ = dump - .get_raw_stream(LinuxCmdLine as u32) - .expect("Couldn't find LinuxCmdLine"); - let _ = dump - .get_raw_stream(LinuxEnviron as u32) - .expect("Couldn't find LinuxEnviron"); - let _ = dump - .get_raw_stream(LinuxAuxv as u32) - .expect("Couldn't find LinuxAuxv"); - let _ = dump - .get_raw_stream(LinuxMaps as u32) - .expect("Couldn't find LinuxMaps"); - let _ = dump - .get_raw_stream(LinuxDsoDebug as u32) - .expect("Couldn't find LinuxDsoDebug"); -} + child.kill().expect("Failed to kill process"); + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); -#[test] -fn test_write_and_read_dump_from_parent() { - test_write_and_read_dump_from_parent_helper(Context::Without) -} + // Read dump file and check its contents + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); -#[cfg(not(any(target_arch = "mips", target_arch = "arm")))] -#[test] -fn test_write_and_read_dump_from_parent_with_context() { - test_write_and_read_dump_from_parent_helper(Context::With) -} + let section: MinidumpMemoryList = dump.get_stream().expect("Couldn't find MinidumpMemoryList"); + let region = section + .memory_at_address(memory_addr as u64) + .expect("Couldn't find memory region"); -fn test_write_with_additional_memory_helper(context: Context) { - let mut child = start_child_and_return(&["spawn_alloc_wait"]); - let pid = child.id() as i32; + assert_eq!(region.base_address, memory_addr as u64); + assert_eq!(region.size, memory_size as u64); - let mut tmpfile = tempfile::Builder::new() - .prefix("additional_memory") - .tempfile() - .unwrap(); + let mut values = Vec::::with_capacity(memory_size); + for idx in 0..memory_size { + values.push((idx % 255) as u8); + } - let mut f = BufReader::new(child.stdout.as_mut().expect("Can't open stdout")); - let mut buf = String::new(); - let _ = f - .read_line(&mut buf) - .expect("Couldn't read address provided by child"); - let mut output = buf.split_whitespace(); - let memory_addr = usize::from_str_radix(output.next().unwrap().trim_start_matches("0x"), 16) - .expect("unable to parse mmap_addr"); - let memory_size = output - .next() - .unwrap() - .parse() - .expect("unable to parse memory_size"); - - let app_memory = AppMemory { - ptr: memory_addr, - length: memory_size, - }; + // Verify memory contents. + assert_eq!(region.bytes, values); + } + + fn test_skip_if_requested(context: Context) { + let num_of_threads = 1; + let mut child = start_child_and_wait_for_threads(num_of_threads); + let pid = child.id() as i32; + + let mut tmpfile = tempfile::Builder::new() + .prefix("skip_if_requested") + .tempfile() + .unwrap(); - let mut tmp = MinidumpWriter::new(pid, pid); - #[cfg(not(any(target_arch = "mips", target_arch = "arm")))] - if context == Context::With { - let crash_context = get_crash_context(pid); - tmp.set_crash_context(crash_context); + let mut tmp = context.minidump_writer(pid); + + let pr_mapping_addr; + #[cfg(target_pointer_width = "64")] + { + pr_mapping_addr = 0x0102030405060708; + } + #[cfg(target_pointer_width = "32")] + { + pr_mapping_addr = 0x010203040; + }; + let res = tmp + .skip_stacks_if_mapping_unreferenced() + .set_principal_mapping_address(pr_mapping_addr) + .dump(&mut tmpfile); + child.kill().expect("Failed to kill process"); + + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); + + assert!(res.is_err()); } - tmp.set_app_memory(vec![app_memory]) - .dump(&mut tmpfile) - .expect("Could not write minidump"); + fn test_sanitized_stacks(context: Context) { + if context == Context::With { + // FIXME the context's stack pointer very often doesn't lie in mapped memory, resulting + // in the stack memory having 0 size (so no slice will match `defaced` in the + // assertion). + return; + } + let num_of_threads = 1; + let mut child = start_child_and_wait_for_threads(num_of_threads); + let pid = child.id() as i32; - child.kill().expect("Failed to kill process"); - // Reap child - let waitres = child.wait().expect("Failed to wait for child"); - let status = waitres.signal().expect("Child did not die due to signal"); - assert_eq!(waitres.code(), None); - assert_eq!(status, Signal::SIGKILL as i32); + let mut tmpfile = tempfile::Builder::new() + .prefix("sanitized_stacks") + .tempfile() + .unwrap(); - // Read dump file and check its contents - let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + let mut tmp = context.minidump_writer(pid); + tmp.sanitize_stack() + .dump(&mut tmpfile) + .expect("Faild to dump minidump"); + child.kill().expect("Failed to kill process"); + + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); - let section: MinidumpMemoryList = dump.get_stream().expect("Couldn't find MinidumpMemoryList"); - let region = section - .memory_at_address(memory_addr as u64) - .expect("Couldn't find memory region"); + // Read dump file and check its contents + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + let dump_array = std::fs::read(tmpfile.path()).expect("Failed to read minidump as vec"); + let thread_list: MinidumpThreadList = + dump.get_stream().expect("Couldn't find MinidumpThreadList"); - assert_eq!(region.base_address, memory_addr as u64); - assert_eq!(region.size, memory_size as u64); + let defaced; + #[cfg(target_pointer_width = "64")] + { + defaced = 0x0defaced0defacedusize.to_ne_bytes(); + } + #[cfg(target_pointer_width = "32")] + { + defaced = 0x0defacedusize.to_ne_bytes() + }; - let mut values = Vec::::with_capacity(memory_size); - for idx in 0..memory_size { - values.push((idx % 255) as u8); + for thread in thread_list.threads { + let mem = thread.raw.stack.memory; + let start = mem.rva as usize; + let end = (mem.rva + mem.data_size) as usize; + let slice = &dump_array.as_slice()[start..end]; + assert!(slice.windows(defaced.len()).any(|window| window == defaced)); + } } - // Verify memory contents. - assert_eq!(region.bytes, values); -} + fn test_write_early_abort(context: Context) { + let mut child = start_child_and_return(&["spawn_alloc_wait"]); + let pid = child.id() as i32; -#[test] -fn test_write_with_additional_memory() { - test_write_with_additional_memory_helper(Context::Without) -} + let mut tmpfile = tempfile::Builder::new() + .prefix("additional_memory") + .tempfile() + .unwrap(); -#[cfg(not(any(target_arch = "mips", target_arch = "arm")))] -#[test] -fn test_write_with_additional_memory_with_context() { - test_write_with_additional_memory_helper(Context::With) + let mut f = BufReader::new(child.stdout.as_mut().expect("Can't open stdout")); + let mut buf = String::new(); + let _ = f + .read_line(&mut buf) + .expect("Couldn't read address provided by child"); + let mut output = buf.split_whitespace(); + // We do not read the actual memory_address, but use NULL, which + // should create an error during dumping and lead to a truncated minidump. + let _ = usize::from_str_radix(output.next().unwrap().trim_start_matches("0x"), 16) + .expect("unable to parse mmap_addr"); + let memory_addr = 0; + let memory_size = output + .next() + .unwrap() + .parse() + .expect("unable to parse memory_size"); + + let app_memory = AppMemory { + ptr: memory_addr, + length: memory_size, + }; + + let mut tmp = context.minidump_writer(pid); + + // This should fail, because during the dump an error is detected (try_from fails) + match tmp.set_app_memory(vec![app_memory]).dump(&mut tmpfile) { + Err(WriterError::SectionAppMemoryError(_)) => (), + _ => panic!("Wrong kind of error returned"), + } + + child.kill().expect("Failed to kill process"); + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); + + // Read dump file and check its contents. There should be a truncated minidump available + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + // Should be there + let _: MinidumpThreadList = dump.get_stream().expect("Couldn't find MinidumpThreadList"); + let _: MinidumpModuleList = dump.get_stream().expect("Couldn't find MinidumpThreadList"); + + // Should be missing: + assert!(dump.get_stream::().is_err()); + } + + fn test_named_threads(context: Context) { + let num_of_threads = 5; + let mut child = start_child_and_wait_for_named_threads(num_of_threads); + let pid = child.id() as i32; + + let mut tmpfile = tempfile::Builder::new() + .prefix("named_threads") + .tempfile() + .unwrap(); + + let mut tmp = context.minidump_writer(pid); + let _ = tmp.dump(&mut tmpfile).expect("Could not write minidump"); + child.kill().expect("Failed to kill process"); + + // Reap child + let waitres = child.wait().expect("Failed to wait for child"); + let status = waitres.signal().expect("Child did not die due to signal"); + assert_eq!(waitres.code(), None); + assert_eq!(status, Signal::SIGKILL as i32); + + // Read dump file and check its contents. There should be a truncated minidump available + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + + let threads: MinidumpThreadList = dump.get_stream().expect("Couldn't find MinidumpThreadList"); + + let thread_names: MinidumpThreadNames = dump + .get_stream() + .expect("Couldn't find MinidumpThreadNames"); + + let thread_ids: Vec<_> = threads.threads.iter().map(|t| t.raw.thread_id).collect(); + let names: HashSet<_> = thread_ids + .iter() + .map(|id| thread_names.get_name(*id).unwrap_or_default()) + .map(|cow| cow.into_owned()) + .collect(); + let mut expected = HashSet::new(); + expected.insert("test".to_string()); + for id in 1..num_of_threads { + expected.insert(format!("thread_{}", id)); + } + assert_eq!(expected, names); + } } #[test] @@ -528,248 +704,24 @@ fn test_with_deleted_binary() { assert_eq!(main_module.debug_identifier(), filtered.parse().ok()); } -fn test_skip_if_requested_helper(context: Context) { - let num_of_threads = 1; - let mut child = start_child_and_wait_for_threads(num_of_threads); - let pid = child.id() as i32; - - let mut tmpfile = tempfile::Builder::new() - .prefix("skip_if_requested") - .tempfile() - .unwrap(); - - let mut tmp = MinidumpWriter::new(pid, pid); - #[cfg(not(any(target_arch = "mips", target_arch = "arm")))] - if context == Context::With { - let crash_context = get_crash_context(pid); - tmp.set_crash_context(crash_context); - } - - let pr_mapping_addr; - #[cfg(target_pointer_width = "64")] - { - pr_mapping_addr = 0x0102030405060708; - } - #[cfg(target_pointer_width = "32")] - { - pr_mapping_addr = 0x010203040; - }; - let res = tmp - .skip_stacks_if_mapping_unreferenced() - .set_principal_mapping_address(pr_mapping_addr) - .dump(&mut tmpfile); - child.kill().expect("Failed to kill process"); - - // Reap child - let waitres = child.wait().expect("Failed to wait for child"); - let status = waitres.signal().expect("Child did not die due to signal"); - assert_eq!(waitres.code(), None); - assert_eq!(status, Signal::SIGKILL as i32); - - assert!(res.is_err()); -} - -#[test] -fn test_skip_if_requested() { - test_skip_if_requested_helper(Context::Without) -} - -#[cfg(not(any(target_arch = "mips", target_arch = "arm")))] #[test] -fn test_skip_if_requested_with_context() { - test_skip_if_requested_helper(Context::With) -} - -fn test_sanitized_stacks_helper(context: Context) { - let num_of_threads = 1; - let mut child = start_child_and_wait_for_threads(num_of_threads); +fn test_memory_info_list_stream() { + let mut child = start_child_and_wait_for_threads(1); let pid = child.id() as i32; let mut tmpfile = tempfile::Builder::new() - .prefix("skip_if_requested") + .prefix("memory_info_list_stream") .tempfile() .unwrap(); - let mut tmp = MinidumpWriter::new(pid, pid); - #[cfg(not(any(target_arch = "mips", target_arch = "arm")))] - if context == Context::With { - let crash_context = get_crash_context(pid); - tmp.set_crash_context(crash_context); - } - tmp.sanitize_stack() + // Write a minidump + MinidumpWriter::new(pid, pid) .dump(&mut tmpfile) - .expect("Faild to dump minidump"); - child.kill().expect("Failed to kill process"); - - // Reap child - let waitres = child.wait().expect("Failed to wait for child"); - let status = waitres.signal().expect("Child did not die due to signal"); - assert_eq!(waitres.code(), None); - assert_eq!(status, Signal::SIGKILL as i32); - - // Read dump file and check its contents - let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); - let dump_array = std::fs::read(tmpfile.path()).expect("Failed to read minidump as vec"); - let thread_list: MinidumpThreadList = - dump.get_stream().expect("Couldn't find MinidumpThreadList"); - - let defaced; - #[cfg(target_pointer_width = "64")] - { - defaced = 0x0defaced0defacedusize.to_ne_bytes(); - } - #[cfg(target_pointer_width = "32")] - { - defaced = 0x0defacedusize.to_ne_bytes() - }; - - for thread in thread_list.threads { - let mem = thread.raw.stack.memory; - let start = mem.rva as usize; - let end = (mem.rva + mem.data_size) as usize; - let slice = &dump_array.as_slice()[start..end]; - assert!(slice.windows(defaced.len()).any(|window| window == defaced)); - } -} - -#[test] -fn test_sanitized_stacks() { - test_sanitized_stacks_helper(Context::Without) -} - -#[cfg(not(any(target_arch = "mips", target_arch = "arm")))] -#[test] -fn test_sanitized_stacks_with_context() { - test_sanitized_stacks_helper(Context::Without) -} - -fn test_write_early_abort_helper(context: Context) { - let mut child = start_child_and_return(&["spawn_alloc_wait"]); - let pid = child.id() as i32; - - let mut tmpfile = tempfile::Builder::new() - .prefix("additional_memory") - .tempfile() - .unwrap(); - - let mut f = BufReader::new(child.stdout.as_mut().expect("Can't open stdout")); - let mut buf = String::new(); - let _ = f - .read_line(&mut buf) - .expect("Couldn't read address provided by child"); - let mut output = buf.split_whitespace(); - // We do not read the actual memory_address, but use NULL, which - // should create an error during dumping and lead to a truncated minidump. - let _ = usize::from_str_radix(output.next().unwrap().trim_start_matches("0x"), 16) - .expect("unable to parse mmap_addr"); - let memory_addr = 0; - let memory_size = output - .next() - .unwrap() - .parse() - .expect("unable to parse memory_size"); - - let app_memory = AppMemory { - ptr: memory_addr, - length: memory_size, - }; - - let mut tmp = MinidumpWriter::new(pid, pid); - #[cfg(not(any(target_arch = "mips", target_arch = "arm")))] - if context == Context::With { - let crash_context = get_crash_context(pid); - tmp.set_crash_context(crash_context); - } - - // This should fail, because during the dump an error is detected (try_from fails) - match tmp.set_app_memory(vec![app_memory]).dump(&mut tmpfile) { - Err(WriterError::SectionAppMemoryError(_)) => (), - _ => panic!("Wrong kind of error returned"), - } - - child.kill().expect("Failed to kill process"); - // Reap child - let waitres = child.wait().expect("Failed to wait for child"); - let status = waitres.signal().expect("Child did not die due to signal"); - assert_eq!(waitres.code(), None); - assert_eq!(status, Signal::SIGKILL as i32); - - // Read dump file and check its contents. There should be a truncated minidump available - let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); - // Should be there - let _: MinidumpThreadList = dump.get_stream().expect("Couldn't find MinidumpThreadList"); - let _: MinidumpModuleList = dump.get_stream().expect("Couldn't find MinidumpThreadList"); - - // Should be missing: - assert!(dump.get_stream::().is_err()); -} - -#[test] -fn test_write_early_abort() { - test_write_early_abort_helper(Context::Without) -} - -#[cfg(not(any(target_arch = "mips", target_arch = "arm")))] -#[test] -fn test_write_early_abort_with_context() { - test_write_early_abort_helper(Context::With) -} - -fn test_named_threads_helper(context: Context) { - let num_of_threads = 5; - let mut child = start_child_and_wait_for_named_threads(num_of_threads); - let pid = child.id() as i32; - - let mut tmpfile = tempfile::Builder::new() - .prefix("named_threads") - .tempfile() - .unwrap(); - - let mut tmp = MinidumpWriter::new(pid, pid); - #[cfg(not(any(target_arch = "mips", target_arch = "arm")))] - if context == Context::With { - let crash_context = get_crash_context(pid); - tmp.set_crash_context(crash_context); - } - let _ = tmp.dump(&mut tmpfile).expect("Could not write minidump"); + .expect("cound not write minidump"); child.kill().expect("Failed to kill process"); - // Reap child - let waitres = child.wait().expect("Failed to wait for child"); - let status = waitres.signal().expect("Child did not die due to signal"); - assert_eq!(waitres.code(), None); - assert_eq!(status, Signal::SIGKILL as i32); - - // Read dump file and check its contents. There should be a truncated minidump available - let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); - - let threads: MinidumpThreadList = dump.get_stream().expect("Couldn't find MinidumpThreadList"); - - let thread_names: MinidumpThreadNames = dump - .get_stream() - .expect("Couldn't find MinidumpThreadNames"); - - let thread_ids: Vec<_> = threads.threads.iter().map(|t| t.raw.thread_id).collect(); - let names: HashSet<_> = thread_ids - .iter() - .map(|id| thread_names.get_name(*id).unwrap_or_default()) - .map(|cow| cow.into_owned()) - .collect(); - let mut expected = HashSet::new(); - expected.insert("test".to_string()); - for id in 1..num_of_threads { - expected.insert(format!("thread_{}", id)); - } - assert_eq!(expected, names); -} - -#[test] -fn test_named_threads() { - test_named_threads_helper(Context::Without) -} - -#[cfg(not(any(target_arch = "mips", target_arch = "arm")))] -#[test] -fn test_named_threads_with_context() { - test_named_threads_helper(Context::With) + // 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"); + let list: MinidumpMemoryInfoList = dump.get_stream().expect("no memory info list"); + assert!(list.iter().count() > 1); }