crashpad/client/crashpad_client_ios.cc
Ben Hamilton 8df174c64c [ios] Fix TSAN issue and Mach port leak in CrashpadClient
There were two issues with the iOS implementation of CrashpadClient
which I reported in https://crbug.com/crashpad/481:

1) TSAN found a data race in ResetForTesting() when it modified the
ScopedMachReceiveRight while the Mach exception port thread was
reading it

2) The Mach port connected to the exception server was never deallocated

This CL fixes both issues.

Change-Id: I5bd4f79ae6d0eccca954d663be7a36f8ceb0a0e8
Bug: https://crbug.com/crashpad/481
Bug: b:332305593
Reviewed-on: https://chromium-review.googlesource.com/c/crashpad/crashpad/+/5410301
Reviewed-by: Mark Mentovai <mark@chromium.org>
Commit-Queue: Justin Cohen <justincohen@chromium.org>
2024-04-11 17:00:05 +00:00

526 lines
20 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2020 The Crashpad Authors
//
// 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 <signal.h>
#include <unistd.h>
#include <atomic>
#include <ios>
#include <iterator>
#include "base/apple/mach_logging.h"
#include "base/apple/scoped_mach_port.h"
#include "base/logging.h"
#include "client/ios_handler/exception_processor.h"
#include "client/ios_handler/in_process_handler.h"
#include "util/ios/raw_logging.h"
#include "util/mach/exc_server_variants.h"
#include "util/mach/exception_ports.h"
#include "util/mach/mach_extensions.h"
#include "util/mach/mach_message.h"
#include "util/mach/mach_message_server.h"
#include "util/misc/initialization_state_dcheck.h"
#include "util/posix/signals.h"
#include "util/thread/thread.h"
namespace {
bool IsBeingDebugged() {
kinfo_proc kern_proc_info;
int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};
size_t len = sizeof(kern_proc_info);
if (sysctl(mib, std::size(mib), &kern_proc_info, &len, nullptr, 0) == 0)
return kern_proc_info.kp_proc.p_flag & P_TRACED;
return false;
}
} // namespace
namespace crashpad {
namespace {
// Thread-safe version of `base::apple::ScopedMachReceiveRight` which allocates
// the Mach port upon construction and deallocates it upon destruction.
class ThreadSafeScopedMachPortWithReceiveRight {
public:
ThreadSafeScopedMachPortWithReceiveRight()
: port_(NewMachPort(MACH_PORT_RIGHT_RECEIVE)) {}
ThreadSafeScopedMachPortWithReceiveRight(
const ThreadSafeScopedMachPortWithReceiveRight&) = delete;
ThreadSafeScopedMachPortWithReceiveRight& operator=(
const ThreadSafeScopedMachPortWithReceiveRight&) = delete;
~ThreadSafeScopedMachPortWithReceiveRight() { reset(); }
mach_port_t get() { return port_.load(); }
void reset() {
mach_port_t old_port = port_.exchange(MACH_PORT_NULL);
if (old_port == MACH_PORT_NULL) {
// Already reset, nothing to do.
return;
}
kern_return_t kr = mach_port_mod_refs(
mach_task_self(), old_port, MACH_PORT_RIGHT_RECEIVE, -1);
MACH_LOG_IF(ERROR, kr != KERN_SUCCESS, kr)
<< "ThreadSafeScopedMachPortWithReceiveRight mach_port_mod_refs";
kr = mach_port_deallocate(mach_task_self(), old_port);
MACH_LOG_IF(ERROR, kr != KERN_SUCCESS, kr)
<< "ThreadSafeScopedMachPortWithReceiveRight mach_port_deallocate";
}
private:
std::atomic<mach_port_t> port_;
};
// A base class for signal handler and Mach exception server.
class CrashHandler : public Thread,
public UniversalMachExcServer::Interface,
public ObjcExceptionDelegate {
public:
CrashHandler(const CrashHandler&) = delete;
CrashHandler& operator=(const CrashHandler&) = delete;
static CrashHandler* Get() {
if (!instance_)
instance_ = new CrashHandler();
return instance_;
}
static void ResetForTesting() {
delete instance_;
instance_ = nullptr;
}
bool Initialize(
const base::FilePath& database,
const std::string& url,
const std::map<std::string, std::string>& annotations,
internal::InProcessHandler::ProcessPendingReportsObservationCallback
callback) {
INITIALIZATION_STATE_SET_INITIALIZING(initialized_);
if (!in_process_handler_.Initialize(database, url, annotations, callback) ||
!InstallMachExceptionHandler() ||
// xnu turns hardware faults into Mach exceptions, so the only signal
// left to register is SIGABRT, which never starts off as a hardware
// fault. Installing a handler for other signals would lead to
// recording exceptions twice. As a consequence, Crashpad will not
// generate intermediate dumps for anything manually calling
// raise(SIG*). In practice, this doesnt actually happen for crash
// signals that originate as hardware faults.
!Signals::InstallHandler(
SIGABRT, CatchAndReraiseSignal, 0, &old_action_)) {
LOG(ERROR) << "Unable to initialize Crashpad.";
return false;
}
// For applications that haven't ignored or set a handler for SIGPIPE:
// Its OK for an application to set its own SIGPIPE handler (including
// SIG_IGN) before initializing Crashpad, because Crashpad will discover the
// existing handler and not install its own.
// Its OK for Crashpad to install its own SIGPIPE handler and for the
// application to subsequently install its own (including SIG_IGN)
// afterwards, because its handler will replace Crashpads.
// This is useful to cover the default situation where nobody installs a
// SIGPIPE handler and the disposition is at SIG_DFL, because SIGPIPE is a
// “kill” signal (bsd/sys/signalvar.h sigprop). In that case, without
// Crashpad, SIGPIPE results in a silent and unreported kill (and not even
// ReportCrash will record it), but developers probably want to be alerted
// to the conditon.
struct sigaction sa;
if (sigaction(SIGPIPE, nullptr, &sa) == 0 && sa.sa_handler == SIG_DFL) {
Signals::InstallHandler(
SIGPIPE, CatchAndReraiseSignalDefaultAction, 0, nullptr);
}
InstallObjcExceptionPreprocessor(this);
INITIALIZATION_STATE_SET_VALID(initialized_);
return true;
}
void ProcessIntermediateDumps(
const std::map<std::string, std::string>& annotations) {
in_process_handler_.ProcessIntermediateDumps(annotations);
}
void ProcessIntermediateDump(
const base::FilePath& file,
const std::map<std::string, std::string>& annotations) {
in_process_handler_.ProcessIntermediateDump(file, annotations);
}
void DumpWithoutCrash(NativeCPUContext* context, bool process_dump) {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
base::FilePath path;
if (!in_process_handler_.DumpExceptionFromSimulatedMachException(
context, kMachExceptionSimulated, &path)) {
return;
}
if (process_dump) {
in_process_handler_.ProcessIntermediateDump(path);
}
}
void DumpWithoutCrashAtPath(NativeCPUContext* context,
const base::FilePath& path) {
in_process_handler_.DumpExceptionFromSimulatedMachExceptionAtPath(
context, kMachExceptionSimulated, path);
}
void StartProcessingPendingReports(UploadBehavior upload_behavior) {
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
in_process_handler_.StartProcessingPendingReports(upload_behavior);
}
void SetMachExceptionCallbackForTesting(void (*callback)()) {
in_process_handler_.SetMachExceptionCallbackForTesting(callback);
}
uint64_t GetThreadIdForTesting() { return Thread::GetThreadIdForTesting(); }
private:
CrashHandler() = default;
~CrashHandler() {
UninstallObjcExceptionPreprocessor();
Signals::InstallDefaultHandler(SIGABRT);
UninstallMachExceptionHandler();
}
bool InstallMachExceptionHandler() {
mach_port_t exception_port = exception_port_.get();
if (exception_port == MACH_PORT_NULL) {
return false;
}
kern_return_t kr = mach_port_insert_right(mach_task_self(),
exception_port,
exception_port,
MACH_MSG_TYPE_MAKE_SEND);
if (kr != KERN_SUCCESS) {
MACH_LOG(ERROR, kr) << "mach_port_insert_right";
return false;
}
// TODO: Use SwapExceptionPort instead and put back EXC_MASK_BREAKPOINT.
// Until then, remove |EXC_MASK_BREAKPOINT| while attached to a debugger.
exception_mask_t mask =
ExcMaskAll() &
~(EXC_MASK_EMULATION | EXC_MASK_SOFTWARE | EXC_MASK_RPC_ALERT |
EXC_MASK_GUARD | (IsBeingDebugged() ? EXC_MASK_BREAKPOINT : 0));
ExceptionPorts exception_ports(ExceptionPorts::kTargetTypeTask, TASK_NULL);
if (!exception_ports.GetExceptionPorts(mask, &original_handlers_) ||
!exception_ports.SetExceptionPort(
mask,
exception_port,
EXCEPTION_STATE_IDENTITY | MACH_EXCEPTION_CODES,
MACHINE_THREAD_STATE)) {
return false;
}
mach_handler_running_ = true;
Start();
return true;
}
void UninstallMachExceptionHandler() {
mach_handler_running_ = false;
exception_port_.reset();
Join();
}
// Thread:
void ThreadMain() override {
UniversalMachExcServer universal_mach_exc_server(this);
while (mach_handler_running_) {
mach_msg_return_t mr =
MachMessageServer::Run(&universal_mach_exc_server,
exception_port_.get(),
MACH_MSG_OPTION_NONE,
MachMessageServer::kPersistent,
MachMessageServer::kReceiveLargeIgnore,
kMachMessageTimeoutWaitIndefinitely);
MACH_CHECK(
mach_handler_running_
? mr == MACH_SEND_INVALID_DEST // This shouldn't happen for
// exception messages that come
// from the kernel itself, but if
// something else in-process sends
// exception messages and breaks,
// handle that case.
: (mr == MACH_RCV_PORT_CHANGED || // Port was closed while the
// thread was listening.
mr == MACH_RCV_INVALID_NAME), // Port was closed before the
// thread started listening.
mr)
<< "MachMessageServer::Run";
}
}
// UniversalMachExcServer::Interface:
kern_return_t CatchMachException(exception_behavior_t behavior,
exception_handler_t exception_port,
thread_t thread,
task_t task,
exception_type_t exception,
const mach_exception_data_type_t* code,
mach_msg_type_number_t code_count,
thread_state_flavor_t* flavor,
ConstThreadState old_state,
mach_msg_type_number_t old_state_count,
thread_state_t new_state,
mach_msg_type_number_t* new_state_count,
const mach_msg_trailer_t* trailer,
bool* destroy_complex_request) override {
*destroy_complex_request = true;
// TODO(justincohen): Forward exceptions to original_handlers_ with
// UniversalExceptionRaise.
// iOS shouldn't have any child processes, but just in case, those will
// inherit the task exception ports, and this process isnt prepared to
// handle them
if (task != mach_task_self()) {
CRASHPAD_RAW_LOG("MachException task != mach_task_self()");
return KERN_FAILURE;
}
HandleMachException(behavior,
thread,
exception,
code,
code_count,
*flavor,
old_state,
old_state_count);
// Respond with KERN_FAILURE so the system will continue to handle this
// exception. xnu will turn this Mach exception into a signal and take the
// default action to terminate the process. However, if sigprocmask is
// called before this Mach exception returns (such as by another thread
// calling abort, see: Libc-1506.40.4/stdlib/FreeBSD/abort.c), the Mach
// exception will be converted into a signal but delivery will be blocked.
// Since concurrent exceptions lead to the losing thread sleeping
// indefinitely, if the abort thread never returns, the thread that
// triggered this Mach exception will repeatedly trap and the process will
// never terminate. If the abort thread didnt have a user-space signal
// handler that slept forever, the abort would terminate the process even if
// all other signals had been blocked. Instead, unblock all signals
// corresponding to all Mach exceptions Crashpad is registered for before
// returning KERN_FAILURE. There is still racy behavior possible with this
// call to sigprocmask, but the repeated calls to CatchMachException here
// will eventually lead to termination.
sigset_t unblock_set;
sigemptyset(&unblock_set);
sigaddset(&unblock_set, SIGILL); // EXC_BAD_INSTRUCTION
sigaddset(&unblock_set, SIGTRAP); // EXC_BREAKPOINT
sigaddset(&unblock_set, SIGFPE); // EXC_ARITHMETIC
sigaddset(&unblock_set, SIGBUS); // EXC_BAD_ACCESS
sigaddset(&unblock_set, SIGSEGV); // EXC_BAD_ACCESS
if (sigprocmask(SIG_UNBLOCK, &unblock_set, nullptr) != 0) {
CRASHPAD_RAW_LOG("sigprocmask");
}
return KERN_FAILURE;
}
void HandleMachException(exception_behavior_t behavior,
thread_t thread,
exception_type_t exception,
const mach_exception_data_type_t* code,
mach_msg_type_number_t code_count,
thread_state_flavor_t flavor,
ConstThreadState old_state,
mach_msg_type_number_t old_state_count) {
in_process_handler_.DumpExceptionFromMachException(behavior,
thread,
exception,
code,
code_count,
flavor,
old_state,
old_state_count);
}
void HandleUncaughtNSException(const uint64_t* frames,
const size_t num_frames) override {
in_process_handler_.DumpExceptionFromNSExceptionWithFrames(frames,
num_frames);
// After uncaught exceptions are reported, the system immediately triggers a
// call to std::terminate()/abort(). Remove the abort handler so a second
// dump isn't generated.
CHECK(Signals::InstallDefaultHandler(SIGABRT));
}
void HandleUncaughtNSExceptionWithContext(
NativeCPUContext* context) override {
base::FilePath path;
in_process_handler_.DumpExceptionFromSimulatedMachException(
context, kMachExceptionFromNSException, &path);
// After uncaught exceptions are reported, the system immediately triggers a
// call to std::terminate()/abort(). Remove the abort handler so a second
// dump isn't generated.
CHECK(Signals::InstallDefaultHandler(SIGABRT));
}
void HandleUncaughtNSExceptionWithContextAtPath(
NativeCPUContext* context,
const base::FilePath& path) override {
in_process_handler_.DumpExceptionFromSimulatedMachExceptionAtPath(
context, kMachExceptionFromNSException, path);
}
bool MoveIntermediateDumpAtPathToPending(
const base::FilePath& path) override {
if (in_process_handler_.MoveIntermediateDumpAtPathToPending(path)) {
// After uncaught exceptions are reported, the system immediately triggers
// a call to std::terminate()/abort(). Remove the abort handler so a
// second dump isn't generated.
CHECK(Signals::InstallDefaultHandler(SIGABRT));
return true;
}
return false;
}
// The signal handler installed at OS-level.
static void CatchAndReraiseSignal(int signo,
siginfo_t* siginfo,
void* context) {
Get()->HandleAndReraiseSignal(signo,
siginfo,
reinterpret_cast<ucontext_t*>(context),
&(Get()->old_action_));
}
static void CatchAndReraiseSignalDefaultAction(int signo,
siginfo_t* siginfo,
void* context) {
Get()->HandleAndReraiseSignal(
signo, siginfo, reinterpret_cast<ucontext_t*>(context), nullptr);
}
void HandleAndReraiseSignal(int signo,
siginfo_t* siginfo,
ucontext_t* context,
struct sigaction* old_action) {
in_process_handler_.DumpExceptionFromSignal(siginfo, context);
// Always call system handler.
Signals::RestoreHandlerAndReraiseSignalOnReturn(siginfo, old_action);
}
ThreadSafeScopedMachPortWithReceiveRight exception_port_;
ExceptionPorts::ExceptionHandlerVector original_handlers_;
struct sigaction old_action_ = {};
internal::InProcessHandler in_process_handler_;
static CrashHandler* instance_;
std::atomic<bool> mach_handler_running_ = false;
InitializationStateDcheck initialized_;
};
CrashHandler* CrashHandler::instance_ = nullptr;
} // namespace
CrashpadClient::CrashpadClient() {}
CrashpadClient::~CrashpadClient() {}
// static
bool CrashpadClient::StartCrashpadInProcessHandler(
const base::FilePath& database,
const std::string& url,
const std::map<std::string, std::string>& annotations,
ProcessPendingReportsObservationCallback callback) {
CrashHandler* crash_handler = CrashHandler::Get();
DCHECK(crash_handler);
return crash_handler->Initialize(database, url, annotations, callback);
}
// static
void CrashpadClient::ProcessIntermediateDumps(
const std::map<std::string, std::string>& annotations) {
CrashHandler* crash_handler = CrashHandler::Get();
DCHECK(crash_handler);
crash_handler->ProcessIntermediateDumps(annotations);
}
// static
void CrashpadClient::ProcessIntermediateDump(
const base::FilePath& file,
const std::map<std::string, std::string>& annotations) {
CrashHandler* crash_handler = CrashHandler::Get();
DCHECK(crash_handler);
crash_handler->ProcessIntermediateDump(file, annotations);
}
// static
void CrashpadClient::StartProcessingPendingReports(
UploadBehavior upload_behavior) {
CrashHandler* crash_handler = CrashHandler::Get();
DCHECK(crash_handler);
crash_handler->StartProcessingPendingReports(upload_behavior);
}
// static
void CrashpadClient::DumpWithoutCrash(NativeCPUContext* context) {
CrashHandler* crash_handler = CrashHandler::Get();
DCHECK(crash_handler);
crash_handler->DumpWithoutCrash(context, /*process_dump=*/true);
}
// static
void CrashpadClient::DumpWithoutCrashAndDeferProcessing(
NativeCPUContext* context) {
CrashHandler* crash_handler = CrashHandler::Get();
DCHECK(crash_handler);
crash_handler->DumpWithoutCrash(context, /*process_dump=*/false);
}
// static
void CrashpadClient::DumpWithoutCrashAndDeferProcessingAtPath(
NativeCPUContext* context,
const base::FilePath path) {
CrashHandler* crash_handler = CrashHandler::Get();
DCHECK(crash_handler);
crash_handler->DumpWithoutCrashAtPath(context, path);
}
void CrashpadClient::ResetForTesting() {
CrashHandler* crash_handler = CrashHandler::Get();
DCHECK(crash_handler);
crash_handler->ResetForTesting();
}
void CrashpadClient::SetMachExceptionCallbackForTesting(void (*callback)()) {
CrashHandler* crash_handler = CrashHandler::Get();
DCHECK(crash_handler);
crash_handler->SetMachExceptionCallbackForTesting(callback);
}
uint64_t CrashpadClient::GetThreadIdForTesting() {
CrashHandler* crash_handler = CrashHandler::Get();
DCHECK(crash_handler);
return crash_handler->GetThreadIdForTesting();
}
} // namespace crashpad