summaryrefslogtreecommitdiffstats
path: root/third_party/rust/minidump-writer/src/windows/minidump_writer.rs
blob: 70cc420e579a0d297d12c677596e9731c32940e0 (plain)
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
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
#![allow(unsafe_code)]

use crate::windows::errors::Error;
use crate::windows::ffi::{
    capture_context, CloseHandle, GetCurrentProcess, GetCurrentThreadId, GetThreadContext,
    MiniDumpWriteDump, MinidumpType, OpenProcess, OpenThread, ResumeThread, SuspendThread,
    EXCEPTION_POINTERS, EXCEPTION_RECORD, FALSE, HANDLE, MINIDUMP_EXCEPTION_INFORMATION,
    MINIDUMP_USER_STREAM, MINIDUMP_USER_STREAM_INFORMATION, PROCESS_ALL_ACCESS,
    STATUS_NONCONTINUABLE_EXCEPTION, THREAD_GET_CONTEXT, THREAD_QUERY_INFORMATION,
    THREAD_SUSPEND_RESUME,
};
use minidump_common::format::{BreakpadInfoValid, MINIDUMP_BREAKPAD_INFO, MINIDUMP_STREAM_TYPE};
use scroll::Pwrite;
use std::os::windows::io::AsRawHandle;

pub struct MinidumpWriter {
    /// Optional exception information
    exc_info: Option<MINIDUMP_EXCEPTION_INFORMATION>,
    /// Handle to the crashing process, which could be ourselves
    crashing_process: HANDLE,
    /// The id of the process we are dumping
    pid: u32,
    /// The id of the 'crashing' thread
    tid: u32,
    /// The exception code for the dump
    #[allow(dead_code)]
    exception_code: i32,
    /// Whether we are dumping the current process or not
    is_external_process: bool,
}

impl MinidumpWriter {
    /// Creates a minidump of the current process, optionally including an
    /// exception code and the CPU context of the specified thread. If no thread
    /// is specified the current thread CPU context is used.
    ///
    /// Note that it is inherently unreliable to dump the currently running
    /// process, at least in the event of an actual exception. It is recommended
    /// to dump from an external process if possible via [`Self::dump_crash_context`]
    ///
    /// # Errors
    ///
    /// In addition to the errors described in [`Self::dump_crash_context`], this
    /// function can also fail if `thread_id` is specified and we are unable to
    /// acquire the thread's context
    pub fn dump_local_context(
        exception_code: Option<i32>,
        thread_id: Option<u32>,
        minidump_type: Option<MinidumpType>,
        destination: &mut std::fs::File,
    ) -> Result<(), Error> {
        let exception_code = exception_code.unwrap_or(STATUS_NONCONTINUABLE_EXCEPTION);

        // SAFETY: syscalls, while this encompasses most of the function, the user
        // has no invariants to uphold so the entire function is not marked unsafe
        unsafe {
            let mut exception_context = if let Some(tid) = thread_id {
                let mut ec = std::mem::MaybeUninit::uninit();

                // We need to suspend the thread to get its context, which would be bad
                // if it's the current thread, so we check it early before regrets happen
                if tid == GetCurrentThreadId() {
                    capture_context(ec.as_mut_ptr());
                } else {
                    // We _could_ just fallback to the current thread if we can't get the
                    // thread handle, but probably better for this to fail with a specific
                    // error so that the caller can do that themselves if they want to
                    // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openthread
                    let thread_handle = OpenThread(
                        THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION | THREAD_SUSPEND_RESUME, // desired access rights, we only need to get the context, which also requires suspension
                        FALSE, // inherit handles
                        tid,   // thread id
                    );

                    if thread_handle == 0 {
                        return Err(Error::ThreadOpen(std::io::Error::last_os_error()));
                    }

                    struct OwnedHandle(HANDLE);

                    impl Drop for OwnedHandle {
                        fn drop(&mut self) {
                            // SAFETY: syscall
                            unsafe { CloseHandle(self.0) };
                        }
                    }

                    let thread_handle = OwnedHandle(thread_handle);

                    // As noted in the GetThreadContext docs, we have to suspend the thread before we can get its context
                    // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-suspendthread
                    if SuspendThread(thread_handle.0) == u32::MAX {
                        return Err(Error::ThreadSuspend(std::io::Error::last_os_error()));
                    }

                    // https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadcontext
                    if GetThreadContext(thread_handle.0, ec.as_mut_ptr()) == 0 {
                        // Try to be a good citizen and resume the thread
                        ResumeThread(thread_handle.0);

                        return Err(Error::ThreadContext(std::io::Error::last_os_error()));
                    }

                    // _presumably_ this will not fail if SuspendThread succeeded, but if it does
                    // there's really not much we can do about it, thus we don't bother checking the
                    // return value
                    ResumeThread(thread_handle.0);
                }

                ec.assume_init()
            } else {
                let mut ec = std::mem::MaybeUninit::uninit();
                capture_context(ec.as_mut_ptr());
                ec.assume_init()
            };

            let mut exception_record: EXCEPTION_RECORD = std::mem::zeroed();

            let exception_ptrs = EXCEPTION_POINTERS {
                ExceptionRecord: &mut exception_record,
                ContextRecord: &mut exception_context,
            };

            exception_record.ExceptionCode = exception_code;

            let cc = crash_context::CrashContext {
                exception_pointers: (&exception_ptrs as *const EXCEPTION_POINTERS).cast(),
                process_id: std::process::id(),
                thread_id: thread_id.unwrap_or_else(|| GetCurrentThreadId()),
                exception_code,
            };

            Self::dump_crash_context(cc, minidump_type, destination)
        }
    }

