Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions Test/BlockCapture_arc.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#include "Test.h"

// Regression test: objc_retain must return the same pointer it was called
// with, regardless of whether the argument is a stack or heap block. Clang's
// ARC codegen declares llvm.objc.retain with the `returned` attribute; at -O2+
// the optimizer relies on that to replace uses of the retain result with the
// original pointer. A previous version of libobjc2's objc_retain forwarded
// unconditionally to Block_copy, which for stack blocks allocates a heap copy
// and returns a different pointer. The optimizer discarded the heap-copy
// pointer and later released the original stack pointer (a no-op for stack
// blocks), so the heap copy and every strong reference it captured leaked.

static int liveCount;

@interface Capture : Test @end
@implementation Capture
+ (id)new
{
liveCount++;
return [super new];
}
- (void)dealloc
{
liveCount--;
}
- (int)value
{
return 42;
}
@end

// A method (not a plain function) so message dispatch prevents inlining and
// forces the compiler to treat the block parameter as escapable.
@interface Runner : Test @end
@implementation Runner
- (void)runWith:(int (^)(void))b
{
// Capturing `b` into another block requires ARC to retain the block
// value — this is the exact site where the bug fires.
int (^wrapper)(void) = ^{ return b(); };
(void)wrapper();
}
@end

int main(void)
{
Runner *r = [Runner new];
for (int i = 0; i < 100; i++)
{
@autoreleasepool
{
Capture *c = [Capture new];
[r runWith:^{ return [c value]; }];
}
}
assert(liveCount == 0);
return 0;
}
1 change: 1 addition & 0 deletions Test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ set(TESTS
AllocatePair.m
AssociatedObject.m
AssociatedObject2.m
BlockCapture_arc.m
BlockTest_arc.m
ConstantString.m
Category.m
Expand Down
14 changes: 14 additions & 0 deletions arc.mm
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,20 @@ static inline id retain(id obj, BOOL isWeak)
Class cls = obj->isa;
if (UNLIKELY(objc_test_class_flag(cls, objc_class_flag_is_block)))
{
// objc_retain is declared with the `returned` attribute, so callers
// (and the LLVM ARC optimizer) assume the return value equals the
// input. Block_copy honours that for heap blocks (it just bumps the
// refcount) but violates it for stack blocks, where it allocates a
// new heap copy with a different address. Under -O2 the optimizer
// then discards the returned heap pointer and later releases the
// original stack pointer — which is a no-op — leaking the heap copy
// and everything it captured. Return the input for stack blocks;
// any real escape goes through _Block_object_assign / objc_retainBlock,
// which do the Block_copy correctly.
if (cls == static_cast<void*>(&_NSConcreteStackBlock))
{
return obj;
}
return Block_copy(obj);
}
if (objc_test_class_flag(cls, objc_class_flag_fast_arc))
Expand Down
Loading