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