Home > database >  Warning block captures an autoreleasing out-parameter
Warning block captures an autoreleasing out-parameter

Time:01-11

In a third-party lib I use, I am getting the warning

"Block captures an autoreleasing out-parameter"

What is the problem and how can I fix it?

- (BOOL)register:(NSString *)param error:(NSError **)errPtr
{   
    __block BOOL result = YES;
    __block NSError *err = nil;
    
    dispatch_block_t block = ^{ @autoreleasepool {

         NSMutableArray *elements = [NSMutableArray array];

        /**** Block captures an autoreleasing out-parameter,
           which may result in use-after-free bugs ****/

        /* on errPtr */

        [self registerWithElements:elements error:errPtr];
        
    }};
    
    if (errPtr)
        *errPtr = err;
    
    return result;
}

CodePudding user response:

When you have a method with an indirect non-const parameter (T **param) Clang with ARC automatically qualify such a parameter with __autoreleasing (T *__autoreleasing*). This happens because Clang reasonably assumes, that the calling side is not always required to release such object, so it puts a requirement on the function to assign autoreleasing objects only. Thus this:

- (void)myMethod:(NSObject **)param {
    *param = [NSObject new];
}

Turns into this under ARC:

- (void)myMethod:(NSObject *__autoreleasing*)param {
    *param = [[NSObject new] autorelease];
}

This in turn imposes special requirements on the arguments for such a method, so in common scenario where you actually just pass some (strongly retained) object to the function:

NSObject *obj;
[self myMethod:&obj];

ARC in fact makes a temporary autoreleasing argument:

NSObject *__strong obj = nil;
NSObject *__autoreleasing tmp = obj;
[self myMethod:&tmp];
obj = [tmp retain];

What is the problem...

If, instead of (indirectly) passing strongly retained pointer, you pass your own indirect pointer, ARC doesn't make any temporary in between:

NSObject *__autoreleasing obj;
NSObject *__autoreleasing *objPtr = &obj;
[self myMethod:objPtr];

It means that the object "returned" by myMethod: doesn't get retained anymore, thus will be destroyed when current autorelease pool is drained. The same is true if you pass a parameter with the same semantic:

- (void)anotherMethod:(NSObject **)param {
    [self myMethod:param];
}

Thus if, for any reason, you decide to wrap the invocation of myMethod: with an autorelease block, the code here ends up with a zombie object:

- (void)anotherMethod:(NSObject **)param {
    @autoreleasepool {
        [self myMethod:param]; // object was created and assigned to a autoreleasing pointer
    } // ref. count of the object reached zero. `*param` refers to a released object
}

The same can potentially happen if you wrap the invocation with a block:

- (void)anotherMethod:(NSObject **)param {
    void(^block)(void) = ^{
        // "Block captures an autoreleasing out-parameter, which may result in use-after-free bugs" warning appears
        [self myMethod:param];
    };
    block();
}

For this specific implementation no problem will happen, and you could just silence the error by explicitly giving the indirect pointer __autoreleasing qualifier (by which you inform Clang that you are well aware of possible consequences):

- (void)anotherMethod:(NSObject *__autoreleasing*)param {
    void(^block)(void) = ^{
        [self myMethod:param];
    };
    block();
}

But now you has to be very careful, because block is a first-party object, which can be retained and called from anywhere and there are countless scenarios where additional autorelease pool is spawned. E.g. this code will case the same zombie-object error:

- (void)anotherMethod:(NSObject *__autoreleasing*)param {
    void(^block)(void) = ^{
        [self myMethod:param];
    };
    ... come code here ...
    @autoreleasepool {
        block();
    }

}

The same if the autorelease pool is right in the block body:

- (void)anotherMethod:(NSObject **)param {
    void(^block)(void) = ^{
        @autoreleasepool {
            [self myMethod:param];
        }
    };
    block();
}

Having that said, Clangs doesn't warn about the error (it's actually obvious in your case, because you wrap the body of your block with an @autoreleasepool block), it just wants you to double check that you are aware of possible problems (as you can see, it's still possible to implement things like that, but you will have hard time to track all the errors if they appear).


how can I fix it?

This depends on your definition of "fix". You either can remove the autorelease pool block from the body of your block and qualify __autoreleasing parameter explicitly (provided it's merely called in the same thread somewhere in the method):

- (BOOL)register:(NSString *)param error:(NSError *__autoreleasing*)errPtr {   
    ....
    
    dispatch_block_t block = ^{
        ....
        [self registerWithElements:elements error:errPtr];
        
    };
    block();

    ....
}

Or you can introduce another "local" variable to capture and pass it inside a block:

- (BOOL)register:(NSString *)param error:(NSError **)errPtr {
    ....
    __block NSError *err;    
    dispatch_block_t block = ^{
        @autoreleasepool {
            ....
            [self registerWithElements:elements error:&err];
        }
    };
    block();
    *errPtr = err;
    ....
}

This again implies that the block is called synchronously in the method, but not necessarily within the same autorelease pool block. If you want to store the block for later use, or call it asynchronously, then you will need another NSError variable with prolonged lifetime to capture inside the block.

  • Related