objective-cautomatic-ref-countingunsafe-unretained

Returning __unsafe_unretained pointer to released object in Objective C causes a crash


Problem:

I have an __unsafe_unretained id pointer that points to an already released object. So far so good, as long as I do not "use" the pointer at all (in particular, I do not call any method through the pointer). However, when I try to return its value from a method, it crashes, even if I have explicitly specified that the return value has the type __unsafe_unretained id. Why is that? I thought if I use __unsafe_unretained, it would not call methods like retain / release / autorelease at all? I thought I can use __unsafe_unretained id pretty much as if it is a void* (meaning that it only does simple, native assignments)?

Environment:

Source Code:

// Declare my class with 1 member.
@interface MyClass : NSObject
{
    __unsafe_unretained id      m_MyMember;
}
@end

// **************************************************************************************************** //

// Implement my class.
@implementation MyClass

// Setter
-(void)SetMember:(__unsafe_unretained id)member
{
    m_MyMember = member;
}

// Getter: by passing parameter by reference
-(void)GetMember1:(__unsafe_unretained id*)member
{
    *member = m_MyMember;   // No problem.
}

// Getter: by return value
-(__unsafe_unretained id)GetMember2
{
    return m_MyMember;  // Crashed in here!
}

@end

// **************************************************************************************************** //

//! Application entry point.
int main(int argc, char *argv[])
{
    @autoreleasepool
    {       
        {
            // Create an object that dies immediately. deadObj is a dangling pointer.
            __unsafe_unretained id deadObj = [[NSMutableString alloc] initWithFormat:@"%d", 12];

            // Create my object.
            MyClass* myObject = [[MyClass alloc] init];

            // Assign my member.
            [myObject SetMember:deadObj];

            // Get back my member: by passing parameter by reference
            __unsafe_unretained id unsafePointer1;
            [myObject GetMember1:&unsafePointer1];  // No problem.

            // Get back my member: by return value
            __unsafe_unretained id unsafePointer2;
            unsafePointer2 = [myObject GetMember2]; // Crashed in here!

            int BreakpointHere = 0;
        }
    }
}

Call Stack (iPhone 4.3 Simulator/iOS 4.3 Device):

#0  0x011db09b in objc_msgSend ()
#1  0x00106712 in __arclite_objc_retainAutoreleaseReturnValue at /SourceCache/arclite_host/arclite-29.1/source/arclite.m:259
#2  0x00001fec in -[MyClass GetMember2] at /Users/user/SourceCode/main.m:28
#3  0x00002147 in main at /Users/user/SourceCode/main.m:56

Call Stack (iPhone 5.0/5.1 Simulator):

#0  0x014f6d25 in objc_retain ()
#1  0x014f7fe3 in objc_retainAutoreleaseReturnValue ()
#2  0x00001fec in -[MyClass GetMember2] at /Users/user/SourceCode/main.m:28
#3  0x00002147 in main at /Users/user/SourceCode/main.m:56

Solution

  • I think the behavior can be explained with the following information 3.2.3. Unretained return values in the Automatic Reference Counting documentation:

    A method or function which returns a retainable object type but does not return a retained value must ensure that the object is still valid across the return boundary.

    When returning from such a function or method, ARC retains the value at the point of evaluation of the return statement, then leaves all local scopes, and then balances out the retain while ensuring that the value lives across the call boundary. In the worst case, this may involve an autorelease, but callers must not assume that the value is actually in the autorelease pool.

    Your function GetMember2 returns an id, which is a retainable object type. Therefore the ARC compiler adds retain/autorelease calls to make sure that the returned object is still valid when the function returns. This crashes because m_MyMember does not point to a valid object.

    Declaring the return type as (__unsafe_unretained id) does not change this behavior, in fact I assume that the __unsafe_unretained is ignored here.