Skip to content

objc_retain() copies stack Blocks, which violates ARC's pointer-identity contract #397

Description

@cmpwi

objc_retain() currently handles every Block by calling Block_copy() upon it:

if (UNLIKELY(objc_test_class_flag(cls, objc_class_flag_is_block)))
{
	return Block_copy(obj);
}

I believe this violates Clang's ARC runtime contract, with emphasis on that last line:

id objc_retain(id value);

Precondition: value is null or a pointer to a valid object.

If value is null, this call has no effect. Otherwise, it performs a retain operation exactly as if the object had been sent the retain message.

Always returns value.

From my reading, objc_retainBlock() is the (only?) routine permitted to copy a stack Block and return a different address if the Block transitions from the stack to the heap:

id objc_retainBlock(id value);

Precondition: value is null or a pointer to a valid block object.

If value is null, this call has no effect. Otherwise, if the block pointed to by value is still on the stack, it is copied to the heap and the address of the copy is returned. Otherwise a retain operation is performed on the block exactly as if it had been sent the retain message.

A simple example (a NSData-like class implementing a no-copy initializer with a custom deallocator Block) that illustrates the issue is as follows:

#import "Test.h"
#include <stdlib.h>

typedef void (^Deallocator)(void *, unsigned long);

typedef struct Buffer {
	void *bytes;
	unsigned long count;
	Deallocator deallocator;
} Buffer;

@interface Data : Test {
	Buffer *_buffer;
}

- (id)initWithBytesNoCopy:(void *)bytes count:(unsigned long)count deallocator:(Deallocator)deallocator;

@end

@implementation Data

- (id)initWithBytesNoCopy:(void *)bytes count:(unsigned long)count deallocator:(Deallocator)deallocator
{
	_buffer = calloc(1, sizeof(*_buffer));
	_buffer->bytes = bytes;
	_buffer->count = count;
	_buffer->deallocator = deallocator;
	return self;
}

- (void)dealloc
{
	_buffer->deallocator(_buffer->bytes, _buffer->count);
	_buffer->deallocator = nil;
	free(_buffer);
}

@end

static __weak id weakToken;

int
main(void)
{
	@autoreleasepool {
		Test *token = [Test new];
		void *bytes = malloc(1);
		Data *data;

		weakToken = token;
		assert(bytes != nullptr);
		data = [[Data alloc] initWithBytesNoCopy:bytes count:1
			deallocator:^(void *pointer, unsigned long count) {
				assert(token != nil);
				assert(count == 1);
				free(pointer);
			}];
	}

	assert(weakToken == nil);
	return 0;
}

At -O0, Clang calls objc_storeStrong() on the Block parameter in -initWithBytesNoCopy:count:deallocator:. objc_storeStrong() calls objc_retain() on the Block, creating a heap Block which is subsequently assigned through the id *addr parameter. *addr is what gets assigned to deallocator in the buffer structure, so both the stack Block and the heap Block are properly released at the end of the @autoreleasepool's scope.

At -01 and above, Clang calls objc_retain() on the Block parameter, but it keeps using the original stack Block pointer (which is a valid optimization to make). Of course, this causes the newly allocated heap Block to leak, which triggers assert(weakToken == nil);, since at this point, there should be no Blocks in existence referencing token.

I propose making retain() check for stack Blocks and return them as-is:

if (UNLIKELY(objc_test_class_flag(cls, objc_class_flag_is_block)))
{
        // objc_retain() must return its argument. Only objc_retainBlock() may
        // promote a stack block to a heap block with a different address.
        if (cls == static_cast<void*>(&_NSConcreteStackBlock))
        {
                return obj;
        } 
        return Block_copy(obj);
}

This aligns the behaviour of objc_retain() for Blocks with my reading of the ARC spec.

All libobjc2 tests continue to pass with this change. Even though I don't like to make statements carrying words of absolute certainty, I'm 100% certain that this change will not break GNUstep's libs-base, unlike my oopsie from last time :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions