1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
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"));
}
|