summaryrefslogtreecommitdiffstats
path: root/chrome/common/mac
diff options
context:
space:
mode:
authorshess@chromium.org <shess@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-09-06 17:25:15 +0000
committershess@chromium.org <shess@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2011-09-06 17:25:15 +0000
commitd02437b6e66f851f1b627418db1bb2e200497e3a (patch)
treeb8db39af85ca5c8a56081538e3a36277fc9c2c65 /chrome/common/mac
parentdceaa916f0c01a0d9f87ef77c91dc6bbce3b1979 (diff)
downloadchromium_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/DEPS5
-rw-r--r--chrome/common/mac/objc_method_swizzle.h28
-rw-r--r--chrome/common/mac/objc_method_swizzle.mm58
-rw-r--r--chrome/common/mac/objc_method_swizzle_unittest.mm76
-rw-r--r--chrome/common/mac/objc_zombie.h47
-rw-r--r--chrome/common/mac/objc_zombie.mm561
-rw-r--r--chrome/common/mac/objc_zombie_unittest.mm134
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