    /// Writes a minidump for the context described by [`crash_context::CrashContext`].
    ///
    /// # Errors
    ///
    /// Fails if the process specified in the context is not the local process
    /// and we are unable to open it due to eg. security reasons, or we fail to
    /// write the minidump, which can be due to a host of issues with both acquiring
    /// the process information as well as writing the actual minidump contents to disk
    ///
    /// # Safety
    ///
    /// If [`crash_context::CrashContext::exception_pointers`] is specified, it
    /// is the responsibility of the caller to ensure that the pointer is valid
    /// for the duration of this function call.
    pub fn dump_crash_context(
        crash_context: crash_context::CrashContext,
        minidump_type: Option<MinidumpType>,
        destination: &mut std::fs::File,
    ) -> Result<(), Error> {
        let pid = crash_context.process_id;

        // SAFETY: syscalls
        let (crashing_process, is_external_process) = unsafe {
            if pid != std::process::id() {
                let proc = OpenProcess(
                    PROCESS_ALL_ACCESS, // desired access
                    FALSE,              // inherit handles
                    pid,                // pid
                );

                if proc == 0 {
                    return Err(std::io::Error::last_os_error().into());
                }

                (proc, true)
            } else {
                (GetCurrentProcess(), false)
            }
        };

        let pid = crash_context.process_id;
        let tid = crash_context.thread_id;
        let exception_code = crash_context.exception_code;

        let exc_info = (!crash_context.exception_pointers.is_null()).then_some(
            // https://docs.microsoft.com/en-us/windows/win32/api/minidumpapiset/ns-minidumpapiset-minidump_exception_information
            MINIDUMP_EXCEPTION_INFORMATION {
                ThreadId: crash_context.thread_id,
                // This is a mut pointer for some reason...I don't _think_ it is
                // actually mut in practice...?
                ExceptionPointers: crash_context.exception_pointers as *mut _,
                /// The `EXCEPTION_POINTERS` contained in crash context is a pointer into the
                /// memory of the process that crashed, as it contains an `EXCEPTION_RECORD`
                /// record which is an internally linked list, so in the case that we are
                /// dumping a process other than the current one, we need to tell
                /// `MiniDumpWriteDump` that the pointers come from an external process so that
                /// it can use eg `ReadProcessMemory` to get the contextual information from
                /// the crash, rather than from the current process
                ClientPointers: i32::from(is_external_process),
            },
        );

        let mdw = Self {
            exc_info,
            crashing_process,
            pid,
            tid,
            exception_code,
            is_external_process,
        };

        mdw.dump(minidump_type, destination)
    }

