• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
26OBJC_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
48namespace {
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.
57const size_t kBacktraceDepth = 20;
58
59// The original implementation for |-[NSObject dealloc]|.
60IMP 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).
66Class g_zombieClass = Nil;  // cached [CrZombie class]
67Class g_fatZombieClass = Nil;  // cached [CrFatZombie class]
68size_t g_fatZombieSize = 0;
69
70// Whether to zombie all freed objects, or only those which return YES
71// from |-shouldBecomeCrZombie|.
72BOOL g_zombieAllObjects = NO;
73
74// Protects |g_zombieCount|, |g_zombieIndex|, and |g_zombies|.
75base::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.
79size_t g_zombieCount = 0;
80size_t g_zombieIndex = 0;
81
82typedef 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
89ZombieRecord* g_zombies = NULL;
90
91// Replacement |-dealloc| which turns objects into zombies and places
92// them into |g_zombies| to be freed later.
93void 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.
161BOOL 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.
176BOOL 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:).
190void 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.
237BOOL 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
319namespace ObjcEvilDoers {
320
321bool 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
399void 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
434