[ios] Bring up first half of UncaughtExceptionHandler.

When code raises an Objective-C exception, unwind the stack looking for
any exception handlers. If an exception handler is encountered, test to
see if it is a function known to be a catch-and-rethrow 'sinkhole'
exception handler. Various routines in UIKit and elsewhere do this, and
they obscure the exception stack, since the original throw location is
no longer present on the stack (just the re-throw) when Crashpad
captures the crash report. In the case of sinkholes, trigger an
immediate exception to capture the original stack.

The is an improvement over the alternative,
NSSetUncaughtExceptionHandler, which passes along the stack frames, but
not the stack memory contents and full exception context itself.

The details of what happens after a fatal exception is triggered are
unresolved in this CL.  For now, simply call std::terminate.

This code was inspired by chromium/src/chrome/browser/mac/
exception_processor.mm.

Bug: crashpad:31
Change-Id: Ieebc6476a0507c466c8219c10f790ec0a624e58c
Reviewed-on: https://chromium-review.googlesource.com/c/crashpad/crashpad/+/2125254
Commit-Queue: Justin Cohen <justincohen@chromium.org>
Reviewed-by: Robert Sesek <rsesek@chromium.org>
Reviewed-by: Mark Mentovai <mark@chromium.org>
This commit is contained in:
Justin Cohen 2020-04-08 15:37:55 -04:00 committed by Commit Bot
parent de5bc33b8b
commit b2fd7d5307
10 changed files with 519 additions and 34 deletions

View File

