blob: d8c94e5430fab948049fc10e36915566d594eb19 [file] [log] [blame]
// Copyright 2020 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/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 <exception>
#include <type_traits>
#include <typeinfo>
#include "base/bit_cast.h"
#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 {
// 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 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);
}
crashpad::ObjcExceptionDelegate* g_exception_delegate;
objc_exception_preprocessor g_next_preprocessor;
NSUncaughtExceptionHandler* g_next_uncaught_exception_handler;
static void SetNSExceptionAnnotations(id exception,
std::string& name,
std::string& reason) {
name = base::SysNSStringToUTF8([exception name]);
reason = base::SysNSStringToUTF8([exception reason]);
static crashpad::StringAnnotation<256> nameKey("exceptionName");
nameKey.Set(name);
static crashpad::StringAnnotation<512> reasonKey("exceptionReason");
reasonKey.Set(reason);
}
static void ObjcUncaughtExceptionHandler(NSException* exception) {
std::string name, reason;
SetNSExceptionAnnotations(exception, name, reason);
NSArray<NSNumber*>* addressArray = [exception callStackReturnAddresses];
if ([addressArray count] > 0) {
static crashpad::StringAnnotation<256> nameKey("UncaughtNSException");
nameKey.Set("true");
std::vector<uint64_t> addresses;
NSArray<NSNumber*>* addressArray = [exception callStackReturnAddresses];
for (NSNumber* address in addressArray)
addresses.push_back([address unsignedLongLongValue]);
g_exception_delegate->HandleUncaughtNSException(&addresses[0],
addresses.size());
} else {
LOG(WARNING) << "Uncaught Objective-C exception name: " << name
<< " reason: " << reason << " with no "
<< " -callStackReturnAddresses.";
crashpad::NativeCPUContext cpu_context;
crashpad::CaptureContext(&cpu_context);
g_exception_delegate->HandleUncaughtNSExceptionWithContext(&cpu_context);
}
}
// 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;
SetNSExceptionAnnotations(exception, name, reason);
LOG(WARNING) << "Handling Objective-C exception name: " << name
<< " reason: " << reason << " with sinkhole: " << sinkhole;
crashpad::NativeCPUContext cpu_context;
crashpad::CaptureContext(&cpu_context);
g_exception_delegate->HandleUncaughtNSExceptionWithContext(&cpu_context);
// Remove the uncaught exception handler so we don't record this twice.
NSSetUncaughtExceptionHandler(g_next_uncaught_exception_handler);
g_next_uncaught_exception_handler = nullptr;
return g_next_preprocessor ? g_next_preprocessor(exception) : 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
}
id ObjcExceptionPreprocessor(id exception) {
static bool seen_first_exception;
static crashpad::StringAnnotation<256> firstexception("firstexception");
static crashpad::StringAnnotation<256> lastexception("lastexception");
static crashpad::StringAnnotation<1024> firstexception_bt(
"firstexception_bt");
static crashpad::StringAnnotation<1024> lastexception_bt("lastexception_bt");
auto* key = seen_first_exception ? &lastexception : &firstexception;
auto* bt_key = seen_first_exception ? &lastexception_bt : &firstexception_bt;
NSString* value = [NSString
stringWithFormat:@"%@ reason %@", [exception name], [exception reason]];
key->Set(base::SysNSStringToUTF8(value));
// 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)));
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);
}
}
}
// 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
// UIGestureEnvironment. That means a very hacky way to detect this are to
// check if the calling method IMP is within the range of all
// UIGestureEnvironment 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) {
auto uigestureimp_lambda = [](IMP* max) {
IMP min = *max = bit_cast<IMP>(nullptr);
unsigned int method_count = 0;
std::unique_ptr<Method[], base::FreeDeleter> method_list(
class_copyMethodList(NSClassFromString(@"UIGestureEnvironment"),
&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 gesture_environment_max_imp;
static IMP gesture_environment_min_imp =
uigestureimp_lambda(&gesture_environment_max_imp);
if (gesture_environment_min_imp && gesture_environment_max_imp &&
caller_frame_info.start_ip >=
reinterpret_cast<unw_word_t>(gesture_environment_min_imp) &&
caller_frame_info.start_ip <=
reinterpret_cast<unw_word_t>(gesture_environment_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 g_next_preprocessor ? g_next_preprocessor(exception) : exception;
}
} // namespace
namespace crashpad {
void InstallObjcExceptionPreprocessor(ObjcExceptionDelegate* delegate) {
DCHECK(!g_next_preprocessor);
// Preprocessor.
g_next_preprocessor =
objc_setExceptionPreprocessor(&ObjcExceptionPreprocessor);
// Uncaught processor.
g_exception_delegate = delegate;
g_next_uncaught_exception_handler = NSGetUncaughtExceptionHandler();
NSSetUncaughtExceptionHandler(&ObjcUncaughtExceptionHandler);
}
void UninstallObjcExceptionPreprocessor() {
DCHECK(g_next_preprocessor);
objc_setExceptionPreprocessor(g_next_preprocessor);
g_exception_delegate = nullptr;
NSSetUncaughtExceptionHandler(g_next_uncaught_exception_handler);
g_next_uncaught_exception_handler = nullptr;
g_next_preprocessor = nullptr;
}
} // namespace crashpad