1// Copyright 2016 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "chrome/browser/mac/exception_processor.h" 6 7#include <dlfcn.h> 8#import <Foundation/Foundation.h> 9#include <libunwind.h> 10#include <objc/objc-exception.h> 11 12#include <type_traits> 13 14#include "base/compiler_specific.h" 15#include "base/debug/crash_logging.h" 16#include "base/debug/stack_trace.h" 17#include "base/logging.h" 18#include "base/metrics/histogram_macros.h" 19#include "base/stl_util.h" 20#include "base/strings/sys_string_conversions.h" 21#include "components/crash/core/common/crash_key.h" 22 23namespace chrome { 24 25// Maximum number of known named exceptions we'll support. There is 26// no central registration, but I only find about 75 possibilities in 27// the system frameworks, and many of them are probably not 28// interesting to track in aggregate (those relating to distributed 29// objects, for instance). 30constexpr size_t kKnownNSExceptionCount = 25; 31 32const size_t kUnknownNSException = kKnownNSExceptionCount; 33 34size_t BinForException(NSException* exception) { 35 // A list of common known exceptions. The list position will 36 // determine where they live in the histogram, so never move them 37 // around, only add to the end. 38 NSString* const kKnownNSExceptionNames[] = { 39 // Grab-bag exception, not very common. CFArray (or other 40 // container) mutated while being enumerated is one case seen in 41 // production. 42 NSGenericException, 43 44 // Out-of-range on NSString or NSArray. Quite common. 45 NSRangeException, 46 47 // Invalid arg to method, unrecognized selector. Quite common. 48 NSInvalidArgumentException, 49 50 // malloc() returned null in object creation, I think. Turns out 51 // to be very uncommon in production, because of the OOM killer. 52 NSMallocException, 53 54 // This contains things like windowserver errors, trying to draw 55 // views which aren't in windows, unable to read nib files. By 56 // far the most common exception seen on the crash server. 57 NSInternalInconsistencyException, 58 }; 59 60 // Make sure our array hasn't outgrown our abilities to track it. 61 static_assert(base::size(kKnownNSExceptionNames) < kKnownNSExceptionCount, 62 "Cannot track more exceptions"); 63 64 NSString* name = [exception name]; 65 for (size_t i = 0; i < base::size(kKnownNSExceptionNames); ++i) { 66 if (name == kKnownNSExceptionNames[i]) { 67 return i; 68 } 69 } 70 return kUnknownNSException; 71} 72 73void RecordExceptionWithUma(NSException* exception) { 74 UMA_HISTOGRAM_ENUMERATION("OSX.NSException", 75 BinForException(exception), kUnknownNSException); 76} 77 78static objc_exception_preprocessor g_next_preprocessor = nullptr; 79 80static const char* const kExceptionSinkholes[] = { 81 "CFRunLoopRunSpecific", 82 "_CFXNotificationPost", 83 "__CFRunLoopRun", 84 "__NSFireDelayedPerform", 85 "_dispatch_client_callout", 86}; 87 88// This function is used to make it clear to the crash processor that this is 89// a forced exception crash. 90static NOINLINE void TERMINATING_FROM_UNCAUGHT_NSEXCEPTION(id exception) { 91 NSString* exception_message_ns = [NSString 92 stringWithFormat:@"%@: %@", [exception name], [exception reason]]; 93 std::string exception_message = base::SysNSStringToUTF8(exception_message_ns); 94 95 static crash_reporter::CrashKeyString<256> crash_key("nsexception"); 96 crash_key.Set(exception_message); 97 98 LOG(FATAL) << "Terminating from Objective-C exception: " << exception_message; 99} 100 101static id ObjcExceptionPreprocessor(id exception) { 102 static bool seen_first_exception = false; 103 104 // Record UMA and crash keys about the exception. 105 RecordExceptionWithUma(exception); 106 107 static crash_reporter::CrashKeyString<256> firstexception("firstexception"); 108 static crash_reporter::CrashKeyString<256> lastexception("lastexception"); 109 110 static crash_reporter::CrashKeyString<1024> firstexception_bt( 111 "firstexception_bt"); 112 static crash_reporter::CrashKeyString<1024> lastexception_bt( 113 "lastexception_bt"); 114 115 auto* key = seen_first_exception ? &lastexception : &firstexception; 116 auto* bt_key = seen_first_exception ? &lastexception_bt : &firstexception_bt; 117 118 NSString* value = [NSString stringWithFormat:@"%@ reason %@", 119 [exception name], [exception reason]]; 120 key->Set(base::SysNSStringToUTF8(value)); 121 122 // This exception preprocessor runs prior to the one in libobjc, which sets 123 // the -[NSException callStackReturnAddresses]. 124 crash_reporter::SetCrashKeyStringToStackTrace(bt_key, 125 base::debug::StackTrace()); 126 127 seen_first_exception = true; 128 129 ////////////////////////////////////////////////////////////////////////////// 130 131 // Unwind the stack looking for any exception handlers. If an exception 132 // handler is encountered, test to see if it is a function known to catch- 133 // and-rethrow as a "top-level" exception handler. Various routines in 134 // Cocoa do this, and it obscures the crashing stack, since the original 135 // throw location is no longer present on the stack (just the re-throw) when 136 // Crashpad captures the crash report. 137 unw_context_t context; 138 unw_getcontext(&context); 139 140 unw_cursor_t cursor; 141 unw_init_local(&cursor, &context); 142 143 // Get the base address for the image that contains this function. 144 Dl_info dl_info; 145 const void* this_base_address = 0; 146 if (dladdr(reinterpret_cast<const void*>(&ObjcExceptionPreprocessor), 147 &dl_info) != 0) { 148 this_base_address = dl_info.dli_fbase; 149 } 150 151 while (unw_step(&cursor) > 0) { 152 unw_proc_info_t frame_info; 153 if (unw_get_proc_info(&cursor, &frame_info) != UNW_ESUCCESS) { 154 continue; 155 } 156 157 // This frame has an exception handler. 158 if (frame_info.handler != 0) { 159 char proc_name[64]; 160 unw_word_t offset; 161 if (unw_get_proc_name(&cursor, proc_name, sizeof(proc_name), 162 &offset) != UNW_ESUCCESS) { 163 // The symbol has no name, so see if it belongs to the same image as 164 // this function. 165 if (dladdr(reinterpret_cast<const void*>(frame_info.start_ip), 166 &dl_info) != 0) { 167 if (dl_info.dli_fbase == this_base_address) { 168 // This is a handler in our image, so allow it to run. 169 break; 170 } 171 } 172 173 // This handler does not belong to us, so continue the search. 174 continue; 175 } 176 177 // Check if the function is one that is known to obscure (by way of 178 // catch-and-rethrow) exception stack traces. If it is, sinkhole it 179 // by crashing here at the point of throw. 180 for (const char* sinkhole : kExceptionSinkholes) { 181 if (strcmp(sinkhole, proc_name) == 0) { 182 TERMINATING_FROM_UNCAUGHT_NSEXCEPTION(exception); 183 } 184 } 185 186 VLOG(1) << "Stopping search for exception handler at " << proc_name; 187 188 break; 189 } 190 } 191 192 // Forward to the next preprocessor. 193 if (g_next_preprocessor) 194 return g_next_preprocessor(exception); 195 196 return exception; 197} 198 199void InstallObjcExceptionPreprocessor() { 200 if (g_next_preprocessor) 201 return; 202 203 g_next_preprocessor = 204 objc_setExceptionPreprocessor(&ObjcExceptionPreprocessor); 205} 206 207void UninstallObjcExceptionPreprocessor() { 208 objc_setExceptionPreprocessor(g_next_preprocessor); 209 g_next_preprocessor = nullptr; 210} 211 212} // namespace chrome 213