Add WER runtime exception helper module for Windows

This adds a runtime exception helper (& test module) for Windows and
plumbing to allow the module to be registered by the crashpad client,
and to trigger the crashpad handler. Embedders can build their own
module to control which exceptions are passed to the handler.

See: go/chrome-windows-runtime-exception-helper for motivation.

When registered (which is the responsibility of the embedding
application), the helper is loaded by WerFault.exe when Windows
Error Reporting receives crashes that are not caught by crashpad's
normal handlers - for instance a control-flow violation when a
module is compiled with /guard:cf.

Registration:

The embedder must arrange for the full path to the helper to
be added in the appropriate Windows Error Reporting\
RuntimeExceptionHelperModules registry key.

Once an embedder's crashpad client is connected to a crashpad
handler (e.g. through SetIpcPipeName()) the embedder calls
RegisterWerModule. Internally, this registration includes handles
used to trigger the crashpad handler, an area reserved to hold an
exception and context, and structures needed by the crashpad handler.

Following a crash:

WerFault.exe handles the crash then validates and loads the helper
module. WER hands the helper module a handle to the crashing target
process and copies of the exception and context for the faulting thread.

The helper then copies out the client's registration data and
duplicates handles to the crashpad handler, then fills back the various structures in the paused client that the crashpad handler will need.

The helper then signals the crashpad handler, which collects a dump then
notifies the helper that it is done.

Support:

WerRegisterExceptionHelperModule has been availble since at least
Windows 7 but WerFault would not pass on the exceptions that crashpad
could not already handle. This changed in Windows 10 20H1 (19041),
which supports HKCU and HKLM registrations, and passes in more types of
crashes. It is harmless to register the module for earlier versions
of Windows as it simply won't be loaded by WerFault.exe.

Tests:

snapshot/win/end_to_end_test.py has been refactored slightly to
group crash generation and output validation in main() by breaking
up RunTests into smaller functions.

As the module works by being loaded in WerFault.exe it is tested
in end_to_end_test.py.

Bug: crashpad:133, 866033, 865632
Change-Id: Id668bd15a510a24c79753e1bb03e9456f41a9780
Reviewed-on: https://chromium-review.googlesource.com/c/crashpad/crashpad/+/3677284
Reviewed-by: Joshua Peraza <jperaza@chromium.org>
Commit-Queue: Alex Gough <ajgo@chromium.org>
This commit is contained in:
Alex Gough 2022-07-06 13:53:12 -07:00 committed by Crashpad LUCI CQ
parent bac699ef47
commit 80520bd937
15 changed files with 849 additions and 48 deletions

View File

@ -195,7 +195,10 @@ source_set("client_test") {
}
if (crashpad_is_win) {
data_deps += [ "../handler:crashpad_handler_console" ]
data_deps += [
"../handler:crashpad_handler_console",
"../handler/win/wer:crashpad_wer_handler",
]
}
}

View File

@ -669,6 +669,21 @@ class CrashpadClient {
//! error message will have been logged.
bool WaitForHandlerStart(unsigned int timeout_ms);
//! \brief Register a DLL using WerRegisterExceptionModule().
//!
//! This method should only be called after a successful call to
//! SetHandlerIPCPipe() or StartHandler(). The registration is valid for the
//! lifetime of this object.
//!
//! \param[in] full_path The full path to the DLL that will be registered.
//! The DLL path should also be set in an appropriate
//! `Windows Error Reporting` registry key.
//!
//! \return `true` if the DLL was registered. Note: Windows just stashes the
//! path somewhere so this returns `true` even if the DLL is not yet
//! set in an appropriate registry key, or does not exist.
bool RegisterWerModule(const std::wstring& full_path);
//! \brief Requests that the handler capture a dump even though there hasn't
//! been a crash.
//!

View File

@ -16,6 +16,8 @@
#include <windows.h>
#include <werapi.h>
#include <signal.h>
#include <stdint.h>
#include <string.h>
@ -61,19 +63,25 @@ HANDLE g_signal_exception = INVALID_HANDLE_VALUE;
// Where we store the exception information that the crash handler reads.
ExceptionInformation g_crash_exception_information;
// These handles are never closed. g_signal_non_crash_dump is used to signal to
// the server to take a dump (not due to an exception), and the server will
// signal g_non_crash_dump_done when the dump is completed.
HANDLE g_signal_non_crash_dump = INVALID_HANDLE_VALUE;
HANDLE g_non_crash_dump_done = INVALID_HANDLE_VALUE;
// Guards multiple simultaneous calls to DumpWithoutCrash(). This is leaked.
// Guards multiple simultaneous calls to DumpWithoutCrash() in the client.
// This is leaked.
base::Lock* g_non_crash_dump_lock;
// Where we store a pointer to the context information when taking a non-crash
// dump.
ExceptionInformation g_non_crash_exception_information;
// Context for the out-of-process exception handler module and holds non-crash
// dump handles. Handles are never closed once created.
WerRegistration g_wer_registration = {WerRegistration::kWerRegistrationVersion,
INVALID_HANDLE_VALUE,
INVALID_HANDLE_VALUE,
false,
nullptr,
{0},
{0},
{0}};
enum class StartupState : int {
kNotReady = 0, // This must be value 0 because it is the initial value of a
// global AtomicWord.
@ -402,8 +410,8 @@ bool StartHandlerProcess(
InitialClientData initial_client_data(
g_signal_exception,
g_signal_non_crash_dump,
g_non_crash_dump_done,
g_wer_registration.dump_without_crashing,
g_wer_registration.dump_completed,
data->ipc_pipe_handle.get(),
this_process.get(),
FromPointerCast<WinVMAddress>(&g_crash_exception_information),
@ -467,8 +475,8 @@ bool StartHandlerProcess(
handle_list.reserve(8);
handle_list.push_back(g_signal_exception);
handle_list.push_back(g_signal_non_crash_dump);
handle_list.push_back(g_non_crash_dump_done);
handle_list.push_back(g_wer_registration.dump_without_crashing);
handle_list.push_back(g_wer_registration.dump_completed);
handle_list.push_back(data->ipc_pipe_handle.get());
handle_list.push_back(this_process.get());
AddHandleToListIfValidAndInheritable(&handle_list,
@ -625,9 +633,9 @@ bool CrashpadClient::StartHandler(
g_signal_exception =
CreateEvent(&security_attributes, false /* auto reset */, false, nullptr);
g_signal_non_crash_dump =
g_wer_registration.dump_without_crashing =
CreateEvent(&security_attributes, false /* auto reset */, false, nullptr);
g_non_crash_dump_done =
g_wer_registration.dump_completed =
CreateEvent(&security_attributes, false /* auto reset */, false, nullptr);
CommonInProcessInitialization();
@ -679,8 +687,8 @@ bool CrashpadClient::SetHandlerIPCPipe(const std::wstring& ipc_pipe) {
DCHECK(!ipc_pipe_.empty());
DCHECK_EQ(g_signal_exception, INVALID_HANDLE_VALUE);
DCHECK_EQ(g_signal_non_crash_dump, INVALID_HANDLE_VALUE);
DCHECK_EQ(g_non_crash_dump_done, INVALID_HANDLE_VALUE);
DCHECK_EQ(g_wer_registration.dump_without_crashing, INVALID_HANDLE_VALUE);
DCHECK_EQ(g_wer_registration.dump_completed, INVALID_HANDLE_VALUE);
DCHECK(!g_critical_section_with_debug_info.DebugInfo);
DCHECK(!g_non_crash_dump_lock);
@ -712,9 +720,9 @@ bool CrashpadClient::SetHandlerIPCPipe(const std::wstring& ipc_pipe) {
// The server returns these already duplicated to be valid in this process.
g_signal_exception =
IntToHandle(response.registration.request_crash_dump_event);
g_signal_non_crash_dump =
g_wer_registration.dump_without_crashing =
IntToHandle(response.registration.request_non_crash_dump_event);
g_non_crash_dump_done =
g_wer_registration.dump_completed =
IntToHandle(response.registration.non_crash_dump_completed_event);
return true;
@ -749,10 +757,29 @@ bool CrashpadClient::WaitForHandlerStart(unsigned int timeout_ms) {
return exit_code == 0;
}
bool CrashpadClient::RegisterWerModule(const std::wstring& path) {
if (g_wer_registration.dump_completed == INVALID_HANDLE_VALUE ||
g_wer_registration.dump_without_crashing == INVALID_HANDLE_VALUE) {
LOG(ERROR) << "not connected";
return false;
}
// We cannot point (*context).exception_pointers to our pointers yet as it
// might get used for other non-crash dumps.
g_wer_registration.crashpad_exception_info =
&g_non_crash_exception_information;
// we can point these as we are the only users.
g_wer_registration.pointers.ExceptionRecord = &g_wer_registration.exception;
g_wer_registration.pointers.ContextRecord = &g_wer_registration.context;
HRESULT res =
WerRegisterRuntimeExceptionModule(path.c_str(), &g_wer_registration);
return res == S_OK;
}
// static
void CrashpadClient::DumpWithoutCrash(const CONTEXT& context) {
if (g_signal_non_crash_dump == INVALID_HANDLE_VALUE ||
g_non_crash_dump_done == INVALID_HANDLE_VALUE) {
if (g_wer_registration.dump_without_crashing == INVALID_HANDLE_VALUE ||
g_wer_registration.dump_completed == INVALID_HANDLE_VALUE) {
LOG(ERROR) << "not connected";
return;
}
@ -760,7 +787,7 @@ void CrashpadClient::DumpWithoutCrash(const CONTEXT& context) {
if (BlockUntilHandlerStartedOrFailed() == StartupState::kFailed) {
// If we know for certain that the handler has failed to start, then abort
// here, as we would otherwise wait indefinitely for the
// g_non_crash_dump_done event that would never be signalled.
// g_wer_registration.dump_completed event that would never be signalled.
LOG(ERROR) << "crash server failed to launch, no dump captured";
return;
}
@ -798,11 +825,14 @@ void CrashpadClient::DumpWithoutCrash(const CONTEXT& context) {
g_non_crash_exception_information.exception_pointers =
FromPointerCast<WinVMAddress>(&exception_pointers);
bool set_event_result = !!SetEvent(g_signal_non_crash_dump);
g_wer_registration.in_dump_without_crashing = true;
bool set_event_result = !!SetEvent(g_wer_registration.dump_without_crashing);
PLOG_IF(ERROR, !set_event_result) << "SetEvent";
DWORD wfso_result = WaitForSingleObject(g_non_crash_dump_done, INFINITE);
DWORD wfso_result =
WaitForSingleObject(g_wer_registration.dump_completed, INFINITE);
PLOG_IF(ERROR, wfso_result != WAIT_OBJECT_0) << "WaitForSingleObject";
g_wer_registration.in_dump_without_crashing = false;
}
// static

View File

@ -145,7 +145,10 @@ source_set("handler_test") {
]
if (crashpad_is_win) {
deps += [ "../minidump:test_support" ]
deps += [
"../minidump:test_support",
"win/wer:crashpad_wer_test",
]
data_deps = [
":crashpad_handler_test_extended_handler",
@ -353,4 +356,20 @@ if (crashpad_is_win) {
]
}
}
config("enable_cfg") {
cflags = [ "/guard:cf" ]
ldflags = [ "/guard:cf" ]
}
crashpad_executable("fastfail_program") {
testonly = true
sources = [ "win/fastfail_test_program.cc" ]
configs = [ ":enable_cfg" ]
deps = [
"../client",
"../third_party/mini_chromium:base",
]
}
}

View File

@ -0,0 +1,146 @@
// Copyright 2022 The Crashpad Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <string.h>
#include "base/files/file_path.h"
#include "base/logging.h"
#include "client/crashpad_client.h"
#include "util/misc/paths.h"
#include <Windows.h>
// We set up a program that crashes with a CFG exception so must be built and
// linked with /guard:cf. We register the crashpad runtime exception helper
// module to intercept and trigger the crashpad handler. Note that Windows only
// loads the module in WerFault after the crash for Windows 10 >= 20h1 (19041).
namespace crashpad {
namespace {
// This function should not be on our stack as CFG prevents the modified
// icall from happening.
int CallRffeManyTimes() {
RaiseFailFastException(nullptr, nullptr, 0);
RaiseFailFastException(nullptr, nullptr, 0);
RaiseFailFastException(nullptr, nullptr, 0);
RaiseFailFastException(nullptr, nullptr, 0);
return 1;
}
using FuncType = decltype(&CallRffeManyTimes);
void IndirectCall(FuncType* func) {
// This code always generates CFG guards.
(*func)();
}
void CfgCrash() {
// Call into the middle of the Crashy function.
FuncType func = reinterpret_cast<FuncType>(
(reinterpret_cast<uintptr_t>(CallRffeManyTimes)) + 16);
__try {
// Generates a STATUS_STACK_BUFFER_OVERRUN exception if CFG triggers.
IndirectCall(&func);
} __except (EXCEPTION_EXECUTE_HANDLER) {
// CFG fast fail should never be caught.
CHECK(false);
}
// Should only reach here if CFG is disabled.
abort();
}
void FastFailCrash() {
__fastfail(77);
}
int CrashyMain(int argc, wchar_t* argv[]) {
static CrashpadClient* client = new crashpad::CrashpadClient();
std::wstring type;
if (argc == 3) {
type = argv[2];
// We call this from end_to_end_test.py.
if (!client->SetHandlerIPCPipe(argv[1])) {
LOG(ERROR) << "SetHandler";
return EXIT_FAILURE;
}
} else if (argc == 4) {
type = argv[3];
// This is helpful for debugging.
if (!client->StartHandler(base::FilePath(argv[1]),
base::FilePath(argv[2]),
base::FilePath(),
std::string(),
std::map<std::string, std::string>(),
std::vector<std::string>(),
false,
true)) {
LOG(ERROR) << "StartHandler";
return EXIT_FAILURE;
}
// Got to have a handler & registration.
if (!client->WaitForHandlerStart(10000)) {
LOG(ERROR) << "Handler failed to start";
return EXIT_FAILURE;
}
} else {
fprintf(stderr, "Usage: %ls <server_pipe_name> [cf|ff]\n", argv[0]);
fprintf(
stderr, " %ls <handler_path> <database_path> [cf|ff]\n", argv[0]);
return EXIT_FAILURE;
}
base::FilePath exe_path;
if (!Paths::Executable(&exe_path)) {
LOG(ERROR) << "Failed getting path";
return EXIT_FAILURE;
}
// Find the module.
auto mod_path = exe_path.DirName().Append(L"crashpad_wer.dll");
// Make sure it is registered in the registry.
DWORD dwOne = 1;
LSTATUS reg_res =
RegSetKeyValueW(HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\Windows Error Reporting\\"
L"RuntimeExceptionHelperModules",
mod_path.value().c_str(),
REG_DWORD,
&dwOne,
sizeof(DWORD));
if (reg_res != ERROR_SUCCESS) {
LOG(ERROR) << "RegSetKeyValueW";
return EXIT_FAILURE;
}
if (!client->RegisterWerModule(mod_path.value())) {
LOG(ERROR) << "WerRegisterRuntimeExceptionModule";
return EXIT_FAILURE;
}
if (type == L"cf")
CfgCrash();
if (type == L"ff")
FastFailCrash();
LOG(ERROR) << "Invalid type or exception failed.";
return EXIT_FAILURE;
}
} // namespace
} // namespace crashpad
int wmain(int argc, wchar_t* argv[]) {
return crashpad::CrashyMain(argc, argv);
}

47
handler/win/wer/BUILD.gn Normal file
View File

@ -0,0 +1,47 @@
# Copyright 2022 The Crashpad Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import("../../../build/crashpad_buildconfig.gni")
assert(crashpad_is_win)
# Allows other projects (e.g. Chrome) to wrap this as a dll.
source_set("crashpad_wer_handler") {
sources = [
"crashpad_wer.cc",
"crashpad_wer.h",
]
deps = [ "../../../util:util_registration_protocol" ]
}
crashpad_loadable_module("crashpad_wer") {
sources = [
"crashpad_wer.def",
"crashpad_wer_main.cc",
]
deps = [ ":crashpad_wer_handler" ]
configs = [ "../../../:crashpad_config" ]
}
source_set("crashpad_wer_test") {
testonly = true
sources = [ "crashpad_wer_module_unittest.cc" ]
deps = [
":crashpad_wer",
":crashpad_wer_handler",
"../../../client:client",
"../../../test:test",
"../../../third_party/googletest:googletest",
]
}

View File

@ -0,0 +1,177 @@
// Copyright 2022 The Crashpad Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// See:
// https://docs.microsoft.com/en-us/windows/win32/api/werapi/nf-werapi-werregisterruntimeexceptionmodule
#include "handler/win/wer/crashpad_wer.h"
#include "util/misc/address_types.h"
#include "util/win/registration_protocol_win.h"
#include <Windows.h>
#include <werapi.h>
namespace crashpad::wer {
namespace {
using crashpad::WerRegistration;
// We have our own version of this to avoid pulling in //base.
class ScopedHandle {
public:
ScopedHandle() : handle_(INVALID_HANDLE_VALUE) {}
ScopedHandle(HANDLE from) : handle_(from) {}
~ScopedHandle() {
if (IsValid())
CloseHandle(handle_);
}
bool IsValid() {
if (handle_ == INVALID_HANDLE_VALUE || handle_ == 0)
return false;
return true;
}
HANDLE Get() { return handle_; }
private:
HANDLE handle_;
};
ScopedHandle DuplicateFromTarget(HANDLE target_process, HANDLE target_handle) {
HANDLE hTmp;
if (!DuplicateHandle(target_process,
target_handle,
GetCurrentProcess(),
&hTmp,
SYNCHRONIZE | EVENT_MODIFY_STATE,
false,
0)) {
return ScopedHandle();
}
return ScopedHandle(hTmp);
}
bool ProcessException(std::vector<DWORD>& handled_exceptions,
const PVOID pContext,
const PWER_RUNTIME_EXCEPTION_INFORMATION e_info) {
// Need to have been given a context.
if (!pContext)
return false;
if (!e_info->bIsFatal)
return false;
// Only deal with exceptions that crashpad would not have handled.
if (handled_exceptions.size() &&
std::find(handled_exceptions.begin(),
handled_exceptions.end(),
e_info->exceptionRecord.ExceptionCode) ==
handled_exceptions.end()) {
return false;
}
// Grab out the handles to the crashpad server.
WerRegistration target_registration = {};
if (!ReadProcessMemory(e_info->hProcess,
pContext,
&target_registration,
sizeof(target_registration),
nullptr)) {
return false;
}
// Validate version of registration struct.
if (target_registration.version != WerRegistration::kWerRegistrationVersion)
return false;
// Dupe handles for triggering the dump.
auto dump_start = DuplicateFromTarget(
e_info->hProcess, target_registration.dump_without_crashing);
auto dump_done =
DuplicateFromTarget(e_info->hProcess, target_registration.dump_completed);
if (!dump_start.IsValid() || !dump_done.IsValid())
return false;
// It's possible that the target crashed while inside a DumpWithoutCrashing
// call - either in the DumpWithoutCrashing call or in another thread - if so
// we cannot trigger the dump until the first call's crash is dealth with as
// the crashpad handler might be reading from structures we will write to. We
// give the event a short while to be triggered and give up if it is not
// signalled.
if (target_registration.in_dump_without_crashing) {
constexpr DWORD kOneSecondInMs = 1000;
DWORD wait_result = WaitForSingleObject(dump_done.Get(), kOneSecondInMs);
if (wait_result != WAIT_OBJECT_0)
return false;
}
// Set up the crashpad handler's info structure.
crashpad::ExceptionInformation target_non_crash_exception_info{};
target_non_crash_exception_info.thread_id = GetThreadId(e_info->hThread);
target_non_crash_exception_info.exception_pointers =
static_cast<crashpad::VMAddress>(reinterpret_cast<uintptr_t>(pContext)) +
offsetof(WerRegistration, pointers);
if (!WriteProcessMemory(e_info->hProcess,
target_registration.crashpad_exception_info,
&target_non_crash_exception_info,
sizeof(target_non_crash_exception_info),
nullptr)) {
return false;
}
// Write Exception & Context to the areas reserved by the client.
if (!WriteProcessMemory(
e_info->hProcess,
reinterpret_cast<PVOID>(target_registration.pointers.ExceptionRecord),
&e_info->exceptionRecord,
sizeof(e_info->exceptionRecord),
nullptr)) {
return false;
}
if (!WriteProcessMemory(
e_info->hProcess,
reinterpret_cast<PVOID>(target_registration.pointers.ContextRecord),
&e_info->context,
sizeof(e_info->context),
nullptr)) {
return false;
}
// Request dump.
if (!SetEvent(dump_start.Get()))
return false;
constexpr DWORD kTenSecondsInMs = 10 * 1000;
DWORD result = WaitForSingleObject(dump_done.Get(), kTenSecondsInMs);
if (result == WAIT_OBJECT_0) {
// The handler signalled that it has written a dump, so we can terminate the
// target - this takes over from WER, sorry WER.
TerminateProcess(e_info->hProcess, e_info->exceptionRecord.ExceptionCode);
return true;
}
// Maybe some other handler can have a go.
return false;
}
} // namespace
bool ExceptionEvent(
std::vector<DWORD>& handled_exceptions,
const PVOID pContext,
const PWER_RUNTIME_EXCEPTION_INFORMATION pExceptionInformation) {
return ProcessException(handled_exceptions, pContext, pExceptionInformation);
}
} // namespace crashpad::wer

View File

@ -0,0 +1,20 @@
; Copyright 2022 The Crashpad Authors. All rights reserved.
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
LIBRARY "crashpad_wer.dll"
EXPORTS
; These are required for the WER api.
OutOfProcessExceptionEventCallback
OutOfProcessExceptionEventSignatureCallback
OutOfProcessExceptionEventDebuggerLaunchCallback

View File

@ -0,0 +1,45 @@
// Copyright 2022 The Crashpad Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef CRASHPAD_HANDLER_WIN_WER_CRASHPAD_WER_H_
#define CRASHPAD_HANDLER_WIN_WER_CRASHPAD_WER_H_
#include <vector>
#include <Windows.h>
#include <werapi.h>
namespace crashpad::wer {
//! \brief Embedder calls this from OutOfProcessExceptionEventCallback().
//!
//! In the embedder's WER runtime exception helper, call this during
//! OutOfProcessExceptionEventCallback().
//!
//! \param[in] handled_exceptions is a list of exception codes that the helper
//! should pass on to crashpad handler (if possible). Provide an empty list
//! to pass every exception on to the crashpad handler.
//! \param[in] pContext is the context provided by WerFault to the helper.
//! \param[in] pExceptionInformation is the exception information provided by
//! WerFault to the helper DLL.
//!
//! \return `true` if the target process was dumped by the crashpad handler then
//! terminated, or `false` otherwise.
bool ExceptionEvent(
std::vector<DWORD>& handled_exceptions,
const PVOID pContext,
const PWER_RUNTIME_EXCEPTION_INFORMATION pExceptionInformation);
} // namespace crashpad::wer
#endif // CRASHPAD_HANDLER_WIN_WER_CRASHPAD_WER_H_

View File

@ -0,0 +1,2 @@
INTERNAL_NAME=crashpad_wer
ORIGINAL_FILENAME=crashpad_wer.dll

View File

@ -0,0 +1,83 @@
// Copyright 2022 The Crashpad Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// See:
// https://docs.microsoft.com/en-us/windows/win32/api/werapi/nf-werapi-werregisterruntimeexceptionmodule
#include "handler/win/wer/crashpad_wer.h"
#include <Windows.h>
#include <werapi.h>
// Functions that will be exported from the DLL.
extern "C" {
BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved) {
return true;
}
// PFN_WER_RUNTIME_EXCEPTION_EVENT
// pContext is the address of a crashpad::internal::WerRegistration in the
// target process.
HRESULT OutOfProcessExceptionEventCallback(
PVOID pContext,
const PWER_RUNTIME_EXCEPTION_INFORMATION pExceptionInformation,
BOOL* pbOwnershipClaimed,
PWSTR pwszEventName,
PDWORD pchSize,
PDWORD pdwSignatureCount) {
std::vector<DWORD> wanted_exceptions = {
0xC0000602, // STATUS_FAIL_FAST_EXCEPTION
0xC0000409, // STATUS_STACK_BUFFER_OVERRUN
};
// Default to not-claiming as bailing out is easier.
*pbOwnershipClaimed = FALSE;
bool result = crashpad::wer::ExceptionEvent(
wanted_exceptions, pContext, pExceptionInformation);
if (result) {
*pbOwnershipClaimed = TRUE;
// Technically we failed as we terminated the process.
return E_FAIL;
}
// Pass.
return S_OK;
}
// PFN_WER_RUNTIME_EXCEPTION_EVENT_SIGNATURE
HRESULT OutOfProcessExceptionEventSignatureCallback(
PVOID pContext,
const PWER_RUNTIME_EXCEPTION_INFORMATION pExceptionInformation,
DWORD dwIndex,
PWSTR pwszName,
PDWORD pchName,
PWSTR pwszValue,
PDWORD pchValue) {
// We handle everything in the call to OutOfProcessExceptionEventCallback.
// This function should never be called.
return E_FAIL;
}
// PFN_WER_RUNTIME_EXCEPTION_DEBUGGER_LAUNCH
HRESULT OutOfProcessExceptionEventDebuggerLaunchCallback(
PVOID pContext,
const PWER_RUNTIME_EXCEPTION_INFORMATION pExceptionInformation,
PBOOL pbIsCustomDebugger,
PWSTR pwszDebuggerLaunch,
PDWORD pchDebuggerLaunch,
PBOOL pbIsDebuggerAutolaunch) {
// We handle everything in the call to OutOfProcessExceptionEventCallback.
// This function should never be called.
return E_FAIL;
}
} // extern "C"

View File

@ -0,0 +1,86 @@
// Copyright 2022 The Crashpad Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "client/crashpad_client.h"
#include "base/files/file_path.h"
#include "gtest/gtest.h"
#include "test/test_paths.h"
#include "util/win/registration_protocol_win.h"
#include <Windows.h>
#include <werapi.h>
namespace crashpad {
namespace test {
namespace {
base::FilePath ModulePath() {
auto dir = TestPaths::Executable().DirName();
return dir.Append(FILE_PATH_LITERAL("crashpad_wer.dll"));
}
// Quick sanity check of the module, can't really test dumping etc. outside of
// WerFault.exe loading it.
TEST(CrashpadWerModule, Basic) {
HRESULT res = 0;
// Module loads.
HMODULE hMod = LoadLibraryW(ModulePath().value().c_str());
ASSERT_TRUE(hMod);
// Required functions exist.
auto wref = reinterpret_cast<PFN_WER_RUNTIME_EXCEPTION_EVENT>(
GetProcAddress(hMod, WER_RUNTIME_EXCEPTION_EVENT_FUNCTION));
ASSERT_TRUE(wref);
auto wrees = reinterpret_cast<PFN_WER_RUNTIME_EXCEPTION_EVENT_SIGNATURE>(
GetProcAddress(hMod, WER_RUNTIME_EXCEPTION_EVENT_SIGNATURE_FUNCTION));
ASSERT_TRUE(wrees);
auto wredl = reinterpret_cast<PFN_WER_RUNTIME_EXCEPTION_DEBUGGER_LAUNCH>(
GetProcAddress(hMod, WER_RUNTIME_EXCEPTION_DEBUGGER_LAUNCH));
ASSERT_TRUE(wredl);
// Not-implemented functions return E_FAIL as expected.
res = wrees(nullptr, nullptr, 0, nullptr, nullptr, nullptr, nullptr);
ASSERT_EQ(res, E_FAIL);
res = wredl(nullptr, nullptr, nullptr, nullptr, nullptr, nullptr);
ASSERT_EQ(res, E_FAIL);
// Dummy args for OutOfProcessExceptionEventCallback.
crashpad::WerRegistration registration;
WER_RUNTIME_EXCEPTION_INFORMATION wer_ex;
BOOL bClaimed = FALSE;
// No context => skip.
res = wref(nullptr, &wer_ex, &bClaimed, nullptr, nullptr, nullptr);
ASSERT_EQ(res, S_OK);
ASSERT_EQ(bClaimed, FALSE);
// Non-fatal exceptions are skipped.
wer_ex.bIsFatal = FALSE;
res = wref(&registration, &wer_ex, &bClaimed, nullptr, nullptr, nullptr);
ASSERT_EQ(res, S_OK);
ASSERT_EQ(bClaimed, FALSE);
// Fatal exception with unhandled code is skipped.
wer_ex.bIsFatal = TRUE;
wer_ex.exceptionRecord.ExceptionCode = 0xc0000005;
res = wref(&registration, &wer_ex, &bClaimed, nullptr, nullptr, nullptr);
ASSERT_EQ(res, S_OK);
ASSERT_EQ(bClaimed, FALSE);
FreeLibrary(hMod);
}
} // namespace
} // namespace test
} // namespace crashpad

View File

@ -87,6 +87,15 @@ def GetCdbPath():
return None
def Win32_20H1():
(major, _, build) = platform.win32_ver()[1].split('.')
if int(major) < 10:
return False
if int(build) >= 19041:
return True
return False
def NamedPipeExistsAndReady(pipe_name):
"""Returns False if pipe_name does not exist. If pipe_name does exist,
blocks until the pipe is ready to service clients, and then returns True.
@ -203,6 +212,12 @@ def GetDumpFromZ7Program(out_dir, pipe_name):
win32con.EXCEPTION_ACCESS_VIOLATION)
def GetDumpFromFastFailProgram(out_dir, pipe_name, *args):
STATUS_STACK_BUFFER_OVERRUN = 0xc0000409
return GetDumpFromProgram(out_dir, pipe_name, 'fastfail_program.exe',
STATUS_STACK_BUFFER_OVERRUN, *args)
class CdbRun(object):
"""Run cdb.exe passing it a cdb command and capturing the output.
`Check()` searches for regex patterns in sequence allowing verification of
@ -215,18 +230,25 @@ class CdbRun(object):
self.out = subprocess.check_output(
[cdb_path, '-z', dump_path, '-c', command + ';q'], text=True)
def Check(self, pattern, message, re_flags=0):
def Check(self, pattern, message, re_flags=0, must_not_match=False):
match_obj = re.search(pattern, self.out, re_flags)
if match_obj:
if match_obj and not must_not_match:
# Matched. Consume up to end of match.
self.out = self.out[match_obj.end(0):]
print('ok - %s' % message)
sys.stdout.flush()
elif must_not_match and not match_obj:
# Did not match and did not want to match.
print('ok - %s' % message)
sys.stdout.flush()
else:
print('-' * 80, file=sys.stderr)
print('FAILED - %s' % message, file=sys.stderr)
print('-' * 80, file=sys.stderr)
print('did not match:\n %s' % pattern, file=sys.stderr)
if must_not_match:
print('unexpected match:\n %s' % pattern, file=sys.stderr)
else:
print('did not match:\n %s' % pattern, file=sys.stderr)
print('-' * 80, file=sys.stderr)
print('remaining output was:\n %s' % self.out, file=sys.stderr)
print('-' * 80, file=sys.stderr)
@ -244,8 +266,7 @@ class CdbRun(object):
def RunTests(cdb_path, dump_path, start_handler_dump_path, destroyed_dump_path,
z7_dump_path, other_program_path, other_program_no_exception_path,
sigabrt_main_path, sigabrt_background_path, pipe_name):
pipe_name):
"""Runs various tests in sequence. Runs a new cdb instance on the dump for
each block of tests to reduce the chances that output from one command is
confused for output from another.
@ -373,17 +394,22 @@ def RunTests(cdb_path, dump_path, start_handler_dump_path, destroyed_dump_path,
out.Check(r'type \?\?\? \(333333\), size 00001000', 'first user stream')
out.Check(r'type \?\?\? \(222222\), size 00000080', 'second user stream')
if z7_dump_path:
out = CdbRun(cdb_path, z7_dump_path, '.ecxr;lm')
out.Check('This dump file has an exception of interest stored in it',
'captured exception in z7 module')
# Older versions of cdb display relative to exports for /Z7 modules,
# newer ones just display the offset.
out.Check(r'z7_test(!CrashMe\+0xe|\+0x100e):',
'exception in z7 at correct location')
out.Check(r'z7_test C \(codeview symbols\) z7_test\.dll',
'expected non-pdb symbol format')
def Run7zDumpTest(cdb_path, z7_dump_path):
"""Validate output when non-pdb symbols are in a module."""
out = CdbRun(cdb_path, z7_dump_path, '.ecxr;lm')
out.Check('This dump file has an exception of interest stored in it',
'captured exception in z7 module')
# Older versions of cdb display relative to exports for /Z7 modules,
# newer ones just display the offset.
out.Check(r'z7_test(!CrashMe\+0xe|\+0x100e):',
'exception in z7 at correct location')
out.Check(r'z7_test C \(codeview symbols\) z7_test\.dll',
'expected non-pdb symbol format')
def RunOtherProgramTests(cdb_path, other_program_path,
other_program_no_exception_path):
out = CdbRun(cdb_path, other_program_path, '.ecxr;k;~')
out.Check('Unknown exception - code deadbea7',
'other program dump exception code')
@ -407,6 +433,9 @@ def RunTests(cdb_path, dump_path, start_handler_dump_path, destroyed_dump_path,
'other program with no exception given')
out.Check('!RaiseException', 'other program in RaiseException()')
def RunSigAbrtTest(cdb_path, sigabrt_main_path, sigabrt_background_path):
"""Validate that abort signals are collected."""
out = CdbRun(cdb_path, sigabrt_main_path, '.ecxr')
out.Check('code 40000015', 'got sigabrt signal')
out.Check('::HandleAbortSignal', ' stack in expected location')
@ -415,6 +444,32 @@ def RunTests(cdb_path, dump_path, start_handler_dump_path, destroyed_dump_path,
out.Check('code 40000015', 'got sigabrt signal from background thread')
def RunFastFailDumpTest(cdb_path, fastfail_path):
"""Runs tests on __fastfail() caught using the runtime exception helper."""
out = CdbRun(cdb_path, fastfail_path, '.ecxr;k')
out.Check('This dump file has an exception of interest stored in it',
'captured exception from __fastfail() crash()')
out.Check(r'Subcode: 0x4d \(unknown subcode\)', 'See expected subcode.')
out.Check('FastFailCrash', 'See expected throwing function.')
out = CdbRun(cdb_path, fastfail_path, '.ecxr;k')
def RunCfgDumpTest(cdb_path, cfg_path):
"""Runs tests on a cfg crash caught using the runtime exception helper."""
out = CdbRun(cdb_path, cfg_path, '.ecxr;k')
out.Check('This dump file has an exception of interest stored in it',
'captured exception from cfg crash()')
out.Check('Subcode: 0xa FAST_FAIL_GUARD_ICALL_CHECK_FAILURE',
'See expected cfg error code.')
out.Check('RtlFailFast',
'See expected Windows exception throwing function.')
out.Check('::CfgCrash', 'expected crashy function is on the stack.')
out = CdbRun(cdb_path, cfg_path, '.ecxr;k')
out.Check(r'CallRffeManyTimes',
'Do not see the function we fiddled the pointer for.',
must_not_match=True)
def main(args):
try:
if len(args) != 1:
@ -437,6 +492,7 @@ def main(args):
pipe_name = r'\\.\pipe\end-to-end_%s_%s' % (os.getpid(),
str(random.getrandbits(64)))
# Basic tests.
crashy_dump_path = GetDumpFromCrashyProgram(args[0], pipe_name)
if not crashy_dump_path:
return 1
@ -450,12 +506,10 @@ def main(args):
if not destroyed_dump_path:
return 1
z7_dump_path = None
if not args[0].endswith('_x64'):
z7_dump_path = GetDumpFromZ7Program(args[0], pipe_name)
if not z7_dump_path:
return 1
RunTests(cdb_path, crashy_dump_path, start_handler_dump_path,
destroyed_dump_path, pipe_name)
# Other program dumps.
other_program_path = GetDumpFromOtherProgram(args[0], pipe_name)
if not other_program_path:
return 1
@ -465,6 +519,10 @@ def main(args):
if not other_program_no_exception_path:
return 1
RunOtherProgramTests(cdb_path, other_program_path,
other_program_no_exception_path)
# SIGABRT.
sigabrt_main_path = GetDumpFromSignal(args[0], pipe_name, 'main')
if not sigabrt_main_path:
return 1
@ -474,10 +532,25 @@ def main(args):
if not sigabrt_background_path:
return 1
RunTests(cdb_path, crashy_dump_path, start_handler_dump_path,
destroyed_dump_path, z7_dump_path, other_program_path,
other_program_no_exception_path, sigabrt_main_path,
sigabrt_background_path, pipe_name)
RunSigAbrtTest(cdb_path, sigabrt_main_path, sigabrt_background_path)
# Can only build the z7 program on x86.
if not args[0].endswith('_x64'):
z7_dump_path = GetDumpFromZ7Program(args[0], pipe_name)
if not z7_dump_path:
return 1
Run7zDumpTest(cdb_path, z7_dump_path)
# __fastfail() & CFG crash caught by WerRuntimeExceptionHelperModule.
if (Win32_20H1()):
cfg_path = GetDumpFromFastFailProgram(args[0], pipe_name, "cf")
if not cfg_path:
return 1
RunCfgDumpTest(cdb_path, cfg_path)
fastfail_path = GetDumpFromFastFailProgram(args[0], pipe_name, "ff")
if not fastfail_path:
return 1
RunFastFailDumpTest(cdb_path, fastfail_path)
return 1 if g_had_failures else 0
finally:

View File

@ -159,6 +159,19 @@ if (crashpad_is_mac || crashpad_is_ios) {
}
}
# Used by crashpad_wer_handler to avoid linking all of :util.
if (crashpad_is_win) {
source_set("util_registration_protocol") {
sources = [
"misc/address_types.h",
"win/address_types.h",
"win/registration_protocol_win.h",
]
public_deps = [ "../third_party/mini_chromium:build" ]
public_configs = [ "..:crashpad_config" ]
}
}
crashpad_static_library("util") {
sources = [
"file/delimited_file_reader.cc",

View File

@ -37,6 +37,48 @@ struct ExceptionInformation {
DWORD thread_id;
};
//! \brief Context to be passed to WerRegisterRuntimeExceptionModule().
//!
//! Used by the crashpad client, and the WER exception DLL.
struct WerRegistration {
//! \brief The expected value of `version`. This should be changed whenever
//! this struct is modified incompatibly.
enum { kWerRegistrationVersion = 1 };
//! \brief Version field to detect skew between target process and helper.
//! Should be set to kWerRegistrationVersion.
int version;
//! \brief Used by DumpWithoutCrashing and the WER module to initiate a dump.
//! These handles are leaked in the client process.
HANDLE dump_without_crashing;
//! \brief Used by DumpWithoutCrashing to signal that a dump has been taken.
//! These handles are leaked in the client process.
HANDLE dump_completed;
//! \brief Set just before and cleared just after the events above are
//! triggered or signalled in a normal DumpWithoutCrashing call.
//! When `true` the WER handler should not set the exception structures until
//! after dump_completed has been signalled.
bool in_dump_without_crashing;
//! \brief Address of g_non_crash_exception_information.
//!
//! Provided by the target process. Just before dumping we will point
//! (*crashpad_exception_info).exception_pointers at `pointers`. As WerFault
//! loads the helper with the same bitness as the client this can be void*.
void* crashpad_exception_info;
//! \brief These will point into the `exception` and `context` members in this
//! structure.
//!
//! Filled in by the helper DLL.
EXCEPTION_POINTERS pointers;
//! \brief The exception provided by WerFault.
//!
//! Filled in by the helper DLL.
EXCEPTION_RECORD exception;
//! \brief The context provided by WerFault.
//!
//! Filled in by the helper DLL.
CONTEXT context;
};
//! \brief A client registration request.
struct RegistrationRequest {
//! \brief The expected value of `version`. This should be changed whenever