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