| Index: chrome/browser/cocoa/objc_zombie.mm
|
| diff --git a/chrome/browser/cocoa/objc_zombie.mm b/chrome/browser/cocoa/objc_zombie.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..a9d906395a1666fd33624abaa8de42fd457687d9
|
| --- /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
|
|
|