crashpad/client/crashpad_client_mac.cc

536 lines
20 KiB
C++
Raw Normal View History

// Copyright 2014 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 <errno.h>
#include <mach/mach.h>
#include <pthread.h>
#include <stdint.h>
#include <memory>
#include <utility>
#include "base/logging.h"
#include "base/mac/mach_logging.h"
#include "base/strings/stringprintf.h"
#include "util/mac/mac_util.h"
#include "util/mach/bootstrap.h"
#include "util/mach/child_port_handshake.h"
#include "util/mach/exception_ports.h"
#include "util/mach/mach_extensions.h"
#include "util/mach/mach_message.h"
#include "util/mach/notify_server.h"
#include "util/misc/clock.h"
#include "util/misc/implicit_cast.h"
mac: Tests that crash intentionally shouldn’t go to ReportCrash Crashpad has many tests that crash intentionally. Some of these are gtest death tests, and others arrange for intentional crashes to test Crashpad’s own crash-catching logic. On macOS, all of the gtest death tests and some of the other intentional crashes were being logged by ReportCrash, the system’s crash reporter. Since these reports corresponded to intentional crashes, they were never useful, and served only to clutter ~/Library/Logs/DiagnosticReports. Since Crashpad is adept at handling exceptions on its own, this introduces the “exception swallowing server”, crashpad_exception_swallower, which is a Mach exception server that implements a no-op exception handler routine for all exceptions received. The exception swallowing server is established as the task handler for EXC_CRASH and EXC_CORPSE_NOTIFY exceptions during gtest death tests invoked by {ASSERT,EXPECT}_DEATH_{CHECK,CRASH}, and for all child processes invoked by the Multiprocess test infrastructure. The exception swallowing server is not in effect at other times, so unexpected crashes in test code can still be handled by ReportCrash or another crash reporter. With this change in place, no new reports are generated in the user-level ~/Library/Logs/DiagnosticReports or the system’s /Library/Logs/DiagnosticReports during a run of Crashpad’s full test suite on macOS. Bug: crashpad:33 Change-Id: I13891853a7e25accc30da21fa7ea8bd7d1f3bd2f Reviewed-on: https://chromium-review.googlesource.com/777859 Commit-Queue: Mark Mentovai <mark@chromium.org> Reviewed-by: Robert Sesek <rsesek@chromium.org>
2017-11-20 13:32:26 -05:00
#include "util/posix/double_fork_and_exec.h"
namespace crashpad {
namespace {
std::string FormatArgumentString(const std::string& name,
const std::string& value) {
return base::StringPrintf("--%s=%s", name.c_str(), value.c_str());
}
std::string FormatArgumentInt(const std::string& name, int value) {
return base::StringPrintf("--%s=%d", name.c_str(), value);
}
// Set the exception handler for EXC_CRASH, EXC_RESOURCE, and EXC_GUARD.
//
// EXC_CRASH is how most crashes are received. Most other exception types such
// as EXC_BAD_ACCESS are delivered to a host-level exception handler in the
// kernel where they are converted to POSIX signals. See 10.9.5
// xnu-2422.115.4/bsd/uxkern/ux_exception.c catch_mach_exception_raise(). If a
// core-generating signal (triggered through this hardware mechanism or a
// software mechanism such as abort() sending SIGABRT) is unhandled and the
// process exits, or if the process is killed with SIGKILL for code-signing
// reasons, an EXC_CRASH exception will be sent. See 10.9.5
// xnu-2422.115.4/bsd/kern/kern_exit.c proc_prepareexit().
//
// EXC_RESOURCE and EXC_GUARD do not become signals or EXC_CRASH exceptions. The
// host-level exception handler in the kernel does not receive these exception
// types, and even if it did, it would not map them to signals. Instead, the
// first Mach service loaded by the root (process ID 1) launchd with a boolean
// “ExceptionServer” property in its job dictionary (regardless of its value) or
// with any subdictionary property will become the host-level exception handler
// for EXC_CRASH, EXC_RESOURCE, and EXC_GUARD. See 10.9.5
// launchd-842.92.1/src/core.c job_setup_exception_port(). Normally, this job is
// com.apple.ReportCrash.Root, the systemwide Apple Crash Reporter. Since it is
// impossible to receive EXC_RESOURCE and EXC_GUARD exceptions through the
// EXC_CRASH mechanism, an exception handler must be registered for them by name
// if it is to receive these exception types. The default task-level handler for
// these exception types is set by launchd in a similar manner.
//
// EXC_MASK_RESOURCE and EXC_MASK_GUARD are not available on all systems, and
// the kernel will reject attempts to use them if it does not understand them,
// so AND them with ExcMaskValid(). EXC_MASK_CRASH is always supported.
bool SetCrashExceptionPorts(exception_handler_t exception_handler) {
ExceptionPorts exception_ports(ExceptionPorts::kTargetTypeTask, TASK_NULL);
return exception_ports.SetExceptionPort(
(EXC_MASK_CRASH | EXC_MASK_RESOURCE | EXC_MASK_GUARD) & ExcMaskValid(),
exception_handler,
EXCEPTION_STATE_IDENTITY | MACH_EXCEPTION_CODES,
MACHINE_THREAD_STATE);
}
class ScopedPthreadAttrDestroy {
public:
explicit ScopedPthreadAttrDestroy(pthread_attr_t* pthread_attr)
: pthread_attr_(pthread_attr) {
}
~ScopedPthreadAttrDestroy() {
errno = pthread_attr_destroy(pthread_attr_);
PLOG_IF(WARNING, errno != 0) << "pthread_attr_destroy";
}
private:
pthread_attr_t* pthread_attr_;
DISALLOW_COPY_AND_ASSIGN(ScopedPthreadAttrDestroy);
};
//! \brief Starts a Crashpad handler, possibly restarting it if it dies.
class HandlerStarter final : public NotifyServer::DefaultInterface {
public:
~HandlerStarter() {}
//! \brief Starts a Crashpad handler initially, as opposed to starting it for
//! subsequent restarts.
//!
//! All parameters are as in CrashpadClient::StartHandler().
//!
//! \return On success, a send right to the Crashpad handler that has been
//! started. On failure, `MACH_PORT_NULL` with a message logged.
static base::mac::ScopedMachSendRight InitialStart(
const base::FilePath& handler,
const base::FilePath& database,
const base::FilePath& metrics_dir,
const std::string& url,
const std::map<std::string, std::string>& annotations,
const std::vector<std::string>& arguments,
bool restartable) {
base::mac::ScopedMachReceiveRight receive_right(
NewMachPort(MACH_PORT_RIGHT_RECEIVE));
if (!receive_right.is_valid()) {
return base::mac::ScopedMachSendRight();
}
mach_port_t port;
mach_msg_type_name_t right_type;
kern_return_t kr = mach_port_extract_right(mach_task_self(),
receive_right.get(),
MACH_MSG_TYPE_MAKE_SEND,
&port,
&right_type);
if (kr != KERN_SUCCESS) {
MACH_LOG(ERROR, kr) << "mach_port_extract_right";
return base::mac::ScopedMachSendRight();
}
base::mac::ScopedMachSendRight send_right(port);
DCHECK_EQ(port, receive_right.get());
DCHECK_EQ(right_type,
implicit_cast<mach_msg_type_name_t>(MACH_MSG_TYPE_PORT_SEND));
std::unique_ptr<HandlerStarter> handler_restarter;
if (restartable) {
handler_restarter.reset(new HandlerStarter());
if (!handler_restarter->notify_port_.is_valid()) {
// This is an error that NewMachPort() would have logged. Proceed anyway
// without the ability to restart.
handler_restarter.reset();
}
}
if (!CommonStart(handler,
database,
metrics_dir,
url,
annotations,
arguments,
std::move(receive_right),
handler_restarter.get(),
false)) {
return base::mac::ScopedMachSendRight();
}
if (handler_restarter &&
handler_restarter->StartRestartThread(
handler, database, metrics_dir, url, annotations, arguments)) {
// The thread owns the object now.
ignore_result(handler_restarter.release());
}
// If StartRestartThread() failed, proceed without the ability to restart.
// handler_restarter will be released when this function returns.
return send_right;
}
// NotifyServer::DefaultInterface:
kern_return_t DoMachNotifyPortDestroyed(notify_port_t notify,
mach_port_t rights,
const mach_msg_trailer_t* trailer,
bool* destroy_request) override {
// The receive right corresponding to this process crash exception port is
// now owned by this process. Any crashes that occur before the receive
// right is moved to a new handler process will cause the process to hang in
// an unkillable state prior to OS X 10.10.
if (notify != notify_port_) {
LOG(WARNING) << "notify port mismatch";
return KERN_FAILURE;
}
// If CommonStart() fails, the receive right will die, and this will just
// be called again for another try.
CommonStart(handler_,
database_,
metrics_dir_,
url_,
annotations_,
arguments_,
base::mac::ScopedMachReceiveRight(rights),
this,
true);
return KERN_SUCCESS;
}
private:
HandlerStarter()
: NotifyServer::DefaultInterface(),
handler_(),
database_(),
metrics_dir_(),
url_(),
annotations_(),
arguments_(),
notify_port_(NewMachPort(MACH_PORT_RIGHT_RECEIVE)),
last_start_time_(0) {
}
//! \brief Starts a Crashpad handler.
//!
//! All parameters are as in CrashpadClient::StartHandler(), with these
//! additions:
//!
//! \param[in] receive_right The receive right to move to the Crashpad
//! handler. The handler will use this receive right to run its exception
//! server.
//! \param[in] handler_restarter If CrashpadClient::StartHandler() was invoked
//! with \a restartable set to `true`, this is the restart state object.
//! Otherwise, this is `nullptr`.
//! \param[in] restart If CrashpadClient::StartHandler() was invoked with \a
//! restartable set to `true` and CommonStart() is being called to restart
//! a previously-started handler, this is `true`. Otherwise, this is
//! `false`.
//!
//! \return `true` on success, `false` on failure, with a message logged.
//! Failures include failure to start the handler process and failure to
//! rendezvous with it via ChildPortHandshake.
static bool CommonStart(const base::FilePath& handler,
const base::FilePath& database,
const base::FilePath& metrics_dir,
const std::string& url,
const std::map<std::string, std::string>& annotations,
const std::vector<std::string>& arguments,
base::mac::ScopedMachReceiveRight receive_right,
HandlerStarter* handler_restarter,
bool restart) {
DCHECK(!restart || handler_restarter);
if (handler_restarter) {
// The port-destroyed notification must be requested each time. It uses
// a send-once right, so once the notification is received, it wont be
// sent again unless re-requested.
mach_port_t previous;
kern_return_t kr =
mach_port_request_notification(mach_task_self(),
receive_right.get(),
MACH_NOTIFY_PORT_DESTROYED,
0,
handler_restarter->notify_port_.get(),
MACH_MSG_TYPE_MAKE_SEND_ONCE,
&previous);
if (kr != KERN_SUCCESS) {
MACH_LOG(WARNING, kr) << "mach_port_request_notification";
// This will cause the restart thread to terminate after this restart
// attempt. Theres no longer any need for it, because no more
// port-destroyed notifications can be delivered.
handler_restarter->notify_port_.reset();
} else {
base::mac::ScopedMachSendRight previous_owner(previous);
DCHECK(restart || !previous_owner.is_valid());
}
if (restart) {
// If the handler was ever started before, dont restart it too quickly.
constexpr uint64_t kNanosecondsPerSecond = 1E9;
constexpr uint64_t kMinimumStartInterval = 1 * kNanosecondsPerSecond;
const uint64_t earliest_next_start_time =
handler_restarter->last_start_time_ + kMinimumStartInterval;
const uint64_t now_time = ClockMonotonicNanoseconds();
if (earliest_next_start_time > now_time) {
const uint64_t sleep_time = earliest_next_start_time - now_time;
LOG(INFO) << "restarting handler"
<< base::StringPrintf(" in %.3fs",
static_cast<double>(sleep_time) /
kNanosecondsPerSecond);
SleepNanoseconds(earliest_next_start_time - now_time);
} else {
LOG(INFO) << "restarting handler";
}
}
handler_restarter->last_start_time_ = ClockMonotonicNanoseconds();
}
ChildPortHandshake child_port_handshake;
base::ScopedFD server_write_fd = child_port_handshake.ServerWriteFD();
// Use handler as argv[0], followed by arguments directed by this methods
// parameters and a --handshake-fd argument. |arguments| are added first so
// that if it erroneously contains an argument such as --url, the actual
// |url| argument passed to this method will supersede it. In normal
// command-line processing, the last parameter wins in the case of a
// conflict.
std::vector<std::string> argv(1, handler.value());
argv.reserve(1 + arguments.size() + 2 + annotations.size() + 1);
for (const std::string& argument : arguments) {
argv.push_back(argument);
}
if (!database.value().empty()) {
argv.push_back(FormatArgumentString("database", database.value()));
}
if (!metrics_dir.value().empty()) {
argv.push_back(FormatArgumentString("metrics-dir", metrics_dir.value()));
}
if (!url.empty()) {
argv.push_back(FormatArgumentString("url", url));
}
for (const auto& kv : annotations) {
argv.push_back(
FormatArgumentString("annotation", kv.first + '=' + kv.second));
}
argv.push_back(FormatArgumentInt("handshake-fd", server_write_fd.get()));
mac: Tests that crash intentionally shouldn’t go to ReportCrash Crashpad has many tests that crash intentionally. Some of these are gtest death tests, and others arrange for intentional crashes to test Crashpad’s own crash-catching logic. On macOS, all of the gtest death tests and some of the other intentional crashes were being logged by ReportCrash, the system’s crash reporter. Since these reports corresponded to intentional crashes, they were never useful, and served only to clutter ~/Library/Logs/DiagnosticReports. Since Crashpad is adept at handling exceptions on its own, this introduces the “exception swallowing server”, crashpad_exception_swallower, which is a Mach exception server that implements a no-op exception handler routine for all exceptions received. The exception swallowing server is established as the task handler for EXC_CRASH and EXC_CORPSE_NOTIFY exceptions during gtest death tests invoked by {ASSERT,EXPECT}_DEATH_{CHECK,CRASH}, and for all child processes invoked by the Multiprocess test infrastructure. The exception swallowing server is not in effect at other times, so unexpected crashes in test code can still be handled by ReportCrash or another crash reporter. With this change in place, no new reports are generated in the user-level ~/Library/Logs/DiagnosticReports or the system’s /Library/Logs/DiagnosticReports during a run of Crashpad’s full test suite on macOS. Bug: crashpad:33 Change-Id: I13891853a7e25accc30da21fa7ea8bd7d1f3bd2f Reviewed-on: https://chromium-review.googlesource.com/777859 Commit-Queue: Mark Mentovai <mark@chromium.org> Reviewed-by: Robert Sesek <rsesek@chromium.org>
2017-11-20 13:32:26 -05:00
// When restarting, reset the system default crash handler first. Otherwise,
// the crash exception port in the handler will have been inherited from
// this parent process, which was probably using the exception server now
// being restarted. The handler cant monitor itself for its own crashes via
// this interface.
if (!DoubleForkAndExec(
argv,
nullptr,
mac: Tests that crash intentionally shouldn’t go to ReportCrash Crashpad has many tests that crash intentionally. Some of these are gtest death tests, and others arrange for intentional crashes to test Crashpad’s own crash-catching logic. On macOS, all of the gtest death tests and some of the other intentional crashes were being logged by ReportCrash, the system’s crash reporter. Since these reports corresponded to intentional crashes, they were never useful, and served only to clutter ~/Library/Logs/DiagnosticReports. Since Crashpad is adept at handling exceptions on its own, this introduces the “exception swallowing server”, crashpad_exception_swallower, which is a Mach exception server that implements a no-op exception handler routine for all exceptions received. The exception swallowing server is established as the task handler for EXC_CRASH and EXC_CORPSE_NOTIFY exceptions during gtest death tests invoked by {ASSERT,EXPECT}_DEATH_{CHECK,CRASH}, and for all child processes invoked by the Multiprocess test infrastructure. The exception swallowing server is not in effect at other times, so unexpected crashes in test code can still be handled by ReportCrash or another crash reporter. With this change in place, no new reports are generated in the user-level ~/Library/Logs/DiagnosticReports or the system’s /Library/Logs/DiagnosticReports during a run of Crashpad’s full test suite on macOS. Bug: crashpad:33 Change-Id: I13891853a7e25accc30da21fa7ea8bd7d1f3bd2f Reviewed-on: https://chromium-review.googlesource.com/777859 Commit-Queue: Mark Mentovai <mark@chromium.org> Reviewed-by: Robert Sesek <rsesek@chromium.org>
2017-11-20 13:32:26 -05:00
server_write_fd.get(),
true,
restart ? CrashpadClient::UseSystemDefaultHandler : nullptr)) {
return false;
}
// Close the write side of the pipe, so that the handler process is the only
// process that can write to it.
server_write_fd.reset();
// Rendezvous with the handler running in the grandchild process.
if (!child_port_handshake.RunClient(receive_right.get(),
MACH_MSG_TYPE_MOVE_RECEIVE)) {
return false;
}
ignore_result(receive_right.release());
return true;
}
bool StartRestartThread(const base::FilePath& handler,
const base::FilePath& database,
const base::FilePath& metrics_dir,
const std::string& url,
const std::map<std::string, std::string>& annotations,
const std::vector<std::string>& arguments) {
handler_ = handler;
database_ = database;
metrics_dir_ = metrics_dir;
url_ = url;
annotations_ = annotations;
arguments_ = arguments;
pthread_attr_t pthread_attr;
errno = pthread_attr_init(&pthread_attr);
if (errno != 0) {
PLOG(WARNING) << "pthread_attr_init";
return false;
}
ScopedPthreadAttrDestroy pthread_attr_owner(&pthread_attr);
errno = pthread_attr_setdetachstate(&pthread_attr, PTHREAD_CREATE_DETACHED);
if (errno != 0) {
PLOG(WARNING) << "pthread_attr_setdetachstate";
return false;
}
pthread_t pthread;
errno = pthread_create(&pthread, &pthread_attr, RestartThreadMain, this);
if (errno != 0) {
PLOG(WARNING) << "pthread_create";
return false;
}
return true;
}
static void* RestartThreadMain(void* argument) {
HandlerStarter* self = reinterpret_cast<HandlerStarter*>(argument);
NotifyServer notify_server(self);
mach_msg_return_t mr;
do {
mr = MachMessageServer::Run(&notify_server,
self->notify_port_.get(),
0,
MachMessageServer::kPersistent,
MachMessageServer::kReceiveLargeError,
kMachMessageTimeoutWaitIndefinitely);
MACH_LOG_IF(ERROR, mr != MACH_MSG_SUCCESS, mr)
<< "MachMessageServer::Run";
} while (self->notify_port_.is_valid() && mr == MACH_MSG_SUCCESS);
delete self;
return nullptr;
}
base::FilePath handler_;
base::FilePath database_;
base::FilePath metrics_dir_;
std::string url_;
std::map<std::string, std::string> annotations_;
std::vector<std::string> arguments_;
base::mac::ScopedMachReceiveRight notify_port_;
uint64_t last_start_time_;
DISALLOW_COPY_AND_ASSIGN(HandlerStarter);
};
} // namespace
CrashpadClient::CrashpadClient() : exception_port_(MACH_PORT_NULL) {
}
CrashpadClient::~CrashpadClient() {
}
bool CrashpadClient::StartHandler(
const base::FilePath& handler,
const base::FilePath& database,
const base::FilePath& metrics_dir,
const std::string& url,
const std::map<std::string, std::string>& annotations,
const std::vector<std::string>& arguments,
bool restartable,
bool asynchronous_start,
const std::vector<base::FilePath>& attachments) {
// Attachments are not implemented on MacOS yet.
DCHECK(attachments.empty());
// The “restartable” behavior can only be selected on OS X 10.10 and later. In
// previous OS versions, if the initial client were to crash while attempting
// to restart the handler, it would become an unkillable process.
base::mac::ScopedMachSendRight exception_port(
HandlerStarter::InitialStart(handler,
database,
metrics_dir,
url,
annotations,
arguments,
restartable && MacOSXMinorVersion() >= 10));
if (!exception_port.is_valid()) {
return false;
}
SetHandlerMachPort(std::move(exception_port));
return true;
}
bool CrashpadClient::SetHandlerMachService(const std::string& service_name) {
base::mac::ScopedMachSendRight exception_port(BootstrapLookUp(service_name));
if (!exception_port.is_valid()) {
return false;
}
SetHandlerMachPort(std::move(exception_port));
return true;
}
bool CrashpadClient::SetHandlerMachPort(
base::mac::ScopedMachSendRight exception_port) {
DCHECK(!exception_port_.is_valid());
DCHECK(exception_port.is_valid());
if (!SetCrashExceptionPorts(exception_port.get())) {
return false;
}
exception_port_.swap(exception_port);
return true;
}
base::mac::ScopedMachSendRight CrashpadClient::GetHandlerMachPort() const {
DCHECK(exception_port_.is_valid());
// For the purposes of this method, only return a port set by
// SetHandlerMachPort().
//
// It would be possible to use task_get_exception_ports() to look up the
// EXC_CRASH task exception port, but thats probably not what users of this
// interface really want. If CrashpadClient is asked for the handler Mach
// port, it should only return a port that it knows about by virtue of having
// set it. It shouldnt return any EXC_CRASH task exception port in effect if
// SetHandlerMachPort() was never called, and it shouldnt return any
// EXC_CRASH task exception port that might be set by other code after
// SetHandlerMachPort() is called.
//
// The caller is accepting its own new ScopedMachSendRight, so increment the
// reference count of the underlying right.
kern_return_t kr = mach_port_mod_refs(
mach_task_self(), exception_port_.get(), MACH_PORT_RIGHT_SEND, 1);
if (kr != KERN_SUCCESS) {
MACH_LOG(ERROR, kr) << "mach_port_mod_refs";
return base::mac::ScopedMachSendRight(MACH_PORT_NULL);
}
return base::mac::ScopedMachSendRight(exception_port_.get());
}
// static
void CrashpadClient::UseSystemDefaultHandler() {
base::mac::ScopedMachSendRight
system_crash_reporter_handler(SystemCrashReporterHandler());
// Proceed even if SystemCrashReporterHandler() failed, setting MACH_PORT_NULL
// to clear the current exception ports.
if (!SetCrashExceptionPorts(system_crash_reporter_handler.get())) {
SetCrashExceptionPorts(MACH_PORT_NULL);
}
}
} // namespace crashpad