summaryrefslogtreecommitdiffstats
path: root/chrome
diff options
context:
space:
mode:
authorshess@chromium.org <shess@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-05-19 16:25:13 +0000
committershess@chromium.org <shess@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-05-19 16:25:13 +0000
commitd9c320a8f7ff01a475a831924797001f54443a85 (patch)
tree55d91a0e3712092c6704d46cbe5bf8982680090e /chrome
parent2a026d42f29297f6d040eca9c8fa0efe96afa0ba (diff)
downloadchromium_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.mm7
-rw-r--r--chrome/browser/cocoa/objc_zombie.h34
-rw-r--r--chrome/browser/cocoa/objc_zombie.mm399
-rw-r--r--chrome/chrome_browser.gypi2
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',