diff options
Diffstat (limited to 'third_party/rust/minidump-writer/src/windows/minidump_writer.rs')
-rw-r--r-- | third_party/rust/minidump-writer/src/windows/minidump_writer.rs | 309 |
1 files changed, 309 insertions, 0 deletions
diff --git a/third_party/rust/minidump-writer/src/windows/minidump_writer.rs b/third_party/rust/minidump-writer/src/windows/minidump_writer.rs new file mode 100644 index 0000000000..70cc420e57 --- /dev/null +++ b/third_party/rust/minidump-writer/src/windows/minidump_writer.rs @@ -0,0 +1,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) }; + } +} |