Chromium Code Reviews| Index: util/win/exception_handler_server.cc |
| diff --git a/util/win/exception_handler_server.cc b/util/win/exception_handler_server.cc |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..f8b765a4ca858b6afcbfa909061eca93674d246b |
| --- /dev/null |
| +++ b/util/win/exception_handler_server.cc |
| @@ -0,0 +1,379 @@ |
| +// Copyright 2015 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 "util/win/exception_handler_server.h" |
| + |
| +#include "base/logging.h" |
| +#include "base/strings/stringprintf.h" |
| +#include "base/strings/utf_string_conversions.h" |
| +#include "minidump/minidump_file_writer.h" |
| +#include "snapshot/crashpad_info_client_options.h" |
| +#include "snapshot/win/process_snapshot_win.h" |
| +#include "util/file/file_writer.h" |
| +#include "util/misc/tri_state.h" |
| +#include "util/misc/uuid.h" |
| +#include "util/win/registration_protocol_win.h" |
| + |
| +namespace crashpad { |
| + |
| +namespace { |
| + |
| +//! \brief Context information for the named pipe handler threads. |
| +class PipeServiceContext { |
| + public: |
| + PipeServiceContext(HANDLE port, |
| + HANDLE pipe, |
| + ExceptionHandlerServer::Delegate* delegate, |
| + base::Lock* clients_lock, |
| + std::set<internal::ClientData*>* clients) |
| + : port_(port), |
| + pipe_(pipe), |
| + delegate_(delegate), |
| + clients_lock_(clients_lock), |
| + clients_(clients) {} |
| + |
| + HANDLE port() { return port_; } |
| + HANDLE pipe() { return pipe_.get(); } |
| + ExceptionHandlerServer::Delegate* delegate() { return delegate_; } |
| + base::Lock* clients_lock() { return clients_lock_; } |
| + std::set<internal::ClientData*>* clients() { return clients_; } |
| + |
| + private: |
| + HANDLE port_; // weak |
| + ScopedKernelHANDLE pipe_; |
| + ExceptionHandlerServer::Delegate* delegate_; // weak |
| + base::Lock* clients_lock_; // weak |
| + std::set<internal::ClientData*>* clients_; // weak |
| + |
| + DISALLOW_COPY_AND_ASSIGN(PipeServiceContext); |
| +}; |
| + |
| +decltype(GetNamedPipeClientProcessId)* GetNamedPipeClientProcessIdFunction() { |
| + static decltype(GetNamedPipeClientProcessId)* func = |
| + reinterpret_cast<decltype(GetNamedPipeClientProcessId)*>(GetProcAddress( |
| + GetModuleHandle(L"kernel32.dll"), "GetNamedPipeClientProcessId")); |
| + return func; |
| +} |
| + |
| +HANDLE DuplicateEvent(HANDLE process, HANDLE event) { |
| + HANDLE handle; |
| + if (DuplicateHandle(GetCurrentProcess(), |
| + event, |
| + process, |
| + &handle, |
| + SYNCHRONIZE | EVENT_MODIFY_STATE, |
| + false, |
| + 0)) { |
| + return handle; |
| + } |
| + return nullptr; |
| +} |
| + |
| +} // namespace |
| + |
| +namespace internal { |
| + |
| +//! \brief The context data for registered threadpool waits. |
| +//! |
| +//! This object must be created and destroyed on the main thread. Access must be |
| +//! guarded by use of the lock() with the exception of the threadpool wait |
| +//! variables which are accessed only by the main thread. |
| +class ClientData { |
| + public: |
| + ClientData(HANDLE port, |
| + ExceptionHandlerServer::Delegate* delegate, |
| + ScopedKernelHANDLE process, |
| + WinVMAddress exception_information_address, |
| + WAITORTIMERCALLBACK dump_request_callback, |
| + WAITORTIMERCALLBACK process_end_callback) |
| + : dump_request_thread_pool_wait_(INVALID_HANDLE_VALUE), |
| + process_end_thread_pool_wait_(INVALID_HANDLE_VALUE), |
| + lock_(), |
| + port_(port), |
| + delegate_(delegate), |
| + dump_requested_event_( |
| + CreateEvent(nullptr, false /* auto reset */, false, nullptr)), |
| + process_(process.Pass()), |
| + exception_information_address_(exception_information_address) { |
| + RegisterThreadPoolWaits(dump_request_callback, process_end_callback); |
| + } |
| + |
| + ~ClientData() { |
| + // It is important that this only access the threadpool waits (it's called |
| + // from the main thread) until the waits are unregistered, to ensure that |
| + // any outstanding callbacks are complete. |
| + UnregisterThreadPoolWaits(); |
| + } |
| + |
| + base::Lock* lock() { return &lock_; } |
| + HANDLE port() { return port_; } |
| + ExceptionHandlerServer::Delegate* delegate() { return delegate_; } |
| + HANDLE dump_requested_event() { return dump_requested_event_.get(); } |
| + WinVMAddress exception_information_address() const { |
| + return exception_information_address_; |
| + } |
| + HANDLE process() { return process_.get(); } |
| + |
| + private: |
| + void RegisterThreadPoolWaits(WAITORTIMERCALLBACK dump_request_callback, |
| + WAITORTIMERCALLBACK process_end_callback) { |
| + if (!RegisterWaitForSingleObject(&dump_request_thread_pool_wait_, |
| + dump_requested_event_.get(), |
| + dump_request_callback, |
| + this, |
| + INFINITE, |
| + WT_EXECUTEDEFAULT)) { |
| + LOG(ERROR) << "RegisterWaitForSingleObject dump requested"; |
| + } |
| + |
| + if (!RegisterWaitForSingleObject(&process_end_thread_pool_wait_, |
| + process_.get(), |
| + process_end_callback, |
| + this, |
| + INFINITE, |
| + WT_EXECUTEONLYONCE)) { |
| + LOG(ERROR) << "RegisterWaitForSingleObject process end"; |
| + } |
| + } |
| + |
| + // This blocks until outstanding calls complete so that we know it's safe to |
| + // delete this object. Because of this, it must be executed on the main |
| + // thread, not a threadpool thread. |
| + void UnregisterThreadPoolWaits() { |
| + UnregisterWaitEx(dump_request_thread_pool_wait_, INVALID_HANDLE_VALUE); |
| + dump_request_thread_pool_wait_ = INVALID_HANDLE_VALUE; |
| + UnregisterWaitEx(process_end_thread_pool_wait_, INVALID_HANDLE_VALUE); |
| + process_end_thread_pool_wait_ = INVALID_HANDLE_VALUE; |
| + } |
| + |
| + // These are only accessed on the main thread. |
| + HANDLE dump_request_thread_pool_wait_; |
| + HANDLE process_end_thread_pool_wait_; |
| + |
| + base::Lock lock_; |
| + // Access to these fields must be guarded by lock_. |
| + HANDLE port_; // weak |
| + ExceptionHandlerServer::Delegate* delegate_; // weak |
| + ScopedKernelHANDLE dump_requested_event_; |
| + ScopedKernelHANDLE process_; |
| + WinVMAddress exception_information_address_; |
| + |
| + DISALLOW_COPY_AND_ASSIGN(ClientData); |
| +}; |
| + |
| +} // namespace internal |
| + |
| +ExceptionHandlerServer::Delegate::~Delegate() { |
| +} |
| + |
| +ExceptionHandlerServer::ExceptionHandlerServer(Delegate* delegate) |
| + : delegate_(delegate), |
| + port_(CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, 1)), |
| + clients_lock_(), |
| + clients_() { |
| +} |
| + |
| +ExceptionHandlerServer::~ExceptionHandlerServer() { |
| +} |
| + |
| +void ExceptionHandlerServer::Run(const std::string& pipe_name) { |
| + // We create two pipe instances, so that there's one listening while the |
| + // PipeServiceProc is processing a registration. |
| + const int kPipeInstances = 2; |
| + ScopedKernelHANDLE thread_handles[kPipeInstances]; |
| + for (int i = 0; i < kPipeInstances; ++i) { |
| + HANDLE pipe = |
| + CreateNamedPipe(base::UTF8ToUTF16(pipe_name).c_str(), |
|
Mark Mentovai
2015/08/26 21:40:28
UTF8ToUTF16 outside of the loop?
scottmg
2015/08/27 01:04:38
Done.
|
| + PIPE_ACCESS_DUPLEX, |
| + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, |
| + kPipeInstances, |
| + 512, |
| + 512, |
| + 0, |
| + nullptr); |
| + |
| + // Ownership of this object (and the pipe instance) is given to the new |
| + // thread. We close the thread handles at the end of the scope, but need not |
| + // wait to join them. They clean up the context object and the pipe |
|
Mark Mentovai
2015/08/26 21:40:28
“Need not wait to join them”—really? Doesn’t that
scottmg
2015/08/27 01:04:38
Done.
|
| + // instance on termination. |
| + PipeServiceContext* context = new PipeServiceContext( |
| + port_.get(), pipe, delegate_, &clients_lock_, &clients_); |
| + thread_handles[i].reset( |
| + CreateThread(nullptr, 0, &PipeServiceProc, context, 0, nullptr)); |
| + } |
| + |
| + delegate_->OnStarted(); |
| + |
| + // This is the main loop of the server. Most work is done on the threadpool, |
| + // other than process end handing which is posted back to this main thread, as |
|
Mark Mentovai
2015/08/26 21:40:28
process end handing?
scottmg
2015/08/27 01:04:38
Done.
|
| + // we must unregister the threadpool waits here. |
| + for (;;) { |
| + OVERLAPPED* ov = nullptr; |
| + ULONG_PTR key = 0; |
| + DWORD bytes = 0; |
| + GetQueuedCompletionStatus(port_.get(), &bytes, &key, &ov, INFINITE); |
| + if (!key) { |
| + // Shutting down. |
| + break; |
| + } |
| + |
| + // Otherwise, this is a request to unregister and destroy the given client. |
| + // delete'ing the ClientData blocks in UnregisterWaitEx to ensure all |
| + // outstanding threadpool waits are complete. This is important because the |
| + // process handle can be signalled *before* the dump request is signalled. |
| + internal::ClientData* client = reinterpret_cast<internal::ClientData*>(key); |
| + { |
| + base::AutoLock lock(clients_lock_); |
| + clients_.erase(client); |
| + } |
| + delete client; |
| + } |
| + |
| + // Signal to the named pipe instances that they should terminate. |
| + for (int i = 0; i < kPipeInstances; ++i) { |
| + RegistrationRequest shutdown_request = {0}; |
| + shutdown_request.client_process_id = GetCurrentProcessId(); |
| + RegistrationResponse response; |
| + internal::RegisterWithCrashHandlerServer( |
| + base::UTF8ToUTF16(pipe_name), shutdown_request, &response); |
| + } |
| + |
| + // Deleting ClientData does a blocking wait until the threadpool executions |
| + // have terminated when unregistering them. |
| + { |
| + base::AutoLock lock(clients_lock_); |
| + for (auto* client : clients_) |
| + delete client; |
| + clients_.clear(); |
| + } |
| + |
| + delegate_->OnShutdown(); |
|
Mark Mentovai
2015/08/26 21:40:28
Since this happens as the last thing in Run(), is
scottmg
2015/08/27 01:04:37
Remove OnShutdown. Started is useful to know that
|
| +} |
| + |
| +void ExceptionHandlerServer::Stop() { |
| + // Post a null key (third argument) to trigger shutdown. |
| + PostQueuedCompletionStatus(port_.get(), 0, 0, nullptr); |
| +} |
| + |
| +// static |
| +DWORD __stdcall ExceptionHandlerServer::PipeServiceProc(void* ctx) { |
| + PipeServiceContext* service_context = |
| + reinterpret_cast<PipeServiceContext*>(ctx); |
| + |
| + for (;;) { |
| + // Connect can false if the client connects before we do. Continue and |
|
Mark Mentovai
2015/08/26 21:40:28
What’s this comment mean?
scottmg
2015/08/27 01:04:37
Removed, and made it check for the condition the c
|
| + // attempt a read anyway. |
| + ConnectNamedPipe(service_context->pipe(), nullptr); |
| + RegistrationRequest request; |
| + if (!LoggingReadFile(service_context->pipe(), &request, sizeof(request))) |
| + goto AbortAndResumeListening; |
|
Mark Mentovai
2015/08/26 21:40:28
Can you restructure all of this to avoid “goto”? P
scottmg
2015/08/27 01:04:38
Done.
|
| + |
| + // Shutdown request. |
| + if (request.client_process_id == GetCurrentProcessId()) { |
|
Mark Mentovai
2015/08/26 21:40:28
This lets any evil client crash the party for ever
scottmg
2015/08/27 01:04:37
Not that I know of (a better way). AFAIK, we'd hav
scottmg
2015/08/27 04:18:00
WRT this first part, added a shared token so the r
|
| + RegistrationResponse shutdown_response = {0}; |
| + LoggingWriteFile(service_context->pipe(), |
| + &shutdown_response, |
| + sizeof(shutdown_response)); |
| + break; |
| + } |
| + |
| + decltype(GetNamedPipeClientProcessId)* get_named_pipe_client_process_id = |
| + GetNamedPipeClientProcessIdFunction(); |
| + if (get_named_pipe_client_process_id) { |
| + // GetNamedPipeClientProcessId is only available on Vista+. |
| + DWORD real_pid = 0; |
| + if (get_named_pipe_client_process_id(service_context->pipe(), |
| + &real_pid) && |
| + request.client_process_id != real_pid) { |
| + LOG(ERROR) << "forged client pid"; |
|
Mark Mentovai
2015/08/26 21:40:28
Log the evil real pid too?
scottmg
2015/08/27 01:04:38
Done.
|
| + goto AbortAndResumeListening; |
| + } |
| + } |
| + |
| + // We attempt to open the process as us. This is the main case that should |
| + // almost always succeed as the server will generally be more privileged. If |
| + // we're running as a different user, it may be that we will fail to open |
| + // the process, but the client will be able to, so we make a second attempt |
| + // having impersonated the client. |
|
Mark Mentovai
2015/08/26 21:40:28
Can we have the client pass its own process handle
scottmg
2015/08/27 01:04:37
I have to defer to the Windows handle people again
|
| + HANDLE client_process = |
| + OpenProcess(PROCESS_ALL_ACCESS, false, request.client_process_id); |
| + if (!client_process) { |
| + if (!ImpersonateNamedPipeClient(service_context->pipe())) { |
| + PLOG(ERROR) << "ImpersonateNamedPipeClient"; |
| + goto AbortAndResumeListening; |
| + } |
| + HANDLE client_process = |
| + OpenProcess(PROCESS_ALL_ACCESS, false, request.client_process_id); |
| + RevertToSelf(); |
|
Mark Mentovai
2015/08/26 21:40:28
On the fence about a scoper to ensure this happens
scottmg
2015/08/27 01:04:38
PCHECKd.
|
| + if (!client_process) { |
| + LOG(ERROR) << "failed to open " << request.client_process_id; |
|
Mark Mentovai
2015/08/26 21:40:28
I’d say PLOG, but that intervening RevertToSelf()
scottmg
2015/08/27 01:04:37
Right.
|
| + goto AbortAndResumeListening; |
| + } |
| + } |
| + |
| + internal::ClientData* client; |
| + { |
| + base::AutoLock lock(*service_context->clients_lock()); |
| + client = new internal::ClientData(service_context->port(), |
| + service_context->delegate(), |
| + ScopedKernelHANDLE(client_process), |
| + request.exception_information, |
| + &OnDumpEvent, |
| + &OnProcessEnd); |
| + service_context->clients()->insert(client); |
| + } |
| + |
| + // Duplicate the events back to the client so they can request a dump. |
| + RegistrationResponse response; |
| + response.request_report_event = reinterpret_cast<uint32_t>( |
| + DuplicateEvent(client->process(), client->dump_requested_event())); |
| + |
| + if (!LoggingWriteFile(service_context->pipe(), &response, sizeof(response))) |
| + goto AbortAndResumeListening; |
| + |
| + AbortAndResumeListening: |
| + DisconnectNamedPipe(service_context->pipe()); |
| + } |
| + |
| + delete service_context; |
| + |
| + return 0; |
| +} |
| + |
| +// static |
| +void __stdcall ExceptionHandlerServer::OnDumpEvent(void* ctx, BOOLEAN) { |
| + // This function is executed on the thread pool. |
| + internal::ClientData* client = reinterpret_cast<internal::ClientData*>(ctx); |
| + base::AutoLock lock(*client->lock()); |
| + |
| + // Capture the exception. |
| + client->delegate()->OnException(client->process(), |
| + client->exception_information_address()); |
| + |
| + // Terminate the client with a known exit code. |
|
Mark Mentovai
2015/08/26 21:40:28
Is this what the system would do in the absence of
scottmg
2015/08/27 01:04:38
Yes, you're right. I was sort of thinking that thi
|
| + const UINT kCrashExitCode = 0xffff7002; |
| + TerminateProcess(client->process(), kCrashExitCode); |
| +} |
| + |
| +// static |
| +void __stdcall ExceptionHandlerServer::OnProcessEnd(void* ctx, BOOLEAN) { |
| + // This function is executed on the thread pool. |
| + internal::ClientData* client = reinterpret_cast<internal::ClientData*>(ctx); |
| + base::AutoLock lock(*client->lock()); |
| + |
| + // Post back to the main thread to have it delete this client record. |
| + PostQueuedCompletionStatus(client->port(), 0, ULONG_PTR(client), nullptr); |
| +} |
| + |
| +} // namespace crashpad |