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