// 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 // ). This define must before all includes. #define _LIBCPP_ENABLE_CXX17_REMOVED_UNEXPECTED_FUNCTIONS #include "client/ios_handler/exception_processor.h" #include #import #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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& 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 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* 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 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 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(&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( __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(&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(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(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 "", 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(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 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 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_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(uiwindow_min_imp) && caller_frame_info.start_ip <= reinterpret_cast(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