1 // Copyright (c) 2012 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/auto_reset.h" 8 #include "base/debug/crash_logging.h" 9 #include "base/debug/stack_trace.h" 10 #import "base/logging.h" 11 #import "base/mac/scoped_nsexception_enabler.h" 12 #import "base/mac/scoped_nsobject.h" 13 #import "base/metrics/histogram.h" 14 #include "base/strings/stringprintf.h" 15 #import "base/strings/sys_string_conversions.h" 16 #import "chrome/browser/app_controller_mac.h" 17 #include "chrome/browser/ui/tab_contents/tab_contents_iterator.h" 18 #include "chrome/common/crash_keys.h" 19 #import "chrome/common/mac/objc_method_swizzle.h" 20 #import "chrome/common/mac/objc_zombie.h" 21 #include "content/public/browser/browser_accessibility_state.h" 22 #include "content/public/browser/render_view_host.h" 23 #include "content/public/browser/web_contents.h" 24 25 // The implementation of NSExceptions break various assumptions in the 26 // Chrome code. This category defines a replacement for 27 // -initWithName:reason:userInfo: for purposes of forcing a break in 28 // the debugger when an exception is raised. -raise sounds more 29 // obvious to intercept, but it doesn't catch the original throw 30 // because the objc runtime doesn't use it. 31 @interface NSException (CrNSExceptionSwizzle) 32 - (id)crInitWithName:(NSString*)aName 33 reason:(NSString*)aReason 34 userInfo:(NSDictionary*)someUserInfo; 35 @end 36 37 static IMP gOriginalInitIMP = NULL; 38 39 @implementation NSException (CrNSExceptionSwizzle) 40 - (id)crInitWithName:(NSString*)aName 41 reason:(NSString*)aReason 42 userInfo:(NSDictionary*)someUserInfo { 43 // Method only called when swizzled. 44 DCHECK(_cmd == @selector(initWithName:reason:userInfo:)); 45 46 // Parts of Cocoa rely on creating and throwing exceptions. These are not 47 // worth bugging-out over. It is very important that there be zero chance that 48 // any Chromium code is on the stack; these must be created by Apple code and 49 // then immediately consumed by Apple code. 50 static NSString* const kAcceptableNSExceptionNames[] = { 51 // If an object does not support an accessibility attribute, this will 52 // get thrown. 53 NSAccessibilityException, 54 55 nil 56 }; 57 58 BOOL found = NO; 59 for (int i = 0; kAcceptableNSExceptionNames[i]; ++i) { 60 if (aName == kAcceptableNSExceptionNames[i]) { 61 found = YES; 62 } 63 } 64 65 if (!found) { 66 // Update breakpad with the exception info. 67 std::string value = base::StringPrintf("%s reason %s", 68 [aName UTF8String], [aReason UTF8String]); 69 base::debug::SetCrashKeyValue(crash_keys::mac::kNSException, value); 70 base::debug::SetCrashKeyToStackTrace(crash_keys::mac::kNSExceptionTrace, 71 base::debug::StackTrace()); 72 73 // Force crash for selected exceptions to generate crash dumps. 74 BOOL fatal = NO; 75 if (aName == NSInternalInconsistencyException) { 76 NSString* const kNSMenuItemArrayBoundsCheck = 77 @"Invalid parameter not satisfying: (index >= 0) && " 78 @"(index < [_itemArray count])"; 79 if ([aReason isEqualToString:kNSMenuItemArrayBoundsCheck]) { 80 fatal = YES; 81 } 82 83 NSString* const kNoWindowCheck = @"View is not in any window"; 84 if ([aReason isEqualToString:kNoWindowCheck]) { 85 fatal = YES; 86 } 87 } 88 89 // Mostly "unrecognized selector sent to (instance|class)". A 90 // very small number of things like inappropriate nil being passed. 91 if (aName == NSInvalidArgumentException) { 92 fatal = YES; 93 94 // TODO(shess): http://crbug.com/85463 throws this exception 95 // from ImageKit. Our code is not on the stack, so it needs to 96 // be whitelisted for now. 97 NSString* const kNSURLInitNilCheck = 98 @"*** -[NSURL initFileURLWithPath:isDirectory:]: " 99 @"nil string parameter"; 100 if ([aReason isEqualToString:kNSURLInitNilCheck]) { 101 fatal = NO; 102 } 103 } 104 105 // Dear reader: Something you just did provoked an NSException. 106 // NSException is implemented in terms of setjmp()/longjmp(), 107 // which does poor things when combined with C++ scoping 108 // (destructors are skipped). Chrome should be NSException-free, 109 // please check your backtrace and see if you can't file a bug 110 // with a repro case. 111 const bool allow = base::mac::GetNSExceptionsAllowed(); 112 if (fatal && !allow) { 113 LOG(FATAL) << "Someone is trying to raise an exception! " 114 << value; 115 } else { 116 // Make sure that developers see when their code throws 117 // exceptions. 118 DCHECK(allow) << "Someone is trying to raise an exception! " 119 << value; 120 } 121 } 122 123 // Forward to the original version. 124 return gOriginalInitIMP(self, _cmd, aName, aReason, someUserInfo); 125 } 126 @end 127 128 namespace chrome_browser_application_mac { 129 130 // Maximum number of known named exceptions we'll support. There is 131 // no central registration, but I only find about 75 possibilities in 132 // the system frameworks, and many of them are probably not 133 // interesting to track in aggregate (those relating to distributed 134 // objects, for instance). 135 const size_t kKnownNSExceptionCount = 25; 136 137 const size_t kUnknownNSException = kKnownNSExceptionCount; 138 139 size_t BinForException(NSException* exception) { 140 // A list of common known exceptions. The list position will 141 // determine where they live in the histogram, so never move them 142 // around, only add to the end. 143 static NSString* const kKnownNSExceptionNames[] = { 144 // Grab-bag exception, not very common. CFArray (or other 145 // container) mutated while being enumerated is one case seen in 146 // production. 147 NSGenericException, 148 149 // Out-of-range on NSString or NSArray. Quite common. 150 NSRangeException, 151 152 // Invalid arg to method, unrecognized selector. Quite common. 153 NSInvalidArgumentException, 154 155 // malloc() returned null in object creation, I think. Turns out 156 // to be very uncommon in production, because of the OOM killer. 157 NSMallocException, 158 159 // This contains things like windowserver errors, trying to draw 160 // views which aren't in windows, unable to read nib files. By 161 // far the most common exception seen on the crash server. 162 NSInternalInconsistencyException, 163 164 nil 165 }; 166 167 // Make sure our array hasn't outgrown our abilities to track it. 168 DCHECK_LE(arraysize(kKnownNSExceptionNames), kKnownNSExceptionCount); 169 170 NSString* name = [exception name]; 171 for (int i = 0; kKnownNSExceptionNames[i]; ++i) { 172 if (name == kKnownNSExceptionNames[i]) { 173 return i; 174 } 175 } 176 return kUnknownNSException; 177 } 178 179 void RecordExceptionWithUma(NSException* exception) { 180 UMA_HISTOGRAM_ENUMERATION("OSX.NSException", 181 BinForException(exception), kUnknownNSException); 182 } 183 184 void RegisterBrowserCrApp() { 185 [BrowserCrApplication sharedApplication]; 186 }; 187 188 void Terminate() { 189 [NSApp terminate:nil]; 190 } 191 192 void CancelTerminate() { 193 [NSApp cancelTerminate:nil]; 194 } 195 196 } // namespace chrome_browser_application_mac 197 198 namespace { 199 200 void SwizzleInit() { 201 // Do-nothing wrapper so that we can arrange to only swizzle 202 // -[NSException raise] when DCHECK() is turned on (as opposed to 203 // replicating the preprocess logic which turns DCHECK() on). 204 gOriginalInitIMP = ObjcEvilDoers::SwizzleImplementedInstanceMethods( 205 [NSException class], 206 @selector(initWithName:reason:userInfo:), 207 @selector(crInitWithName:reason:userInfo:)); 208 } 209 210 } // namespace 211 212 // These methods are being exposed for the purposes of overriding. 213 // Used to determine when a Panel window can become the key window. 214 @interface NSApplication (PanelsCanBecomeKey) 215 - (void)_cycleWindowsReversed:(BOOL)arg1; 216 - (id)_removeWindow:(NSWindow*)window; 217 - (id)_setKeyWindow:(NSWindow*)window; 218 @end 219 220 @interface BrowserCrApplication (PrivateInternal) 221 222 // This must be called under the protection of previousKeyWindowsLock_. 223 - (void)removePreviousKeyWindow:(NSWindow*)window; 224 225 @end 226 227 @implementation BrowserCrApplication 228 229 + (void)initialize { 230 // Turn all deallocated Objective-C objects into zombies, keeping 231 // the most recent 10,000 of them on the treadmill. 232 ObjcEvilDoers::ZombieEnable(true, 10000); 233 } 234 235 - (id)init { 236 SwizzleInit(); 237 self = [super init]; 238 239 // Sanity check to alert if overridden methods are not supported. 240 DCHECK([NSApplication 241 instancesRespondToSelector:@selector(_cycleWindowsReversed:)]); 242 DCHECK([NSApplication 243 instancesRespondToSelector:@selector(_removeWindow:)]); 244 DCHECK([NSApplication 245 instancesRespondToSelector:@selector(_setKeyWindow:)]); 246 247 return self; 248 } 249 250 // Initialize NSApplication using the custom subclass. Check whether NSApp 251 // was already initialized using another class, because that would break 252 // some things. 253 + (NSApplication*)sharedApplication { 254 NSApplication* app = [super sharedApplication]; 255 256 // +sharedApplication initializes the global NSApp, so if a specific 257 // NSApplication subclass is requested, require that to be the one 258 // delivered. The practical effect is to require a consistent NSApp 259 // across the executable. 260 CHECK([NSApp isKindOfClass:self]) 261 << "NSApp must be of type " << [[self className] UTF8String] 262 << ", not " << [[NSApp className] UTF8String]; 263 264 // If the message loop was initialized before NSApp is setup, the 265 // message pump will be setup incorrectly. Failing this implies 266 // that RegisterBrowserCrApp() should be called earlier. 267 CHECK(base::MessagePumpMac::UsingCrApp()) 268 << "MessagePumpMac::Create() is using the wrong pump implementation" 269 << " for " << [[self className] UTF8String]; 270 271 return app; 272 } 273 274 //////////////////////////////////////////////////////////////////////////////// 275 // HISTORICAL COMMENT (by viettrungluu, from 276 // http://codereview.chromium.org/1520006 with mild editing): 277 // 278 // A quick summary of the state of things (before the changes to shutdown): 279 // 280 // Currently, we are totally hosed (put in a bad state in which Cmd-W does the 281 // wrong thing, and which will probably eventually lead to a crash) if we begin 282 // quitting but termination is aborted for some reason. 283 // 284 // I currently know of two ways in which termination can be aborted: 285 // (1) Common case: a window has an onbeforeunload handler which pops up a 286 // "leave web page" dialog, and the user answers "no, don't leave". 287 // (2) Uncommon case: popups are enabled (in Content Settings, i.e., the popup 288 // blocker is disabled), and some nasty web page pops up a new window on 289 // closure. 290 // 291 // I don't know of other ways in which termination can be aborted, but they may 292 // exist (or may be added in the future, for that matter). 293 // 294 // My CL [see above] does the following: 295 // a. Should prevent being put in a bad state (which breaks Cmd-W and leads to 296 // crash) under all circumstances. 297 // b. Should completely handle (1) properly. 298 // c. Doesn't (yet) handle (2) properly and puts it in a weird state (but not 299 // that bad). 300 // d. Any other ways of aborting termination would put it in that weird state. 301 // 302 // c. can be fixed by having the global flag reset on browser creation or 303 // similar (and doing so might also fix some possible d.'s as well). I haven't 304 // done this yet since I haven't thought about it carefully and since it's a 305 // corner case. 306 // 307 // The weird state: a state in which closing the last window quits the browser. 308 // This might be a bit annoying, but it's not dangerous in any way. 309 //////////////////////////////////////////////////////////////////////////////// 310 311 // |-terminate:| is the entry point for orderly "quit" operations in Cocoa. This 312 // includes the application menu's quit menu item and keyboard equivalent, the 313 // application's dock icon menu's quit menu item, "quit" (not "force quit") in 314 // the Activity Monitor, and quits triggered by user logout and system restart 315 // and shutdown. 316 // 317 // The default |-terminate:| implementation ends the process by calling exit(), 318 // and thus never leaves the main run loop. This is unsuitable for Chrome since 319 // Chrome depends on leaving the main run loop to perform an orderly shutdown. 320 // We support the normal |-terminate:| interface by overriding the default 321 // implementation. Our implementation, which is very specific to the needs of 322 // Chrome, works by asking the application delegate to terminate using its 323 // |-tryToTerminateApplication:| method. 324 // 325 // |-tryToTerminateApplication:| differs from the standard 326 // |-applicationShouldTerminate:| in that no special event loop is run in the 327 // case that immediate termination is not possible (e.g., if dialog boxes 328 // allowing the user to cancel have to be shown). Instead, this method sets a 329 // flag and tries to close all browsers. This flag causes the closure of the 330 // final browser window to begin actual tear-down of the application. 331 // Termination is cancelled by resetting this flag. The standard 332 // |-applicationShouldTerminate:| is not supported, and code paths leading to it 333 // must be redirected. 334 // 335 // When the last browser has been destroyed, the BrowserList calls 336 // chrome::OnAppExiting(), which is the point of no return. That will cause 337 // the NSApplicationWillTerminateNotification to be posted, which ends the 338 // NSApplication event loop, so final post- MessageLoop::Run() work is done 339 // before exiting. 340 - (void)terminate:(id)sender { 341 AppController* appController = static_cast<AppController*>([NSApp delegate]); 342 [appController tryToTerminateApplication:self]; 343 // Return, don't exit. The application is responsible for exiting on its own. 344 } 345 346 - (void)cancelTerminate:(id)sender { 347 AppController* appController = static_cast<AppController*>([NSApp delegate]); 348 [appController stopTryingToTerminateApplication:self]; 349 } 350 351 - (BOOL)sendAction:(SEL)anAction to:(id)aTarget from:(id)sender { 352 // The Dock menu contains an automagic section where you can select 353 // amongst open windows. If a window is closed via JavaScript while 354 // the menu is up, the menu item for that window continues to exist. 355 // When a window is selected this method is called with the 356 // now-freed window as |aTarget|. Short-circuit the call if 357 // |aTarget| is not a valid window. 358 if (anAction == @selector(_selectWindow:)) { 359 // Not using -[NSArray containsObject:] because |aTarget| may be a 360 // freed object. 361 BOOL found = NO; 362 for (NSWindow* window in [self windows]) { 363 if (window == aTarget) { 364 found = YES; 365 break; 366 } 367 } 368 if (!found) { 369 return NO; 370 } 371 } 372 373 // When a Cocoa control is wired to a freed object, we get crashers 374 // in the call to |super| with no useful information in the 375 // backtrace. Attempt to add some useful information. 376 377 // If the action is something generic like -commandDispatch:, then 378 // the tag is essential. 379 NSInteger tag = 0; 380 if ([sender isKindOfClass:[NSControl class]]) { 381 tag = [sender tag]; 382 if (tag == 0 || tag == -1) { 383 tag = [sender selectedTag]; 384 } 385 } else if ([sender isKindOfClass:[NSMenuItem class]]) { 386 tag = [sender tag]; 387 } 388 389 NSString* actionString = NSStringFromSelector(anAction); 390 std::string value = base::StringPrintf("%s tag %ld sending %s to %p", 391 [[sender className] UTF8String], 392 static_cast<long>(tag), 393 [actionString UTF8String], 394 aTarget); 395 396 base::debug::ScopedCrashKey key(crash_keys::mac::kSendAction, value); 397 398 // Certain third-party code, such as print drivers, can still throw 399 // exceptions and Chromium cannot fix them. This provides a way to 400 // work around those on a spot basis. 401 bool enableNSExceptions = false; 402 403 // http://crbug.com/80686 , an Epson printer driver. 404 if (anAction == @selector(selectPDE:)) { 405 enableNSExceptions = true; 406 } 407 408 // Minimize the window by keeping this close to the super call. 409 scoped_ptr<base::mac::ScopedNSExceptionEnabler> enabler; 410 if (enableNSExceptions) 411 enabler.reset(new base::mac::ScopedNSExceptionEnabler()); 412 return [super sendAction:anAction to:aTarget from:sender]; 413 } 414 415 - (BOOL)isHandlingSendEvent { 416 return handlingSendEvent_; 417 } 418 419 - (void)setHandlingSendEvent:(BOOL)handlingSendEvent { 420 handlingSendEvent_ = handlingSendEvent; 421 } 422 423 - (void)sendEvent:(NSEvent*)event { 424 base::mac::ScopedSendingEvent sendingEventScoper; 425 [super sendEvent:event]; 426 } 427 428 // NSExceptions which are caught by the event loop are logged here. 429 // NSException uses setjmp/longjmp, which can be very bad for C++, so 430 // we attempt to track and report them. 431 - (void)reportException:(NSException *)anException { 432 // If we throw an exception in this code, we can create an infinite 433 // loop. If we throw out of the if() without resetting 434 // |reportException|, we'll stop reporting exceptions for this run. 435 static BOOL reportingException = NO; 436 DCHECK(!reportingException); 437 if (!reportingException) { 438 reportingException = YES; 439 chrome_browser_application_mac::RecordExceptionWithUma(anException); 440 441 // http://crbug.com/45928 is a bug about needing to double-close 442 // windows sometimes. One theory is that |-isHandlingSendEvent| 443 // gets latched to always return |YES|. Since scopers are used to 444 // manipulate that value, that should not be possible. One way to 445 // sidestep scopers is setjmp/longjmp (see above). The following 446 // is to "fix" this while the more fundamental concern is 447 // addressed elsewhere. 448 [self setHandlingSendEvent:NO]; 449 450 // If |ScopedNSExceptionEnabler| is used to allow exceptions, and an 451 // uncaught exception is thrown, it will throw past all of the scopers. 452 // Reset the flag so that future exceptions are not masked. 453 base::mac::SetNSExceptionsAllowed(false); 454 455 // Store some human-readable information in breakpad keys in case 456 // there is a crash. Since breakpad does not provide infinite 457 // storage, we track two exceptions. The first exception thrown 458 // is tracked because it may be the one which caused the system to 459 // go off the rails. The last exception thrown is tracked because 460 // it may be the one most directly associated with the crash. 461 static BOOL trackedFirstException = NO; 462 463 const char* const kExceptionKey = 464 trackedFirstException ? crash_keys::mac::kLastNSException 465 : crash_keys::mac::kFirstNSException; 466 NSString* value = [NSString stringWithFormat:@"%@ reason %@", 467 [anException name], [anException reason]]; 468 base::debug::SetCrashKeyValue(kExceptionKey, [value UTF8String]); 469 470 // Encode the callstack from point of throw. 471 // TODO(shess): Our swizzle plus the 23-frame limit plus Cocoa 472 // overhead may make this less than useful. If so, perhaps skip 473 // some items and/or use two keys. 474 const char* const kExceptionBtKey = 475 trackedFirstException ? crash_keys::mac::kLastNSExceptionTrace 476 : crash_keys::mac::kFirstNSExceptionTrace; 477 NSArray* addressArray = [anException callStackReturnAddresses]; 478 NSUInteger addressCount = [addressArray count]; 479 if (addressCount) { 480 // SetCrashKeyFromAddresses() only encodes 23, so that's a natural limit. 481 const NSUInteger kAddressCountMax = 23; 482 void* addresses[kAddressCountMax]; 483 if (addressCount > kAddressCountMax) 484 addressCount = kAddressCountMax; 485 486 for (NSUInteger i = 0; i < addressCount; ++i) { 487 addresses[i] = reinterpret_cast<void*>( 488 [[addressArray objectAtIndex:i] unsignedIntegerValue]); 489 } 490 base::debug::SetCrashKeyFromAddresses( 491 kExceptionBtKey, addresses, static_cast<size_t>(addressCount)); 492 } else { 493 base::debug::ClearCrashKey(kExceptionBtKey); 494 } 495 trackedFirstException = YES; 496 497 reportingException = NO; 498 } 499 500 [super reportException:anException]; 501 } 502 503 - (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute { 504 if ([attribute isEqualToString:@"AXEnhancedUserInterface"] && 505 [value intValue] == 1) { 506 content::BrowserAccessibilityState::GetInstance()->OnScreenReaderDetected(); 507 for (TabContentsIterator it; !it.done(); it.Next()) { 508 if (content::WebContents* contents = *it) 509 if (content::RenderViewHost* rvh = contents->GetRenderViewHost()) 510 rvh->EnableFullAccessibilityMode(); 511 } 512 } 513 return [super accessibilitySetValue:value forAttribute:attribute]; 514 } 515 516 - (void)_cycleWindowsReversed:(BOOL)arg1 { 517 base::AutoReset<BOOL> pin(&cyclingWindows_, YES); 518 [super _cycleWindowsReversed:arg1]; 519 } 520 521 - (BOOL)isCyclingWindows { 522 return cyclingWindows_; 523 } 524 525 - (id)_removeWindow:(NSWindow*)window { 526 { 527 base::AutoLock lock(previousKeyWindowsLock_); 528 [self removePreviousKeyWindow:window]; 529 } 530 id result = [super _removeWindow:window]; 531 532 // Ensure app has a key window after a window is removed. 533 // OS wants to make a panel browser window key after closing an app window 534 // because panels use a higher priority window level, but panel windows may 535 // refuse to become key, leaving the app with no key window. The OS does 536 // not seem to consider other windows after the first window chosen refuses 537 // to become key. Force consideration of other windows here. 538 if ([self isActive] && [self keyWindow] == nil) { 539 NSWindow* key = 540 [self makeWindowsPerform:@selector(canBecomeKeyWindow) inOrder:YES]; 541 [key makeKeyWindow]; 542 } 543 544 // Return result from the super class. It appears to be the app that 545 // owns the removed window (determined via experimentation). 546 return result; 547 } 548 549 - (id)_setKeyWindow:(NSWindow*)window { 550 // |window| is nil when the current key window is being closed. 551 // A separate call follows with a new value when a new key window is set. 552 // Closed windows are not tracked in previousKeyWindows_. 553 if (window != nil) { 554 base::AutoLock lock(previousKeyWindowsLock_); 555 [self removePreviousKeyWindow:window]; 556 NSWindow* currentKeyWindow = [self keyWindow]; 557 if (currentKeyWindow != nil && currentKeyWindow != window) 558 previousKeyWindows_.push_back(currentKeyWindow); 559 } 560 561 return [super _setKeyWindow:window]; 562 } 563 564 - (NSWindow*)previousKeyWindow { 565 base::AutoLock lock(previousKeyWindowsLock_); 566 return previousKeyWindows_.empty() ? nil : previousKeyWindows_.back(); 567 } 568 569 - (void)removePreviousKeyWindow:(NSWindow*)window { 570 previousKeyWindowsLock_.AssertAcquired(); 571 std::vector<NSWindow*>::iterator window_iterator = 572 std::find(previousKeyWindows_.begin(), 573 previousKeyWindows_.end(), 574 window); 575 if (window_iterator != previousKeyWindows_.end()) { 576 previousKeyWindows_.erase(window_iterator); 577 } 578 } 579 580 @end 581