mirror of
https://github.com/chromium/crashpad.git
synced 2025-03-09 14:06:33 +00:00
win: Support dumping another process by causing it to crash
Adds a new client API which allows causing an exception in another process. This is accomplished by injecting a thread that calls RaiseException(). A special exception code is used that indicates to the handler that the exception arguments contain a thread id and exception code, which are in turn used to fabricate an exception record. This is so that the API can allow the client to "blame" a particular thread in the target process. The target process must also be a registered Crashpad client, as the normal exception mechanism is used to handle the exception. The injection of a thread is used instead of DebugBreakProcess() which does not cause the UnhandledExceptionFilter() to be executed. NtCreateThreadEx() is used in lieu of CreateRemoteThread() as it allows passing of a flag which avoids calling DllMain()s. This is necessary to allow thread creation to succeed even when the target process is deadlocked on the loader lock. BUG=crashpad:103 Change-Id: I797007bd2b1e3416afe3f37a6566c0cdb259b106 Reviewed-on: https://chromium-review.googlesource.com/339263 Reviewed-by: Mark Mentovai <mark@chromium.org>
This commit is contained in:
parent
dbfcb5d032
commit
6a6a0c27ed
@ -19,6 +19,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#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.
|
||||
|
@ -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>* handle_list,
|
||||
}
|
||||
}
|
||||
|
||||
void AddUint32(std::vector<unsigned char>* data_vector, uint32_t data) {
|
||||
data_vector->push_back(static_cast<unsigned char>(data & 0xff));
|
||||
data_vector->push_back(static_cast<unsigned char>((data & 0xff00) >> 8));
|
||||
data_vector->push_back(static_cast<unsigned char>((data & 0xff0000) >> 16));
|
||||
data_vector->push_back(static_cast<unsigned char>((data & 0xff000000) >> 24));
|
||||
}
|
||||
|
||||
void AddUint64(std::vector<unsigned char>* data_vector, uint64_t data) {
|
||||
AddUint32(data_vector, static_cast<uint32_t>(data & 0xffffffffULL));
|
||||
AddUint32(data_vector,
|
||||
static_cast<uint32_t>((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<WinVMAddress>(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<WinVMAddress>(&RaiseException);
|
||||
|
||||
WinVMAddress code_entry_point = 0;
|
||||
std::vector<unsigned char> 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, <data_array_address>
|
||||
// mov r8d, 2 ; nNumberOfArguments
|
||||
// mov edx, 1 ; dwExceptionFlags = EXCEPTION_NONCONTINUABLE
|
||||
// mov ecx, 0xcca11ed ; dwExceptionCode, interpreted specially by the
|
||||
// ; handler.
|
||||
// jmp <address_of_RaiseException>
|
||||
//
|
||||
// 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 <data_array_address>
|
||||
// push 2 ; nNumberOfArguments
|
||||
// push 1 ; dwExceptionFlags = EXCEPTION_NONCONTINUABLE
|
||||
// push 0xcca11ed ; dwExceptionCode, interpreted specially by the handler.
|
||||
// call <address_of_RaiseException>
|
||||
// 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<uint32_t>(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<uint32_t>(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<void*>(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<void*>(inject_memory), bytes_written)) {
|
||||
PLOG(ERROR) << "FlushInstructionCache";
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD old_protect;
|
||||
if (!VirtualProtectEx(process,
|
||||
reinterpret_cast<void*>(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<void*>(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
|
||||
|
@ -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',
|
||||
|
120
handler/win/crash_other_program.cc
Normal file
120
handler/win/crash_other_program.cc
Normal file
@ -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 <stdlib.h>
|
||||
#include <windows.h>
|
||||
#include <tlhelp32.h>
|
||||
|
||||
#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 <server_pipe_name> [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);
|
||||
}
|
86
handler/win/hanging_program.cc
Normal file
86
handler/win/hanging_program.cc
Normal file
@ -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 <stdio.h>
|
||||
#include <windows.h>
|
||||
|
||||
#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 <server_pipe_name>\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;
|
||||
}
|
28
handler/win/loader_lock_dll.cc
Normal file
28
handler/win/loader_lock_dll.cc
Normal file
@ -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 <windows.h>
|
||||
|
||||
// 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;
|
||||
}
|
@ -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
|
||||
|
@ -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<internal::ThreadSnapshotWin>& 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<EXCEPTION_RECORD64,
|
||||
process_types::EXCEPTION_POINTERS64>(
|
||||
*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<EXCEPTION_RECORD32,
|
||||
process_types::EXCEPTION_POINTERS32>(
|
||||
*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 <class ExceptionRecordType,
|
||||
bool ExceptionSnapshotWin::InitializeFromExceptionPointers(
|
||||
const ProcessReaderWin& process_reader,
|
||||
WinVMAddress exception_pointers_address,
|
||||
ContextType* context_record) {
|
||||
const PointerVector<internal::ThreadSnapshotWin>& 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<WinVMAddress>(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<DWORD>(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<WinVMAddress>(exception_pointers.ContextRecord),
|
||||
sizeof(context_record),
|
||||
&context_record)) {
|
||||
LOG(ERROR) << "ContextRecord";
|
||||
return false;
|
||||
}
|
||||
|
||||
native_to_cpu_context(context_record, &context_, &context_union_);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@ -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<internal::ThreadSnapshotWin>& threads);
|
||||
|
||||
// ExceptionSnapshot:
|
||||
|
||||
@ -69,15 +78,16 @@ class ExceptionSnapshotWin final : public ExceptionSnapshot {
|
||||
template <class ExceptionRecordType,
|
||||
class ExceptionPointersType,
|
||||
class ContextType>
|
||||
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<internal::ThreadSnapshotWin>& 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<uint64_t> codes_;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user