diff options
Diffstat (limited to 'third_party/rust/minidump-writer/tests')
6 files changed, 1755 insertions, 0 deletions
diff --git a/third_party/rust/minidump-writer/tests/common/mod.rs b/third_party/rust/minidump-writer/tests/common/mod.rs new file mode 100644 index 0000000000..2c1ded5f20 --- /dev/null +++ b/third_party/rust/minidump-writer/tests/common/mod.rs @@ -0,0 +1,95 @@ +use std::error; +use std::io::{BufRead, BufReader, Write}; +use std::process::{Child, Command, Stdio}; +use std::result; + +#[allow(unused)] +type Error = Box<dyn error::Error + std::marker::Send + std::marker::Sync>; +#[allow(unused)] +pub type Result<T> = result::Result<T, Error>; + +#[allow(unused)] +pub fn spawn_child(command: &str, args: &[&str]) { + let mut cmd_object = Command::new("cargo"); + let mut cmd_ref = cmd_object + .env("RUST_BACKTRACE", "1") + .arg("run") + .arg("-q") + .arg("--bin") + .arg("test") + .arg("--") + .arg(command); + for arg in args { + cmd_ref = cmd_ref.arg(arg); + } + let child = cmd_ref.output().expect("failed to execute child"); + + println!("Child output:"); + std::io::stdout().write_all(&child.stdout).unwrap(); + std::io::stdout().write_all(&child.stderr).unwrap(); + assert_eq!(child.status.code().expect("No return value"), 0); +} + +fn start_child_and_wait_for_threads_helper(cmd: &str, num: usize) -> Child { + let mut child = Command::new("cargo") + .env("RUST_BACKTRACE", "1") + .arg("run") + .arg("-q") + .arg("--bin") + .arg("test") + .arg("--") + .arg(cmd) + .arg(format!("{}", num)) + .stdout(Stdio::piped()) + .spawn() + .expect("failed to execute child"); + + wait_for_threads(&mut child, num); + child +} + +#[allow(unused)] +pub fn start_child_and_wait_for_threads(num: usize) -> Child { + start_child_and_wait_for_threads_helper("spawn_and_wait", num) +} + +#[allow(unused)] +pub fn start_child_and_wait_for_named_threads(num: usize) -> Child { + start_child_and_wait_for_threads_helper("spawn_name_wait", num) +} + +#[allow(unused)] +pub fn wait_for_threads(child: &mut Child, num: usize) { + let mut f = BufReader::new(child.stdout.as_mut().expect("Can't open stdout")); + let mut lines = 0; + while lines < num { + let mut buf = String::new(); + match f.read_line(&mut buf) { + Ok(_) => { + if buf == "1\n" { + lines += 1; + } + } + Err(e) => { + std::panic::panic_any(e); + } + } + } +} + +#[allow(unused)] +pub fn start_child_and_return(args: &[&str]) -> Child { + let mut child = Command::new("cargo") + .env("RUST_BACKTRACE", "1") + .arg("run") + .arg("-q") + .arg("--bin") + .arg("test") + .arg("--") + .args(args) + .stdout(Stdio::piped()) + .spawn() + .expect("failed to execute child"); + + child +} diff --git a/third_party/rust/minidump-writer/tests/linux_minidump_writer.rs b/third_party/rust/minidump-writer/tests/linux_minidump_writer.rs new file mode 100644 index 0000000000..cd2ca7375d --- /dev/null +++ b/third_party/rust/minidump-writer/tests/linux_minidump_writer.rs @@ -0,0 +1,775 @@ +#![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, + ptrace_dumper::PtraceDumper, + thread_info::Pid, +}; +use nix::{errno::Errno, sys::signal::Signal}; +use std::collections::HashSet; + +use std::{ + io::{BufRead, BufReader}, + os::unix::process::ExitStatusExt, + process::{Command, Stdio}, +}; + +mod common; +use common::*; + +#[derive(Debug, PartialEq)] +enum Context { + #[cfg(not(any(target_arch = "mips", target_arch = "arm")))] + With, + Without, +} + +#[cfg(not(any(target_arch = "mips", target_arch = "arm")))] +fn get_ucontext() -> Result<crash_context::ucontext_t> { + let mut context = std::mem::MaybeUninit::uninit(); + unsafe { + let res = crash_context::crash_context_getcontext(context.as_mut_ptr()); + Errno::result(res)?; + + Ok(context.assume_init()) + } +} + +#[cfg(not(any(target_arch = "mips", target_arch = "arm")))] +fn get_crash_context(tid: Pid) -> CrashContext { + let siginfo: libc::signalfd_siginfo = unsafe { std::mem::zeroed() }; + let context = get_ucontext().expect("Failed to get ucontext"); + let float_state = unsafe { std::mem::zeroed() }; + CrashContext { + inner: crash_context::CrashContext { + siginfo, + pid: std::process::id() as _, + tid, + context, + float_state, + }, + } +} + +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; + + let mut tmpfile = tempfile::Builder::new() + .prefix("write_dump") + .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 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); + + let meta = std::fs::metadata(tmpfile.path()).expect("Couldn't get metadata for tempfile"); + assert!(meta.len() > 0); + + 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); +} + +#[test] +fn test_write_dump() { + test_write_dump_helper(Context::Without) +} + +#[cfg(not(any(target_arch = "mips", target_arch = "arm")))] +#[test] +fn test_write_dump_with_context() { + test_write_dump_helper(Context::With) +} + +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 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 { + 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 = 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.set_user_mapping_list(vec![entry]) + .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); + + 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"); +} + +#[test] +fn test_write_and_read_dump_from_parent() { + test_write_and_read_dump_from_parent_helper(Context::Without) +} + +#[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) +} + +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; + + 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(); + 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 = 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.set_app_memory(vec![app_memory]) + .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 + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + + 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"); + + assert_eq!(region.base_address, memory_addr as u64); + assert_eq!(region.size, memory_size as u64); + + let mut values = Vec::<u8>::with_capacity(memory_size); + for idx in 0..memory_size { + values.push((idx % 255) as u8); + } + + // Verify memory contents. + assert_eq!(region.bytes, values); +} + +#[test] +fn test_write_with_additional_memory() { + test_write_with_additional_memory_helper(Context::Without) +} + +#[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) +} + +#[test] +fn test_minidump_size_limit() { + let num_of_threads = 40; + let mut child = start_child_and_wait_for_threads(num_of_threads); + let pid = child.id() as i32; + + let mut total_normal_stack_size = 0; + let normal_file_size; + // First, write a minidump with no size limit. + { + let mut tmpfile = tempfile::Builder::new() + .prefix("write_dump_unlimited") + .tempfile() + .unwrap(); + + MinidumpWriter::new(pid, pid) + .dump(&mut tmpfile) + .expect("Could not write minidump"); + + let meta = std::fs::metadata(tmpfile.path()).expect("Couldn't get metadata for tempfile"); + assert!(meta.len() > 0); + + normal_file_size = meta.len(); + + // Read dump file and check its contents + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + let thread_list: MinidumpThreadList = + dump.get_stream().expect("Couldn't find MinidumpThreadList"); + for thread in thread_list.threads { + assert!(thread.raw.thread_id > 0); + assert!(thread.raw.stack.memory.data_size > 0); + total_normal_stack_size += thread.raw.stack.memory.data_size; + } + } + + // Second, write a minidump with a size limit big enough to not trigger + // anything. + { + // Set size limit arbitrarily 2MiB larger than the normal file size -- such + // that the limiting code will not kick in. + let minidump_size_limit = normal_file_size + 2 * 1024 * 1024; + + let mut tmpfile = tempfile::Builder::new() + .prefix("write_dump_pseudolimited") + .tempfile() + .unwrap(); + + MinidumpWriter::new(pid, pid) + .set_minidump_size_limit(minidump_size_limit) + .dump(&mut tmpfile) + .expect("Could not write minidump"); + + let meta = std::fs::metadata(tmpfile.path()).expect("Couldn't get metadata for tempfile"); + + // Make sure limiting wasn't actually triggered. NOTE: If you fail this, + // first make sure that "minidump_size_limit" above is indeed set to a + // large enough value -- the limit-checking code in minidump_writer.rs + // does just a rough estimate. + // TODO: Fix this properly + // There are occasionally CI failures where the sizes are off by 1 due + // some minor difference in (probably) a string somewhere in the dump + // since the state capture is not going to be 100% the same + //assert_eq!(meta.len(), normal_file_size); + let min = std::cmp::min(meta.len(), normal_file_size); + let max = std::cmp::max(meta.len(), normal_file_size); + + assert!(max - min < 10); + } + + // Third, write a minidump with a size limit small enough to be triggered. + { + // Set size limit to some arbitrary amount, such that the limiting code + // will kick in. The equation used to set this value was determined by + // simply reversing the size-limit logic a little bit in order to pick a + // size we know will trigger it. + + // Copyied from sections/thread_list_stream.rs + const LIMIT_AVERAGE_THREAD_STACK_LENGTH: u64 = 8 * 1024; + let mut minidump_size_limit = LIMIT_AVERAGE_THREAD_STACK_LENGTH * 40; + + // If, in reality, each of the threads' stack is *smaller* than + // kLimitAverageThreadStackLength, the normal file size could very well be + // smaller than the arbitrary limit that was just set. In that case, + // either of these numbers should trigger the size-limiting code, but we + // might as well pick the smallest. + if normal_file_size < minidump_size_limit { + minidump_size_limit = normal_file_size; + } + + let mut tmpfile = tempfile::Builder::new() + .prefix("write_dump_limited") + .tempfile() + .unwrap(); + + MinidumpWriter::new(pid, pid) + .set_minidump_size_limit(minidump_size_limit) + .dump(&mut tmpfile) + .expect("Could not write minidump"); + + let meta = std::fs::metadata(tmpfile.path()).expect("Couldn't get metadata for tempfile"); + assert!(meta.len() > 0); + // Make sure the file size is at least smaller than the original. If this + // fails because it's the same size, then the size-limit logic didn't kick + // in like it was supposed to. + assert!(meta.len() < normal_file_size); + + let mut total_limit_stack_size = 0; + // Read dump file and check its contents + let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); + let thread_list: MinidumpThreadList = + dump.get_stream().expect("Couldn't find MinidumpThreadList"); + for thread in thread_list.threads { + assert!(thread.raw.thread_id > 0); + assert!(thread.raw.stack.memory.data_size > 0); + total_limit_stack_size += thread.raw.stack.memory.data_size; + } + + // Make sure stack size shrunk by at least 1KB per extra thread. + // Note: The 1KB is arbitrary, and assumes that the thread stacks are big + // enough to shrink by that much. For example, if each thread stack was + // originally only 2KB, the current size-limit logic wouldn't actually + // shrink them because that's the size to which it tries to shrink. If + // you fail this part of the test due to something like that, the test + // logic should probably be improved to account for your situation. + + // Copyied from sections/thread_list_stream.rs + const LIMIT_BASE_THREAD_COUNT: usize = 20; + const MIN_PER_EXTRA_THREAD_STACK_REDUCTION: usize = 1024; + let min_expected_reduction = + (40 - LIMIT_BASE_THREAD_COUNT) * MIN_PER_EXTRA_THREAD_STACK_REDUCTION; + assert!(total_limit_stack_size < total_normal_stack_size - min_expected_reduction as u32); + } + + 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_with_deleted_binary() { + let num_of_threads = 1; + let binary_copy_dir = tempfile::Builder::new() + .prefix("deleted_binary") + .tempdir() + .unwrap(); + let binary_copy = binary_copy_dir.as_ref().join("binary_copy"); + + let path: &'static str = std::env!("CARGO_BIN_EXE_test"); + + std::fs::copy(path, &binary_copy).expect("Failed to copy binary"); + let mem_slice = std::fs::read(&binary_copy).expect("Failed to read binary"); + + let mut child = Command::new(&binary_copy) + .env("RUST_BACKTRACE", "1") + .arg("spawn_and_wait") + .arg(format!("{}", num_of_threads)) + .stdout(Stdio::piped()) + .spawn() + .expect("failed to execute child"); + wait_for_threads(&mut child, num_of_threads); + + let pid = child.id() as i32; + + let build_id = PtraceDumper::elf_file_identifier_from_mapped_file(&mem_slice) + .expect("Failed to get build_id"); + + let guid = GUID { + data1: u32::from_ne_bytes(build_id[0..4].try_into().unwrap()), + data2: u16::from_ne_bytes(build_id[4..6].try_into().unwrap()), + data3: u16::from_ne_bytes(build_id[6..8].try_into().unwrap()), + data4: build_id[8..16].try_into().unwrap(), + }; + + // guid_to_string() is not public in minidump, so copied it here + // And append a zero, because module IDs include an "age" field + // which is always zero on Linux. + let filtered = format!( + "{:08X}{:04X}{:04X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}{:02X}0", + guid.data1, + guid.data2, + guid.data3, + guid.data4[0], + guid.data4[1], + guid.data4[2], + guid.data4[3], + guid.data4[4], + guid.data4[5], + guid.data4[6], + guid.data4[7], + ); + // Strip out dashes + //let mut filtered: String = identifier.chars().filter(|x| *x != '-').collect(); + + std::fs::remove_file(&binary_copy).expect("Failed to remove binary"); + + let mut tmpfile = tempfile::Builder::new() + .prefix("deleted_binary") + .tempfile() + .unwrap(); + + MinidumpWriter::new(pid, pid) + .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); + + // Begin checks on dump + let meta = std::fs::metadata(tmpfile.path()).expect("Couldn't get metadata for tempfile"); + assert!(meta.len() > 0); + + 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 main_module = module_list + .main_module() + .expect("Could not get main module"); + assert_eq!(main_module.code_file(), binary_copy.to_string_lossy()); + 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); + 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); + } + 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); + + // 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::<MinidumpMemoryList>().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"); + 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) +} diff --git a/third_party/rust/minidump-writer/tests/mac_minidump_writer.rs b/third_party/rust/minidump-writer/tests/mac_minidump_writer.rs new file mode 100644 index 0000000000..4dafb1a77a --- /dev/null +++ b/third_party/rust/minidump-writer/tests/mac_minidump_writer.rs @@ -0,0 +1,227 @@ +#![cfg(target_os = "macos")] + +mod common; +use common::start_child_and_return; + +use minidump::{ + CrashReason, Minidump, MinidumpBreakpadInfo, MinidumpMemoryList, MinidumpMiscInfo, + MinidumpModuleList, MinidumpSystemInfo, MinidumpThreadList, +}; +use minidump_writer::minidump_writer::MinidumpWriter; + +fn get_crash_reason<'a, T: std::ops::Deref<Target = [u8]> + 'a>( + md: &Minidump<'a, T>, +) -> CrashReason { + let exc: minidump::MinidumpException<'_> = + md.get_stream().expect("unable to find exception stream"); + + exc.get_crash_reason( + minidump::system_info::Os::MacOs, + if cfg!(target_arch = "x86_64") { + minidump::system_info::Cpu::X86_64 + } else if cfg!(target_arch = "aarch64") { + minidump::system_info::Cpu::Arm64 + } else { + unimplemented!() + }, + ) +} + +struct Captured<'md> { + #[allow(dead_code)] + task: u32, + thread: u32, + minidump: Minidump<'md, memmap2::Mmap>, +} + +fn capture_minidump(name: &str, exception_kind: u32) -> Captured<'_> { + // Create a mach port server to retrieve the crash details from the child + let mut server = crash_context::ipc::Server::create(&std::ffi::CString::new(name).unwrap()) + .expect("failed to create mach port service"); + + let mut child = start_child_and_return(&[name, &exception_kind.to_string()]); + + // Wait for the child to spinup and report a crash context to us + let mut rcc = server + .try_recv_crash_context(Some(std::time::Duration::from_secs(5))) + .expect("failed to receive context") + .expect("receive timed out"); + + let mut tmpfile = tempfile::Builder::new().prefix(name).tempfile().unwrap(); + + let task = rcc.crash_context.task; + let thread = rcc.crash_context.thread; + + let mut dumper = MinidumpWriter::with_crash_context(rcc.crash_context); + + dumper + .dump(tmpfile.as_file_mut()) + .expect("failed to write minidump"); + + // Signal the child that we've received and processed the crash context + rcc.acker + .send_ack(1, Some(std::time::Duration::from_secs(2))) + .expect("failed to send ack"); + + child.kill().expect("failed to kill child"); + + let minidump = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); + + Captured { + task, + thread, + minidump, + } +} + +#[test] +fn dump_external_process() { + if std::env::var("CI").is_ok() { + println!("test disabled, consistently times out because of potato runners"); + return; + } + + let approximate_proc_start_time = std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let md = capture_minidump( + "dump_external_process", + mach2::exception_types::EXC_BREAKPOINT, + ) + .minidump; + + let crash_reason = get_crash_reason(&md); + + assert!(matches!( + crash_reason, + CrashReason::MacGeneral(minidump_common::errors::ExceptionCodeMac::EXC_BREAKPOINT, _) + )); + + let _: MinidumpModuleList = md.get_stream().expect("Couldn't find MinidumpModuleList"); + let _: MinidumpThreadList = md.get_stream().expect("Couldn't find MinidumpThreadList"); + let _: MinidumpMemoryList = md.get_stream().expect("Couldn't find MinidumpMemoryList"); + let _: MinidumpSystemInfo = md.get_stream().expect("Couldn't find MinidumpSystemInfo"); + let _: MinidumpBreakpadInfo = md.get_stream().expect("Couldn't find MinidumpBreakpadInfo"); + + let misc_info: MinidumpMiscInfo = md.get_stream().expect("Couldn't find MinidumpMiscInfo"); + + if let minidump::RawMiscInfo::MiscInfo2(mi) = &misc_info.raw { + // Unfortunately the minidump format only has 32-bit precision for the + // process start time + let process_create_time = mi.process_create_time as u64; + + assert!( + process_create_time >= approximate_proc_start_time + && process_create_time <= approximate_proc_start_time + 2 + ); + + // I've tried busy looping to spend CPU time to get this up, but + // MACH_TASK_BASIC_INFO which should give terminated thread times only ever + // reports 0, and TASK_THREAD_TIMES_INFO which should show active thread + // times I've only been able to get upt to a few thousand microseconds + // even when busy looping for well over a second, and those get truncated + // to whole seconds. And it seems that crashpad doesn't have tests around + // this, though that's hard to say given how tedious it is finding stuff + // in that bloated codebase + // assert!(mi.process_user_time > 0); + // assert!(mi.process_kernel_time > 0); + + // These aren't currently available on aarch64, or if they are, they + // are not via the same sysctlbyname mechanism. Would be nice if Apple + // documented...anything + if cfg!(target_arch = "x86_64") { + assert!(mi.processor_max_mhz > 0); + assert!(mi.processor_current_mhz > 0); + } + } else { + panic!("unexpected misc info type {:?}", misc_info); + } +} + +/// Validates we can actually walk the stack for each thread in the minidump, +/// this is using minidump-processor, which (currently) depends on breakpad +/// symbols, however https://github.com/mozilla/dump_syms is not available as +/// a library https://github.com/mozilla/dump_syms/issues/253, so we just require +/// that it already be installed, hence the ignore +#[test] +fn stackwalks() { + if std::env::var("CI").is_ok() { + println!("test disabled, consistently times out because of potato runners"); + return; + } + + println!("generating minidump..."); + let md = capture_minidump("stackwalks", mach2::exception_types::EXC_BREAKPOINT); + + // Generate the breakpad symbols + println!("generating symbols..."); + dump_syms::dumper::single_file( + &dump_syms::dumper::Config { + output: dump_syms::dumper::Output::Store(".test-symbols".into()), + symbol_server: None, + debug_id: None, + code_id: None, + arch: if cfg!(target_arch = "aarch64") { + "arm64" + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else { + panic!("invalid MacOS target architecture") + }, + num_jobs: 2, // default this + check_cfi: false, + emit_inlines: false, + mapping_var: None, + mapping_src: None, + mapping_dest: None, + mapping_file: None, + }, + "target/debug/test", + ) + .expect("failed to dump symbols"); + + let provider = + minidump_processor::Symbolizer::new(minidump_processor::simple_symbol_supplier(vec![ + ".test-symbols".into(), + ])); + + let state = futures::executor::block_on(async { + minidump_processor::process_minidump(&md.minidump, &provider).await + }) + .unwrap(); + + //state.print(&mut std::io::stdout()).map_err(|_| ()).unwrap(); + + // We expect at least 2 threads, one of which is the fake crashing thread + let fake_crash_thread = state + .threads + .iter() + .find(|cs| cs.thread_id == md.thread) + .expect("failed to find crash thread"); + + assert_eq!( + fake_crash_thread.thread_name.as_deref(), + Some("test-thread") + ); + + assert!( + fake_crash_thread.frames.iter().any(|sf| { + sf.function_name + .as_ref() + .map_or(false, |fname| fname.ends_with("wait_until_killed")) + }), + "unable to locate expected function" + ); + + let mod_list: MinidumpModuleList = md + .minidump + .get_stream() + .expect("Couldn't find MinidumpModuleList"); + + // Ensure we found dyld + assert!(mod_list + .iter() + .any(|module| &module.name == "/usr/lib/dyld")); +} diff --git a/third_party/rust/minidump-writer/tests/ptrace_dumper.rs b/third_party/rust/minidump-writer/tests/ptrace_dumper.rs new file mode 100644 index 0000000000..02ba836a86 --- /dev/null +++ b/third_party/rust/minidump-writer/tests/ptrace_dumper.rs @@ -0,0 +1,316 @@ +//! 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::io::AsRawFd; +use std::os::unix::process::ExitStatusExt; + +mod common; +use common::*; + +#[test] +fn test_setup() { + spawn_child("setup", &[]); +} + +#[test] +fn test_thread_list_from_child() { + // Child spawns and looks in the parent (== this process) for its own thread-ID + spawn_child("thread_list", &[]); +} + +#[test] +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).expect("Couldn't init dumper"); + assert_eq!(dumper.threads.len(), num_of_threads); + dumper.suspend_threads().expect("Could not suspend threads"); + + // let mut matching_threads = 0; + for (idx, curr_thread) in dumper.threads.iter().enumerate() { + println!("curr_thread: {:?}", curr_thread); + let info = dumper + .get_thread_info_by_index(idx) + .expect("Could not get thread info by index"); + let (_stack_ptr, stack_len) = dumper + .get_stack_info(info.stack_pointer) + .expect("Could not get stack_pointer"); + assert!(stack_len > 0); + + // TODO: I currently know of no way to write the thread_id into the registers using Rust, + // so this check is deactivated for now, because it always fails + /* + // In the helper program, we stored a pointer to the thread id in a + // specific register. Check that we can recover its value. + #[cfg(target_arch = "x86_64")] + let process_tid_location = info.regs.rcx; + #[cfg(target_arch = "x86")] + let process_tid_location = info.regs.ecx; + #[cfg(target_arch = "arm")] + let process_tid_location = info.regs.uregs[3]; + #[cfg(target_arch = "aarch64")] + let process_tid_location = info.regs.regs[3]; + #[cfg(target_arch = "mips")] + let process_tid_location = info.mcontext.gregs[1]; + + let thread_id_data = PtraceDumper::copy_from_process( + *curr_thread, + process_tid_location as *mut libc::c_void, + 4, + ) + .expect("Could not copy from process"); + let found_thread_id = i32::from_ne_bytes( + thread_id_data + .as_slice() + .try_into() + .expect("couldn't parse i32 from read data"), + ); + matching_threads += if *curr_thread == found_thread_id { + 1 + } else { + 0 + }; */ + } + dumper.resume_threads().expect("Failed to resume threads"); + 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); + + // We clean up the child process before checking the final result + // TODO: I currently know of no way to write the thread_id into the registers using Rust, + // so this check is deactivated for now, because it always fails + // assert_eq!(matching_threads, num_of_threads); +} + +// #[cfg(not(any(target_arch = "mips", target_arch = "arm-eabi"))] +#[cfg(not(target_arch = "mips"))] +#[test] +// Ensure that the linux-gate VDSO is included in the mapping list. +fn test_mappings_include_linux_gate() { + spawn_child("mappings_include_linux_gate", &[]); +} + +#[test] +fn test_linux_gate_mapping_id() { + if std::env::var("CI").is_ok() { + println!("disabled on CI, but works locally"); + return; + } + + spawn_child("linux_gate_mapping_id", &[]); +} + +#[test] +fn test_merged_mappings() { + let page_size = nix::unistd::sysconf(nix::unistd::SysconfVar::PAGE_SIZE).unwrap(); + let page_size = std::num::NonZeroUsize::new(page_size.unwrap() as usize).unwrap(); + let map_size = std::num::NonZeroUsize::new(3 * page_size.get()).unwrap(); + + let path: &'static str = std::env!("CARGO_BIN_EXE_test"); + let file = std::fs::File::open(path).unwrap(); + + // mmap two segments out of the helper binary, one + // enclosed in the other, but with different protections. + let mapped_mem = unsafe { + mmap( + None, + map_size, + ProtFlags::PROT_READ, + MapFlags::MAP_SHARED, + file.as_raw_fd(), + 0, + ) + .unwrap() + }; + + // Carve a page out of the first mapping with different permissions. + let _inside_mapping = unsafe { + mmap( + std::num::NonZeroUsize::new(mapped_mem as usize + 2 * page_size.get()), + page_size, + ProtFlags::PROT_NONE, + MapFlags::MAP_SHARED | MapFlags::MAP_FIXED, + file.as_raw_fd(), + // Map a different offset just to + // better test real-world conditions. + page_size.get().try_into().unwrap(), // try_into() in order to work for 32 and 64 bit + ) + }; + + spawn_child( + "merged_mappings", + &[ + path, + &format!("{}", mapped_mem as usize), + &format!("{map_size}"), + ], + ); +} + +#[test] +// Ensure that the linux-gate VDSO is included in the mapping list. +fn test_file_id() { + spawn_child("file_id", &[]); +} + +#[test] +fn test_find_mapping() { + spawn_child( + "find_mappings", + &[ + &format!("{}", libc::printf as *const () as usize), + &format!("{}", String::new as *const () as usize), + ], + ); +} + +#[test] +fn test_copy_from_process_self() { + if std::env::var("CI").is_ok() { + println!("disabled on CI, but works locally"); + return; + } + + let stack_var: libc::c_long = 0x11223344; + let heap_var: Box<libc::c_long> = Box::new(0x55667788); + spawn_child( + "copy_from_process", + &[ + &format!("{}", &stack_var as *const libc::c_long as usize), + &format!("{}", heap_var.as_ref() as *const libc::c_long as usize), + ], + ); +} + +#[test] +fn test_sanitize_stack_copy() { + let num_of_threads = 1; + let mut child = start_child_and_return(&["spawn_alloc_wait"]); + let pid = child.id() as i32; + + 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 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).expect("Couldn't init dumper"); + assert_eq!(dumper.threads.len(), num_of_threads); + dumper.suspend_threads().expect("Could not suspend threads"); + let thread_info = dumper + .get_thread_info_by_index(0) + .expect("Couldn't find thread_info"); + + let defaced; + #[cfg(target_pointer_width = "64")] + { + defaced = 0x0defaced0defacedusize.to_ne_bytes() + } + #[cfg(target_pointer_width = "32")] + { + defaced = 0x0defacedusize.to_ne_bytes() + }; + + let mut simulated_stack = vec![0xffu8; 2 * size_of::<usize>()]; + // Pointers into the stack shouldn't be sanitized. + simulated_stack[size_of::<usize>()..].copy_from_slice(&thread_info.stack_pointer.to_ne_bytes()); + + dumper + .sanitize_stack_copy( + &mut simulated_stack, + thread_info.stack_pointer, + size_of::<usize>(), + ) + .expect("Could not sanitize stack"); + + assert!(simulated_stack[size_of::<usize>()..] != defaced); + // Memory prior to the stack pointer should be cleared. + assert_eq!( + &simulated_stack[0..size_of::<usize>()], + vec![0u8; size_of::<usize>()].as_slice() + ); + + // Small integers should not be sanitized. + for ii in -4096..=4096isize { + simulated_stack = vec![0u8; 2 * size_of::<usize>()]; + simulated_stack[0..size_of::<usize>()].copy_from_slice(&(ii as usize).to_ne_bytes()); + dumper + .sanitize_stack_copy(&mut simulated_stack, thread_info.stack_pointer, 0) + .expect("Failed to sanitize with small integers"); + assert!(simulated_stack[size_of::<usize>()..] != defaced); + } + + // The instruction pointer definitely should point into an executable mapping. + let instr_ptr = thread_info.get_instruction_pointer(); + let mapping_info = dumper + .find_mapping_no_bias(instr_ptr) + .expect("Failed to find mapping info"); + assert!(mapping_info.executable); + + // Pointers to code shouldn't be sanitized. + simulated_stack = vec![0u8; 2 * size_of::<usize>()]; + simulated_stack[size_of::<usize>()..].copy_from_slice(&instr_ptr.to_ne_bytes()); + dumper + .sanitize_stack_copy(&mut simulated_stack, thread_info.stack_pointer, 0) + .expect("Failed to sanitize with instr_ptr"); + assert!(simulated_stack[0..size_of::<usize>()] != defaced); + assert!(simulated_stack[size_of::<usize>()..] != defaced); + + // String fragments should be sanitized. + let junk = "abcdefghijklmnop".as_bytes(); + simulated_stack.copy_from_slice(&junk[0..2 * size_of::<usize>()]); + dumper + .sanitize_stack_copy(&mut simulated_stack, thread_info.stack_pointer, 0) + .expect("Failed to sanitize with junk"); + assert_eq!(simulated_stack[0..size_of::<usize>()], defaced); + assert_eq!(simulated_stack[size_of::<usize>()..], defaced); + + // Heap pointers should be sanititzed. + + // NOTE: The original test used the heap-address below, but here thread_info.regs.rcx + // is the instruction pointer, and thus in direct conflict with the "instruction pointer" + // testcase. + // Instead we just allocate something on the heap in the child and pass that address to this test. + // #[cfg(target_arch = "x86_64")] + // let heap_addr = thread_info.regs.rcx as usize; + // #[cfg(target_arch = "x86")] + // let heap_addr = thread_info.regs.ecx as usize; + // #[cfg(target_arch = "arm")] + // let heap_addr = thread_info.regs.uregs[3] as usize; + // #[cfg(target_arch = "aarch64")] + // let heap_addr = thread_info.regs.regs[3] as usize; + // #[cfg(target_arch = "mips")] + // let heap_addr = thread_info.mcontext.gregs[1] as usize; + + simulated_stack = vec![0u8; 2 * size_of::<usize>()]; + + simulated_stack[0..size_of::<usize>()].copy_from_slice(&heap_addr.to_ne_bytes()); + dumper + .sanitize_stack_copy(&mut simulated_stack, thread_info.stack_pointer, 0) + .expect("Failed to sanitize with heap addr"); + + assert_eq!(simulated_stack[0..size_of::<usize>()], defaced); + + dumper.resume_threads().expect("Failed to resume threads"); + 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); +} diff --git a/third_party/rust/minidump-writer/tests/task_dumper.rs b/third_party/rust/minidump-writer/tests/task_dumper.rs new file mode 100644 index 0000000000..1411acc34a --- /dev/null +++ b/third_party/rust/minidump-writer/tests/task_dumper.rs @@ -0,0 +1,158 @@ +//! All of these tests are specific to the MacOS task dumper +#![cfg(target_os = "macos")] + +use minidump_writer::{mach::LoadCommand, task_dumper::TaskDumper}; +use std::fmt::Write; + +fn call_otool(args: &[&str]) -> String { + let mut cmd = std::process::Command::new("otool"); + cmd.args(args); + + let exe_path = std::env::current_exe().expect("unable to retrieve test executable path"); + cmd.arg(exe_path); + + let output = cmd.output().expect("failed to spawn otool"); + + assert!(output.status.success()); + + String::from_utf8(output.stdout).expect("stdout was invalid utf-8") +} + +/// Validates we can iterate the load commands for all of the images in the task +#[test] +fn iterates_load_commands() { + let lc_str = call_otool(&["-l"]); + + let mut expected = String::new(); + let mut lc_index = 0; + + expected.push('\n'); + + while let Some(nlc) = lc_str[lc_index..].find("Load command ") { + lc_index += nlc; + + let block = match lc_str[lc_index + 13..].find("Load command ") { + Some(ind) => &lc_str[lc_index + 13..lc_index + 13 + ind], + None => &lc_str[lc_index..], + }; + + // otool prints the load command index for each command, but we only + // handle the small subset of the available load commands we care about + // so just ignore that + let block = &block[block.find('\n').unwrap() + 1..]; + + // otool also prints all the sections for LC_SEGMENT_* commands, but + // we don't care about those, so ignore them + let block = match block.find("Section") { + Some(ind) => &block[..ind], + None => block, + }; + + lc_index += 13; + + let cmd = block + .find("cmd ") + .expect("load commnd didn't specify cmd kind"); + let cmd_end = block[cmd..] + .find('\n') + .expect("load cmd didn't end with newline"); + if matches!( + &block[cmd + 4..cmd + cmd_end], + "LC_SEGMENT_64" | "LC_UUID" | "LC_ID_DYLIB" | "LC_LOAD_DYLINKER" + ) { + expected.push_str(block); + } + } + + let task_dumper = TaskDumper::new( + // SAFETY: syscall + unsafe { mach2::traps::mach_task_self() }, + ); + + let mut actual = String::new(); + + // Unfortunately, Apple decided to move dynamic libs into a shared cache, + // removing them from the file system completely, and unless I'm missing it + // there is no way to get the load commands for the dylibs since otool + // only understands file paths? So we just get the load commands for the main + // executable instead, this means that we miss the `LC_ID_DYLIB` commands + // since they only apply to dylibs, but this test is more that we can + // correctly iterate through the load commands themselves, so this _should_ + // be fine... + let exe_img = task_dumper + .read_executable_image() + .expect("failed to read executable image"); + + { + let lcmds = task_dumper + .read_load_commands(&exe_img) + .expect("failed to read load commands"); + + for lc in lcmds.iter() { + match lc { + LoadCommand::Segment(seg) => { + let segname = std::str::from_utf8(&seg.segment_name).unwrap(); + let segname = &segname[..segname.find('\0').unwrap()]; + write!( + &mut actual, + " + cmd LC_SEGMENT_64 + cmdsize {} + segname {} + vmaddr 0x{:016x} + vmsize 0x{:016x} + fileoff {} + filesize {} + maxprot 0x{:08x} + initprot 0x{:08x} + nsects {} + flags 0x{:x}", + seg.cmd_size, + segname, + seg.vm_addr, + seg.vm_size, + seg.file_off, + seg.file_size, + seg.max_prot, + seg.init_prot, + seg.num_sections, + seg.flags, + ) + .unwrap(); + } + LoadCommand::Dylib(_dylib) => { + unreachable!(); + } + LoadCommand::Uuid(uuid) => { + let id = uuid::Uuid::from_bytes(uuid.uuid); + let mut uuid_buf = [0u8; uuid::fmt::Hyphenated::LENGTH]; + let uuid_str = id.hyphenated().encode_upper(&mut uuid_buf); + + write!( + &mut actual, + " + cmd LC_UUID + cmdsize {} + uuid {uuid_str} +", + uuid.cmd_size, + ) + .unwrap(); + } + LoadCommand::DylinkerCommand(dy_cmd) => { + write!( + &mut actual, + " + cmd LC_LOAD_DYLINKER + cmdsize {} + name {} (offset {})", + dy_cmd.cmd_size, dy_cmd.name, dy_cmd.name_offset, + ) + .unwrap(); + } + } + } + } + + similar_asserts::assert_eq!(expected, actual); +} diff --git a/third_party/rust/minidump-writer/tests/windows_minidump_writer.rs b/third_party/rust/minidump-writer/tests/windows_minidump_writer.rs new file mode 100644 index 0000000000..1b2c1d4bb9 --- /dev/null +++ b/third_party/rust/minidump-writer/tests/windows_minidump_writer.rs @@ -0,0 +1,184 @@ +#![cfg(all(target_os = "windows", target_arch = "x86_64"))] + +use minidump::{ + CrashReason, Minidump, MinidumpBreakpadInfo, MinidumpMemoryList, MinidumpSystemInfo, + MinidumpThreadList, +}; +use minidump_writer::minidump_writer::MinidumpWriter; +mod common; +use common::start_child_and_return; + +const EXCEPTION_ILLEGAL_INSTRUCTION: i32 = -1073741795; +const STATUS_INVALID_PARAMETER: i32 = -1073741811; +#[link(name = "kernel32")] +extern "system" { + fn GetCurrentThreadId() -> u32; +} + +fn get_crash_reason<'a, T: std::ops::Deref<Target = [u8]> + 'a>( + md: &Minidump<'a, T>, +) -> CrashReason { + let exc: minidump::MinidumpException<'_> = + md.get_stream().expect("unable to find exception stream"); + + exc.get_crash_reason( + minidump::system_info::Os::Windows, + minidump::system_info::Cpu::X86_64, + ) +} + +/// Ensures that we can write minidumps for the current process, even if this is +/// not necessarily the primary intended use case of out-of-process dumping +#[test] +fn dump_current_process() { + let mut tmpfile = tempfile::Builder::new() + .prefix("windows_current_process") + .tempfile() + .unwrap(); + + MinidumpWriter::dump_local_context( + Some(STATUS_INVALID_PARAMETER), + None, + None, + tmpfile.as_file_mut(), + ) + .expect("failed to write minidump"); + + let md = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); + + let _: MinidumpThreadList = md.get_stream().expect("Couldn't find MinidumpThreadList"); + let _: MinidumpMemoryList = md.get_stream().expect("Couldn't find MinidumpMemoryList"); + let _: MinidumpSystemInfo = md.get_stream().expect("Couldn't find MinidumpSystemInfo"); + + let crash_reason = get_crash_reason(&md); + + assert_eq!( + crash_reason, + CrashReason::from_windows_error(STATUS_INVALID_PARAMETER as u32) + ); + + // SAFETY: syscall + let thread_id = unsafe { GetCurrentThreadId() }; + + let bp_info: MinidumpBreakpadInfo = + md.get_stream().expect("Couldn't find MinidumpBreakpadInfo"); + + assert_eq!(bp_info.dump_thread_id.unwrap(), thread_id); + assert_eq!(bp_info.requesting_thread_id.unwrap(), thread_id); +} + +#[test] +fn dump_specific_thread() { + let mut tmpfile = tempfile::Builder::new() + .prefix("windows_current_process") + .tempfile() + .unwrap(); + + let (tx, rx) = std::sync::mpsc::channel(); + + let jh = std::thread::spawn(move || { + // SAFETY: syscall + let thread_id = unsafe { GetCurrentThreadId() }; + while tx.send(thread_id).is_ok() { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + }); + + let crashing_thread_id = rx.recv().unwrap(); + + MinidumpWriter::dump_local_context( + Some(STATUS_INVALID_PARAMETER), + Some(crashing_thread_id), + None, + tmpfile.as_file_mut(), + ) + .expect("failed to write minidump"); + + drop(rx); + jh.join().unwrap(); + + let md = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); + + let _: MinidumpThreadList = md.get_stream().expect("Couldn't find MinidumpThreadList"); + let _: MinidumpMemoryList = md.get_stream().expect("Couldn't find MinidumpMemoryList"); + let _: MinidumpSystemInfo = md.get_stream().expect("Couldn't find MinidumpSystemInfo"); + + let crash_reason = get_crash_reason(&md); + + assert_eq!( + crash_reason, + CrashReason::from_windows_error(STATUS_INVALID_PARAMETER as u32) + ); + + // SAFETY: syscall + let requesting_thread_id = unsafe { GetCurrentThreadId() }; + + let bp_info: MinidumpBreakpadInfo = + md.get_stream().expect("Couldn't find MinidumpBreakpadInfo"); + + assert_eq!(bp_info.dump_thread_id.unwrap(), crashing_thread_id); + assert_eq!(bp_info.requesting_thread_id.unwrap(), requesting_thread_id); +} + +/// Ensures that we can write minidumps for an external process. Unfortunately +/// this requires us to know the actual pointer in the client process for the +/// exception, as the `MiniDumpWriteDump` syscall directly reads points from +/// the process memory, so we communicate that back from the client process +/// via stdout +#[test] +fn dump_external_process() { + use std::io::BufRead; + + let mut child = start_child_and_return(&[&format!("{:x}", EXCEPTION_ILLEGAL_INSTRUCTION)]); + + let (process_id, exception_pointers, thread_id, exception_code) = { + let mut f = std::io::BufReader::new(child.stdout.as_mut().expect("Can't open stdout")); + let mut buf = String::new(); + f.read_line(&mut buf).expect("failed to read stdout"); + assert!(!buf.is_empty()); + + let mut biter = buf.trim().split(' '); + + let process_id: u32 = biter.next().unwrap().parse().unwrap(); + let exception_pointers: usize = biter.next().unwrap().parse().unwrap(); + let thread_id: u32 = biter.next().unwrap().parse().unwrap(); + let exception_code = u32::from_str_radix(biter.next().unwrap(), 16).unwrap(); + + (process_id, exception_pointers, thread_id, exception_code) + }; + + let exception_code = exception_code as i32; + assert_eq!(exception_code, EXCEPTION_ILLEGAL_INSTRUCTION); + + let crash_context = crash_context::CrashContext { + exception_pointers: exception_pointers as _, + process_id, + thread_id, + exception_code, + }; + + let mut tmpfile = tempfile::Builder::new() + .prefix("windows_external_process") + .tempfile() + .unwrap(); + + // SAFETY: We keep the process we are dumping alive until the minidump is written + // and the test process keep the pointers it sent us alive until it is killed + MinidumpWriter::dump_crash_context(crash_context, None, tmpfile.as_file_mut()) + .expect("failed to write minidump"); + + child.kill().expect("failed to kill child"); + + let md = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); + + let _: MinidumpThreadList = md.get_stream().expect("Couldn't find MinidumpThreadList"); + let _: MinidumpMemoryList = md.get_stream().expect("Couldn't find MinidumpMemoryList"); + let _: MinidumpSystemInfo = md.get_stream().expect("Couldn't find MinidumpSystemInfo"); + + let crash_reason = get_crash_reason(&md); + + assert_eq!( + crash_reason, + CrashReason::from_windows_code(EXCEPTION_ILLEGAL_INSTRUCTION as u32) + ); +} |