| 1 |
// Copyright (c) 2012 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/common/mac/objc_zombie.h"
|
| 6 |
|
| 7 |
#include <AvailabilityMacros.h>
|
| 8 |
|
| 9 |
#include <execinfo.h>
|
| 10 |
#import <objc/runtime.h>
|
| 11 |
|
| 12 |
#include <algorithm>
|
| 13 |
|
| 14 |
#include "base/debug/crash_logging.h"
|
| 15 |
#include "base/debug/stack_trace.h"
|
| 16 |
#include "base/lazy_instance.h"
|
| 17 |
#include "base/logging.h"
|
| 18 |
#include "base/posix/eintr_wrapper.h"
|
| 19 |
#include "base/strings/stringprintf.h"
|
| 20 |
#include "base/synchronization/lock.h"
|
| 21 |
#include "chrome/common/crash_keys.h"
|
| 22 |
|
| 23 |
#if !defined(OS_IOS) && (MAC_OS_X_VERSION_MAX_ALLOWED <= MAC_OS_X_VERSION_10_6)
|
| 24 |
// Apparently objc/runtime.h doesn't define this with the 10.6 SDK yet.
|
| 25 |
// The docs say it exists since 10.6 however.
|
| 26 |
OBJC_EXPORT void *objc_destructInstance(id obj);
|
| 27 |
#endif
|
| 28 |
|
| 29 |
// Deallocated objects are re-classed as |CrZombie|. No superclass
|
| 30 |
// because then the class would have to override many/most of the
|
| 31 |
// inherited methods (|NSObject| is like a category magnet!).
|
| 32 |
// Without the __attribute__, clang's -Wobjc-root-class warns on the missing
|
| 33 |
// superclass.
|
| 34 |
__attribute__((objc_root_class))
|
| 35 |
@interface CrZombie {
|
| 36 |
Class isa;
|
| 37 |
}
|
| 38 |
@end
|
| 39 |
|
| 40 |
// Objects with enough space are made into "fat" zombies, which
|
| 41 |
// directly remember which class they were until reallocated.
|
| 42 |
@interface CrFatZombie : CrZombie {
|
| 43 |
@public
|
| 44 |
Class wasa;
|
| 45 |
}
|
| 46 |
@end
|
| 47 |
|
| 48 |
namespace {
|
| 49 |
|
| 50 |
// The depth of backtrace to store with zombies. This directly influences
|
| 51 |
// the amount of memory required to track zombies, so should be kept as
|
| 52 |
// small as is useful. Unfortunately, too small and it won't poke through
|
| 53 |
// deep autorelease and event loop stacks.
|
| 54 |
// NOTE(shess): Breakpad currently restricts values to 255 bytes. The
|
| 55 |
// trace is hex-encoded with "0x" prefix and " " separators, meaning
|
| 56 |
// the maximum number of 32-bit items which can be encoded is 23.
|
| 57 |
const size_t kBacktraceDepth = 20;
|
| 58 |
|
| 59 |
// The original implementation for |-[NSObject dealloc]|.
|
| 60 |
IMP g_originalDeallocIMP = NULL;
|
| 61 |
|
| 62 |
// Classes which freed objects become. |g_fatZombieSize| is the
|
| 63 |
// minimum object size which can be made into a fat zombie (which can
|
| 64 |
// remember which class it was before free, even after falling off the
|
| 65 |
// treadmill).
|
| 66 |
Class g_zombieClass = Nil; // cached [CrZombie class]
|
| 67 |
Class g_fatZombieClass = Nil; // cached [CrFatZombie class]
|
| 68 |
size_t g_fatZombieSize = 0;
|
| 69 |
|
| 70 |
// Whether to zombie all freed objects, or only those which return YES
|
| 71 |
// from |-shouldBecomeCrZombie|.
|
| 72 |
BOOL g_zombieAllObjects = NO;
|
| 73 |
|
| 74 |
// Protects |g_zombieCount|, |g_zombieIndex|, and |g_zombies|.
|
| 75 |
base::LazyInstance<base::Lock>::Leaky g_lock = LAZY_INSTANCE_INITIALIZER;
|
| 76 |
|
| 77 |
// How many zombies to keep before freeing, and the current head of
|
| 78 |
// the circular buffer.
|
| 79 |
size_t g_zombieCount = 0;
|
| 80 |
size_t g_zombieIndex = 0;
|
| 81 |
|
| 82 |
typedef struct {
|
| 83 |
id object; // The zombied object.
|
| 84 |
Class wasa; // Value of |object->isa| before we replaced it.
|
| 85 |
void* trace[kBacktraceDepth]; // Backtrace at point of deallocation.
|
| 86 |
size_t traceDepth; // Actual depth of trace[].
|
| 87 |
} ZombieRecord;
|
| 88 |
|
| 89 |
ZombieRecord* g_zombies = NULL;
|
| 90 |
|
| 91 |
// Replacement |-dealloc| which turns objects into zombies and places
|
| 92 |
// them into |g_zombies| to be freed later.
|
| 93 |
void ZombieDealloc(id self, SEL _cmd) {
|
| 94 |
// This code should only be called when it is implementing |-dealloc|.
|
| 95 |
DCHECK_EQ(_cmd, @selector(dealloc));
|
| 96 |
|
| 97 |
// Use the original |-dealloc| if the object doesn't wish to be
|
| 98 |
// zombied.
|
| 99 |
if (!g_zombieAllObjects && ![self shouldBecomeCrZombie]) {
|
| 100 |
g_originalDeallocIMP(self, _cmd);
|
| 101 |
return;
|
| 102 |
}
|
| 103 |
|
| 104 |
Class wasa = object_getClass(self);
|
| 105 |
const size_t size = class_getInstanceSize(wasa);
|
| 106 |
|
| 107 |
// Destroy the instance by calling C++ destructors and clearing it
|
| 108 |
// to something unlikely to work well if someone references it.
|
| 109 |
// NOTE(shess): |object_dispose()| will call this again when the
|
| 110 |
// zombie falls off the treadmill! But by then |isa| will be a
|
| 111 |
// class without C++ destructors or associative references, so it
|
| 112 |
// won't hurt anything.
|
| 113 |
objc_destructInstance(self);
|
| 114 |
memset(self, '!', size);
|
| 115 |
|
| 116 |
// If the instance is big enough, make it into a fat zombie and have
|
| 117 |
// it remember the old |isa|. Otherwise make it a regular zombie.
|
| 118 |
// Setting |isa| rather than using |object_setClass()| because that
|
| 119 |
// function is implemented with a memory barrier. The runtime's
|
| 120 |
// |_internal_object_dispose()| (in objc-class.m) does this, so it
|
| 121 |
// should be safe (messaging free'd objects shouldn't be expected to
|
| 122 |
// be thread-safe in the first place).
|
| 123 |
#pragma clang diagnostic push // clang warns about direct access to isa.
|
| 124 |
#pragma clang diagnostic ignored "-Wdeprecated-objc-isa-usage"
|
| 125 |
if (size >= g_fatZombieSize) {
|
| 126 |
self->isa = g_fatZombieClass;
|
| 127 |
static_cast<CrFatZombie*>(self)->wasa = wasa;
|
| 128 |
} else {
|
| 129 |
self->isa = g_zombieClass;
|
| 130 |
}
|
| 131 |
#pragma clang diagnostic pop
|
| 132 |
|
| 133 |
// The new record to swap into |g_zombies|. If |g_zombieCount| is
|
| 134 |
// zero, then |self| will be freed immediately.
|
| 135 |
ZombieRecord zombieToFree = {self, wasa};
|
| 136 |
zombieToFree.traceDepth =
|
| 137 |
std::max(backtrace(zombieToFree.trace, kBacktraceDepth), 0);
|
| 138 |
|
| 139 |
// Don't involve the lock when creating zombies without a treadmill.
|
| 140 |
if (g_zombieCount > 0) {
|
| 141 |
base::AutoLock pin(g_lock.Get());
|
| 142 |
|
| 143 |
// Check the count again in a thread-safe manner.
|
| 144 |
if (g_zombieCount > 0) {
|
| 145 |
// Put the current object on the treadmill and keep the previous
|
| 146 |
// occupant.
|
| 147 |
std::swap(zombieToFree, g_zombies[g_zombieIndex]);
|
| 148 |
|
| 149 |
// Bump the index forward.
|
| 150 |
g_zombieIndex = (g_zombieIndex + 1) % g_zombieCount;
|
| 151 |
}
|
| 152 |
}
|
| 153 |
|
| 154 |
// Do the free out here to prevent any chance of deadlock.
|
| 155 |
if (zombieToFree.object)
|
| 156 |
object_dispose(zombieToFree.object);
|
| 157 |
}
|
| 158 |
|
| 159 |
// Search the treadmill for |object| and fill in |*record| if found.
|
| 160 |
// Returns YES if found.
|
| 161 |
BOOL GetZombieRecord(id object, ZombieRecord* record) {
|
| 162 |
// Holding the lock is reasonable because this should be fast, and
|
| 163 |
// the process is going to crash presently anyhow.
|
| 164 |
base::AutoLock pin(g_lock.Get());
|
| 165 |
for (size_t i = 0; i < g_zombieCount; ++i) {
|
| 166 |
if (g_zombies[i].object == object) {
|
| 167 |
*record = g_zombies[i];
|
| 168 |
return YES;
|
| 169 |
}
|
| 170 |
}
|
| 171 |
return NO;
|
| 172 |
}
|
| 173 |
|
| 174 |
// Dump the symbols. This is pulled out into a function to make it
|
| 175 |
// easy to use DCHECK to dump only in debug builds.
|
| 176 |
BOOL DumpDeallocTrace(const void* const* array, int size) {
|
| 177 |
// Async-signal safe version of fputs, consistent with StackTrace::Print().
|
| 178 |
const char* message = "Backtrace from -dealloc:\n";
|
| 179 |
ignore_result(HANDLE_EINTR(write(STDERR_FILENO, message, strlen(message))));
|
| 180 |
base::debug::StackTrace(array, size).Print();
|
| 181 |
|
| 182 |
return YES;
|
| 183 |
}
|
| 184 |
|
| 185 |
// Log a message to a freed object. |wasa| is the object's original
|
| 186 |
// class. |aSelector| is the selector which the calling code was
|
| 187 |
// attempting to send. |viaSelector| is the selector of the
|
| 188 |
// dispatch-related method which is being invoked to send |aSelector|
|
| 189 |
// (for instance, -respondsToSelector:).
|
| 190 |
void ZombieObjectCrash(id object, SEL aSelector, SEL viaSelector) {
|
| 191 |
ZombieRecord record;
|
| 192 |
BOOL found = GetZombieRecord(object, &record);
|
| 193 |
|
| 194 |
// The object's class can be in the zombie record, but if that is
|
| 195 |
// not available it can also be in the object itself (in most cases).
|
| 196 |
Class wasa = Nil;
|
| 197 |
if (found) {
|
| 198 |
wasa = record.wasa;
|
| 199 |
} else if (object_getClass(object) == g_fatZombieClass) {
|
| 200 |
wasa = static_cast<CrFatZombie*>(object)->wasa;
|
| 201 |
}
|
| 202 |
const char* wasaName = (wasa ? class_getName(wasa) : "<unknown>");
|
| 203 |
|
| 204 |
std::string aString = base::StringPrintf("Zombie <%s: %p> received -%s",
|
| 205 |
wasaName, object, sel_getName(aSelector));
|
| 206 |
if (viaSelector != NULL) {
|
| 207 |
const char* viaName = sel_getName(viaSelector);
|
| 208 |
base::StringAppendF(&aString, " (via -%s)", viaName);
|
| 209 |
}
|
| 210 |
|
| 211 |
// Set a value for breakpad to report.
|
| 212 |
base::debug::SetCrashKeyValue(crash_keys::mac::kZombie, aString);
|
| 213 |
|
| 214 |
// Encode trace into a breakpad key.
|
| 215 |
if (found) {
|
| 216 |
base::debug::SetCrashKeyFromAddresses(
|
| 217 |
crash_keys::mac::kZombieTrace, record.trace, record.traceDepth);
|
| 218 |
}
|
| 219 |
|
| 220 |
// Log -dealloc backtrace in debug builds then crash with a useful
|
| 221 |
// stack trace.
|
| 222 |
if (found && record.traceDepth) {
|
| 223 |
DCHECK(DumpDeallocTrace(record.trace, record.traceDepth));
|
| 224 |
} else {
|
| 225 |
DLOG(WARNING) << "Unable to generate backtrace from -dealloc.";
|
| 226 |
}
|
| 227 |
DLOG(FATAL) << aString;
|
| 228 |
|
| 229 |
// This is how about:crash is implemented. Using instead of
|
| 230 |
// |base::debug::BreakDebugger()| or |LOG(FATAL)| to make the top of
|
| 231 |
// stack more immediately obvious in crash dumps.
|
| 232 |
int* zero = NULL;
|
| 233 |
*zero = 0;
|
| 234 |
}
|
| 235 |
|
| 236 |
// Initialize our globals, returning YES on success.
|
| 237 |
BOOL ZombieInit() {
|
| 238 |
static BOOL initialized = NO;
|
| 239 |
if (initialized)
|
| 240 |
return YES;
|
| 241 |
|
| 242 |
Class rootClass = [NSObject class];
|
| 243 |
g_originalDeallocIMP =
|
| 244 |
class_getMethodImplementation(rootClass, @selector(dealloc));
|
| 245 |
// objc_getClass() so CrZombie doesn't need +class.
|
| 246 |
g_zombieClass = objc_getClass("CrZombie");
|
| 247 |
g_fatZombieClass = objc_getClass("CrFatZombie");
|
| 248 |
g_fatZombieSize = class_getInstanceSize(g_fatZombieClass);
|
| 249 |
|
| 250 |
if (!g_originalDeallocIMP || !g_zombieClass || !g_fatZombieClass)
|
| 251 |
return NO;
|
| 252 |
|
| 253 |
initialized = YES;
|
| 254 |
return YES;
|
| 255 |
}
|
| 256 |
|
| 257 |
} // namespace
|
| 258 |
|
| 259 |
@implementation CrZombie
|
| 260 |
|
| 261 |
// The Objective-C runtime needs to be able to call this successfully.
|
| 262 |
+ (void)initialize {
|
| 263 |
}
|
| 264 |
|
| 265 |
// Any method not explicitly defined will end up here, forcing a
|
| 266 |
// crash.
|
| 267 |
- (id)forwardingTargetForSelector:(SEL)aSelector {
|
| 268 |
ZombieObjectCrash(self, aSelector, NULL);
|
| 269 |
return nil;
|
| 270 |
}
|
| 271 |
|
| 272 |
// Override a few methods often used for dynamic dispatch to log the
|
| 273 |
// message the caller is attempting to send, rather than the utility
|
| 274 |
// method being used to send it.
|
| 275 |
- (BOOL)respondsToSelector:(SEL)aSelector {
|
| 276 |
ZombieObjectCrash(self, aSelector, _cmd);
|
| 277 |
return NO;
|
| 278 |
}
|
| 279 |
|
| 280 |
- (id)performSelector:(SEL)aSelector {
|
| 281 |
ZombieObjectCrash(self, aSelector, _cmd);
|
| 282 |
return nil;
|
| 283 |
}
|
| 284 |
|
| 285 |
- (id)performSelector:(SEL)aSelector withObject:(id)anObject {
|
| 286 |
ZombieObjectCrash(self, aSelector, _cmd);
|
| 287 |
return nil;
|
| 288 |
}
|
| 289 |
|
| 290 |
- (id)performSelector:(SEL)aSelector
|
| 291 |
withObject:(id)anObject
|
| 292 |
withObject:(id)anotherObject {
|
| 293 |
ZombieObjectCrash(self, aSelector, _cmd);
|
| 294 |
return nil;
|
| 295 |
}
|
| 296 |
|
| 297 |
- (void)performSelector:(SEL)aSelector
|
| 298 |
withObject:(id)anArgument
|
| 299 |
afterDelay:(NSTimeInterval)delay {
|
| 300 |
ZombieObjectCrash(self, aSelector, _cmd);
|
| 301 |
}
|
| 302 |
|
| 303 |
@end
|
| 304 |
|
| 305 |
@implementation CrFatZombie
|
| 306 |
|
| 307 |
// This implementation intentionally left empty.
|
| 308 |
|
| 309 |
@end
|
| 310 |
|
| 311 |
@implementation NSObject (CrZombie)
|
| 312 |
|
| 313 |
- (BOOL)shouldBecomeCrZombie {
|
| 314 |
return NO;
|
| 315 |
}
|
| 316 |
|
| 317 |
@end
|
| 318 |
|
| 319 |
namespace ObjcEvilDoers {
|
| 320 |
|
| 321 |
bool ZombieEnable(bool zombieAllObjects,
|
| 322 |
size_t zombieCount) {
|
| 323 |
// Only allow enable/disable on the main thread, just to keep things
|
| 324 |
// simple.
|
| 325 |
DCHECK([NSThread isMainThread]);
|
| 326 |
|
| 327 |
if (!ZombieInit())
|
| 328 |
return false;
|
| 329 |
|
| 330 |
g_zombieAllObjects = zombieAllObjects;
|
| 331 |
|
| 332 |
// Replace the implementation of -[NSObject dealloc].
|
| 333 |
Method m = class_getInstanceMethod([NSObject class], @selector(dealloc));
|
| 334 |
if (!m)
|
| 335 |
return false;
|
| 336 |
|
| 337 |
const IMP prevDeallocIMP = method_setImplementation(m, (IMP)ZombieDealloc);
|
| 338 |
DCHECK(prevDeallocIMP == g_originalDeallocIMP ||
|
| 339 |
prevDeallocIMP == (IMP)ZombieDealloc);
|
| 340 |
|
| 341 |
// Grab the current set of zombies. This is thread-safe because
|
| 342 |
// only the main thread can change these.
|
| 343 |
const size_t oldCount = g_zombieCount;
|
| 344 |
ZombieRecord* oldZombies = g_zombies;
|
| 345 |
|
| 346 |
{
|
| 347 |
base::AutoLock pin(g_lock.Get());
|
| 348 |
|
| 349 |
// Save the old index in case zombies need to be transferred.
|
| 350 |
size_t oldIndex = g_zombieIndex;
|
| 351 |
|
| 352 |
// Create the new zombie treadmill, disabling zombies in case of
|
| 353 |
// failure.
|
| 354 |
g_zombieIndex = 0;
|
| 355 |
g_zombieCount = zombieCount;
|
| 356 |
g_zombies = NULL;
|
| 357 |
if (g_zombieCount) {
|
| 358 |
g_zombies =
|
| 359 |
static_cast<ZombieRecord*>(calloc(g_zombieCount, sizeof(*g_zombies)));
|
| 360 |
if (!g_zombies) {
|
| 361 |
NOTREACHED();
|
| 362 |
g_zombies = oldZombies;
|
| 363 |
g_zombieCount = oldCount;
|
| 364 |
g_zombieIndex = oldIndex;
|
| 365 |
ZombieDisable();
|
| 366 |
return false;
|
| 367 |
}
|
| 368 |
}
|
| 369 |
|
| 370 |
// If the count is changing, allow some of the zombies to continue
|
| 371 |
// shambling forward.
|
| 372 |
const size_t sharedCount = std::min(oldCount, zombieCount);
|
| 373 |
if (sharedCount) {
|
| 374 |
// Get index of the first shared zombie.
|
| 375 |
oldIndex = (oldIndex + oldCount - sharedCount) % oldCount;
|
| 376 |
|
| 377 |
for (; g_zombieIndex < sharedCount; ++ g_zombieIndex) {
|
| 378 |
DCHECK_LT(g_zombieIndex, g_zombieCount);
|
| 379 |
DCHECK_LT(oldIndex, oldCount);
|
| 380 |
std::swap(g_zombies[g_zombieIndex], oldZombies[oldIndex]);
|
| 381 |
oldIndex = (oldIndex + 1) % oldCount;
|
| 382 |
}
|
| 383 |
g_zombieIndex %= g_zombieCount;
|
| 384 |
}
|
| 385 |
}
|
| 386 |
|
| 387 |
// Free the old treadmill and any remaining zombies.
|
| 388 |
if (oldZombies) {
|
| 389 |
for (size_t i = 0; i < oldCount; ++i) {
|
| 390 |
if (oldZombies[i].object)
|
| 391 |
object_dispose(oldZombies[i].object);
|
| 392 |
}
|
| 393 |
free(oldZombies);
|
| 394 |
}
|
| 395 |
|
| 396 |
return true;
|
| 397 |
}
|
| 398 |
|
| 399 |
void ZombieDisable() {
|
| 400 |
// Only allow enable/disable on the main thread, just to keep things
|
| 401 |
// simple.
|
| 402 |
DCHECK([NSThread isMainThread]);
|
| 403 |
|
| 404 |
// |ZombieInit()| was never called.
|
| 405 |
if (!g_originalDeallocIMP)
|
| 406 |
return;
|
| 407 |
|
| 408 |
// Put back the original implementation of -[NSObject dealloc].
|
| 409 |
Method m = class_getInstanceMethod([NSObject class], @selector(dealloc));
|
| 410 |
DCHECK(m);
|
| 411 |
method_setImplementation(m, g_originalDeallocIMP);
|
| 412 |
|
| 413 |
// Can safely grab this because it only happens on the main thread.
|
| 414 |
const size_t oldCount = g_zombieCount;
|
| 415 |
ZombieRecord* oldZombies = g_zombies;
|
| 416 |
|
| 417 |
{
|
| 418 |
base::AutoLock pin(g_lock.Get()); // In case any -dealloc are in progress.
|
| 419 |
g_zombieCount = 0;
|
| 420 |
g_zombies = NULL;
|
| 421 |
}
|
| 422 |
|
| 423 |
// Free any remaining zombies.
|
| 424 |
if (oldZombies) {
|
| 425 |
for (size_t i = 0; i < oldCount; ++i) {
|
| 426 |
if (oldZombies[i].object)
|
| 427 |
object_dispose(oldZombies[i].object);
|
| 428 |
}
|
| 429 |
free(oldZombies);
|
| 430 |
}
|
| 431 |
}
|
| 432 |
|
| 433 |
} // namespace ObjcEvilDoers
|