summaryrefslogtreecommitdiffstats
path: root/third_party/rust/minidump-writer/tests
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/rust/minidump-writer/tests')
-rw-r--r--third_party/rust/minidump-writer/tests/common/mod.rs95
-rw-r--r--third_party/rust/minidump-writer/tests/linux_minidump_writer.rs775
-rw-r--r--third_party/rust/minidump-writer/tests/mac_minidump_writer.rs227
-rw-r--r--third_party/rust/minidump-writer/tests/ptrace_dumper.rs316
-rw-r--r--third_party/rust/minidump-writer/tests/task_dumper.rs158
-rw-r--r--third_party/rust/minidump-writer/tests/windows_minidump_writer.rs184
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)
+ );
+}