diff --git a/client/crashpad_client.h b/client/crashpad_client.h index 28a87a3e..61338a3c 100644 --- a/client/crashpad_client.h +++ b/client/crashpad_client.h @@ -19,6 +19,8 @@ #include #include +#include + #include "base/files/file_path.h" #include "base/macros.h" #include "build/build_config.h" @@ -152,6 +154,41 @@ class CrashpadClient { //! \param[in] exception_pointers An `EXCEPTION_POINTERS`, as would generally //! passed to an unhandled exception filter. static void DumpAndCrash(EXCEPTION_POINTERS* exception_pointers); + + //! \brief Requests that the handler capture a dump of a different process. + //! + //! The target process must be an already-registered Crashpad client. An + //! exception will be triggered in the target process, and the regular dump + //! mechanism used. This function will block until the exception in the target + //! process has been handled by the Crashpad handler. + //! + //! This function is unavailable when running on Windows XP and will return + //! `false`. + //! + //! \param[in] process A `HANDLE` identifying the process to be dumped. + //! \param[in] blame_thread If non-null, a `HANDLE` valid in the caller's + //! process, referring to a thread in the target process. If this is + //! supplied, instead of the exception referring to the location where the + //! exception was injected, an exception record will be fabricated that + //! refers to the current location of the given thread. + //! \param[in] exception_code If \a blame_thread is non-null, this will be + //! used as the exception code in the exception record. + //! + //! \return `true` if the exception was triggered successfully. + bool DumpAndCrashTargetProcess(HANDLE process, + HANDLE blame_thread, + DWORD exception_code) const; + + enum : uint32_t { + //! \brief The exception code (roughly "Client called") used when + //! DumpAndCrashTargetProcess() triggers an exception in a target + //! process. + //! + //! \note This value does not have any bits of the top nibble set, to avoid + //! confusion with real exception codes which tend to have those bits + //! set. + kTriggeredExceptionCode = 0xcca11ed, + }; #endif //! \brief Configures the process to direct its crashes to a Crashpad handler. diff --git a/client/crashpad_client_win.cc b/client/crashpad_client_win.cc index 1f106865..b0fca532 100644 --- a/client/crashpad_client_win.cc +++ b/client/crashpad_client_win.cc @@ -27,12 +27,17 @@ #include "base/strings/utf_string_conversions.h" #include "base/synchronization/lock.h" #include "util/file/file_io.h" +#include "util/win/address_types.h" #include "util/win/command_line.h" #include "util/win/critical_section_with_debug_info.h" #include "util/win/get_function.h" #include "util/win/handle.h" +#include "util/win/nt_internals.h" +#include "util/win/ntstatus_logging.h" +#include "util/win/process_info.h" #include "util/win/registration_protocol_win.h" #include "util/win/scoped_handle.h" +#include "util/win/scoped_process_suspend.h" namespace { @@ -167,6 +172,19 @@ void AddHandleToListIfValidAndInheritable(std::vector* handle_list, } } +void AddUint32(std::vector* data_vector, uint32_t data) { + data_vector->push_back(static_cast(data & 0xff)); + data_vector->push_back(static_cast((data & 0xff00) >> 8)); + data_vector->push_back(static_cast((data & 0xff0000) >> 16)); + data_vector->push_back(static_cast((data & 0xff000000) >> 24)); +} + +void AddUint64(std::vector* data_vector, uint64_t data) { + AddUint32(data_vector, static_cast(data & 0xffffffffULL)); + AddUint32(data_vector, + static_cast((data & 0xffffffff00000000ULL) >> 32)); +} + } // namespace namespace crashpad { @@ -469,4 +487,247 @@ void CrashpadClient::DumpAndCrash(EXCEPTION_POINTERS* exception_pointers) { UnhandledExceptionHandler(exception_pointers); } +bool CrashpadClient::DumpAndCrashTargetProcess(HANDLE process, + HANDLE blame_thread, + DWORD exception_code) const { + // Confirm we're on Vista or later. + const DWORD version = GetVersion(); + const DWORD major_version = LOBYTE(LOWORD(version)); + if (major_version < 6) { + LOG(ERROR) << "unavailable before Vista"; + return false; + } + + // Confirm that our bitness is the same as the process we're crashing. + ProcessInfo process_info; + if (!process_info.Initialize(process)) { + LOG(ERROR) << "ProcessInfo::Initialize"; + return false; + } +#if defined(ARCH_CPU_64_BITS) + if (!process_info.Is64Bit()) { + LOG(ERROR) << "DumpAndCrashTargetProcess currently not supported x64->x86"; + return false; + } +#endif // ARCH_CPU_64_BITS + + ScopedProcessSuspend suspend(process); + + // If no thread handle was provided, or the thread has already exited, we pass + // 0 to the handler, which indicates no fake exception record to be created. + DWORD thread_id = 0; + if (blame_thread) { + // Now that we've suspended the process, if our thread hasn't exited, we + // know we're relatively safe to pass the thread id through. + if (WaitForSingleObject(blame_thread, 0) == WAIT_TIMEOUT) { + static const auto get_thread_id = + GET_FUNCTION_REQUIRED(L"kernel32.dll", ::GetThreadId); + thread_id = get_thread_id(blame_thread); + } + } + + const size_t kInjectBufferSize = 4 * 1024; + WinVMAddress inject_memory = + reinterpret_cast(VirtualAllocEx(process, + nullptr, + kInjectBufferSize, + MEM_RESERVE | MEM_COMMIT, + PAGE_READWRITE)); + if (!inject_memory) { + PLOG(ERROR) << "VirtualAllocEx"; + return false; + } + + // Because we're the same bitness as our target, we can rely kernel32 being + // loaded at the same address in our process as the target, and just look up + // its address here. + WinVMAddress raise_exception_address = + reinterpret_cast(&RaiseException); + + WinVMAddress code_entry_point = 0; + std::vector data_to_write; + if (process_info.Is64Bit()) { + // Data written is first, the data for the 4th argument (lpArguments) to + // RaiseException(). A two element array: + // + // DWORD64: thread_id + // DWORD64: exception_code + // + // Following that, code which sets the arguments to RaiseException() and + // then calls it: + // + // mov r9, + // mov r8d, 2 ; nNumberOfArguments + // mov edx, 1 ; dwExceptionFlags = EXCEPTION_NONCONTINUABLE + // mov ecx, 0xcca11ed ; dwExceptionCode, interpreted specially by the + // ; handler. + // jmp + // + // Note that the first three arguments to RaiseException() are DWORDs even + // on x64, so only the 4th argument (a pointer) is a full-width register. + // + // We also don't need to set up a stack or use call, since the only + // registers modified are volatile ones, and we can just jmp straight to + // RaiseException(). + + // The data array. + AddUint64(&data_to_write, thread_id); + AddUint64(&data_to_write, exception_code); + + // The thread entry point. + code_entry_point = inject_memory + data_to_write.size(); + + // r9 = pointer to data. + data_to_write.push_back(0x49); + data_to_write.push_back(0xb9); + AddUint64(&data_to_write, inject_memory); + + // r8d = 2 for nNumberOfArguments. + data_to_write.push_back(0x41); + data_to_write.push_back(0xb8); + AddUint32(&data_to_write, 2); + + // edx = 1 for dwExceptionFlags. + data_to_write.push_back(0xba); + AddUint32(&data_to_write, 1); + + // ecx = kTriggeredExceptionCode for dwExceptionCode. + data_to_write.push_back(0xb9); + AddUint32(&data_to_write, kTriggeredExceptionCode); + + // jmp to RaiseException() via rax. + data_to_write.push_back(0x48); // mov rax, imm. + data_to_write.push_back(0xb8); + AddUint64(&data_to_write, raise_exception_address); + data_to_write.push_back(0xff); // jmp rax. + data_to_write.push_back(0xe0); + } else { + // Data written is first, the data for the 4th argument (lpArguments) to + // RaiseException(). A two element array: + // + // DWORD: thread_id + // DWORD: exception_code + // + // Following that, code which pushes our arguments to RaiseException() and + // then calls it: + // + // push + // push 2 ; nNumberOfArguments + // push 1 ; dwExceptionFlags = EXCEPTION_NONCONTINUABLE + // push 0xcca11ed ; dwExceptionCode, interpreted specially by the handler. + // call + // ud2 ; Generate invalid opcode to make sure we still crash if we return + // ; for some reason. + // + // No need to clean up the stack, as RaiseException() is __stdcall. + + // The data array. + AddUint32(&data_to_write, thread_id); + AddUint32(&data_to_write, exception_code); + + // The thread entry point. + code_entry_point = inject_memory + data_to_write.size(); + + // Push data address. + data_to_write.push_back(0x68); + AddUint32(&data_to_write, static_cast(inject_memory)); + + // Push 2 for nNumberOfArguments. + data_to_write.push_back(0x6a); + data_to_write.push_back(2); + + // Push 1 for dwExceptionCode. + data_to_write.push_back(0x6a); + data_to_write.push_back(1); + + // Push dwExceptionFlags. + data_to_write.push_back(0x68); + AddUint32(&data_to_write, kTriggeredExceptionCode); + + // Relative call to RaiseException(). + int64_t relative_address_to_raise_exception = + raise_exception_address - (inject_memory + data_to_write.size() + 5); + data_to_write.push_back(0xe8); + AddUint32(&data_to_write, + static_cast(relative_address_to_raise_exception)); + + // ud2. + data_to_write.push_back(0x0f); + data_to_write.push_back(0x0b); + } + + DCHECK_LT(data_to_write.size(), kInjectBufferSize); + + SIZE_T bytes_written; + if (!WriteProcessMemory(process, + reinterpret_cast(inject_memory), + data_to_write.data(), + data_to_write.size(), + &bytes_written)) { + PLOG(ERROR) << "WriteProcessMemory"; + return false; + } + + if (bytes_written != data_to_write.size()) { + LOG(ERROR) << "WriteProcessMemory unexpected number of bytes"; + return false; + } + + if (!FlushInstructionCache( + process, reinterpret_cast(inject_memory), bytes_written)) { + PLOG(ERROR) << "FlushInstructionCache"; + return false; + } + + DWORD old_protect; + if (!VirtualProtectEx(process, + reinterpret_cast(inject_memory), + kInjectBufferSize, + PAGE_EXECUTE_READ, + &old_protect)) { + PLOG(ERROR) << "VirtualProtectEx"; + return false; + } + + // Cause an exception in the target process by creating a thread which calls + // RaiseException with our arguments above. Note that we cannot get away with + // using DebugBreakProcess() (nothing happens unless a debugger is attached) + // and we cannot get away with CreateRemoteThread() because it doesn't work if + // the target is hung waiting for the loader lock. We use NtCreateThreadEx() + // with the SKIP_THREAD_ATTACH flag, which skips various notifications, + // letting this cause an exception, even when the target is stuck in the + // loader lock. + HANDLE injected_thread; + const size_t kStackSize = 0x4000; // This is what DebugBreakProcess() uses. + NTSTATUS status = NtCreateThreadEx(&injected_thread, + STANDARD_RIGHTS_ALL | SPECIFIC_RIGHTS_ALL, + nullptr, + process, + reinterpret_cast(code_entry_point), + nullptr, + THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH, + 0, + kStackSize, + 0, + nullptr); + if (!NT_SUCCESS(status)) { + NTSTATUS_LOG(ERROR, status) << "NtCreateThreadEx"; + return false; + } + + bool result = true; + if (WaitForSingleObject(injected_thread, 60 * 1000) != WAIT_OBJECT_0) { + PLOG(ERROR) << "WaitForSingleObject"; + result = false; + } + + status = NtClose(injected_thread); + if (!NT_SUCCESS(status)) { + NTSTATUS_LOG(ERROR, status) << "NtClose"; + result = false; + } + + return result; +} + } // namespace crashpad diff --git a/handler/handler.gyp b/handler/handler.gyp index 40d5977e..bbb2ce4e 100644 --- a/handler/handler.gyp +++ b/handler/handler.gyp @@ -110,6 +110,40 @@ 'win/crashy_test_program.cc', ], }, + { + 'target_name': 'crash_other_program', + 'type': 'executable', + 'dependencies': [ + '../client/client.gyp:crashpad_client', + '../test/test.gyp:crashpad_test', + '../third_party/mini_chromium/mini_chromium.gyp:base', + '../util/util.gyp:crashpad_util', + ], + 'sources': [ + 'win/crash_other_program.cc', + ], + }, + { + 'target_name': 'hanging_program', + 'type': 'executable', + 'dependencies': [ + '../client/client.gyp:crashpad_client', + '../third_party/mini_chromium/mini_chromium.gyp:base', + ], + 'sources': [ + 'win/hanging_program.cc', + ], + }, + { + 'target_name': 'loader_lock_dll', + 'type': 'loadable_module', + 'sources': [ + 'win/loader_lock_dll.cc', + ], + 'msvs_settings': { + 'NoImportLibrary': 'true', + }, + }, { 'target_name': 'self_destroying_program', 'type': 'executable', diff --git a/handler/win/crash_other_program.cc b/handler/win/crash_other_program.cc new file mode 100644 index 00000000..db532bb5 --- /dev/null +++ b/handler/win/crash_other_program.cc @@ -0,0 +1,120 @@ +// Copyright 2016 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 +#include +#include + +#include "base/files/file_path.h" +#include "base/logging.h" +#include "client/crashpad_client.h" +#include "test/paths.h" +#include "test/win/child_launcher.h" +#include "util/file/file_io.h" +#include "util/win/scoped_handle.h" +#include "util/win/xp_compat.h" + +namespace crashpad { +namespace test { +namespace { + +bool CrashAndDumpTarget(const CrashpadClient& client, HANDLE process) { + DWORD target_pid = GetProcessId(process); + + HANDLE thread_snap_raw = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); + if (thread_snap_raw == INVALID_HANDLE_VALUE) { + LOG(ERROR) << "CreateToolhelp32Snapshot"; + return false; + } + ScopedFileHANDLE thread_snap(thread_snap_raw); + + THREADENTRY32 te32; + te32.dwSize = sizeof(THREADENTRY32); + if (!Thread32First(thread_snap.get(), &te32)) { + LOG(ERROR) << "Thread32First"; + return false; + } + + int thread_count = 0; + do { + if (te32.th32OwnerProcessID == target_pid) { + thread_count++; + if (thread_count == 2) { + // Nominate this lucky thread as our blamee, and dump it. This will be + // "Thread1" in the child. + ScopedKernelHANDLE thread( + OpenThread(kXPThreadAllAccess, false, te32.th32ThreadID)); + if (!client.DumpAndCrashTargetProcess( + process, thread.get(), 0xdeadbea7)) { + LOG(ERROR) << "DumpAndCrashTargetProcess failed"; + return false; + } + return true; + } + } + } while (Thread32Next(thread_snap.get(), &te32)); + + return false; +} + +int CrashOtherProgram(int argc, wchar_t* argv[]) { + CrashpadClient client; + + if (argc == 2 || argc == 3) { + if (!client.SetHandlerIPCPipe(argv[1])) { + LOG(ERROR) << "SetHandler"; + return EXIT_FAILURE; + } + } else { + fprintf(stderr, "Usage: %ls [noexception]\n", argv[0]); + return EXIT_FAILURE; + } + + if (!client.UseHandler()) { + LOG(ERROR) << "UseHandler"; + return EXIT_FAILURE; + } + + // Launch another process that hangs. + base::FilePath test_executable = Paths::Executable(); + std::wstring child_test_executable = + test_executable.DirName().Append(L"hanging_program.exe").value(); + ChildLauncher child(child_test_executable, argv[1]); + child.Start(); + + // Wait until it's ready. + char c; + if (!LoggingReadFile(child.stdout_read_handle(), &c, sizeof(c)) || c != ' ') { + LOG(ERROR) << "failed child communication"; + return EXIT_FAILURE; + } + + if (argc == 3 && wcscmp(argv[2], L"noexception") == 0) { + client.DumpAndCrashTargetProcess(child.process_handle(), 0, 0); + return EXIT_SUCCESS; + } else { + if (CrashAndDumpTarget(client, child.process_handle())) + return EXIT_SUCCESS; + } + + return EXIT_FAILURE; +} + +} // namespace +} // namespace test +} // namespace crashpad + +int wmain(int argc, wchar_t* argv[]) { + return crashpad::test::CrashOtherProgram(argc, argv); +} diff --git a/handler/win/hanging_program.cc b/handler/win/hanging_program.cc new file mode 100644 index 00000000..877578e3 --- /dev/null +++ b/handler/win/hanging_program.cc @@ -0,0 +1,86 @@ +// Copyright 2016 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 +#include + +#include "base/debug/alias.h" +#include "base/logging.h" +#include "base/macros.h" +#include "client/crashpad_client.h" +#include "client/crashpad_info.h" + +DWORD WINAPI Thread1(LPVOID dummy) { + Sleep(INFINITE); + return 0; +} + +DWORD WINAPI Thread2(LPVOID dummy) { + Sleep(INFINITE); + return 0; +} + +DWORD WINAPI Thread3(LPVOID dummy) { + HMODULE dll = LoadLibrary(L"loader_lock_dll.dll"); + if (!dll) + PLOG(ERROR) << "LoadLibrary"; + + // This call is not expected to return. + if (!FreeLibrary(dll)) + PLOG(ERROR) << "FreeLibrary"; + + return 0; +} + +int wmain(int argc, wchar_t* argv[]) { + crashpad::CrashpadClient client; + + if (argc == 2) { + if (!client.SetHandlerIPCPipe(argv[1])) { + LOG(ERROR) << "SetHandler"; + return EXIT_FAILURE; + } + } else { + fprintf(stderr, "Usage: %ls \n", argv[0]); + return EXIT_FAILURE; + } + + if (!client.UseHandler()) { + LOG(ERROR) << "UseHandler"; + return EXIT_FAILURE; + } + + // Make sure this module has a CrashpadInfo structure. + crashpad::CrashpadInfo* crashpad_info = + crashpad::CrashpadInfo::GetCrashpadInfo(); + base::debug::Alias(crashpad_info); + + HANDLE threads[3]; + threads[0] = CreateThread(nullptr, 0, Thread1, nullptr, 0, nullptr); + threads[1] = CreateThread(nullptr, 0, Thread2, nullptr, 0, nullptr); + threads[2] = CreateThread(nullptr, 0, Thread3, nullptr, 0, nullptr); + + // Our whole process is going to hang when the loaded DLL hangs in its + // DllMain(), so we can't signal to our parent that we're "ready". So, use a + // hokey delay of 1s after we spawn the threads, and hope that we make it to + // the FreeLibrary call by then. + Sleep(1000); + + fprintf(stdout, " "); + fflush(stdout); + + WaitForMultipleObjects(ARRAYSIZE(threads), threads, true, INFINITE); + + return 0; +} diff --git a/handler/win/loader_lock_dll.cc b/handler/win/loader_lock_dll.cc new file mode 100644 index 00000000..0a05af3d --- /dev/null +++ b/handler/win/loader_lock_dll.cc @@ -0,0 +1,28 @@ +// Copyright 2016 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 + +// This program intentionally blocks in DllMain which is executed with the +// loader lock locked. This allows us to test that +// CrashpadClient::DumpAndCrashTargetProcess() can still dump the target in this +// case. +BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID) { + switch (reason) { + case DLL_PROCESS_DETACH: + case DLL_THREAD_DETACH: + Sleep(INFINITE); + } + return TRUE; +} diff --git a/snapshot/win/end_to_end_test.py b/snapshot/win/end_to_end_test.py index 53d1b074..a5540e58 100755 --- a/snapshot/win/end_to_end_test.py +++ b/snapshot/win/end_to_end_test.py @@ -79,11 +79,12 @@ def GetCdbPath(): return None -def GetDumpFromProgram(out_dir, pipe_name, executable_name): +def GetDumpFromProgram(out_dir, pipe_name, executable_name, *args): """Initialize a crash database, and run |executable_name| connecting to a crash handler. If pipe_name is set, crashpad_handler will be started first. If pipe_name is empty, the executable is responsible for starting - crashpad_handler. Returns the minidump generated by crashpad_handler for + crashpad_handler. *args will be passed after other arguments to + executable_name. Returns the minidump generated by crashpad_handler for further testing. """ test_database = MakeTempDir() @@ -111,11 +112,12 @@ def GetDumpFromProgram(out_dir, pipe_name, executable_name): printed = True time.sleep(0.1) - subprocess.call([os.path.join(out_dir, executable_name), pipe_name]) + subprocess.call([os.path.join(out_dir, executable_name), pipe_name] + + list(args)) else: subprocess.call([os.path.join(out_dir, executable_name), os.path.join(out_dir, 'crashpad_handler.exe'), - test_database]) + test_database] + list(args)) out = subprocess.check_output([ os.path.join(out_dir, 'crashpad_database_util.exe'), @@ -135,6 +137,11 @@ def GetDumpFromCrashyProgram(out_dir, pipe_name): return GetDumpFromProgram(out_dir, pipe_name, 'crashy_program.exe') +def GetDumpFromOtherProgram(out_dir, pipe_name, *args): + return GetDumpFromProgram(out_dir, pipe_name, 'crash_other_program.exe', + *args) + + def GetDumpFromSelfDestroyingProgram(out_dir, pipe_name): return GetDumpFromProgram(out_dir, pipe_name, 'self_destroying_program.exe') @@ -182,6 +189,8 @@ def RunTests(cdb_path, start_handler_dump_path, destroyed_dump_path, z7_dump_path, + other_program_path, + other_program_no_exception_path, 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 @@ -311,6 +320,18 @@ def RunTests(cdb_path, out.Check(r'z7_test C \(codeview symbols\) z7_test.dll', 'expected non-pdb symbol format') + out = CdbRun(cdb_path, other_program_path, '.ecxr;k;~') + out.Check('Unknown exception - code deadbea7', + 'other program dump exception code') + out.Check('!Sleep', 'other program reasonable location') + out.Check('hanging_program!Thread1', 'other program dump right thread') + out.Check('\. 1 Id', 'other program exception on correct thread') + + out = CdbRun(cdb_path, other_program_no_exception_path, '.ecxr;k') + out.Check('Unknown exception - code 0cca11ed', + 'other program with no exception given') + out.Check('!RaiseException', 'other program in RaiseException()') + def main(args): try: @@ -352,11 +373,22 @@ def main(args): if not z7_dump_path: return 1 + other_program_path = GetDumpFromOtherProgram(args[0], pipe_name) + if not other_program_path: + return 1 + + other_program_no_exception_path = GetDumpFromOtherProgram( + args[0], pipe_name, 'noexception') + if not other_program_no_exception_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, pipe_name) return 0 diff --git a/snapshot/win/exception_snapshot_win.cc b/snapshot/win/exception_snapshot_win.cc index 2abe6d06..ca29e1f0 100644 --- a/snapshot/win/exception_snapshot_win.cc +++ b/snapshot/win/exception_snapshot_win.cc @@ -14,6 +14,7 @@ #include "snapshot/win/exception_snapshot_win.h" +#include "client/crashpad_client.h" #include "snapshot/capture_memory.h" #include "snapshot/memory_snapshot.h" #include "snapshot/win/cpu_context_win.h" @@ -25,6 +26,34 @@ namespace crashpad { namespace internal { +namespace { + +#if defined(ARCH_CPU_32_BITS) +using Context32 = CONTEXT; +#elif defined(ARCH_CPU_64_BITS) +using Context32 = WOW64_CONTEXT; +#endif + +#if defined(ARCH_CPU_64_BITS) +void NativeContextToCPUContext64(const CONTEXT& context_record, + CPUContext* context, + CPUContextUnion* context_union) { + context->architecture = kCPUArchitectureX86_64; + context->x86_64 = &context_union->x86_64; + InitializeX64Context(context_record, context->x86_64); +} +#endif + +void NativeContextToCPUContext32(const Context32& context_record, + CPUContext* context, + CPUContextUnion* context_union) { + context->architecture = kCPUArchitectureX86; + context->x86 = &context_union->x86; + InitializeX86Context(context_record, context->x86); +} + +} // namespace + ExceptionSnapshotWin::ExceptionSnapshotWin() : ExceptionSnapshot(), context_union_(), @@ -40,9 +69,11 @@ ExceptionSnapshotWin::ExceptionSnapshotWin() ExceptionSnapshotWin::~ExceptionSnapshotWin() { } -bool ExceptionSnapshotWin::Initialize(ProcessReaderWin* process_reader, - DWORD thread_id, - WinVMAddress exception_pointers_address) { +bool ExceptionSnapshotWin::Initialize( + ProcessReaderWin* process_reader, + DWORD thread_id, + WinVMAddress exception_pointers_address, + const PointerVector& threads) { INITIALIZATION_STATE_SET_INITIALIZING(initialized_); const ProcessReaderWin::Thread* thread = nullptr; @@ -62,32 +93,28 @@ bool ExceptionSnapshotWin::Initialize(ProcessReaderWin* process_reader, #if defined(ARCH_CPU_32_BITS) const bool is_64_bit = false; - using Context32 = CONTEXT; #elif defined(ARCH_CPU_64_BITS) const bool is_64_bit = process_reader->Is64Bit(); - using Context32 = WOW64_CONTEXT; if (is_64_bit) { - CONTEXT context_record; if (!InitializeFromExceptionPointers( - *process_reader, exception_pointers_address, &context_record)) { + *process_reader, + exception_pointers_address, + threads, + &NativeContextToCPUContext64)) { return false; } - context_.architecture = kCPUArchitectureX86_64; - context_.x86_64 = &context_union_.x86_64; - InitializeX64Context(context_record, context_.x86_64); } #endif if (!is_64_bit) { - Context32 context_record; if (!InitializeFromExceptionPointers( - *process_reader, exception_pointers_address, &context_record)) { + *process_reader, + exception_pointers_address, + threads, + &NativeContextToCPUContext32)) { return false; } - context_.architecture = kCPUArchitectureX86; - context_.x86 = &context_union_.x86; - InitializeX86Context(context_record, context_.x86); } CaptureMemoryDelegateWin capture_memory_delegate( @@ -143,7 +170,10 @@ template & threads, + void (*native_to_cpu_context)(const ContextType& context_record, + CPUContext* context, + CPUContextUnion* context_union)) { ExceptionPointersType exception_pointers; if (!process_reader.ReadMemory(exception_pointers_address, sizeof(exception_pointers), @@ -164,22 +194,60 @@ bool ExceptionSnapshotWin::InitializeFromExceptionPointers( LOG(ERROR) << "ExceptionRecord"; return false; } - exception_code_ = first_record.ExceptionCode; - exception_flags_ = first_record.ExceptionFlags; - exception_address_ = first_record.ExceptionAddress; - for (DWORD i = 0; i < first_record.NumberParameters; ++i) - codes_.push_back(first_record.ExceptionInformation[i]); - if (first_record.ExceptionRecord) { - // https://crashpad.chromium.org/bug/43 - LOG(WARNING) << "dropping chained ExceptionRecord"; - } - if (!process_reader.ReadMemory( - static_cast(exception_pointers.ContextRecord), - sizeof(*context_record), - context_record)) { - LOG(ERROR) << "ContextRecord"; - return false; + if (first_record.ExceptionCode == CrashpadClient::kTriggeredExceptionCode && + first_record.NumberParameters == 2 && + first_record.ExceptionInformation[0] != 0) { + // This special exception code indicates that the target was crashed by + // another client calling CrashpadClient::DumpAndCrashTargetProcess(). In + // this case the parameters are a thread id and an exception code which we + // use to fabricate a new exception record. + using ArgumentType = decltype(first_record.ExceptionInformation[0]); + const ArgumentType thread_id = first_record.ExceptionInformation[0]; + exception_code_ = static_cast(first_record.ExceptionInformation[1]); + exception_flags_ = EXCEPTION_NONCONTINUABLE; + for (const auto* thread : threads) { + if (thread->ThreadID() == thread_id) { + thread_id_ = thread_id; + exception_address_ = thread->Context()->InstructionPointer(); + context_.architecture = thread->Context()->architecture; + if (context_.architecture == kCPUArchitectureX86_64) { + context_union_.x86_64 = *thread->Context()->x86_64; + context_.x86_64 = &context_union_.x86_64; + } else { + context_union_.x86 = *thread->Context()->x86; + context_.x86 = &context_union_.x86; + } + break; + } + } + + if (exception_address_ == 0) { + LOG(WARNING) << "thread " << thread_id << " not found"; + return false; + } + } else { + // Normal case. + exception_code_ = first_record.ExceptionCode; + exception_flags_ = first_record.ExceptionFlags; + exception_address_ = first_record.ExceptionAddress; + for (DWORD i = 0; i < first_record.NumberParameters; ++i) + codes_.push_back(first_record.ExceptionInformation[i]); + if (first_record.ExceptionRecord) { + // https://crashpad.chromium.org/bug/43 + LOG(WARNING) << "dropping chained ExceptionRecord"; + } + + ContextType context_record; + if (!process_reader.ReadMemory( + static_cast(exception_pointers.ContextRecord), + sizeof(context_record), + &context_record)) { + LOG(ERROR) << "ContextRecord"; + return false; + } + + native_to_cpu_context(context_record, &context_, &context_union_); } return true; diff --git a/snapshot/win/exception_snapshot_win.h b/snapshot/win/exception_snapshot_win.h index 3e66dd26..8ff7d381 100644 --- a/snapshot/win/exception_snapshot_win.h +++ b/snapshot/win/exception_snapshot_win.h @@ -22,6 +22,7 @@ #include "build/build_config.h" #include "snapshot/cpu_context.h" #include "snapshot/exception_snapshot.h" +#include "snapshot/win/thread_snapshot_win.h" #include "util/misc/initialization_state_dcheck.h" #include "util/stdlib/pointer_container.h" #include "util/win/address_types.h" @@ -35,6 +36,13 @@ namespace internal { class MemorySnapshotWin; +#if defined(ARCH_CPU_X86_FAMILY) +union CPUContextUnion { + CPUContextX86 x86; + CPUContextX86_64 x86_64; +}; +#endif + class ExceptionSnapshotWin final : public ExceptionSnapshot { public: ExceptionSnapshotWin(); @@ -53,7 +61,8 @@ class ExceptionSnapshotWin final : public ExceptionSnapshot { //! an appropriate message logged. bool Initialize(ProcessReaderWin* process_reader, DWORD thread_id, - WinVMAddress exception_pointers); + WinVMAddress exception_pointers, + const PointerVector& threads); // ExceptionSnapshot: @@ -69,15 +78,16 @@ class ExceptionSnapshotWin final : public ExceptionSnapshot { template - bool InitializeFromExceptionPointers(const ProcessReaderWin& process_reader, - WinVMAddress exception_pointers_address, - ContextType* context_record); + bool InitializeFromExceptionPointers( + const ProcessReaderWin& process_reader, + WinVMAddress exception_pointers_address, + const PointerVector& threads, + void (*native_to_cpu_context)(const ContextType& context_record, + CPUContext* context, + CPUContextUnion* context_union)); #if defined(ARCH_CPU_X86_FAMILY) - union { - CPUContextX86 x86; - CPUContextX86_64 x86_64; - } context_union_; + CPUContextUnion context_union_; #endif CPUContext context_; std::vector codes_; diff --git a/snapshot/win/process_snapshot_win.cc b/snapshot/win/process_snapshot_win.cc index 6af6f200..888a3e02 100644 --- a/snapshot/win/process_snapshot_win.cc +++ b/snapshot/win/process_snapshot_win.cc @@ -107,7 +107,8 @@ bool ProcessSnapshotWin::InitializeException( exception_.reset(new internal::ExceptionSnapshotWin()); if (!exception_->Initialize(&process_reader_, exception_information.thread_id, - exception_information.exception_pointers)) { + exception_information.exception_pointers, + threads_)) { exception_.reset(); return false; } diff --git a/util/win/nt_internals.cc b/util/win/nt_internals.cc index 6fc0999d..8cf96a45 100644 --- a/util/win/nt_internals.cc +++ b/util/win/nt_internals.cc @@ -21,6 +21,18 @@ struct CLIENT_ID; +NTSTATUS NTAPI NtCreateThreadEx(PHANDLE ThreadHandle, + ACCESS_MASK DesiredAccess, + POBJECT_ATTRIBUTES ObjectAttributes, + HANDLE ProcessHandle, + PVOID StartRoutine, + PVOID Argument, + ULONG CreateFlags, + SIZE_T ZeroBits, + SIZE_T StackSize, + SIZE_T MaximumStackSize, + PVOID /*PPS_ATTRIBUTE_LIST*/ AttributeList); + NTSTATUS NTAPI NtOpenThread(HANDLE* ThreadHandle, ACCESS_MASK DesiredAccess, OBJECT_ATTRIBUTES* ObjectAttributes, @@ -30,6 +42,38 @@ void* NTAPI RtlGetUnloadEventTrace(); namespace crashpad { +NTSTATUS NtClose(HANDLE handle) { + static const auto nt_close = GET_FUNCTION_REQUIRED(L"ntdll.dll", ::NtClose); + return nt_close(handle); +} + +NTSTATUS +NtCreateThreadEx(PHANDLE thread_handle, + ACCESS_MASK desired_access, + POBJECT_ATTRIBUTES object_attributes, + HANDLE process_handle, + PVOID start_routine, + PVOID argument, + ULONG create_flags, + SIZE_T zero_bits, + SIZE_T stack_size, + SIZE_T maximum_stack_size, + PVOID attribute_list) { + static const auto nt_create_thread_ex = + GET_FUNCTION_REQUIRED(L"ntdll.dll", ::NtCreateThreadEx); + return nt_create_thread_ex(thread_handle, + desired_access, + object_attributes, + process_handle, + start_routine, + argument, + create_flags, + zero_bits, + stack_size, + maximum_stack_size, + attribute_list); +} + NTSTATUS NtQuerySystemInformation( SYSTEM_INFORMATION_CLASS system_information_class, PVOID system_information, diff --git a/util/win/nt_internals.h b/util/win/nt_internals.h index b6221018..3ced5cc3 100644 --- a/util/win/nt_internals.h +++ b/util/win/nt_internals.h @@ -19,6 +19,23 @@ namespace crashpad { +NTSTATUS NtClose(HANDLE handle); + +// http://processhacker.sourceforge.net/doc/ntpsapi_8h_source.html +#define THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH 0x00000002 +NTSTATUS +NtCreateThreadEx(PHANDLE thread_handle, + ACCESS_MASK desired_access, + POBJECT_ATTRIBUTES object_attributes, + HANDLE process_handle, + PVOID start_routine, + PVOID argument, + ULONG create_flags, + SIZE_T zero_bits, + SIZE_T stack_size, + SIZE_T maximum_stack_size, + PVOID /*PPS_ATTRIBUTE_LIST*/ attribute_list); + // Copied from ntstatus.h because um/winnt.h conflicts with general inclusion of // ntstatus.h. #define STATUS_BUFFER_TOO_SMALL ((NTSTATUS)0xC0000023L)