@ -138,7 +138,7 @@ source_set("client_test") {
}
if (crashpad_is_ios) {
sources += [ "crashpad_client_ios_test.cc" ]
sources += [ "crashpad_client_ios_test.mm" ]
sources -= [
"annotation_list_test.cc",
"annotation_test.cc",

View File

@ -20,6 +20,7 @@
#include "base/strings/stringprintf.h"
#include "client/client_argv_handling.h"
#include "snapshot/ios/process_snapshot_ios.h"
#include "util/ios/exception_processor.h"
#include "util/ios/ios_system_data_collector.h"
#include "util/posix/signals.h"
@ -58,7 +59,6 @@ class SignalHandler {
siginfo_t* siginfo,
void* context) {
HandleCrash(signo, siginfo, context);
// Always call system handler.
Signals::RestoreHandlerAndReraiseSignalOnReturn(
siginfo, old_actions_.ActionForSignal(signo));
@ -84,6 +84,7 @@ CrashpadClient::CrashpadClient() {}
CrashpadClient::~CrashpadClient() {}
bool CrashpadClient::StartCrashpadInProcessHandler() {
InstallObjcExceptionPreprocessor();
return SignalHandler::Get()->Install(nullptr);
}
@ -93,4 +94,5 @@ void CrashpadClient::DumpWithoutCrash() {
siginfo_t siginfo = {};
SignalHandler::Get()->HandleCrash(siginfo.si_signo, &siginfo, nullptr);
}
} // namespace crashpad

View File

@ -1,32 +0,0 @@
// 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/crashpad_client.h"
#include "gtest/gtest.h"
namespace crashpad {
namespace test {
namespace {
// TODO(justincohen): This is a placeholder.
TEST(CrashpadIOSClient, DumpWithoutCrash) {
crashpad::CrashpadClient client;
client.StartCrashpadInProcessHandler();
client.DumpWithoutCrash();
}
} // namespace
} // namespace test
} // namespace crashpad

View File

@ -0,0 +1,56 @@
// 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/crashpad_client.h"
#import <Foundation/Foundation.h>
#include <vector>
#include "gtest/gtest.h"
namespace crashpad {
namespace test {
namespace {
// TODO(justincohen): This is a placeholder.
TEST(CrashpadIOSClient, DumpWithoutCrash) {
CrashpadClient client;
client.StartCrashpadInProcessHandler();
client.DumpWithoutCrash();
}
// This test is covered by a similar XCUITest, but for development purposes
// it's sometimes easier and faster to run as a gtest. However, there's no
// way to correctly run this as a gtest. Leave the test here, disabled, for use
// during development only.
TEST(CrashpadIOSClient, DISABLED_ThrowNSException) {
CrashpadClient client;
client.StartCrashpadInProcessHandler();
[NSException raise:@"GtestNSException" format:@"ThrowException"];
}
// This test is covered by a similar XCUITest, but for development purposes
// it's sometimes easier and faster to run as a gtest. However, there's no
// way to correctly run this as a gtest. Leave the test here, disabled, for use
// during development only.
TEST(CrashpadIOSClient, DISABLED_ThrowException) {
CrashpadClient client;
client.StartCrashpadInProcessHandler();
std::vector<int> empty_vector = {};
empty_vector.at(42);
}
} // namespace
} // namespace test
} // namespace crashpad

View File

@ -145,4 +145,82 @@
XCTAssertTrue(_app.state == XCUIApplicationStateRunningForeground);
}
- (void)testException {
XCTAssertTrue(_app.state == XCUIApplicationStateRunningForeground);
// Crash the app.
CPTestSharedObject* rootObject = [EDOClientService rootObjectWithPort:12345];
[rootObject crashException];
// Confirm the app is not running.
XCTAssertTrue([_app waitForState:XCUIApplicationStateNotRunning timeout:15]);
XCTAssertTrue(_app.state == XCUIApplicationStateNotRunning);
// TODO: Query the app for crash data
[_app launch];
XCTAssertTrue(_app.state == XCUIApplicationStateRunningForeground);
}
- (void)testNSException {
XCTAssertTrue(_app.state == XCUIApplicationStateRunningForeground);
// Crash the app.
CPTestSharedObject* rootObject = [EDOClientService rootObjectWithPort:12345];
[rootObject crashNSException];
// Confirm the app is not running.
XCTAssertTrue([_app waitForState:XCUIApplicationStateNotRunning timeout:15]);
XCTAssertTrue(_app.state == XCUIApplicationStateNotRunning);
// TODO: Query the app for crash data
[_app launch];
XCTAssertTrue(_app.state == XCUIApplicationStateRunningForeground);
}
- (void)testCrashUnreocgnizedSelectorAfterDelay {
XCTAssertTrue(_app.state == XCUIApplicationStateRunningForeground);
// Crash the app.
CPTestSharedObject* rootObject = [EDOClientService rootObjectWithPort:12345];
[rootObject crashUnreocgnizedSelectorAfterDelay];
// Confirm the app is not running.
XCTAssertTrue([_app waitForState:XCUIApplicationStateNotRunning timeout:15]);
XCTAssertTrue(_app.state == XCUIApplicationStateNotRunning);
// TODO: Query the app for crash data
[_app launch];
XCTAssertTrue(_app.state == XCUIApplicationStateRunningForeground);
}
- (void)testCatchNSException {
XCTAssertTrue(_app.state == XCUIApplicationStateRunningForeground);
// The app should not crash
CPTestSharedObject* rootObject = [EDOClientService rootObjectWithPort:12345];
[rootObject catchNSException];
XCTAssertTrue(_app.state == XCUIApplicationStateRunningForeground);
}
- (void)testRecursion {
// TODO(justincohen): Crashpad iOS does not currently support stack type
// crashes.
return;
XCTAssertTrue(_app.state == XCUIApplicationStateRunningForeground);
// Crash the app.
CPTestSharedObject* rootObject = [EDOClientService rootObjectWithPort:12345];
[rootObject crashRecursion];
// Confirm the app is not running.
XCTAssertTrue([_app waitForState:XCUIApplicationStateNotRunning timeout:15]);
XCTAssertTrue(_app.state == XCUIApplicationStateNotRunning);
// TODO: Query the app for crash data
[_app launch];
XCTAssertTrue(_app.state == XCUIApplicationStateRunningForeground);
}
@end

View File

@ -14,6 +14,10 @@
#import "test/ios/host/application_delegate.h"
#include <dispatch/dispatch.h>
#include <vector>
#import "Service/Sources/EDOHostNamingService.h"
#import "Service/Sources/EDOHostService.h"
#include "client/crashpad_client.h"
@ -75,4 +79,38 @@
abort();
}
- (void)crashException {
std::vector<int> empty_vector = {};
empty_vector.at(42);
}
- (void)crashNSException {
// EDO has its own sinkhole.
dispatch_async(dispatch_get_main_queue(), ^{
NSArray* empty_array = @[];
[empty_array objectAtIndex:42];
});
}
- (void)catchNSException {
@try {
NSArray* empty_array = @[];
[empty_array objectAtIndex:42];
} @catch (NSException* exception) {
} @finally {
}
}
- (void)crashUnreocgnizedSelectorAfterDelay {
[self performSelector:@selector(does_not_exist) withObject:nil afterDelay:1];
}
- (void)recurse {
[self recurse];
}
- (void)crashRecursion {
[self recurse];
}
@end

