diff options
author | shess@chromium.org <shess@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-09-06 17:25:15 +0000 |
---|---|---|
committer | shess@chromium.org <shess@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2011-09-06 17:25:15 +0000 |
commit | d02437b6e66f851f1b627418db1bb2e200497e3a (patch) | |
tree | b8db39af85ca5c8a56081538e3a36277fc9c2c65 /chrome/common/mac | |
parent | dceaa916f0c01a0d9f87ef77c91dc6bbce3b1979 (diff) | |
download | chromium_src-d02437b6e66f851f1b627418db1bb2e200497e3a.zip chromium_src-d02437b6e66f851f1b627418db1bb2e200497e3a.tar.gz chromium_src-d02437b6e66f851f1b627418db1bb2e200497e3a.tar.bz2 |
[Mac] Enable CrZombie for all processes.
For the browser process, this will be overridden by
-[BrowserCrApplication init] (which raises it).
BUG=94551
TEST=Developers don't complain about zombie objects in other processes.
Review URL: http://codereview.chromium.org/7826016
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@99754 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/common/mac')
-rw-r--r-- | chrome/common/mac/DEPS | 5 | ||||
-rw-r--r-- | chrome/common/mac/objc_method_swizzle.h | 28 | ||||
-rw-r--r-- | chrome/common/mac/objc_method_swizzle.mm | 58 | ||||
-rw-r--r-- | chrome/common/mac/objc_method_swizzle_unittest.mm | 76 | ||||
-rw-r--r-- | chrome/common/mac/objc_zombie.h | 47 | ||||
-rw-r--r-- | chrome/common/mac/objc_zombie.mm | 561 | ||||
-rw-r--r-- | chrome/common/mac/objc_zombie_unittest.mm | 134 |
7 files changed, 909 insertions, 0 deletions
diff --git a/chrome/common/mac/DEPS b/chrome/common/mac/DEPS index 77a551b..b0c96ed 100644 --- a/chrome/common/mac/DEPS +++ b/chrome/common/mac/DEPS @@ -1,3 +1,8 @@ include_rules = [ + # TODO(shess): http://crbug.com/95272 + # This dependency needed by objc_zombie.mm. Unfortunately, putting + # objc_zombie.mm in chrome/app calls into existence unspeakable horrors. + "+chrome/app", + "+third_party/mach_override", ] diff --git a/chrome/common/mac/objc_method_swizzle.h b/chrome/common/mac/objc_method_swizzle.h new file mode 100644 index 0000000..a6fd19a --- /dev/null +++ b/chrome/common/mac/objc_method_swizzle.h @@ -0,0 +1,28 @@ +// Copyright (c) 2011 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_COMMON_MAC_OBJC_METHOD_SWIZZLE_H_ +#define CHROME_COMMON_MAC_OBJC_METHOD_SWIZZLE_H_ +#pragma once + +#import <objc/objc-class.h> + +// You should think twice every single time you use anything from this +// namespace. +namespace ObjcEvilDoers { + +// This is similar to class_getInstanceMethod(), except that it +// returns NULL if |aClass| does not directly implement |aSelector|. +Method GetImplementedInstanceMethod(Class aClass, SEL aSelector); + +// Exchanges the implementation of |originalSelector| and +// |alternateSelector| within |aClass|. Both selectors must be +// implemented directly by |aClass|, not inherited. The IMP returned +// is for |originalSelector| (for purposes of forwarding). +IMP SwizzleImplementedInstanceMethods( + Class aClass, const SEL originalSelector, const SEL alternateSelector); + +} // namespace ObjcEvilDoers + +#endif // CHROME_COMMON_MAC_OBJC_METHOD_SWIZZLE_H_ diff --git a/chrome/common/mac/objc_method_swizzle.mm b/chrome/common/mac/objc_method_swizzle.mm new file mode 100644 index 0000000..4ed4df8 --- /dev/null +++ b/chrome/common/mac/objc_method_swizzle.mm @@ -0,0 +1,58 @@ +// Copyright (c) 2011 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/common/mac/objc_method_swizzle.h" + +#import "base/logging.h" +#import "base/memory/scoped_nsobject.h" + +namespace ObjcEvilDoers { + +Method GetImplementedInstanceMethod(Class aClass, SEL aSelector) { + Method method = NULL; + unsigned int methodCount = 0; + Method* methodList = class_copyMethodList(aClass, &methodCount); + if (methodList) { + for (unsigned int i = 0; i < methodCount; ++i) { + if (method_getName(methodList[i]) == aSelector) { + method = methodList[i]; + break; + } + } + free(methodList); + } + return method; +} + +IMP SwizzleImplementedInstanceMethods( + Class aClass, const SEL originalSelector, const SEL alternateSelector) { + // The methods must both be implemented by the target class, not + // inherited from a superclass. + Method original = GetImplementedInstanceMethod(aClass, originalSelector); + Method alternate = GetImplementedInstanceMethod(aClass, alternateSelector); + DCHECK(original); + DCHECK(alternate); + if (!original || !alternate) { + return NULL; + } + + // The argument and return types must match exactly. + const char* originalTypes = method_getTypeEncoding(original); + const char* alternateTypes = method_getTypeEncoding(alternate); + DCHECK(originalTypes); + DCHECK(alternateTypes); + DCHECK(0 == strcmp(originalTypes, alternateTypes)); + if (!originalTypes || !alternateTypes || + strcmp(originalTypes, alternateTypes)) { + return NULL; + } + + IMP ret = method_getImplementation(original); + if (ret) { + method_exchangeImplementations(original, alternate); + } + return ret; +} + +} // namespace ObjcEvilDoers diff --git a/chrome/common/mac/objc_method_swizzle_unittest.mm b/chrome/common/mac/objc_method_swizzle_unittest.mm new file mode 100644 index 0000000..261e24d --- /dev/null +++ b/chrome/common/mac/objc_method_swizzle_unittest.mm @@ -0,0 +1,76 @@ +// Copyright (c) 2011 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/common/mac/objc_method_swizzle.h" + +#include "base/memory/scoped_nsobject.h" +#include "testing/gtest/include/gtest/gtest.h" + +@interface ObjcMethodSwizzleTest : NSObject +- (id)self; + +- (NSInteger)one; +- (NSInteger)two; +@end + +@implementation ObjcMethodSwizzleTest : NSObject +- (id)self { + return [super self]; +} + +- (NSInteger)one { + return 1; +} +- (NSInteger)two { + return 2; +} +@end + +@interface ObjcMethodSwizzleTest (ObjcMethodSwizzleTestCategory) +- (NSUInteger)hash; +@end + +@implementation ObjcMethodSwizzleTest (ObjcMethodSwizzleTestCategory) +- (NSUInteger)hash { + return [super hash]; +} +@end + +namespace ObjcEvilDoers { + +TEST(ObjcMethodSwizzleTest, GetImplementedInstanceMethod) { + EXPECT_EQ(class_getInstanceMethod([NSObject class], @selector(dealloc)), + GetImplementedInstanceMethod([NSObject class], @selector(dealloc))); + EXPECT_EQ(class_getInstanceMethod([NSObject class], @selector(self)), + GetImplementedInstanceMethod([NSObject class], @selector(self))); + EXPECT_EQ(class_getInstanceMethod([NSObject class], @selector(hash)), + GetImplementedInstanceMethod([NSObject class], @selector(hash))); + + Class testClass = [ObjcMethodSwizzleTest class]; + EXPECT_EQ(class_getInstanceMethod(testClass, @selector(self)), + GetImplementedInstanceMethod(testClass, @selector(self))); + EXPECT_NE(class_getInstanceMethod([NSObject class], @selector(self)), + class_getInstanceMethod(testClass, @selector(self))); + + EXPECT_TRUE(class_getInstanceMethod(testClass, @selector(dealloc))); + EXPECT_FALSE(GetImplementedInstanceMethod(testClass, @selector(dealloc))); +} + +TEST(ObjcMethodSwizzleTest, SwizzleImplementedInstanceMethods) { + scoped_nsobject<ObjcMethodSwizzleTest> object( + [[ObjcMethodSwizzleTest alloc] init]); + EXPECT_EQ([object one], 1); + EXPECT_EQ([object two], 2); + + Class testClass = [object class]; + SwizzleImplementedInstanceMethods(testClass, @selector(one), @selector(two)); + EXPECT_EQ([object one], 2); + EXPECT_EQ([object two], 1); + + SwizzleImplementedInstanceMethods(testClass, @selector(one), @selector(two)); + EXPECT_EQ([object one], 1); + EXPECT_EQ([object two], 2); +} + +} // namespace ObjcEvilDoers diff --git a/chrome/common/mac/objc_zombie.h b/chrome/common/mac/objc_zombie.h new file mode 100644 index 0000000..cca3694 --- /dev/null +++ b/chrome/common/mac/objc_zombie.h @@ -0,0 +1,47 @@ +// Copyright (c) 2011 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_COMMON_MAC_OBJC_ZOMBIE_H_ +#define CHROME_COMMON_MAC_OBJC_ZOMBIE_H_ +#pragma once + +#include "base/basictypes.h" + +// You should think twice every single time you use anything from this +// namespace. +namespace ObjcEvilDoers { + +// Enable zombie object debugging. This implements a variant of Apple's +// NSZombieEnabled which can help expose use-after-free errors where messages +// are sent to freed Objective-C objects in production builds. +// +// 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 + +#if defined(OS_MACOSX) +#if defined(__OBJC__) + +#import <Foundation/Foundation.h> + +@interface NSObject (CrZombie) +- (BOOL)shouldBecomeCrZombie; +@end + +#endif // __OBJC__ +#endif // OS_MACOSX + +#endif // CHROME_COMMON_MAC_OBJC_ZOMBIE_H_ diff --git a/chrome/common/mac/objc_zombie.mm b/chrome/common/mac/objc_zombie.mm new file mode 100644 index 0000000..c60981b --- /dev/null +++ b/chrome/common/mac/objc_zombie.mm @@ -0,0 +1,561 @@ +// Copyright (c) 2011 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/common/mac/objc_zombie.h" + +#include <dlfcn.h> +#include <execinfo.h> +#include <mach-o/dyld.h> +#include <mach-o/nlist.h> + +#import <objc/objc-class.h> + +#include <algorithm> +#include <iostream> + +#include "base/debug/stack_trace.h" +#include "base/logging.h" +#include "base/mac/mac_util.h" +#include "base/metrics/histogram.h" +#include "base/synchronization/lock.h" +#import "chrome/app/breakpad_mac.h" +#import "chrome/common/mac/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 { + +// The depth of backtrace to store with zombies. This directly influences +// the amount of memory required to track zombies, so should be kept as +// small as is useful. Unfortunately, too small and it won't poke through +// deep autorelease and event loop stacks. +// NOTE(shess): Breakpad currently restricts values to 255 bytes. The +// trace is hex-encoded with "0x" prefix and " " separators, meaning +// the maximum number of 32-bit items which can be encoded is 23. +const size_t kBacktraceDepth = 20; + +// Function which destroys the contents of an object without freeing +// the object. On 10.5 this is |object_cxxDestruct()|, which +// traverses the class hierarchy to run the C++ destructors. On 10.6 +// this is |objc_destructInstance()| which calls +// |object_cxxDestruct()| and removes associative references. +// |objc_destructInstance()| returns |void*| but pretending it has no +// return value makes the code simpler. +typedef void DestructFn(id obj); +DestructFn* g_objectDestruct = 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|. +base::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. + void* trace[kBacktraceDepth]; // Backtrace at point of deallocation. + size_t traceDepth; // Actual depth of trace[]. +} ZombieRecord; + +ZombieRecord* g_zombies = NULL; + +const char* LookupObjcRuntimePath() { + const void* addr = reinterpret_cast<void*>(&object_getClass); + Dl_info info; + + // |dladdr()| doesn't document how long |info| will stay valid... + if (dladdr(addr, &info)) + return info.dli_fname; + + return NULL; +} + +// Lookup |objc_destructInstance()| dynamically because it isn't +// available on 10.5, but we link with the 10.5 SDK. +DestructFn* LookupObjectDestruct_10_6() { + const char* objc_runtime_path = LookupObjcRuntimePath(); + if (!objc_runtime_path) + return NULL; + + void* handle = dlopen(objc_runtime_path, RTLD_LAZY | RTLD_LOCAL); + if (!handle) + return NULL; + + void* fn = dlsym(handle, "objc_destructInstance"); + + // |fn| would normally be expected to become invalid after this + // |dlclose()|, but since the |dlopen()| was on a library + // containing an already-mapped symbol, it will remain valid. + dlclose(handle); + return reinterpret_cast<DestructFn*>(fn); +} + +// Under 10.5 |object_cxxDestruct()| is used but unfortunately it is +// |__private_extern__| in the runtime, meaning |dlsym()| cannot reach it. +DestructFn* LookupObjectDestruct_10_5() { + // |nlist()| is only present for 32-bit. +#if ARCH_CPU_32_BITS + const char* objc_runtime_path = LookupObjcRuntimePath(); + if (!objc_runtime_path) + return NULL; + + struct nlist nl[3]; + bzero(&nl, sizeof(nl)); + + nl[0].n_un.n_name = const_cast<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 = const_cast<char*>("_class_addIvar"); + + if (nlist(objc_runtime_path, nl) < 0 || + nl[0].n_type == N_UNDF || nl[1].n_type == N_UNDF) + return NULL; + + // Back out offset to |class_addIvar()| to get the baseline, then + // add back offset to |object_cxxDestruct()|. + uintptr_t reference_addr = reinterpret_cast<uintptr_t>(&class_addIvar); + reference_addr -= nl[1].n_value; + reference_addr += nl[0].n_value; + + return reinterpret_cast<DestructFn*>(reference_addr); +#endif + + return NULL; +} + +// 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 |g_objectDestruct| 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_objectDestruct); + if (!g_objectDestruct) { + 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. + // NOTE(shess): |object_dispose()| will call this again when the + // zombie falls off the treadmill! But by then |isa| will be a + // class without C++ destructors or associative references, so it + // won't hurt anything. + (*g_objectDestruct)(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}; + zombieToFree.traceDepth = + std::max(backtrace(zombieToFree.trace, kBacktraceDepth), 0); + + // Don't involve the lock when creating zombies without a treadmill. + if (g_zombieCount > 0) { + base::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) + object_dispose(zombieToFree.object); +} + +// Search the treadmill for |object| and fill in |*record| if found. +// Returns YES if found. +BOOL GetZombieRecord(id object, ZombieRecord* record) { + // Holding the lock is reasonable because this should be fast, and + // the process is going to crash presently anyhow. + base::AutoLock pin(lock_); + for (size_t i = 0; i < g_zombieCount; ++i) { + if (g_zombies[i].object == object) { + *record = g_zombies[i]; + return YES; + } + } + return NO; +} + +// Dump the symbols. This is pulled out into a function to make it +// easy to use DCHECK to dump only in debug builds. +BOOL DumpDeallocTrace(const void* const* array, int size) { + // |cerr| because that's where PrintBacktrace() sends output. + std::cerr << "Backtrace from -dealloc:\n"; + base::debug::StackTrace(array, size).PrintBacktrace(); + + return YES; +} + +// 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) { + ZombieRecord record; + BOOL found = GetZombieRecord(object, &record); + + // The object's class can be in the zombie record, but if that is + // not available it can also be in the object itself (in most cases). + Class wasa = Nil; + if (found) { + wasa = record.wasa; + } else if (object_getClass(object) == g_fatZombieClass) { + wasa = static_cast<CrFatZombie*>(object)->wasa; + } + 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. + SetCrashKeyValue(@"zombie", aString); + + // Hex-encode the backtrace and tuck it into a breakpad key. + NSString* deallocTrace = @"<unknown>"; + if (found && record.traceDepth) { + NSMutableArray* hexBacktrace = + [NSMutableArray arrayWithCapacity:record.traceDepth]; + for (size_t i = 0; i < record.traceDepth; ++i) { + NSString* s = [NSString stringWithFormat:@"%p", record.trace[i]]; + [hexBacktrace addObject:s]; + } + deallocTrace = [hexBacktrace componentsJoinedByString:@" "]; + + // Warn someone if this exceeds the breakpad limits. + DCHECK_LE(strlen([deallocTrace UTF8String]), 255U); + } + SetCrashKeyValue(@"zombie_dealloc_bt", deallocTrace); + + // Log -dealloc backtrace in debug builds then crash with a useful + // stack trace. + if (found && record.traceDepth) { + DCHECK(DumpDeallocTrace(record.trace, record.traceDepth)); + } else { + DLOG(INFO) << "Unable to generate backtrace from -dealloc."; + } + DLOG(FATAL) << [aString UTF8String]; + + // This is how about:crash is implemented. Using instead of + // |base::debug::BreakDebugger()| or |LOG(FATAL)| to make the top of + // stack more immediately obvious in crash dumps. + int* zero = NULL; + *zero = 0; +} + +// For monitoring failures in |ZombieInit()|. +enum ZombieFailure { + FAILED_10_5, + FAILED_10_6, + + // Add new versions before here. + FAILED_MAX, +}; + +void RecordZombieFailure(ZombieFailure failure) { + UMA_HISTOGRAM_ENUMERATION("OSX.ZombieInitFailure", failure, FAILED_MAX); +} + +// Initialize our globals, returning YES on success. +BOOL ZombieInit() { + static BOOL initialized = NO; + if (initialized) + return YES; + + // Whitelist releases that are compatible with objc zombies. + if (base::mac::IsOSLeopard()) { + g_objectDestruct = LookupObjectDestruct_10_5(); + if (!g_objectDestruct) { + RecordZombieFailure(FAILED_10_5); + return NO; + } + } else if (base::mac::IsOSSnowLeopard()) { + g_objectDestruct = LookupObjectDestruct_10_6(); + if (!g_objectDestruct) { + RecordZombieFailure(FAILED_10_6); + return NO; + } + } else if (base::mac::IsOSLionOrLater()) { + // Assume the future looks like the present. + g_objectDestruct = LookupObjectDestruct_10_6(); + + // Put all future failures into the MAX bin. New OS releases come + // out infrequently enough that this should always correspond to + // "Next release", and once the next release happens that bin will + // get an official name. + if (!g_objectDestruct) { + RecordZombieFailure(FAILED_MAX); + return NO; + } + } else { + return NO; + } + + Class rootClass = [NSObject class]; + 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_objectDestruct || !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 false; + + g_zombieAllObjects = zombieAllObjects; + + // Replace the implementation of -[NSObject dealloc]. + Method m = class_getInstanceMethod([NSObject class], @selector(dealloc)); + if (!m) + return false; + + 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; + + { + base::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 false; + } + } + + // 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) + object_dispose(oldZombies[i].object); + } + free(oldZombies); + } + + return true; +} + +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; + + { + base::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) + object_dispose(oldZombies[i].object); + } + free(oldZombies); + } +} + +} // namespace ObjcEvilDoers diff --git a/chrome/common/mac/objc_zombie_unittest.mm b/chrome/common/mac/objc_zombie_unittest.mm new file mode 100644 index 0000000..df91d53 --- /dev/null +++ b/chrome/common/mac/objc_zombie_unittest.mm @@ -0,0 +1,134 @@ +// Copyright (c) 2011 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 <Cocoa/Cocoa.h> +#include <dlfcn.h> + +#include "base/logging.h" +#import "base/memory/scoped_nsobject.h" +#import "chrome/common/mac/objc_zombie.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/platform_test.h" + +namespace { + +// Dynamically look up |objc_setAssociatedObject()|, which isn't +// available until the 10.6 SDK. + +typedef void objc_setAssociatedObjectFn(id object, void *key, id value, + int policy); +objc_setAssociatedObjectFn* LookupSetAssociatedObjectFn() { + return reinterpret_cast<objc_setAssociatedObjectFn*>( + dlsym(RTLD_DEFAULT, "objc_setAssociatedObject")); +} + +} // namespace + +@interface ZombieCxxDestructTest : NSObject +{ + scoped_nsobject<id> aRef_; +} +- (id)initWith:(id)anObject; +@end + +@implementation ZombieCxxDestructTest +- (id)initWith:(id)anObject { + self = [super init]; + if (self) { + aRef_.reset([anObject retain]); + } + return self; +} +@end + +@interface ZombieAssociatedObjectTest : NSObject ++ (BOOL)supportsAssociatedObjects; +- (id)initWithAssociatedObject:(id)anObject; +@end + +@implementation ZombieAssociatedObjectTest + ++ (BOOL)supportsAssociatedObjects { + if (LookupSetAssociatedObjectFn()) + return YES; + return NO; +} + +- (id)initWithAssociatedObject:(id)anObject { + self = [super init]; + if (self) { + objc_setAssociatedObjectFn* fn = LookupSetAssociatedObjectFn(); + if (fn) { + // Cribbed from 10.6 <objc/runtime.h>. + static const int kObjcAssociationRetain = 01401; + + // The address of the variable itself is the unique key, the + // contents don't matter. + static char kAssociatedObjectKey = 'x'; + + (*fn)(self, &kAssociatedObjectKey, anObject, kObjcAssociationRetain); + } + } + return self; +} + +@end + +namespace { + +// Verify that the C++ destructors run when the last reference to the +// object is released. +// NOTE(shess): To test the negative, comment out the |g_objectDestruct()| +// call in |ZombieDealloc()|. +TEST(ObjcZombieTest, CxxDestructors) { + scoped_nsobject<id> anObject([[NSObject alloc] init]); + EXPECT_EQ(1u, [anObject retainCount]); + + ASSERT_TRUE(ObjcEvilDoers::ZombieEnable(YES, 100)); + + scoped_nsobject<ZombieCxxDestructTest> soonInfected( + [[ZombieCxxDestructTest alloc] initWith:anObject]); + EXPECT_EQ(2u, [anObject retainCount]); + + // When |soonInfected| becomes a zombie, the C++ destructors should + // run and release a reference to |anObject|. + soonInfected.reset(); + EXPECT_EQ(1u, [anObject retainCount]); + + // The local reference should remain (C++ destructors aren't re-run). + ObjcEvilDoers::ZombieDisable(); + EXPECT_EQ(1u, [anObject retainCount]); +} + +// Verify that the associated objects are released when the object is +// released. +// NOTE(shess): To test the negative, hardcode |g_objectDestruct| to +// the 10.5 version in |ZombieInit()|, and run this test on 10.6. +TEST(ObjcZombieTest, AssociatedObjectsReleased) { + if (![ZombieAssociatedObjectTest supportsAssociatedObjects]) { + LOG(ERROR) + << "ObjcZombieTest.AssociatedObjectsReleased not supported on 10.5"; + return; + } + + scoped_nsobject<id> anObject([[NSObject alloc] init]); + EXPECT_EQ(1u, [anObject retainCount]); + + ASSERT_TRUE(ObjcEvilDoers::ZombieEnable(YES, 100)); + + scoped_nsobject<ZombieAssociatedObjectTest> soonInfected( + [[ZombieAssociatedObjectTest alloc] initWithAssociatedObject:anObject]); + EXPECT_EQ(2u, [anObject retainCount]); + + // When |soonInfected| becomes a zombie, the associated object + // should be released. + soonInfected.reset(); + EXPECT_EQ(1u, [anObject retainCount]); + + // The local reference should remain (associated objects not re-released). + ObjcEvilDoers::ZombieDisable(); + EXPECT_EQ(1u, [anObject retainCount]); +} + +} // namespace |