    /// Writes a minidump to the specified file
    fn dump(
        mut self,
        minidump_type: Option<MinidumpType>,
        destination: &mut std::fs::File,
    ) -> Result<(), Error> {
        let exc_info = self.exc_info.take();

        let mut user_streams = Vec::with_capacity(1);

        let mut breakpad_info = self.fill_breakpad_stream();

        if let Some(bp_info) = &mut breakpad_info {
            user_streams.push(MINIDUMP_USER_STREAM {
                Type: MINIDUMP_STREAM_TYPE::BreakpadInfoStream as u32,
                BufferSize: bp_info.len() as u32,
                // Again with the mut pointer
                Buffer: bp_info.as_mut_ptr().cast(),
            });
        }

        let user_stream_infos = MINIDUMP_USER_STREAM_INFORMATION {
            UserStreamCount: user_streams.len() as u32,
            UserStreamArray: user_streams.as_mut_ptr(),
        };

        // Write the actual minidump
        // https://docs.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump
        // SAFETY: syscall
        let ret = unsafe {
            MiniDumpWriteDump(
                self.crashing_process, // HANDLE to the process with the crash we want to capture
                self.pid,              // process id
                destination.as_raw_handle() as HANDLE, // file to write the minidump to
                minidump_type.unwrap_or(MinidumpType::Normal),
                exc_info
                    .as_ref()
                    .map_or(std::ptr::null(), |ei| ei as *const _), // exceptionparam - the actual exception information
                &user_stream_infos, // user streams
                std::ptr::null(),   // callback, unused
            )
        };

        if ret == 0 {
            Err(std::io::Error::last_os_error().into())
        } else {
            Ok(())
        }
    }

    /// Create an MDRawBreakpadInfo stream to the minidump, to provide additional
    /// information about the exception handler to the Breakpad processor.
    /// The information will help the processor determine which threads are
    /// relevant. The Breakpad processor does not require this information but
    /// can function better with Breakpad-generated dumps when it is present.
    /// The native debugger is not harmed by the presence of this information.
    ///
    /// This info is only relevant for in-process dumping
    fn fill_breakpad_stream(&self) -> Option<[u8; 12]> {
        if self.is_external_process {
            return None;
        }

        let mut breakpad_info = [0u8; 12];

        let bp_info = MINIDUMP_BREAKPAD_INFO {
            validity: BreakpadInfoValid::DumpThreadId.bits()
                | BreakpadInfoValid::RequestingThreadId.bits(),
            dump_thread_id: self.tid,
            // SAFETY: syscall
            requesting_thread_id: unsafe { GetCurrentThreadId() },
        };

        // TODO: derive Pwrite for MINIDUMP_BREAKPAD_INFO
        // https://github.com/rust-minidump/rust-minidump/pull/534
        let mut offset = 0;
        breakpad_info.gwrite(bp_info.validity, &mut offset).ok()?;
        breakpad_info
            .gwrite(bp_info.dump_thread_id, &mut offset)
            .ok()?;
        breakpad_info
            .gwrite(bp_info.requesting_thread_id, &mut offset)
            .ok()?;

        Some(breakpad_info)
    }
}

impl Drop for MinidumpWriter {
    fn drop(&mut self) {
        // Note we close the handle regardless of whether it is the local handle
        // or an external one, as noted in the docs
        //
        // > The pseudo handle need not be closed when it is no longer needed.
        // > Calling the CloseHandle function with a pseudo handle has no effect.
        // SAFETY: syscall
        unsafe { CloseHandle(self.crashing_process) };
    }
}