Home | History | Annotate | Download | only in browser
      1 // Copyright (c) 2011 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/chrome_browser_application_mac.h"
      6 
      7 #import "base/logging.h"
      8 #import "base/metrics/histogram.h"
      9 #import "base/memory/scoped_nsobject.h"
     10 #include "base/sys_info.h"
     11 #import "base/sys_string_conversions.h"
     12 #import "chrome/app/breakpad_mac.h"
     13 #import "chrome/browser/app_controller_mac.h"
     14 #import "chrome/browser/ui/cocoa/objc_method_swizzle.h"
     15 #import "chrome/browser/ui/cocoa/objc_zombie.h"
     16 
     17 // The implementation of NSExceptions break various assumptions in the
     18 // Chrome code.  This category defines a replacement for
     19 // -initWithName:reason:userInfo: for purposes of forcing a break in
     20 // the debugger when an exception is raised.  -raise sounds more
     21 // obvious to intercept, but it doesn't catch the original throw
     22 // because the objc runtime doesn't use it.
     23 @interface NSException (NSExceptionSwizzle)
     24 - (id)chromeInitWithName:(NSString*)aName
     25                   reason:(NSString*)aReason
     26                 userInfo:(NSDictionary *)someUserInfo;
     27 @end
     28 
     29 static IMP gOriginalInitIMP = NULL;
     30 
     31 @implementation NSException (NSExceptionSwizzle)
     32 - (id)chromeInitWithName:(NSString*)aName
     33                   reason:(NSString*)aReason
     34                 userInfo:(NSDictionary *)someUserInfo {
     35   // Method only called when swizzled.
     36   DCHECK(_cmd == @selector(initWithName:reason:userInfo:));
     37 
     38   // Parts of Cocoa rely on creating and throwing exceptions. These are not
     39   // worth bugging-out over. It is very important that there be zero chance that
     40   // any Chromium code is on the stack; these must be created by Apple code and
     41   // then immediately consumed by Apple code.
     42   static NSString* const kAcceptableNSExceptionNames[] = {
     43     // If an object does not support an accessibility attribute, this will
     44     // get thrown.
     45     NSAccessibilityException,
     46 
     47     nil
     48   };
     49 
     50   BOOL found = NO;
     51   for (int i = 0; kAcceptableNSExceptionNames[i]; ++i) {
     52     if (aName == kAcceptableNSExceptionNames[i]) {
     53       found = YES;
     54     }
     55   }
     56 
     57   if (!found) {
     58     // Update breakpad with the exception info.
     59     static NSString* const kNSExceptionKey = @"nsexception";
     60     NSString* value =
     61         [NSString stringWithFormat:@"%@ reason %@", aName, aReason];
     62     SetCrashKeyValue(kNSExceptionKey, value);
     63 
     64     // Force crash for selected exceptions to generate crash dumps.
     65     BOOL fatal = NO;
     66     if (aName == NSInternalInconsistencyException) {
     67       NSString* const kNSMenuItemArrayBoundsCheck =
     68           @"Invalid parameter not satisfying: (index >= 0) && "
     69           @"(index < [_itemArray count])";
     70       if ([aReason isEqualToString:kNSMenuItemArrayBoundsCheck]) {
     71         fatal = YES;
     72       }
     73 
     74       NSString* const kNoWindowCheck = @"View is not in any window";
     75       if ([aReason isEqualToString:kNoWindowCheck]) {
     76         fatal = YES;
     77       }
     78     }
     79 
     80     // Mostly "unrecognized selector sent to (instance|class)".  A
     81     // very small number of things like nil being passed to an
     82     // inappropriate receiver.
     83     if (aName == NSInvalidArgumentException) {
     84       fatal = YES;
     85     }
     86 
     87     // Dear reader: Something you just did provoked an NSException.
     88     // NSException is implemented in terms of setjmp()/longjmp(),
     89     // which does poor things when combined with C++ scoping
     90     // (destructors are skipped).  Chrome should be NSException-free,
     91     // please check your backtrace and see if you can't file a bug
     92     // with a repro case.
     93     if (fatal) {
     94       LOG(FATAL) << "Someone is trying to raise an exception!  "
     95                  << base::SysNSStringToUTF8(value);
     96     } else {
     97       // Make sure that developers see when their code throws
     98       // exceptions.
     99       DLOG(ERROR) << "Someone is trying to raise an exception!  "
    100                   << base::SysNSStringToUTF8(value);
    101       NOTREACHED();
    102     }
    103   }
    104 
    105   // Forward to the original version.
    106   return gOriginalInitIMP(self, _cmd, aName, aReason, someUserInfo);
    107 }
    108 @end
    109 
    110 namespace chrome_browser_application_mac {
    111 
    112 // Maximum number of known named exceptions we'll support.  There is
    113 // no central registration, but I only find about 75 possibilities in
    114 // the system frameworks, and many of them are probably not
    115 // interesting to track in aggregate (those relating to distributed
    116 // objects, for instance).
    117 const size_t kKnownNSExceptionCount = 25;
    118 
    119 const size_t kUnknownNSException = kKnownNSExceptionCount;
    120 
    121 size_t BinForException(NSException* exception) {
    122   // A list of common known exceptions.  The list position will
    123   // determine where they live in the histogram, so never move them
    124   // around, only add to the end.
    125   static NSString* const kKnownNSExceptionNames[] = {
    126     // Grab-bag exception, not very common.  CFArray (or other
    127     // container) mutated while being enumerated is one case seen in
    128     // production.
    129     NSGenericException,
    130 
    131     // Out-of-range on NSString or NSArray.  Quite common.
    132     NSRangeException,
    133 
    134     // Invalid arg to method, unrecognized selector.  Quite common.
    135     NSInvalidArgumentException,
    136 
    137     // malloc() returned null in object creation, I think.  Turns out
    138     // to be very uncommon in production, because of the OOM killer.
    139     NSMallocException,
    140 
    141     // This contains things like windowserver errors, trying to draw
    142     // views which aren't in windows, unable to read nib files.  By
    143     // far the most common exception seen on the crash server.
    144     NSInternalInconsistencyException,
    145 
    146     nil
    147   };
    148 
    149   // Make sure our array hasn't outgrown our abilities to track it.
    150   DCHECK_LE(arraysize(kKnownNSExceptionNames), kKnownNSExceptionCount);
    151 
    152   NSString* name = [exception name];
    153   for (int i = 0; kKnownNSExceptionNames[i]; ++i) {
    154     if (name == kKnownNSExceptionNames[i]) {
    155       return i;
    156     }
    157   }
    158   return kUnknownNSException;
    159 }
    160 
    161 void RecordExceptionWithUma(NSException* exception) {
    162   UMA_HISTOGRAM_ENUMERATION("OSX.NSException",
    163       BinForException(exception), kUnknownNSException);
    164 }
    165 
    166 void RegisterBrowserCrApp() {
    167   [BrowserCrApplication sharedApplication];
    168 };
    169 
    170 void Terminate() {
    171   [NSApp terminate:nil];
    172 }
    173 
    174 void CancelTerminate() {
    175   [NSApp cancelTerminate:nil];
    176 }
    177 
    178 }  // namespace chrome_browser_application_mac
    179 
    180 namespace {
    181 
    182 // Do-nothing wrapper so that we can arrange to only swizzle
    183 // -[NSException raise] when DCHECK() is turned on (as opposed to
    184 // replicating the preprocess logic which turns DCHECK() on).
    185 BOOL SwizzleNSExceptionInit() {
    186   gOriginalInitIMP = ObjcEvilDoers::SwizzleImplementedInstanceMethods(
    187       [NSException class],
    188       @selector(initWithName:reason:userInfo:),
    189       @selector(chromeInitWithName:reason:userInfo:));
    190   return YES;
    191 }
    192 
    193 }  // namespace
    194 
    195 @implementation BrowserCrApplication
    196 
    197 + (void)initialize {
    198   // Whitelist releases that are compatible with objc zombies.
    199   int32 major_version = 0, minor_version = 0, bugfix_version = 0;
    200   base::SysInfo::OperatingSystemVersionNumbers(
    201       &major_version, &minor_version, &bugfix_version);
    202   if (major_version == 10 && (minor_version == 5 || minor_version == 6)) {
    203     // Turn all deallocated Objective-C objects into zombies, keeping
    204     // the most recent 10,000 of them on the treadmill.
    205     ObjcEvilDoers::ZombieEnable(YES, 10000);
    206   }
    207 }
    208 
    209 - init {
    210   CHECK(SwizzleNSExceptionInit());
    211   return [super init];
    212 }
    213 
    214 ////////////////////////////////////////////////////////////////////////////////
    215 // HISTORICAL COMMENT (by viettrungluu, from
    216 // http://codereview.chromium.org/1520006 with mild editing):
    217 //
    218 // A quick summary of the state of things (before the changes to shutdown):
    219 //
    220 // Currently, we are totally hosed (put in a bad state in which Cmd-W does the
    221 // wrong thing, and which will probably eventually lead to a crash) if we begin
    222 // quitting but termination is aborted for some reason.
    223 //
    224 // I currently know of two ways in which termination can be aborted:
    225 // (1) Common case: a window has an onbeforeunload handler which pops up a
    226 //     "leave web page" dialog, and the user answers "no, don't leave".
    227 // (2) Uncommon case: popups are enabled (in Content Settings, i.e., the popup
    228 //     blocker is disabled), and some nasty web page pops up a new window on
    229 //     closure.
    230 //
    231 // I don't know of other ways in which termination can be aborted, but they may
    232 // exist (or may be added in the future, for that matter).
    233 //
    234 // My CL [see above] does the following:
    235 // a. Should prevent being put in a bad state (which breaks Cmd-W and leads to
    236 //    crash) under all circumstances.
    237 // b. Should completely handle (1) properly.
    238 // c. Doesn't (yet) handle (2) properly and puts it in a weird state (but not
    239 //    that bad).
    240 // d. Any other ways of aborting termination would put it in that weird state.
    241 //
    242 // c. can be fixed by having the global flag reset on browser creation or
    243 // similar (and doing so might also fix some possible d.'s as well). I haven't
    244 // done this yet since I haven't thought about it carefully and since it's a
    245 // corner case.
    246 //
    247 // The weird state: a state in which closing the last window quits the browser.
    248 // This might be a bit annoying, but it's not dangerous in any way.
    249 ////////////////////////////////////////////////////////////////////////////////
    250 
    251 // |-terminate:| is the entry point for orderly "quit" operations in Cocoa. This
    252 // includes the application menu's quit menu item and keyboard equivalent, the
    253 // application's dock icon menu's quit menu item, "quit" (not "force quit") in
    254 // the Activity Monitor, and quits triggered by user logout and system restart
    255 // and shutdown.
    256 //
    257 // The default |-terminate:| implementation ends the process by calling exit(),
    258 // and thus never leaves the main run loop. This is unsuitable for Chrome since
    259 // Chrome depends on leaving the main run loop to perform an orderly shutdown.
    260 // We support the normal |-terminate:| interface by overriding the default
    261 // implementation. Our implementation, which is very specific to the needs of
    262 // Chrome, works by asking the application delegate to terminate using its
    263 // |-tryToTerminateApplication:| method.
    264 //
    265 // |-tryToTerminateApplication:| differs from the standard
    266 // |-applicationShouldTerminate:| in that no special event loop is run in the
    267 // case that immediate termination is not possible (e.g., if dialog boxes
    268 // allowing the user to cancel have to be shown). Instead, this method sets a
    269 // flag and tries to close all browsers. This flag causes the closure of the
    270 // final browser window to begin actual tear-down of the application.
    271 // Termination is cancelled by resetting this flag. The standard
    272 // |-applicationShouldTerminate:| is not supported, and code paths leading to it
    273 // must be redirected.
    274 - (void)terminate:(id)sender {
    275   AppController* appController = static_cast<AppController*>([NSApp delegate]);
    276   if ([appController tryToTerminateApplication:self]) {
    277     [[NSNotificationCenter defaultCenter]
    278         postNotificationName:NSApplicationWillTerminateNotification
    279                       object:self];
    280   }
    281 
    282   // Return, don't exit. The application is responsible for exiting on its own.
    283 }
    284 
    285 - (void)cancelTerminate:(id)sender {
    286   AppController* appController = static_cast<AppController*>([NSApp delegate]);
    287   [appController stopTryingToTerminateApplication:self];
    288 }
    289 
    290 - (BOOL)sendAction:(SEL)anAction to:(id)aTarget from:(id)sender {
    291   // The Dock menu contains an automagic section where you can select
    292   // amongst open windows.  If a window is closed via JavaScript while
    293   // the menu is up, the menu item for that window continues to exist.
    294   // When a window is selected this method is called with the
    295   // now-freed window as |aTarget|.  Short-circuit the call if
    296   // |aTarget| is not a valid window.
    297   if (anAction == @selector(_selectWindow:)) {
    298     // Not using -[NSArray containsObject:] because |aTarget| may be a
    299     // freed object.
    300     BOOL found = NO;
    301     for (NSWindow* window in [self windows]) {
    302       if (window == aTarget) {
    303         found = YES;
    304         break;
    305       }
    306     }
    307     if (!found) {
    308       return NO;
    309     }
    310   }
    311 
    312   // When a Cocoa control is wired to a freed object, we get crashers
    313   // in the call to |super| with no useful information in the
    314   // backtrace.  Attempt to add some useful information.
    315   static NSString* const kActionKey = @"sendaction";
    316 
    317   // If the action is something generic like -commandDispatch:, then
    318   // the tag is essential.
    319   NSInteger tag = 0;
    320   if ([sender isKindOfClass:[NSControl class]]) {
    321     tag = [sender tag];
    322     if (tag == 0 || tag == -1) {
    323       tag = [sender selectedTag];
    324     }
    325   } else if ([sender isKindOfClass:[NSMenuItem class]]) {
    326     tag = [sender tag];
    327   }
    328 
    329   NSString* actionString = NSStringFromSelector(anAction);
    330   NSString* value =
    331         [NSString stringWithFormat:@"%@ tag %d sending %@ to %p",
    332                   [sender className], tag, actionString, aTarget];
    333 
    334   ScopedCrashKey key(kActionKey, value);
    335   return [super sendAction:anAction to:aTarget from:sender];
    336 }
    337 
    338 // NSExceptions which are caught by the event loop are logged here.
    339 // NSException uses setjmp/longjmp, which can be very bad for C++, so
    340 // we attempt to track and report them.
    341 - (void)reportException:(NSException *)anException {
    342   // If we throw an exception in this code, we can create an infinite
    343   // loop.  If we throw out of the if() without resetting
    344   // |reportException|, we'll stop reporting exceptions for this run.
    345   static BOOL reportingException = NO;
    346   DCHECK(!reportingException);
    347   if (!reportingException) {
    348     reportingException = YES;
    349     chrome_browser_application_mac::RecordExceptionWithUma(anException);
    350 
    351     // http://crbug.com/45928 is a bug about needing to double-close
    352     // windows sometimes.  One theory is that |-isHandlingSendEvent|
    353     // gets latched to always return |YES|.  Since scopers are used to
    354     // manipulate that value, that should not be possible.  One way to
    355     // sidestep scopers is setjmp/longjmp (see above).  The following
    356     // is to "fix" this while the more fundamental concern is
    357     // addressed elsewhere.
    358     [self clearIsHandlingSendEvent];
    359 
    360     // Store some human-readable information in breakpad keys in case
    361     // there is a crash.  Since breakpad does not provide infinite
    362     // storage, we track two exceptions.  The first exception thrown
    363     // is tracked because it may be the one which caused the system to
    364     // go off the rails.  The last exception thrown is tracked because
    365     // it may be the one most directly associated with the crash.
    366     static NSString* const kFirstExceptionKey = @"firstexception";
    367     static BOOL trackedFirstException = NO;
    368     static NSString* const kLastExceptionKey = @"lastexception";
    369 
    370     // TODO(shess): It would be useful to post some stacktrace info
    371     // from the exception.
    372     // 10.6 has -[NSException callStackSymbols]
    373     // 10.5 has -[NSException callStackReturnAddresses]
    374     // 10.5 has backtrace_symbols().
    375     // I've tried to combine the latter two, but got nothing useful.
    376     // The addresses are right, though, maybe we could train the crash
    377     // server to decode them for us.
    378 
    379     NSString* value = [NSString stringWithFormat:@"%@ reason %@",
    380                                 [anException name], [anException reason]];
    381     if (!trackedFirstException) {
    382       SetCrashKeyValue(kFirstExceptionKey, value);
    383       trackedFirstException = YES;
    384     } else {
    385       SetCrashKeyValue(kLastExceptionKey, value);
    386     }
    387 
    388     reportingException = NO;
    389   }
    390 
    391   [super reportException:anException];
    392 }
    393 
    394 @end
    395