#![cfg(any(target_os = "linux", target_os = "android"))] #![allow(unused_imports, unused_variables)] use minidump::*; use minidump_common::format::{GUID, MINIDUMP_STREAM_TYPE::*}; use minidump_writer::{ app_memory::AppMemory, crash_context::CrashContext, errors::*, maps_reader::{MappingEntry, MappingInfo, SystemMappingInfo}, minidump_writer::MinidumpWriter, module_reader::{BuildId, ReadFromModule}, ptrace_dumper::PtraceDumper, Pid, }; use nix::{errno::Errno, sys::signal::Signal}; use procfs_core::process::MMPermissions; use std::collections::HashSet; use std::{ io::{BufRead, BufReader}, os::unix::process::ExitStatusExt, process::{Command, Stdio}, }; mod common; use common::*; #[derive(Debug, PartialEq)] enum Context { With, Without, } impl Context { pub fn minidump_writer(&self, pid: Pid) -> MinidumpWriter { let mut mw = MinidumpWriter::new(pid, pid); #[cfg(not(target_arch = "mips"))] if self == &Context::With { let crash_context = get_crash_context(pid); mw.set_crash_context(crash_context); } mw } } #[cfg(not(target_arch = "mips"))] fn get_ucontext() -> Result { 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(target_arch = "mips"))] 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"); #[cfg(not(target_arch = "arm"))] let float_state = unsafe { std::mem::zeroed() }; CrashContext { inner: crash_context::CrashContext { siginfo, pid: std::process::id() as _, tid, context, #[cfg(not(target_arch = "arm"))] float_state, }, } } macro_rules! contextual_test { ( $(#[$attr:meta])? fn $name:ident ($ctx:ident : Context) $body:block ) => { mod $name { use super::*; fn test($ctx: Context) $body #[test] $(#[$attr])? fn without_context() { test(Context::Without) } #[cfg(not(target_arch = "mips"))] #[test] $(#[$attr])? fn with_context() { test(Context::With) } } } } contextual_test! { fn write_dump(context: Context) { let num_of_threads = 3; let mut child = start_child_and_wait_for_threads(num_of_threads); let pid = child.id() as i32; let mut tmpfile = tempfile::Builder::new() .prefix("write_dump") .tempfile() .unwrap(); let mut tmp = context.minidump_writer(pid); let in_memory_buffer = tmp.dump(&mut tmpfile).expect("Could not write minidump"); child.kill().expect("Failed to kill process"); // 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); } } contextual_test! { #[ignore] fn write_and_read_dump_from_parent(context: Context) { let mut child = start_child_and_return(&["spawn_mmap_wait"]); let pid = child.id() as i32; let mut tmpfile = tempfile::Builder::new() .prefix("write_and_read_dump") .tempfile() .unwrap(); let mut f = BufReader::new(child.stdout.as_mut().expect("Can't open stdout")); let mut buf = String::new(); let _ = f .read_line(&mut buf) .expect("Couldn't read address provided by child"); let mut output = buf.split_whitespace(); let mmap_addr = output .next() .unwrap() .parse() .expect("unable to parse mmap_addr"); let memory_size = output .next() .unwrap() .parse() .expect("unable to parse memory_size"); // Add information about the mapped memory. let mapping = MappingInfo { start_address: mmap_addr, size: memory_size, offset: 0, permissions: MMPermissions::READ | MMPermissions::WRITE, name: Some("a fake mapping".into()), system_mapping_info: SystemMappingInfo { start_address: mmap_addr, end_address: mmap_addr + memory_size, }, }; let identifier = vec![ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, ]; let entry = MappingEntry { mapping, identifier, }; let mut tmp = context.minidump_writer(pid); tmp.set_user_mapping_list(vec![entry]) .dump(&mut tmpfile) .expect("Could not write minidump"); 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"); let _ = dump .get_raw_stream(MozLinuxLimits as u32) .expect("Couldn't find MozLinuxLimits"); } } contextual_test! { fn write_with_additional_memory(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 = context.minidump_writer(pid); 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::::with_capacity(memory_size); for idx in 0..memory_size { values.push((idx % 255) as u8); } // Verify memory contents. assert_eq!(region.bytes, values); } } contextual_test! { fn skip_if_requested(context: Context) { let num_of_threads = 1; let mut child = start_child_and_wait_for_threads(num_of_threads); let pid = child.id() as i32; let mut tmpfile = tempfile::Builder::new() .prefix("skip_if_requested") .tempfile() .unwrap(); let mut tmp = context.minidump_writer(pid); let pr_mapping_addr; #[cfg(target_pointer_width = "64")] { pr_mapping_addr = 0x0102030405060708; } #[cfg(target_pointer_width = "32")] { pr_mapping_addr = 0x010203040; }; let res = tmp .skip_stacks_if_mapping_unreferenced() .set_principal_mapping_address(pr_mapping_addr) .dump(&mut tmpfile); child.kill().expect("Failed to kill process"); // Reap child let waitres = child.wait().expect("Failed to wait for child"); let status = waitres.signal().expect("Child did not die due to signal"); assert_eq!(waitres.code(), None); assert_eq!(status, Signal::SIGKILL as i32); assert!(res.is_err()); } } contextual_test! { fn sanitized_stacks(context: Context) { if context == Context::With { // FIXME the context's stack pointer very often doesn't lie in mapped memory, resulting // in the stack memory having 0 size (so no slice will match `defaced` in the // assertion). return; } let num_of_threads = 1; let mut child = start_child_and_wait_for_threads(num_of_threads); let pid = child.id() as i32; let mut tmpfile = tempfile::Builder::new() .prefix("sanitized_stacks") .tempfile() .unwrap(); let mut tmp = context.minidump_writer(pid); tmp.sanitize_stack() .dump(&mut tmpfile) .expect("Faild to dump minidump"); child.kill().expect("Failed to kill process"); // Reap child let waitres = child.wait().expect("Failed to wait for child"); let status = waitres.signal().expect("Child did not die due to signal"); assert_eq!(waitres.code(), None); assert_eq!(status, Signal::SIGKILL as i32); // 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)); } } } contextual_test! { fn write_early_abort(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 = context.minidump_writer(pid); // This should fail, because during the dump an error is detected (try_from fails) match tmp.set_app_memory(vec![app_memory]).dump(&mut tmpfile) { Err(WriterError::SectionAppMemoryError(_)) => (), _ => panic!("Wrong kind of error returned"), } child.kill().expect("Failed to kill process"); // Reap child let waitres = child.wait().expect("Failed to wait for child"); let status = waitres.signal().expect("Child did not die due to signal"); assert_eq!(waitres.code(), None); assert_eq!(status, Signal::SIGKILL as i32); // Read dump file and check its contents. There should be a truncated minidump available let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); // Should be there let _: MinidumpThreadList = dump.get_stream().expect("Couldn't find MinidumpThreadList"); let _: MinidumpModuleList = dump.get_stream().expect("Couldn't find MinidumpThreadList"); // Should be missing: assert!(dump.get_stream::().is_err()); } } contextual_test! { fn named_threads(context: Context) { let num_of_threads = 5; let mut child = start_child_and_wait_for_named_threads(num_of_threads); let pid = child.id() as i32; let mut tmpfile = tempfile::Builder::new() .prefix("named_threads") .tempfile() .unwrap(); let mut tmp = context.minidump_writer(pid); let _ = tmp.dump(&mut tmpfile).expect("Could not write minidump"); child.kill().expect("Failed to kill process"); // Reap child let waitres = child.wait().expect("Failed to wait for child"); let status = waitres.signal().expect("Child did not die due to signal"); assert_eq!(waitres.code(), None); assert_eq!(status, Signal::SIGKILL as i32); // Read dump file and check its contents. There should be a truncated minidump available let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); let threads: MinidumpThreadList = dump.get_stream().expect("Couldn't find MinidumpThreadList"); let thread_names: MinidumpThreadNames = dump .get_stream() .expect("Couldn't find MinidumpThreadNames"); let thread_ids: Vec<_> = threads.threads.iter().map(|t| t.raw.thread_id).collect(); let names: HashSet<_> = thread_ids .iter() .map(|id| thread_names.get_name(*id).unwrap_or_default()) .map(|cow| cow.into_owned()) .collect(); let mut expected = HashSet::new(); expected.insert("test".to_string()); for id in 1..num_of_threads { expected.insert(format!("thread_{}", id)); } assert_eq!(expected, names); } } contextual_test! { fn file_descriptors(context: Context) { let num_of_files = 5; let mut child = start_child_and_wait_for_create_files(num_of_files); let pid = child.id() as i32; let mut tmpfile = tempfile::Builder::new() .prefix("testfiles") .tempfile() .unwrap(); let mut tmp = context.minidump_writer(pid); let _ = tmp.dump(&mut tmpfile).expect("Could not write minidump"); child.kill().expect("Failed to kill process"); // Reap child let waitres = child.wait().expect("Failed to wait for child"); let status = waitres.signal().expect("Child did not die due to signal"); assert_eq!(waitres.code(), None); assert_eq!(status, Signal::SIGKILL as i32); // Read dump file and check its contents. There should be a truncated minidump available let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump"); let fds: MinidumpHandleDataStream = dump.get_stream().expect("Couldn't find MinidumpHandleDataStream"); // We check that we create num_of_files plus stdin, stdout and stderr for i in 0..3 { let descriptor = fds.handles.get(i).expect("Descriptor should be present"); let fd = *descriptor.raw.handle().expect("Handle should be populated"); assert_eq!(fd, i as u64); } let non_std_files = &fds.handles[3..]; // We need to handle the android case where additional pipes might be opened and // interspersed with the test_files (emulator? adb?) so that CI doesn't sporadically fail for i in 0..num_of_files { if !non_std_files.iter().any(|descriptor| { let Some(name) = &descriptor.object_name else { return false; }; let Some(file_name) = name.rsplit_once('/').map(|(_, fname)| fname) else { return false; }; if !file_name.starts_with("test_file") { return false; } file_name.ends_with(&i.to_string()) }) { panic!("unable to locate expected file `test_file{i}` in file handle stream"); } } } } #[test] fn 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 //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); // Setting a stack limit limits the size of non-main stacks even before // the limit is reached. This will cause slight variations in size // between a limited and an unlimited minidump. assert!(max - min < 1024, "max = {max:} min = {min:}"); } // 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 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: String = if let Ok(p) = std::env::var("TEST_HELPER") { p } else { std::env!("CARGO_BIN_EXE_test").into() }; 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(num_of_threads.to_string()) .stdout(Stdio::piped()) .spawn() .expect("failed to execute child"); wait_for_threads(&mut child, num_of_threads); let pid = child.id() as i32; let BuildId(mut build_id) = BuildId::read_from_module(mem_slice.as_slice().into()).expect("Failed to get build_id"); 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()); let did = main_module .debug_identifier() .expect("expected value debug id"); { let uuid = did.uuid(); let uuid = uuid.as_bytes(); // Swap bytes in the original to match the expected uuid if cfg!(target_endian = "little") { build_id[..4].reverse(); build_id[4..6].reverse(); build_id[6..8].reverse(); } // The build_id from the binary can be as little as 8 bytes, eg LLD uses // xxhash to calculate the build_id by default from 10+ build_id.resize(16, 0); assert_eq!(uuid.as_slice(), &build_id); } // The 'age'/appendix, always 0 on non-windows targets assert_eq!(did.appendix(), 0); } #[test] fn memory_info_list_stream() { let mut child = start_child_and_wait_for_threads(1); let pid = child.id() as i32; let mut tmpfile = tempfile::Builder::new() .prefix("memory_info_list_stream") .tempfile() .unwrap(); // Write a minidump MinidumpWriter::new(pid, pid) .dump(&mut tmpfile) .expect("cound not write minidump"); child.kill().expect("Failed to kill process"); // Ensure the minidump has a MemoryInfoListStream present and has at least one entry. let dump = Minidump::read_path(tmpfile.path()).expect("failed to read minidump"); let list: MinidumpMemoryInfoList = dump.get_stream().expect("no memory info list"); assert!(list.iter().count() > 1); }