View File

@ -35,6 +35,21 @@
// Trigger a crash with an abort().
- (void)crashAbort;
// Trigger a crash with an uncaught exception.
- (void)crashException;
// Trigger a crash with an uncaught NSException.
- (void)crashNSException;
// Trigger an unrecognized selector after delay.
- (void)crashUnreocgnizedSelectorAfterDelay;
// Trigger a caught NSxception.
- (void)catchNSException;
// Trigger a crash with an infinite recursion.
- (void)crashRecursion;
@end
#endif // CRASHPAD_TEST_IOS_HOST_SHARED_OBJECT_H_

View File

@ -246,6 +246,8 @@ static_library("util") {
if (crashpad_is_ios) {
sources += [
"ios/exception_processor.h",
"ios/exception_processor.mm",
"ios/ios_system_data_collector.h",
"ios/ios_system_data_collector.mm",
"mac/xattr.cc",

View File

@ -0,0 +1,37 @@
// 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.
#ifndef CRASHPAD_UTIL_IOS_EXCEPTION_PROCESSOR_H_
#define CRASHPAD_UTIL_IOS_EXCEPTION_PROCESSOR_H_
namespace crashpad {
//! \brief Installs the Objective-C exception preprocessor.
//!
//! When code raises an Objective-C exception, unwind the stack looking for
//! any exception handlers. If an exception handler is encountered, test to
//! see if it is a function known to be a catch-and-rethrow 'sinkhole' exception
//! handler. Various routines in UIKit do this, and they obscure 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. In the case of sinkholes, trigger an immediate exception to
//! capture the original stack.
//!
//! This should be installed at the same time the CrashpadClient installs the
//! signal handler. It should only be installed once.
void InstallObjcExceptionPreprocessor();
} // namespace crashpad
#endif // CRASHPAD_UTIL_IOS_EXCEPTION_PROCESSOR_H_

View File

@ -0,0 +1,289 @@
// 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 "util/ios/exception_processor.h"
#import <Foundation/Foundation.h>
#include <TargetConditionals.h>
#include <cxxabi.h>
#include <dlfcn.h>
#include <libunwind.h>
#include <mach-o/loader.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/logging.h"
#include "base/strings/sys_string_conversions.h"
#include "build/build_config.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;
};
objc_exception_preprocessor g_next_preprocessor;
bool g_exception_preprocessor_installed;
void TerminatingFromUncaughtNSException(id exception, const char* sinkhole) {
// TODO(justincohen): This is incomplete, as the signal handler will not have
// access to the exception name and reason. Pass that along somehow here.
NSString* exception_message_ns = [NSString
stringWithFormat:@"%@: %@", [exception name], [exception reason]];
std::string exception_message = base::SysNSStringToUTF8(exception_message_ns);
LOG(INFO) << "Terminating from Objective-C exception: " << exception_message
<< " with sinkhole: " << sinkhole;
// TODO(justincohen): This is temporary, as crashpad can capture this
// exception directly instead.
std::terminate();
}
// 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) {
// 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 (unw_step(&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.
__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.
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",
};
for (const char* sinkhole : kExceptionSymbolNameSinkholes) {
if (strcmp(sinkhole, proc_name) == 0) {
TerminatingFromUncaughtNSException(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.
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.
// only).
"/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)) {
TerminatingFromUncaughtNSException(exception, sinkhole);
}
}
}
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, terminate via TerminatingFromUncaughtNSException so
// the exception name and reason are properly recorded.
if (!handler_found) {
TerminatingFromUncaughtNSException(exception, "__cxa_throw");
}
// Forward to the next preprocessor.
if (g_next_preprocessor)
return g_next_preprocessor(exception);
return exception;
}
} // namespace
namespace crashpad {
void InstallObjcExceptionPreprocessor() {
DCHECK(!g_exception_preprocessor_installed);
g_next_preprocessor =
objc_setExceptionPreprocessor(&ObjcExceptionPreprocessor);
g_exception_preprocessor_installed = true;
}
void UninstallObjcExceptionPreprocessor() {
DCHECK(g_exception_preprocessor_installed);
objc_setExceptionPreprocessor(g_next_preprocessor);
g_next_preprocessor = nullptr;
g_exception_preprocessor_installed = false;
}
} // namespace crashpad