Pete Hodgson

Software Delivery Consultant

Objective C memory errors pt II - Dangling Pointers

October 12, 2010

In my last post I talked about memory leaks in Objective C. In this post I'll take about the converse - dangling pointers. In iOS a dangling pointer happens when you maintain a reference to an object which has been deallocated. I'd say the most common cause is failure to take ownership of an object you intend to maintain a reference to. Another common cause is when an object is accidently released multiple times by the same owner, leading to its retain count dropping to 0 and the object being deallocated before all the objects which hold references to the object release those references.


I'm going to illustrate this first type of dangling pointer bug (failing to take ownership of a reference) and try to explain in detail the cause of the error and its effects. We'll use a pedagogical Duck class similar to the one in my previous post:


@interface Duck : NSObject {
  NSArray *_feathers;
}
@end

We have a Duck which holds a reference to an array of feathers. We'd like to ensure that our Duck's feathers array always points to a valid NSArray instance, so we set the reference to an empty array during construction:


- (id) init
{
  self = [super init];
  if (self != nil) {
    _feathers = [NSArray array];
  }
  return self;
}

Once again we have a memory bug here, but this time it's the flip side of the memory management coin from a memory leak - a dangling pointer. Instead of failing to relinquish ownership of a reference we're failing to take ownership. We are asking NSArray to create an instance using the [NSArray array] factory method, which does NOT transfer any ownership to the caller. This is explained quite clearly in Apple's Memory Management Rules - if a method does not start with alloc, new or copy then the caller is not automatically granted ownership of the object, and should take that ownership explicitly by calling retain if it intends to maintain a reference to that object.


I'll illustrate one scenario of how this bug could manifest itself:




1) In our init method we call [NSArray array] which creates an NSArray instance. The creator of the array inside the implementation of [NSArray array] has to keep ownership of the instance for a certain period of time, otherwise its retain count would drop to 0 and it would immediately be deallocated.


2) still in the init method, we update _feathers to refer to the array, but neglect to take any ownership of the array.


3) at some later point while Duck is still alive the creator (and sole owner) of the array instance relinquishes its ownership of the array instance, dropping the array's retain count down to 0. This might be because it has been deallocated, or maybe it is replacing its reference to the array with something else, or some other reason.


4) The array's retain count has dropped to 0, so it is immediately deallocated and its memory reclaimed for future use by the memory management system. Our Duck instance has no way of knowing this, and is still holding a reference to where the array used to be. This is the dangling pointer.


Subsequently when the Duck instance attempts to talk to that array it is going to find itself talking to some random object, or just to arbitrary junk data. Crashes will ensue... eventually.


Note that the sole owner of the array as shown in the diagrams would in a lot of cases be an autorelease pool. This is because the easiest way to handle memory management when implementing factory methods like [NSArray array] is to create the object to be returned using alloc, and then transfer ownership to the autorelease pool before returning the object, like so:


+ (NSArray *) array {
  return [[[NSArray alloc]init]autorelease];
}

What makes this kind of dangling pointer error particularly tricky is that in certain use cases it will not manifest at all. If the usage of Duck is such that it always goes out of scope before the NSArray instance is deallocated then no issues will be detected. This could happen if the Duck instance and the NSArray instance it references are both owned by the same autorelease pool, for instance. However, when the usage patterns of Duck change you could suddenly see errors - even if you haven't changed the (buggy) implementation of the Duck class itself in months!


What should we have done?


Use an object creation method which implicitly grants ownership to the caller

- (id) init
{
  self = [super init];
  if (self != nil) {
    _feathers = [[NSArray alloc]init];
  }
  return self;
}

Use retain to explicitly take ownership


- (id) init
{
  self = [super init];
  if (self != nil) {
    _feathers = [[NSArray array] retain];
  }
  return self;
}