Skip to content

Commit 1c6cbfc

Browse files
NathanWalkerclaude
andcommitted
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. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e271a77 commit 1c6cbfc

2 files changed

Lines changed: 16 additions & 7 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);

0 commit comments

Comments
 (0)