Home | History | Annotate | Download | only in cocoa
      1 // Copyright (c) 2010 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #import "chrome/browser/ui/cocoa/objc_zombie.h"
      6 
      7 #include <dlfcn.h>
      8 #include <mach-o/dyld.h>
      9 #include <mach-o/nlist.h>
     10 
     11 #import <objc/objc-class.h>
     12 
     13 #include "base/logging.h"
     14 #include "base/synchronization/lock.h"
     15 #import "chrome/app/breakpad_mac.h"
     16 #import "chrome/browser/ui/cocoa/objc_method_swizzle.h"
     17 
     18 // Deallocated objects are re-classed as |CrZombie|.  No superclass
     19 // because then the class would have to override many/most of the
     20 // inherited methods (|NSObject| is like a category magnet!).
     21 @interface CrZombie {
     22   Class isa;
     23 }
     24 @end
     25 
     26 // Objects with enough space are made into "fat" zombies, which
     27 // directly remember which class they were until reallocated.
     28 @interface CrFatZombie : CrZombie {
     29  @public
     30   Class wasa;
     31 }
     32 @end
     33 
     34 namespace {
     35 
     36 // |object_cxxDestruct()| is an Objective-C runtime function which
     37 // traverses the object's class tree for ".cxxdestruct" methods which
     38 // are run to call C++ destructors as part of |-dealloc|.  The
     39 // function is not public, so must be looked up using nlist.
     40 typedef void DestructFn(id obj);
     41 DestructFn* g_object_cxxDestruct = NULL;
     42 
     43 // The original implementation for |-[NSObject dealloc]|.
     44 IMP g_originalDeallocIMP = NULL;
     45 
     46 // Classes which freed objects become.  |g_fatZombieSize| is the
     47 // minimum object size which can be made into a fat zombie (which can
     48 // remember which class it was before free, even after falling off the
     49 // treadmill).
     50 Class g_zombieClass = Nil;  // cached [CrZombie class]
     51 Class g_fatZombieClass = Nil;  // cached [CrFatZombie class]
     52 size_t g_fatZombieSize = 0;
     53 
     54 // Whether to zombie all freed objects, or only those which return YES
     55 // from |-shouldBecomeCrZombie|.
     56 BOOL g_zombieAllObjects = NO;
     57 
     58 // Protects |g_zombieCount|, |g_zombieIndex|, and |g_zombies|.
     59 base::Lock lock_;
     60 
     61 // How many zombies to keep before freeing, and the current head of
     62 // the circular buffer.
     63 size_t g_zombieCount = 0;
     64 size_t g_zombieIndex = 0;
     65 
     66 typedef struct {
     67   id object;   // The zombied object.
     68   Class wasa;  // Value of |object->isa| before we replaced it.
     69 } ZombieRecord;
     70 
     71 ZombieRecord* g_zombies = NULL;
     72 
     73 // Lookup the private |object_cxxDestruct| function and return a
     74 // pointer to it.  Returns |NULL| on failure.
     75 DestructFn* LookupObjectCxxDestruct() {
     76 #if ARCH_CPU_64_BITS
     77   // TODO(shess): Port to 64-bit.  I believe using struct nlist_64
     78   // will suffice.  http://crbug.com/44021 .
     79   NOTIMPLEMENTED();
     80   return NULL;
     81 #endif
     82 
     83   struct nlist nl[3];
     84   bzero(&nl, sizeof(nl));
     85 
     86   nl[0].n_un.n_name = (char*)"_object_cxxDestruct";
     87 
     88   // My ability to calculate the base for offsets is apparently poor.
     89   // Use |class_addIvar| as a known reference point.
     90   nl[1].n_un.n_name = (char*)"_class_addIvar";
     91 
     92   if (nlist("/usr/lib/libobjc.dylib", nl) < 0 ||
     93       nl[0].n_type == N_UNDF || nl[1].n_type == N_UNDF)
     94     return NULL;
     95 
     96   return (DestructFn*)((char*)&class_addIvar - nl[1].n_value + nl[0].n_value);
     97 }
     98 
     99 // Replacement |-dealloc| which turns objects into zombies and places
    100 // them into |g_zombies| to be freed later.
    101 void ZombieDealloc(id self, SEL _cmd) {
    102   // This code should only be called when it is implementing |-dealloc|.
    103   DCHECK_EQ(_cmd, @selector(dealloc));
    104 
    105   // Use the original |-dealloc| if the object doesn't wish to be
    106   // zombied.
    107   if (!g_zombieAllObjects && ![self shouldBecomeCrZombie]) {
    108     g_originalDeallocIMP(self, _cmd);
    109     return;
    110   }
    111 
    112   // Use the original |-dealloc| if |object_cxxDestruct| was never
    113   // initialized, because otherwise C++ destructors won't be called.
    114   // This case should be impossible, but doing it wrong would cause
    115   // terrible problems.
    116   DCHECK(g_object_cxxDestruct);
    117   if (!g_object_cxxDestruct) {
    118     g_originalDeallocIMP(self, _cmd);
    119     return;
    120   }
    121 
    122   Class wasa = object_getClass(self);
    123   const size_t size = class_getInstanceSize(wasa);
    124 
    125   // Destroy the instance by calling C++ destructors and clearing it
    126   // to something unlikely to work well if someone references it.
    127   (*g_object_cxxDestruct)(self);
    128   memset(self, '!', size);
    129 
    130   // If the instance is big enough, make it into a fat zombie and have
    131   // it remember the old |isa|.  Otherwise make it a regular zombie.
    132   // Setting |isa| rather than using |object_setClass()| because that
    133   // function is implemented with a memory barrier.  The runtime's
    134   // |_internal_object_dispose()| (in objc-class.m) does this, so it
    135   // should be safe (messaging free'd objects shouldn't be expected to
    136   // be thread-safe in the first place).
    137   if (size >= g_fatZombieSize) {
    138     self->isa = g_fatZombieClass;
    139     static_cast<CrFatZombie*>(self)->wasa = wasa;
    140   } else {
    141     self->isa = g_zombieClass;
    142   }
    143 
    144   // The new record to swap into |g_zombies|.  If |g_zombieCount| is
    145   // zero, then |self| will be freed immediately.
    146   ZombieRecord zombieToFree = {self, wasa};
    147 
    148   // Don't involve the lock when creating zombies without a treadmill.
    149   if (g_zombieCount > 0) {
    150     base::AutoLock pin(lock_);
    151 
    152     // Check the count again in a thread-safe manner.
    153     if (g_zombieCount > 0) {
    154       // Put the current object on the treadmill and keep the previous
    155       // occupant.
    156       std::swap(zombieToFree, g_zombies[g_zombieIndex]);
    157 
    158       // Bump the index forward.
    159       g_zombieIndex = (g_zombieIndex + 1) % g_zombieCount;
    160     }
    161   }
    162 
    163   // Do the free out here to prevent any chance of deadlock.
    164   if (zombieToFree.object)
    165     free(zombieToFree.object);
    166 }
    167 
    168 // Attempt to determine the original class of zombie |object|.
    169 Class ZombieWasa(id object) {
    170   // Fat zombies can hold onto their |wasa| past the point where the
    171   // object was actually freed.  Note that to arrive here at all,
    172   // |object|'s memory must still be accessible.
    173   if (object_getClass(object) == g_fatZombieClass)
    174     return static_cast<CrFatZombie*>(object)->wasa;
    175 
    176   // For instances which weren't big enough to store |wasa|, check if
    177   // the object is still on the treadmill.
    178   base::AutoLock pin(lock_);
    179   for (size_t i=0; i < g_zombieCount; ++i) {
    180     if (g_zombies[i].object == object)
    181       return g_zombies[i].wasa;
    182   }
    183 
    184   return Nil;
    185 }
    186 
    187 // Log a message to a freed object.  |wasa| is the object's original
    188 // class.  |aSelector| is the selector which the calling code was
    189 // attempting to send.  |viaSelector| is the selector of the
    190 // dispatch-related method which is being invoked to send |aSelector|
    191 // (for instance, -respondsToSelector:).
    192 void ZombieObjectCrash(id object, SEL aSelector, SEL viaSelector) {
    193   Class wasa = ZombieWasa(object);
    194   const char* wasaName = (wasa ? class_getName(wasa) : "<unknown>");
    195   NSString* aString =
    196       [NSString stringWithFormat:@"Zombie <%s: %p> received -%s",
    197                                  wasaName, object, sel_getName(aSelector)];
    198   if (viaSelector != NULL) {
    199     const char* viaName = sel_getName(viaSelector);
    200     aString = [aString stringByAppendingFormat:@" (via -%s)", viaName];
    201   }
    202 
    203   // Set a value for breakpad to report, then crash.
    204   SetCrashKeyValue(@"zombie", aString);
    205   LOG(ERROR) << [aString UTF8String];
    206 
    207   // This is how about:crash is implemented.  Using instead of
    208   // |baes::debug::BreakDebugger()| or |LOG(FATAL)| to make the top of
    209   // stack more immediately obvious in crash dumps.
    210   int* zero = NULL;
    211   *zero = 0;
    212 }
    213 
    214 // Initialize our globals, returning YES on success.
    215 BOOL ZombieInit() {
    216   static BOOL initialized = NO;
    217   if (initialized)
    218     return YES;
    219 
    220   Class rootClass = [NSObject class];
    221 
    222   g_object_cxxDestruct = LookupObjectCxxDestruct();
    223   g_originalDeallocIMP =
    224       class_getMethodImplementation(rootClass, @selector(dealloc));
    225   // objc_getClass() so CrZombie doesn't need +class.
    226   g_zombieClass = objc_getClass("CrZombie");
    227   g_fatZombieClass = objc_getClass("CrFatZombie");
    228   g_fatZombieSize = class_getInstanceSize(g_fatZombieClass);
    229 
    230   if (!g_object_cxxDestruct || !g_originalDeallocIMP ||
    231       !g_zombieClass || !g_fatZombieClass)
    232     return NO;
    233 
    234   initialized = YES;
    235   return YES;
    236 }
    237 
    238 }  // namespace
    239 
    240 @implementation CrZombie
    241 
    242 // The Objective-C runtime needs to be able to call this successfully.
    243 + (void)initialize {
    244 }
    245 
    246 // Any method not explicitly defined will end up here, forcing a
    247 // crash.
    248 - (id)forwardingTargetForSelector:(SEL)aSelector {
    249   ZombieObjectCrash(self, aSelector, NULL);
    250   return nil;
    251 }
    252 
    253 // Override a few methods often used for dynamic dispatch to log the
    254 // message the caller is attempting to send, rather than the utility
    255 // method being used to send it.
    256 - (BOOL)respondsToSelector:(SEL)aSelector {
    257   ZombieObjectCrash(self, aSelector, _cmd);
    258   return NO;
    259 }
    260 
    261 - (id)performSelector:(SEL)aSelector {
    262   ZombieObjectCrash(self, aSelector, _cmd);
    263   return nil;
    264 }
    265 
    266 - (id)performSelector:(SEL)aSelector withObject:(id)anObject {
    267   ZombieObjectCrash(self, aSelector, _cmd);
    268   return nil;
    269 }
    270 
    271 - (id)performSelector:(SEL)aSelector
    272            withObject:(id)anObject
    273            withObject:(id)anotherObject {
    274   ZombieObjectCrash(self, aSelector, _cmd);
    275   return nil;
    276 }
    277 
    278 - (void)performSelector:(SEL)aSelector
    279              withObject:(id)anArgument
    280              afterDelay:(NSTimeInterval)delay {
    281   ZombieObjectCrash(self, aSelector, _cmd);
    282 }
    283 
    284 @end
    285 
    286 @implementation CrFatZombie
    287 
    288 // This implementation intentionally left empty.
    289 
    290 @end
    291 
    292 @implementation NSObject (CrZombie)
    293 
    294 - (BOOL)shouldBecomeCrZombie {
    295   return NO;
    296 }
    297 
    298 @end
    299 
    300 namespace ObjcEvilDoers {
    301 
    302 BOOL ZombieEnable(BOOL zombieAllObjects,
    303                   size_t zombieCount) {
    304   // Only allow enable/disable on the main thread, just to keep things
    305   // simple.
    306   CHECK([NSThread isMainThread]);
    307 
    308   if (!ZombieInit())
    309     return NO;
    310 
    311   g_zombieAllObjects = zombieAllObjects;
    312 
    313   // Replace the implementation of -[NSObject dealloc].
    314   Method m = class_getInstanceMethod([NSObject class], @selector(dealloc));
    315   if (!m)
    316     return NO;
    317 
    318   const IMP prevDeallocIMP = method_setImplementation(m, (IMP)ZombieDealloc);
    319   DCHECK(prevDeallocIMP == g_originalDeallocIMP ||
    320          prevDeallocIMP == (IMP)ZombieDealloc);
    321 
    322   // Grab the current set of zombies.  This is thread-safe because
    323   // only the main thread can change these.
    324   const size_t oldCount = g_zombieCount;
    325   ZombieRecord* oldZombies = g_zombies;
    326 
    327   {
    328     base::AutoLock pin(lock_);
    329 
    330     // Save the old index in case zombies need to be transferred.
    331     size_t oldIndex = g_zombieIndex;
    332 
    333     // Create the new zombie treadmill, disabling zombies in case of
    334     // failure.
    335     g_zombieIndex = 0;
    336     g_zombieCount = zombieCount;
    337     g_zombies = NULL;
    338     if (g_zombieCount) {
    339       g_zombies =
    340           static_cast<ZombieRecord*>(calloc(g_zombieCount, sizeof(*g_zombies)));
    341       if (!g_zombies) {
    342         NOTREACHED();
    343         g_zombies = oldZombies;
    344         g_zombieCount = oldCount;
    345         g_zombieIndex = oldIndex;
    346         ZombieDisable();
    347         return NO;
    348       }
    349     }
    350 
    351     // If the count is changing, allow some of the zombies to continue
    352     // shambling forward.
    353     const size_t sharedCount = std::min(oldCount, zombieCount);
    354     if (sharedCount) {
    355       // Get index of the first shared zombie.
    356       oldIndex = (oldIndex + oldCount - sharedCount) % oldCount;
    357 
    358       for (; g_zombieIndex < sharedCount; ++ g_zombieIndex) {
    359         DCHECK_LT(g_zombieIndex, g_zombieCount);
    360         DCHECK_LT(oldIndex, oldCount);
    361         std::swap(g_zombies[g_zombieIndex], oldZombies[oldIndex]);
    362         oldIndex = (oldIndex + 1) % oldCount;
    363       }
    364       g_zombieIndex %= g_zombieCount;
    365     }
    366   }
    367 
    368   // Free the old treadmill and any remaining zombies.
    369   if (oldZombies) {
    370     for (size_t i = 0; i < oldCount; ++i) {
    371       if (oldZombies[i].object)
    372         free(oldZombies[i].object);
    373     }
    374     free(oldZombies);
    375   }
    376 
    377   return YES;
    378 }
    379 
    380 void ZombieDisable() {
    381   // Only allow enable/disable on the main thread, just to keep things
    382   // simple.
    383   CHECK([NSThread isMainThread]);
    384 
    385   // |ZombieInit()| was never called.
    386   if (!g_originalDeallocIMP)
    387     return;
    388 
    389   // Put back the original implementation of -[NSObject dealloc].
    390   Method m = class_getInstanceMethod([NSObject class], @selector(dealloc));
    391   CHECK(m);
    392   method_setImplementation(m, g_originalDeallocIMP);
    393 
    394   // Can safely grab this because it only happens on the main thread.
    395   const size_t oldCount = g_zombieCount;
    396   ZombieRecord* oldZombies = g_zombies;
    397 
    398   {
    399     base::AutoLock pin(lock_);  // In case any |-dealloc| are in-progress.
    400     g_zombieCount = 0;
    401     g_zombies = NULL;
    402   }
    403 
    404   // Free any remaining zombies.
    405   if (oldZombies) {
    406     for (size_t i = 0; i < oldCount; ++i) {
    407       if (oldZombies[i].object)
    408         free(oldZombies[i].object);
    409     }
    410     free(oldZombies);
    411   }
    412 }
    413 
    414 }  // namespace ObjcEvilDoers
    415