summaryrefslogtreecommitdiffstats
path: root/chrome/browser/ui/cocoa/objc_zombie.mm
diff options
context:
space:
mode:
Diffstat (limited to 'chrome/browser/ui/cocoa/objc_zombie.mm')
-rw-r--r--chrome/browser/ui/cocoa/objc_zombie.mm414
1 files changed, 414 insertions, 0 deletions
diff --git a/chrome/browser/ui/cocoa/objc_zombie.mm b/chrome/browser/ui/cocoa/objc_zombie.mm
new file mode 100644
index 0000000..6802fd2
--- /dev/null
+++ b/chrome/browser/ui/cocoa/objc_zombie.mm
@@ -0,0 +1,414 @@
+// 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/ui/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/ui/cocoa/objc_method_swizzle.h"
+
+// Deallocated objects are re-classed as |CrZombie|. No superclass
+// because then the class would have to override many/most of the
+// inherited methods (|NSObject| is like a category magnet!).
+@interface CrZombie {
+ Class isa;
+}
+@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);
+}
+
+// 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.
+ // Setting |isa| rather than using |object_setClass()| because that
+ // function is implemented with a memory barrier. The runtime's
+ // |_internal_object_dispose()| (in objc-class.m) does this, so it
+ // should be safe (messaging free'd objects shouldn't be expected to
+ // be thread-safe in the first place).
+ if (size >= g_fatZombieSize) {
+ self->isa = g_fatZombieClass;
+ static_cast<CrFatZombie*>(self)->wasa = wasa;
+ } else {
+ self->isa = 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};
+
+ // Don't involve the lock when creating zombies without a treadmill.
+ if (g_zombieCount > 0) {
+ AutoLock pin(lock_);
+
+ // Check the count again in a thread-safe manner.
+ 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 ZombieObjectCrash(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 != NULL) {
+ 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(ERROR) << [aString UTF8String];
+
+ // This is how about:crash is implemented. Using instead of
+ // |DebugUtil::BreakDebugger()| or |LOG(FATAL)| to make the top of
+ // stack more immediately obvious in crash dumps.
+ int* zero = NULL;
+ *zero = 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));
+ // objc_getClass() so CrZombie doesn't need +class.
+ g_zombieClass = objc_getClass("CrZombie");
+ g_fatZombieClass = objc_getClass("CrFatZombie");
+ g_fatZombieSize = class_getInstanceSize(g_fatZombieClass);
+
+ if (!g_object_cxxDestruct || !g_originalDeallocIMP ||
+ !g_zombieClass || !g_fatZombieClass)
+ return NO;
+
+ initialized = YES;
+ return YES;
+}
+
+} // namespace
+
+@implementation CrZombie
+
+// The Objective-C runtime needs to be able to call this successfully.
++ (void)initialize {
+}
+
+// Any method not explicitly defined will end up here, forcing a
+// crash.
+- (id)forwardingTargetForSelector:(SEL)aSelector {
+ ZombieObjectCrash(self, aSelector, NULL);
+ return nil;
+}
+
+// Override a few methods often used for dynamic dispatch to log the
+// message the caller is attempting to send, rather than the utility
+// method being used to send it.
+- (BOOL)respondsToSelector:(SEL)aSelector {
+ ZombieObjectCrash(self, aSelector, _cmd);
+ return NO;
+}
+
+- (id)performSelector:(SEL)aSelector {
+ ZombieObjectCrash(self, aSelector, _cmd);
+ return nil;
+}
+
+- (id)performSelector:(SEL)aSelector withObject:(id)anObject {
+ ZombieObjectCrash(self, aSelector, _cmd);
+ return nil;
+}
+
+- (id)performSelector:(SEL)aSelector
+ withObject:(id)anObject
+ withObject:(id)anotherObject {
+ ZombieObjectCrash(self, aSelector, _cmd);
+ return nil;
+}
+
+- (void)performSelector:(SEL)aSelector
+ withObject:(id)anArgument
+ afterDelay:(NSTimeInterval)delay {
+ ZombieObjectCrash(self, aSelector, _cmd);
+}
+
+@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;
+
+ 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