crashpad/client/ios_handler/exception_processor.mm
Avi Drissman 9f896f2581 Qualify bit_cast with base::
The real Chromium base/bit_cast.h is in the base namespace.
mini_chromium's version was just changed to be in the base namespace
as well. Roll to the latest mini_chromium and scope all calls to
bit_cast.

Bug: chromium:1506769
Change-Id: I7b25ee512f67694ef6ed3d0250e4f6a6db151eb3
Reviewed-on: https://chromium-review.googlesource.com/c/crashpad/crashpad/+/5116880
Reviewed-by: Mark Mentovai <mark@chromium.org>
Commit-Queue: Avi Drissman <avi@chromium.org>
2023-12-12 22:52:47 +00:00

639 lines
24 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. 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.
// std::unexpected_handler is deprecated starting in C++11, and removed in
// C++17. But macOS versions we run on still ship it. This define makes
// std::unexpected_handler reappear. If that define ever stops working,
// we hopefully no longer run on macOS versions that still have it.
// (...or we'll have to define it in this file instead of getting it from
// <exception>). This define must before all includes.
#define _LIBCPP_ENABLE_CXX17_REMOVED_UNEXPECTED_FUNCTIONS
#include "client/ios_handler/exception_processor.h"
#include <Availability.h>
#import <Foundation/Foundation.h>
#include <TargetConditionals.h>
#include <cxxabi.h>
#include <dlfcn.h>
#include <libunwind.h>
#include <mach-o/loader.h>
#include <objc/message.h>
#include <objc/objc-exception.h>
#include <objc/objc.h>
#include <objc/runtime.h>
#include <stdint.h>
#include <string.h>
#include <sys/types.h>
#include <unwind.h>
#include <atomic>
#include <exception>
#include <type_traits>
#include <typeinfo>
#include "base/format_macros.h"
#include "base/logging.h"
#include "base/memory/free_deleter.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "build/build_config.h"
#include "client/annotation.h"
#include "client/simulate_crash_ios.h"
namespace crashpad {
namespace {
// From 10.15.0 objc4-779.1/runtime/objc-exception.mm.
struct objc_typeinfo {
const void* const* vtable;
const char* name;
Class cls_unremapped;
};
struct objc_exception {
id __unsafe_unretained obj;
objc_typeinfo tinfo;
};
// From 10.15.0 objc4-779.1/runtime/objc-abi.h.
extern "C" const void* const objc_ehtype_vtable[];
// https://github.com/llvm/llvm-project/blob/09dc884eb2e4/libcxxabi/src/cxa_exception.h
static const uint64_t kOurExceptionClass = 0x434c4e47432b2b00;
struct __cxa_exception {
#if defined(ARCH_CPU_64_BITS)
void* reserve;
size_t referenceCount;
#endif
std::type_info* exceptionType;
void (*exceptionDestructor)(void*);
std::unexpected_handler unexpectedHandler;
std::terminate_handler terminateHandler;
__cxa_exception* nextException;
int handlerCount;
int handlerSwitchValue;
const unsigned char* actionRecord;
const unsigned char* languageSpecificData;
void* catchTemp;
void* adjustedPtr;
#if !defined(ARCH_CPU_64_BITS)
size_t referenceCount;
#endif
_Unwind_Exception unwindHeader;
};
int LoggingUnwStep(unw_cursor_t* cursor) {
int rv = unw_step(cursor);
if (rv < 0) {
LOG(ERROR) << "unw_step: " << rv;
}
return rv;
}
std::string FormatStackTrace(const std::vector<uint64_t>& addresses,
size_t max_length) {
std::string stack_string;
for (uint64_t address : addresses) {
std::string address_string = base::StringPrintf("0x%" PRIx64, address);
if (stack_string.size() + address_string.size() > max_length)
break;
stack_string += address_string + " ";
}
if (!stack_string.empty() && stack_string.back() == ' ') {
stack_string.resize(stack_string.size() - 1);
}
return stack_string;
}
std::string GetTraceString() {
std::vector<uint64_t> addresses;
unw_context_t context;
unw_getcontext(&context);
unw_cursor_t cursor;
unw_init_local(&cursor, &context);
while (LoggingUnwStep(&cursor) > 0) {
unw_word_t ip = 0;
unw_get_reg(&cursor, UNW_REG_IP, &ip);
addresses.push_back(ip);
}
return FormatStackTrace(addresses, 1024);
}
static void SetNSExceptionAnnotations(NSException* exception,
std::string& name,
std::string& reason) {
@try {
name = base::SysNSStringToUTF8(exception.name);
static StringAnnotation<256> nameKey("exceptionName");
nameKey.Set(name);
} @catch (id name_exception) {
LOG(ERROR) << "Unable to read uncaught Objective-C exception name.";
}
@try {
reason = base::SysNSStringToUTF8(exception.reason);
static StringAnnotation<1024> reasonKey("exceptionReason");
reasonKey.Set(reason);
} @catch (id reason_exception) {
LOG(ERROR) << "Unable to read uncaught Objective-C exception reason.";
}
@try {
if (exception.userInfo) {
static StringAnnotation<1024> userInfoKey("exceptionUserInfo");
userInfoKey.Set(base::SysNSStringToUTF8(
[NSString stringWithFormat:@"%@", exception.userInfo]));
}
} @catch (id user_info_exception) {
LOG(ERROR) << "Unable to read uncaught Objective-C exception user info.";
}
}
//! \brief Helper class to own the complex types used by the Objective-C
//! exception preprocessor.
class ExceptionPreprocessorState {
public:
ExceptionPreprocessorState(const ExceptionPreprocessorState&) = delete;
ExceptionPreprocessorState& operator=(const ExceptionPreprocessorState&) =
delete;
static ExceptionPreprocessorState* Get() {
static ExceptionPreprocessorState* instance = []() {
return new ExceptionPreprocessorState();
}();
return instance;
}
// Writes an intermediate dumps to a temporary location to be used by the
// final UncaughtExceptionHandler and notifies the preprocessor chain.
id HandleUncaughtException(NativeCPUContext* cpu_context, id exception) {
// If this isn't the first time the preprocessor has detected an uncaught
// NSException, note this in the second intermediate dump.
objc_exception_preprocessor next_preprocessor = next_preprocessor_;
static bool handled_first_exception;
if (handled_first_exception) {
static StringAnnotation<5> name_key("MultipleHandledUncaughtNSException");
name_key.Set("true");
// Unregister so we stop getting in the way of the exception processor if
// we aren't correctly identifying sinkholes. The final uncaught exception
// handler is still active.
objc_setExceptionPreprocessor(next_preprocessor_);
next_preprocessor_ = nullptr;
}
handled_first_exception = true;
// Use tmp/ for this intermediate dump path. Normally these dumps are
// written to the "pending-serialized-ios-dump" folder and are eligable for
// the next pass to convert pending intermediate dumps to minidump files.
// Since this intermediate dump isn't eligable until the uncaught handler,
// use tmp/.
base::FilePath path(base::SysNSStringToUTF8([NSTemporaryDirectory()
stringByAppendingPathComponent:[[NSUUID UUID] UUIDString]]));
exception_delegate_->HandleUncaughtNSExceptionWithContextAtPath(cpu_context,
path);
last_handled_intermediate_dump_ = path;
return next_preprocessor ? next_preprocessor(exception) : exception;
}
// If the PreprocessException already captured this exception via
// HANDLE_UNCAUGHT_NSEXCEPTION. Move last_handled_intermediate_dump_ to
// the pending intermediate dump directory and return true. Otherwise the
// preprocessor didn't catch anything, so pass the frames or just the context
// to the exception_delegate.
void FinalizeUncaughtNSException(id exception) {
if (last_exception() == (__bridge void*)exception &&
!last_handled_intermediate_dump_.empty() &&
exception_delegate_->MoveIntermediateDumpAtPathToPending(
last_handled_intermediate_dump_)) {
last_handled_intermediate_dump_ = base::FilePath();
return;
}
std::string name, reason;
NSArray<NSNumber*>* address_array = nil;
if ([exception isKindOfClass:[NSException class]]) {
SetNSExceptionAnnotations(exception, name, reason);
address_array = [exception callStackReturnAddresses];
}
if ([address_array count] > 0) {
static StringAnnotation<256> name_key("UncaughtNSException");
name_key.Set("true");
std::vector<uint64_t> addresses;
for (NSNumber* address in address_array)
addresses.push_back([address unsignedLongLongValue]);
exception_delegate_->HandleUncaughtNSException(&addresses[0],
addresses.size());
} else {
LOG(WARNING) << "Uncaught Objective-C exception name: " << name
<< " reason: " << reason << " with no "
<< " -callStackReturnAddresses.";
NativeCPUContext cpu_context;
CaptureContext(&cpu_context);
exception_delegate_->HandleUncaughtNSExceptionWithContext(&cpu_context);
}
}
id MaybeCallNextPreprocessor(id exception) {
return next_preprocessor_ ? next_preprocessor_(exception) : exception;
}
// Register the objc_setExceptionPreprocessor and NSUncaughtExceptionHandler.
void Install(ObjcExceptionDelegate* delegate);
// Restore the objc_setExceptionPreprocessor and NSUncaughtExceptionHandler.
void Uninstall();
void* last_exception() { return last_exception_; }
void set_last_exception(void* exception) { last_exception_ = exception; }
private:
ExceptionPreprocessorState() = default;
~ExceptionPreprocessorState() = default;
// Location of the intermediate dump generated after an exception triggered
// HANDLE_UNCAUGHT_NSEXCEPTION.
base::FilePath last_handled_intermediate_dump_;
// Recorded last NSException pointer in case the exception is caught and
// thrown again (without using objc_exception_rethrow) as an
// unsafe_unretained reference. Stored as a void* as the only safe
// operation is pointer comparison.
std::atomic<void*> last_exception_ = nil;
ObjcExceptionDelegate* exception_delegate_ = nullptr;
objc_exception_preprocessor next_preprocessor_ = nullptr;
NSUncaughtExceptionHandler* next_uncaught_exception_handler_ = nullptr;
};
static void ObjcUncaughtExceptionHandler(NSException* exception) {
ExceptionPreprocessorState::Get()->FinalizeUncaughtNSException(exception);
}
// This function is used to make it clear to the crash processor that an
// uncaught NSException was recorded here.
static __attribute__((noinline)) id HANDLE_UNCAUGHT_NSEXCEPTION(
id exception,
const char* sinkhole) {
std::string name, reason;
if ([exception isKindOfClass:[NSException class]]) {
SetNSExceptionAnnotations(exception, name, reason);
}
LOG(WARNING) << "Handling Objective-C exception name: " << name
<< " reason: " << reason << " with sinkhole: " << sinkhole;
NativeCPUContext cpu_context{};
CaptureContext(&cpu_context);
ExceptionPreprocessorState* preprocessor_state =
ExceptionPreprocessorState::Get();
return preprocessor_state->HandleUncaughtException(&cpu_context, exception);
}
// Returns true if |path| equals |sinkhole| on device. Simulator paths prepend
// much of Xcode's internal structure, so check that |path| ends with |sinkhole|
// for simulator.
bool ModulePathMatchesSinkhole(const char* path, const char* sinkhole) {
#if TARGET_OS_SIMULATOR
size_t path_length = strlen(path);
size_t sinkhole_length = strlen(sinkhole);
if (sinkhole_length > path_length)
return false;
return strncmp(path + path_length - sinkhole_length,
sinkhole,
sinkhole_length) == 0;
#else
return strcmp(path, sinkhole) == 0;
#endif
}
//! \brief Helper to release memory from calls to __cxa_allocate_exception.
class ScopedException {
public:
explicit ScopedException(objc_exception* exception) : exception_(exception) {}
ScopedException(const ScopedException&) = delete;
ScopedException& operator=(const ScopedException&) = delete;
~ScopedException() { __cxxabiv1::__cxa_free_exception(exception_); }
private:
objc_exception* exception_; // weak
};
id ObjcExceptionPreprocessor(id exception) {
// Some sinkholes don't use objc_exception_rethrow when they should, which
// would otherwise prevent the exception_preprocessor from getting called
// again. Because of this, track the most recently seen exception and
// ignore it.
ExceptionPreprocessorState* preprocessor_state =
ExceptionPreprocessorState::Get();
if (preprocessor_state->last_exception() == (__bridge void*)exception) {
return preprocessor_state->MaybeCallNextPreprocessor(exception);
}
preprocessor_state->set_last_exception((__bridge void*)exception);
static bool seen_first_exception;
static StringAnnotation<256> firstexception("firstexception");
static StringAnnotation<256> lastexception("lastexception");
static StringAnnotation<1024> firstexception_bt("firstexception_bt");
static StringAnnotation<1024> lastexception_bt("lastexception_bt");
auto* key = seen_first_exception ? &lastexception : &firstexception;
auto* bt_key = seen_first_exception ? &lastexception_bt : &firstexception_bt;
if ([exception isKindOfClass:[NSException class]]) {
NSString* value = [NSString
stringWithFormat:@"%@ reason %@", [exception name], [exception reason]];
key->Set(base::SysNSStringToUTF8(value));
} else {
key->Set(base::SysNSStringToUTF8([exception description]));
}
// This exception preprocessor runs prior to the one in libobjc, which sets
// the -[NSException callStackReturnAddresses].
bt_key->Set(GetTraceString());
seen_first_exception = true;
// Unwind the stack looking for any exception handlers. If an exception
// handler is encountered, test to see if it is a function known to catch-
// and-rethrow as a "top-level" exception handler. Various routines in
// Cocoa/UIKit do this, and it obscures the crashing stack, since the original
// throw location is no longer present on the stack (just the re-throw) when
// Crashpad captures the crash report.
unw_context_t context;
unw_getcontext(&context);
unw_cursor_t cursor;
unw_init_local(&cursor, &context);
static const void* this_base_address = []() -> const void* {
Dl_info dl_info;
if (!dladdr(reinterpret_cast<const void*>(&ObjcExceptionPreprocessor),
&dl_info)) {
LOG(ERROR) << "dladdr: " << dlerror();
return nullptr;
}
return dl_info.dli_fbase;
}();
// Generate an exception_header for the __personality_routine.
// From 10.15.0 objc4-779.1/runtime/objc-exception.mm objc_exception_throw.
objc_exception* exception_objc = reinterpret_cast<objc_exception*>(
__cxxabiv1::__cxa_allocate_exception(sizeof(objc_exception)));
ScopedException exception_objc_owner(exception_objc);
exception_objc->obj = exception;
exception_objc->tinfo.vtable = objc_ehtype_vtable + 2;
exception_objc->tinfo.name = object_getClassName(exception);
exception_objc->tinfo.cls_unremapped = object_getClass(exception);
// https://github.com/llvm/llvm-project/blob/c5d2746fbea7/libcxxabi/src/cxa_exception.cpp
// __cxa_throw
__cxa_exception* exception_header =
reinterpret_cast<__cxa_exception*>(exception_objc) - 1;
exception_header->unexpectedHandler = std::get_unexpected();
exception_header->terminateHandler = std::get_terminate();
exception_header->exceptionType =
reinterpret_cast<std::type_info*>(&exception_objc->tinfo);
exception_header->unwindHeader.exception_class = kOurExceptionClass;
bool handler_found = false;
while (LoggingUnwStep(&cursor) > 0) {
unw_proc_info_t frame_info;
if (unw_get_proc_info(&cursor, &frame_info) != UNW_ESUCCESS) {
continue;
}
if (frame_info.handler == 0) {
continue;
}
// Check to see if the handler is really an exception handler.
#if defined(__IPHONE_14_5) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_5
using personality_routine = _Unwind_Personality_Fn;
#else
using personality_routine = __personality_routine;
#endif
personality_routine p =
reinterpret_cast<personality_routine>(frame_info.handler);
// From 10.15.0 libunwind-35.4/src/UnwindLevel1.c.
_Unwind_Reason_Code personalityResult = (*p)(
1,
_UA_SEARCH_PHASE,
exception_header->unwindHeader.exception_class,
reinterpret_cast<_Unwind_Exception*>(&exception_header->unwindHeader),
reinterpret_cast<_Unwind_Context*>(&cursor));
switch (personalityResult) {
case _URC_HANDLER_FOUND:
break;
case _URC_CONTINUE_UNWIND:
continue;
default:
break;
}
char proc_name[512];
unw_word_t offset;
if (unw_get_proc_name(&cursor, proc_name, sizeof(proc_name), &offset) !=
UNW_ESUCCESS) {
// The symbol has no name, so see if it belongs to the same image as
// this function.
Dl_info dl_info;
if (dladdr(reinterpret_cast<const void*>(frame_info.start_ip),
&dl_info)) {
if (dl_info.dli_fbase == this_base_address) {
// This is a handler in our image, so allow it to run.
handler_found = true;
break;
}
}
// This handler does not belong to us, so continue the search.
continue;
}
// Check if the function is one that is known to obscure (by way of
// catch-and-rethrow) exception stack traces. If it is, sinkhole it
// by crashing here at the point of throw.
static constexpr const char* kExceptionSymbolNameSinkholes[] = {
// The two CF symbol names will also be captured by the CoreFoundation
// library path check below, but for completeness they are listed here,
// since they appear unredacted.
"CFRunLoopRunSpecific",
"_CFXNotificationPost",
"__NSFireDelayedPerform",
// If this exception is going to end up at EHFrame, record the uncaught
// exception instead.
"_ZN4base3mac15CallWithEHFrameEU13block_pointerFvvE",
};
for (const char* sinkhole : kExceptionSymbolNameSinkholes) {
if (strcmp(sinkhole, proc_name) == 0) {
return HANDLE_UNCAUGHT_NSEXCEPTION(exception, sinkhole);
}
}
// On iOS, function names are often reported as "<redacted>", although they
// do appear when attached to the debugger. When this happens, use the path
// of the image to determine if the handler is an exception sinkhole.
static constexpr const char* kExceptionLibraryPathSinkholes[] = {
// Everything in this library is a sinkhole, specifically
// _dispatch_client_callout. Both are needed here depending on whether
// the debugger is attached (introspection only appears when a simulator
// is attached to a debugger).
"/usr/lib/system/introspection/libdispatch.dylib",
"/usr/lib/system/libdispatch.dylib",
// __CFRunLoopDoTimers and __CFRunLoopRun are sinkholes. Consider also
// checking that a few frames up is CFRunLoopRunSpecific().
"/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation",
};
Dl_info dl_info;
if (dladdr(reinterpret_cast<const void*>(frame_info.start_ip), &dl_info) !=
0) {
for (const char* sinkhole : kExceptionLibraryPathSinkholes) {
if (ModulePathMatchesSinkhole(dl_info.dli_fname, sinkhole)) {
return HANDLE_UNCAUGHT_NSEXCEPTION(exception, sinkhole);
}
}
// Another set of iOS redacted sinkholes appear in CoreAutoLayout.
// However, this is often called by client code, so it's unsafe to simply
// handle an uncaught nsexception here. Instead, skip the frame and
// continue searching for either a handler that belongs to us, or another
// sinkhole. See:
// -[NSISEngine
// performModifications:withUnsatisfiableConstraintsHandler:]:
// -[NSISEngine withBehaviors:performModifications:]
// +[NSLayoutConstraintParser
// constraintsWithVisualFormat:options:metrics:views:]:
static constexpr const char* kCoreAutoLayoutSinkhole =
"/System/Library/PrivateFrameworks/CoreAutoLayout.framework/"
"CoreAutoLayout";
if (ModulePathMatchesSinkhole(dl_info.dli_fname,
kCoreAutoLayoutSinkhole)) {
continue;
}
}
// Some <redacted> sinkholes are harder to find. _UIGestureEnvironmentUpdate
// in UIKitCore is an example. UIKitCore can't be added to
// kExceptionLibraryPathSinkholes because it uses Objective-C exceptions
// internally and also has has non-sinkhole handlers. While all the
// calling methods in UIKit are marked <redacted> starting in iOS14, it's
// currently true that all callers to _UIGestureEnvironmentUpdate are within
// UIWindow sendEvent -> UIGestureEnvironment. That means a very hacky way
// to detect this is to check if the calling (2x) method IMP is within the
// range of all UIWindow methods.
static constexpr const char kUIKitCorePath[] =
"/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore";
if (ModulePathMatchesSinkhole(dl_info.dli_fname, kUIKitCorePath)) {
unw_proc_info_t caller_frame_info;
if (LoggingUnwStep(&cursor) > 0 &&
unw_get_proc_info(&cursor, &caller_frame_info) == UNW_ESUCCESS &&
LoggingUnwStep(&cursor) > 0 &&
unw_get_proc_info(&cursor, &caller_frame_info) == UNW_ESUCCESS) {
auto uiwindowimp_lambda = [](IMP* max) {
IMP min = *max = nullptr;
unsigned int method_count = 0;
std::unique_ptr<Method[], base::FreeDeleter> method_list(
class_copyMethodList(NSClassFromString(@"UIWindow"),
&method_count));
if (method_count > 0) {
min = *max = method_getImplementation(method_list[0]);
for (unsigned int method_index = 1; method_index < method_count;
method_index++) {
IMP method_imp =
method_getImplementation(method_list[method_index]);
*max = std::max(method_imp, *max);
min = std::min(method_imp, min);
}
}
return min;
};
static IMP uiwindow_max_imp;
static IMP uiwindow_min_imp = uiwindowimp_lambda(&uiwindow_max_imp);
if (uiwindow_min_imp && uiwindow_max_imp &&
caller_frame_info.start_ip >=
reinterpret_cast<unw_word_t>(uiwindow_min_imp) &&
caller_frame_info.start_ip <=
reinterpret_cast<unw_word_t>(uiwindow_max_imp)) {
return HANDLE_UNCAUGHT_NSEXCEPTION(exception,
"_UIGestureEnvironmentUpdate");
}
}
}
handler_found = true;
break;
}
// If no handler is found, __cxa_throw would call failed_throw and terminate.
// See:
// https://github.com/llvm/llvm-project/blob/c5d2746fbea7/libcxxabi/src/cxa_exception.cpp
// __cxa_throw. Instead, call HANDLE_UNCAUGHT_NSEXCEPTION so the exception
// name and reason are properly recorded.
if (!handler_found) {
return HANDLE_UNCAUGHT_NSEXCEPTION(exception, "__cxa_throw");
}
// Forward to the next preprocessor.
return preprocessor_state->MaybeCallNextPreprocessor(exception);
}
void ExceptionPreprocessorState::Install(ObjcExceptionDelegate* delegate) {
DCHECK(!next_preprocessor_);
exception_delegate_ = delegate;
// Preprocessor.
next_preprocessor_ =
objc_setExceptionPreprocessor(&ObjcExceptionPreprocessor);
// Uncaught processor.
next_uncaught_exception_handler_ = NSGetUncaughtExceptionHandler();
NSSetUncaughtExceptionHandler(&ObjcUncaughtExceptionHandler);
}
void ExceptionPreprocessorState::Uninstall() {
DCHECK(next_preprocessor_);
objc_setExceptionPreprocessor(next_preprocessor_);
next_preprocessor_ = nullptr;
NSSetUncaughtExceptionHandler(next_uncaught_exception_handler_);
next_uncaught_exception_handler_ = nullptr;
exception_delegate_ = nullptr;
}
} // namespace
void InstallObjcExceptionPreprocessor(ObjcExceptionDelegate* delegate) {
ExceptionPreprocessorState::Get()->Install(delegate);
}
void UninstallObjcExceptionPreprocessor() {
ExceptionPreprocessorState::Get()->Uninstall();
}
} // namespace crashpad