Skip to content

Commit e40de8a

Browse files
fix: own native blocks with Block_copy/Block_release (#394)
* fix: own native blocks with Block_copy/Block_release A block returned from a native method is +0 and may still be a stack block. Interop::GetResult retained it with CFRetain and ObjectManager::DisposeValue released it with CFRelease, but CFRetain does not promote a stack block to the heap. By the time the JS wrapper was finalized during GC the CFRelease ran against a freed stack frame and crashed in objc_release (EXC_BAD_ACCESS). Use Block_copy when taking ownership of the returned block (which moves a stack block to the heap, or bumps the refcount of a heap/global block) and Block_release as the matching counterpart on disposal. * test: add regression test for owned block --------- Co-authored-by: Eduardo Speroni <edusperoni@gmail.com>
1 parent e271a77 commit e40de8a

5 files changed

Lines changed: 76 additions & 22 deletions

File tree

NativeScript/runtime/Interop.mm

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,12 +1055,18 @@ inline bool isBool() {
10551055
return callback;
10561056
}
10571057

1058-
BlockWrapper* blockWrapper = new BlockWrapper(block, typeEncoding, true);
1058+
// Take ownership of the returned block. Block_copy (not CFRetain) is the
1059+
// correct primitive here: a block returned by a native method is +0 and may
1060+
// still be a stack block. CFRetain does not promote a stack block to the
1061+
// heap, so releasing it later (during GC, on a different stack) would touch a
1062+
// dead frame and crash in objc_release. Block_copy moves it to the heap (or
1063+
// just bumps the refcount when it is already a heap/global block); the
1064+
// matching Block_release runs in ObjectManager::DisposeValue.
1065+
JSBlock* ownedBlock = reinterpret_cast<JSBlock*>(Block_copy(block));
1066+
BlockWrapper* blockWrapper = new BlockWrapper(ownedBlock, typeEncoding, true);
10591067
Local<External> ext = External::New(isolate, blockWrapper);
10601068
Local<v8::Function> callback;
10611069

1062-
CFRetain(block);
1063-
10641070
bool success =
10651071
v8::Function::New(
10661072
context,

NativeScript/runtime/ObjectManager.mm

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "ObjectManager.h"
2+
#include <Block.h>
23
#include <CoreFoundation/CoreFoundation.h>
34
#include <sstream>
45
#include "Caches.h"
@@ -108,10 +109,12 @@
108109
case WrapperType::Block: {
109110
BlockWrapper* blockWrapper = static_cast<BlockWrapper*>(wrapper);
110111
if (blockWrapper->OwnsBlock()) {
111-
// Balance the CFRetain taken when a native block was wrapped for JS
112-
// (see Interop::GetResult). This runs the block's dispose helper if
113-
// we held the last reference.
114-
CFRelease(blockWrapper->Block());
112+
// Balance the Block_copy taken when a native block was wrapped for JS
113+
// (see Interop::GetResult). Block_release is the correct counterpart to
114+
// Block_copy and runs the block's dispose helper once we drop the last
115+
// reference. (Using CFRelease here over-released stack blocks that were
116+
// never promoted to the heap, crashing in objc_release during GC.)
117+
Block_release(blockWrapper->Block());
115118
}
116119
// Blocks created from JS callbacks (OwnsBlock() == false) are owned by
117120
// the native code they were handed to (e.g. NSNotificationCenter);

TestFixtures/Api/TNSReturnsRetained.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,14 @@ CF_IMPLICIT_BRIDGING_DISABLED
77

88
id functionExplicitCreateNSObject();
99

10+
typedef int (^TNSIntBlock)(void);
11+
1012
@interface TNSReturnsRetained : NSObject
1113
+ (id)methodReturnsNSRetained NS_RETURNS_RETAINED;
1214
+ (id)methodReturnsCFRetained CF_RETURNS_RETAINED;
1315
+ (id)newNSObjectMethod;
16+
// Returns a +0 __NSStackBlock__ capturing `value` (see implementation). Used to
17+
// regression-test native block ownership: the runtime must take it with
18+
// Block_copy and release it with Block_release, not CFRetain/CFRelease.
19+
+ (TNSIntBlock)blockCapturing:(int)value;
1420
@end
Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,36 @@
11
#import "TNSReturnsRetained.h"
22

3-
id functionReturnsNSRetained() {
4-
return [[NSObject alloc] init];
5-
}
6-
id functionReturnsCFRetained() {
7-
return [[NSObject alloc] init];
8-
}
9-
CFTypeRef functionImplicitCreate() {
10-
return [[NSObject alloc] init];
11-
}
12-
id functionExplicitCreateNSObject() {
13-
return [[NSObject alloc] init];
14-
}
3+
// Identity passthrough: hides the stack-block-ness from the compiler's
4+
// return-stack-address diagnostic without copying the block to the heap.
5+
static TNSIntBlock TNSPassthroughIntBlock(TNSIntBlock block) { return block; }
6+
7+
id functionReturnsNSRetained() { return [[NSObject alloc] init]; }
8+
id functionReturnsCFRetained() { return [[NSObject alloc] init]; }
9+
CFTypeRef functionImplicitCreate() { return [[NSObject alloc] init]; }
10+
id functionExplicitCreateNSObject() { return [[NSObject alloc] init]; }
1511

1612
@implementation TNSReturnsRetained
1713
+ (id)methodReturnsNSRetained {
18-
return [[NSObject alloc] init];
14+
return [[NSObject alloc] init];
1915
}
2016
+ (id)methodReturnsCFRetained {
21-
return [[NSObject alloc] init];
17+
return [[NSObject alloc] init];
2218
}
2319
+ (id)newNSObjectMethod {
24-
return [[TNSReturnsRetained alloc] init];
20+
return [[TNSReturnsRetained alloc] init];
21+
}
22+
+ (TNSIntBlock)blockCapturing:(int)value {
23+
// This file is compiled with -fno-objc-arc. Capturing a non-constant value
24+
// (the parameter) forces a __NSStackBlock__ - capturing only a compile-time
25+
// constant would let clang promote it to a global block, which CFRetain
26+
// handles fine and would not reproduce the bug. The block is routed through
27+
// TNSPassthroughIntBlock so the compiler's "returning a stack block" check
28+
// can't see through the call boundary; it is still a +0 stack block living in
29+
// this frame at runtime. A correct runtime Block_copy's it to take ownership
30+
// (heap-promoting it). CFRetain does not promote a stack block, so the wrapper
31+
// is left pointing at this (about to be dead) stack frame.
32+
return TNSPassthroughIntBlock(^{
33+
return value;
34+
});
2535
}
2636
@end

TestRunner/app/tests/ApiTests.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,35 @@ describe(module.id, function () {
661661
expect(TNSReturnsRetained.newNSObjectMethod().retainCount()).toBe(1);
662662
});
663663

664+
it("takes ownership of +0 stack blocks returned from native", function () {
665+
// Regression for the native-block ownership bug. blockCapturing: returns a
666+
// +0 __NSStackBlock__ capturing its argument. A correct runtime Block_copy's
667+
// each one into its own distinct heap block. The buggy CFRetain path does
668+
// NOT promote a stack block to the heap, so all three wrappers are left
669+
// pointing at the same (reused) stack slot and alias each other - reading
670+
// whichever value was written last (and a later CFRelease would fault on
671+
// the dead frame). See Interop::GetResult and ObjectManager::DisposeValue.
672+
var b1 = TNSReturnsRetained.blockCapturing(11);
673+
var b2 = TNSReturnsRetained.blockCapturing(22);
674+
var b3 = TNSReturnsRetained.blockCapturing(33);
675+
676+
// On the fixed runtime each block is an independent heap copy.
677+
expect(b1()).toBe(11);
678+
expect(b2()).toBe(22);
679+
expect(b3()).toBe(33);
680+
681+
// The wrappers must also be safely releasable on GC (Block_release of a
682+
// heap copy vs. CFRelease of a dead stack frame).
683+
b1 = b2 = b3 = null;
684+
__collect();
685+
var sink = 0;
686+
for (var i = 0; i < 100000; i++) {
687+
sink += i % 7;
688+
}
689+
__collect();
690+
expect(sink).toBeGreaterThan(0);
691+
});
692+
664693
it("unmanaged", function () {
665694
var unmanaged = functionReturnsUnmanaged();
666695
expect('takeRetainedValue' in unmanaged).toBe(true);

0 commit comments

Comments
 (0)