diff options
author | shess@chromium.org <shess@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-05-19 16:25:13 +0000 |
---|---|---|
committer | shess@chromium.org <shess@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-05-19 16:25:13 +0000 |
commit | d9c320a8f7ff01a475a831924797001f54443a85 (patch) | |
tree | 55d91a0e3712092c6704d46cbe5bf8982680090e /chrome | |
parent | 2a026d42f29297f6d040eca9c8fa0efe96afa0ba (diff) | |
download | chromium_src-d9c320a8f7ff01a475a831924797001f54443a85.zip chromium_src-d9c320a8f7ff01a475a831924797001f54443a85.tar.gz chromium_src-d9c320a8f7ff01a475a831924797001f54443a85.tar.bz2 |
[Mac] Implement NSObject zombies.
Apple's NSZombieEnabled setting makes it easier to catch messages to
freed objects, but is mostly only useful in debugging environments.
This implements a facility like NSZombieEnabled with the following
additions:
- The number of outstanding zombies can be configured.
- Classes can opt-in to becoming zombies.
- C++ destructors are correctly called on Leopard.
The goal is to allow us to enable zombies in certain production builds
to help debug some of the message-after-free bugs we have.
BUG=35590,24987
TEST=everything
Review URL: http://codereview.chromium.org/660411
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@47674 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome')
-rw-r--r-- | chrome/browser/chrome_browser_application_mac.mm | 7 | ||||
-rw-r--r-- | chrome/browser/cocoa/objc_zombie.h | 34 | ||||
-rw-r--r-- | chrome/browser/cocoa/objc_zombie.mm | 399 | ||||
-rw-r--r-- | chrome/chrome_browser.gypi | 2 |
4 files changed, 442 insertions, 0 deletions
diff --git a/chrome/browser/chrome_browser_application_mac.mm b/chrome/browser/chrome_browser_application_mac.mm index 07aac53..6e171fd 100644 --- a/chrome/browser/chrome_browser_application_mac.mm +++ b/chrome/browser/chrome_browser_application_mac.mm @@ -11,6 +11,7 @@ #import "chrome/app/breakpad_mac.h" #import "chrome/browser/app_controller_mac.h" #import "chrome/browser/cocoa/objc_method_swizzle.h" +#import "chrome/browser/cocoa/objc_zombie.h" // The implementation of NSExceptions break various assumptions in the // Chrome code. This category defines a replacement for @@ -161,6 +162,12 @@ BOOL SwizzleNSExceptionInit() { @implementation BrowserCrApplication ++ (void)initialize { + // Turn all deallocated Objective-C objects into zombies, keeping + // the most recent 10,000 of them on the treadmill. + DCHECK(ObjcEvilDoers::ZombieEnable(YES, 10000)); +} + - init { DCHECK(SwizzleNSExceptionInit()); return [super init]; diff --git a/chrome/browser/cocoa/objc_zombie.h b/chrome/browser/cocoa/objc_zombie.h new file mode 100644 index 0000000..86508a7 --- /dev/null +++ b/chrome/browser/cocoa/objc_zombie.h @@ -0,0 +1,34 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_COCOA_NSOBJECT_ZOMBIE_H_ +#define CHROME_BROWSER_COCOA_NSOBJECT_ZOMBIE_H_ + +#import <Foundation/Foundation.h> + +// You should think twice every single time you use anything from this +// namespace. +namespace ObjcEvilDoers { + +// Enable zombies. Returns NO if it fails to enable. +// +// When |zombieAllObjects| is YES, all objects inheriting from +// NSObject become zombies on -dealloc. If NO, -shouldBecomeCrZombie +// is queried to determine whether to make the object a zombie. +// +// |zombieCount| controls how many zombies to store before freeing the +// oldest. Set to 0 to free objects immediately after making them +// zombies. +BOOL ZombieEnable(BOOL zombieAllObjects, size_t zombieCount); + +// Disable zombies. +void ZombieDisable(); + +} // namespace ObjcEvilDoers + +@interface NSObject (CrZombie) +- (BOOL)shouldBecomeCrZombie; +@end + +#endif // CHROME_BROWSER_COCOA_NSOBJECT_ZOMBIE_H_ diff --git a/chrome/browser/cocoa/objc_zombie.mm b/chrome/browser/cocoa/objc_zombie.mm new file mode 100644 index 0000000..a9d9063 --- /dev/null +++ b/chrome/browser/cocoa/objc_zombie.mm @@ -0,0 +1,399 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "chrome/browser/cocoa/objc_zombie.h" + +#include <dlfcn.h> +#include <mach-o/dyld.h> +#include <mach-o/nlist.h> + +#import <objc/objc-class.h> + +#include "base/lock.h" +#include "base/logging.h" +#import "chrome/app/breakpad_mac.h" +#import "chrome/browser/cocoa/objc_method_swizzle.h" + +// Deallocated objects are re-classed as |CrZombie|. Overrides most +// |NSObject| methods to log fatal errors. +@interface CrZombie : NSObject { +} + +@end + +// Objects with enough space are made into "fat" zombies, which +// directly remember which class they were until reallocated. +@interface CrFatZombie : CrZombie { + @public + Class wasa; +} +@end + +namespace { + +// |object_cxxDestruct()| is an Objective-C runtime function which +// traverses the object's class tree for ".cxxdestruct" methods which +// are run to call C++ destructors as part of |-dealloc|. The +// function is not public, so must be looked up using nlist. +typedef void DestructFn(id obj); +DestructFn* g_object_cxxDestruct = NULL; + +// The original implementation for |-[NSObject dealloc]|. +IMP g_originalDeallocIMP = NULL; + +// Classes which freed objects become. |g_fatZombieSize| is the +// minimum object size which can be made into a fat zombie (which can +// remember which class it was before free, even after falling off the +// treadmill). +Class g_zombieClass = Nil; // cached [CrZombie class] +Class g_fatZombieClass = Nil; // cached [CrFatZombie class] +size_t g_fatZombieSize = 0; + +// Whether to zombie all freed objects, or only those which return YES +// from |-shouldBecomeCrZombie|. +BOOL g_zombieAllObjects = NO; + +// Protects |g_zombieCount|, |g_zombieIndex|, and |g_zombies|. +Lock lock_; + +// How many zombies to keep before freeing, and the current head of +// the circular buffer. +size_t g_zombieCount = 0; +size_t g_zombieIndex = 0; + +typedef struct { + id object; // The zombied object. + Class wasa; // Value of |object->isa| before we replaced it. +} ZombieRecord; + +ZombieRecord* g_zombies = NULL; + +// Lookup the private |object_cxxDestruct| function and return a +// pointer to it. Returns |NULL| on failure. +DestructFn* LookupObjectCxxDestruct() { +#if ARCH_CPU_64_BITS + // TODO(shess): Port to 64-bit. I believe using struct nlist_64 + // will suffice. http://crbug.com/44021 . + NOTIMPLEMENTED(); + return NULL; +#endif + + struct nlist nl[3]; + bzero(&nl, sizeof(nl)); + + nl[0].n_un.n_name = (char*)"_object_cxxDestruct"; + + // My ability to calculate the base for offsets is apparently poor. + // Use |class_addIvar| as a known reference point. + nl[1].n_un.n_name = (char*)"_class_addIvar"; + + if (nlist("/usr/lib/libobjc.dylib", nl) < 0 || + nl[0].n_type == N_UNDF || nl[1].n_type == N_UNDF) + return NULL; + + return (DestructFn*)((char*)&class_addIvar - nl[1].n_value + nl[0].n_value); +} + +// Replace all methods in |refClass| which are not implemented in +// |aClass| with |anImp|. +void class_replaceUnimplementedWith(Class aClass, Class refClass, IMP anImp) { + unsigned int methodCount = 0; + Method* methodList = class_copyMethodList(refClass, &methodCount); + if (methodList) { + for (unsigned int i = 0; i < methodCount; ++i) { + const SEL name = method_getName(methodList[i]); + const char* types = method_getTypeEncoding(methodList[i]); + + // Fails if the method already exists, which is fine. + class_addMethod(aClass, name, anImp, types); + } + free(methodList); + } +} + +// Replacement |-dealloc| which turns objects into zombies and places +// them into |g_zombies| to be freed later. +void ZombieDealloc(id self, SEL _cmd) { + // This code should only be called when it is implementing |-dealloc|. + DCHECK_EQ(_cmd, @selector(dealloc)); + + // Use the original |-dealloc| if the object doesn't wish to be + // zombied. + if (!g_zombieAllObjects && ![self shouldBecomeCrZombie]) { + g_originalDeallocIMP(self, _cmd); + return; + } + + // Use the original |-dealloc| if |object_cxxDestruct| was never + // initialized, because otherwise C++ destructors won't be called. + // This case should be impossible, but doing it wrong would cause + // terrible problems. + DCHECK(g_object_cxxDestruct); + if (!g_object_cxxDestruct) { + g_originalDeallocIMP(self, _cmd); + return; + } + + Class wasa = object_getClass(self); + const size_t size = class_getInstanceSize(wasa); + + // Destroy the instance by calling C++ destructors and clearing it + // to something unlikely to work well if someone references it. + (*g_object_cxxDestruct)(self); + memset(self, '!', size); + + // If the instance is big enough, make it into a fat zombie and have + // it remember the old isa. Otherwise make it a regular zombie. + if (size >= g_fatZombieSize) { + object_setClass(self, g_fatZombieClass); + static_cast<CrFatZombie*>(self)->wasa = wasa; + } else { + object_setClass(self, g_zombieClass); + } + + // The new record to swap into |g_zombies|. If |g_zombieCount| is + // zero, then |self| will be freed immediately. + ZombieRecord zombieToFree = {self, wasa}; + + { + AutoLock pin(lock_); + if (g_zombieCount > 0) { + // Put the current object on the treadmill and keep the previous + // occupant. + std::swap(zombieToFree, g_zombies[g_zombieIndex]); + + // Bump the index forward. + g_zombieIndex = (g_zombieIndex + 1) % g_zombieCount; + } + } + + // Do the free out here to prevent any chance of deadlock. + if (zombieToFree.object) + free(zombieToFree.object); +} + +// Attempt to determine the original class of zombie |object|. +Class ZombieWasa(id object) { + // Fat zombies can hold onto their |wasa| past the point where the + // object was actually freed. Note that to arrive here at all, + // |object|'s memory must still be accessible. + if (object_getClass(object) == g_fatZombieClass) + return static_cast<CrFatZombie*>(object)->wasa; + + // For instances which weren't big enough to store |wasa|, check if + // the object is still on the treadmill. + AutoLock pin(lock_); + for (size_t i=0; i < g_zombieCount; ++i) { + if (g_zombies[i].object == object) + return g_zombies[i].wasa; + } + + return Nil; +} + +// Log a message to a freed object. |wasa| is the object's original +// class. |aSelector| is the selector which the calling code was +// attempting to send. |viaSelector| is the selector of the +// dispatch-related method which is being invoked to send |aSelector| +// (for instance, -respondsToSelector:). +void LogAndDie(id object, SEL aSelector, SEL viaSelector) { + Class wasa = ZombieWasa(object); + const char* wasaName = (wasa ? class_getName(wasa) : "<unknown>"); + NSString* aString = + [NSString stringWithFormat:@"Zombie <%s: %p> received -%s", + wasaName, object, sel_getName(aSelector)]; + if (viaSelector) { + const char* viaName = sel_getName(viaSelector); + aString = [aString stringByAppendingFormat:@" (via -%s)", viaName]; + } + + // Set a value for breakpad to report, then crash. + SetCrashKeyValue(@"zombie", aString); + LOG(FATAL) << [aString UTF8String]; +} + +// Implements a method which will be used to override NSObject methods +// that zombies don't explicitly implement. +void DoesNotRecognize(id self, SEL _cmd) { + LogAndDie(self, _cmd, 0); +} + +// Initialize our globals, returning YES on success. +BOOL ZombieInit() { + static BOOL initialized = NO; + if (initialized) + return YES; + + Class rootClass = [NSObject class]; + + g_object_cxxDestruct = LookupObjectCxxDestruct(); + g_originalDeallocIMP = + class_getMethodImplementation(rootClass, @selector(dealloc)); + g_zombieClass = [CrZombie class]; + g_fatZombieClass = [CrFatZombie class]; + g_fatZombieSize = class_getInstanceSize(g_fatZombieClass); + + if (!g_object_cxxDestruct || !g_originalDeallocIMP || + !g_zombieClass || !g_fatZombieClass) + return NO; + + // Override any inherited methods from |NSObject| with + // |DoesNotRecognize()|. + class_replaceUnimplementedWith(g_zombieClass, rootClass, + (IMP)DoesNotRecognize); + + initialized = YES; + return YES; +} + +} // namespace + +@implementation CrZombie + +// |DoesNotRecognize()| will log and crash for any selector send to +// instances of the class. Override a few methods related to message +// dispatch to provide more specific diagnostic information. +- (BOOL)respondsToSelector:(SEL)aSelector { + LogAndDie(self, aSelector, _cmd); + return NO; +} +- (id)forwardingTargetForSelector:(SEL)aSelector { + LogAndDie(self, aSelector, _cmd); + return nil; +} + +@end + +@implementation CrFatZombie + +// This implementation intentionally left empty. + +@end + +@implementation NSObject (CrZombie) + +- (BOOL)shouldBecomeCrZombie { + return NO; +} + +@end + +namespace ObjcEvilDoers { + +BOOL ZombieEnable(BOOL zombieAllObjects, + size_t zombieCount) { + // Only allow enable/disable on the main thread, just to keep things + // simple. + CHECK([NSThread isMainThread]); + + if (!ZombieInit()) + return NO; + + if (zombieCount < 0) + return NO; + + g_zombieAllObjects = zombieAllObjects; + + // Replace the implementation of -[NSObject dealloc]. + Method m = class_getInstanceMethod([NSObject class], @selector(dealloc)); + if (!m) + return NO; + + const IMP prevDeallocIMP = method_setImplementation(m, (IMP)ZombieDealloc); + DCHECK(prevDeallocIMP == g_originalDeallocIMP || + prevDeallocIMP == (IMP)ZombieDealloc); + + // Grab the current set of zombies. This is thread-safe because + // only the main thread can change these. + const size_t oldCount = g_zombieCount; + ZombieRecord* oldZombies = g_zombies; + + { + AutoLock pin(lock_); + + // Save the old index in case zombies need to be transferred. + size_t oldIndex = g_zombieIndex; + + // Create the new zombie treadmill, disabling zombies in case of + // failure. + g_zombieIndex = 0; + g_zombieCount = zombieCount; + g_zombies = NULL; + if (g_zombieCount) { + g_zombies = + static_cast<ZombieRecord*>(calloc(g_zombieCount, sizeof(*g_zombies))); + if (!g_zombies) { + NOTREACHED(); + g_zombies = oldZombies; + g_zombieCount = oldCount; + g_zombieIndex = oldIndex; + ZombieDisable(); + return NO; + } + } + + // If the count is changing, allow some of the zombies to continue + // shambling forward. + const size_t sharedCount = std::min(oldCount, zombieCount); + if (sharedCount) { + // Get index of the first shared zombie. + oldIndex = (oldIndex + oldCount - sharedCount) % oldCount; + + for (; g_zombieIndex < sharedCount; ++ g_zombieIndex) { + DCHECK_LT(g_zombieIndex, g_zombieCount); + DCHECK_LT(oldIndex, oldCount); + std::swap(g_zombies[g_zombieIndex], oldZombies[oldIndex]); + oldIndex = (oldIndex + 1) % oldCount; + } + g_zombieIndex %= g_zombieCount; + } + } + + // Free the old treadmill and any remaining zombies. + if (oldZombies) { + for (size_t i = 0; i < oldCount; ++i) { + if (oldZombies[i].object) + free(oldZombies[i].object); + } + free(oldZombies); + } + + return YES; +} + +void ZombieDisable() { + // Only allow enable/disable on the main thread, just to keep things + // simple. + CHECK([NSThread isMainThread]); + + // |ZombieInit()| was never called. + if (!g_originalDeallocIMP) + return; + + // Put back the original implementation of -[NSObject dealloc]. + Method m = class_getInstanceMethod([NSObject class], @selector(dealloc)); + CHECK(m); + method_setImplementation(m, g_originalDeallocIMP); + + // Can safely grab this because it only happens on the main thread. + const size_t oldCount = g_zombieCount; + ZombieRecord* oldZombies = g_zombies; + + { + AutoLock pin(lock_); // In case any |-dealloc| are in-progress. + g_zombieCount = 0; + g_zombies = NULL; + } + + // Free any remaining zombies. + if (oldZombies) { + for (size_t i = 0; i < oldCount; ++i) { + if (oldZombies[i].object) + free(oldZombies[i].object); + } + free(oldZombies); + } +} + +} // namespace ObjcEvilDoers diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index cb52ae8..5131867 100644 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -778,6 +778,8 @@ 'browser/cocoa/nsmenuitem_additions.mm', 'browser/cocoa/objc_method_swizzle.h', 'browser/cocoa/objc_method_swizzle.mm', + 'browser/cocoa/objc_zombie.h', + 'browser/cocoa/objc_zombie.mm', 'browser/cocoa/page_info_window_controller.h', 'browser/cocoa/page_info_window_controller.mm', 'browser/cocoa/page_info_window_mac.h', |