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