Skip to content

fix: own native blocks with Block_copy/Block_release#394

Merged
edusperoni merged 2 commits into
mainfrom
fix/block-over-release-on-gc
Jun 19, 2026
Merged

fix: own native blocks with Block_copy/Block_release#394
edusperoni merged 2 commits into
mainfrom
fix/block-over-release-on-gc

Conversation

@NathanWalker

@NathanWalker NathanWalker commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

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.

This is the stack that resulted while testing the changes from a65b729

Thread 0 Crashed:
0   libobjc.A.dylib               	0x0000000180039684 objc_release_x0 + 16
1   NativeScript                  	0x000000010ce35788 _ZN3tns13ObjectManager12DisposeValueEPN2v87IsolateENS1_5LocalINS1_5ValueEEEb + 772
2   NativeScript                  	0x000000010ce3542c _ZN3tns13ObjectManager17FinalizerCallbackERKN2v816WeakCallbackInfoINS_23ObjectWeakCallbackStateEEE + 64
3   NativeScript                  	0x000000010d1e15d4 _ZN2v88internal13GlobalHandles4Node31PostGarbageCollectionProcessingEPNS0_7IsolateE + 124
4   NativeScript                  	0x000000010d1e215c _ZN2v88internal13GlobalHandles31PostGarbageCollectionProcessingENS0_16GarbageCollectorENS_15GCCallbackFlagsE + 196
5   NativeScript                  	0x000000010d204b1c _ZN2v88internal4Heap14CollectGarbageENS0_15AllocationSpaceENS0_23GarbageCollectionReasonENS_15GCCallbackFlagsE + 3560
6   NativeScript                  	0x000000010d2066c0 _ZN2v88internal4Heap24PreciseCollectAllGarbageEiNS0_23GarbageCollectionReasonENS_15GCCallbackFlagsE + 92
7   NativeScript                  	0x000000010d1d9a70 _ZN2v88internal11GCExtension2GCERKNS_20FunctionCallbackInfoINS_5ValueEEE + 172
8   NativeScript                  	0x000000010cfce80c _ZN2v88internal25FunctionCallbackArguments4CallENS0_15CallHandlerInfoE + 276
9   NativeScript                  	0x000000010cfcde48 _ZN2v88internal12_GLOBAL__N_119HandleApiCallHelperILb0EEENS0_11MaybeHandleINS0_6ObjectEEEPNS0_7IsolateENS0_6HandleINS0_10HeapObjectEEESA_NS8_INS0_20FunctionTemplateInfoEEENS8_IS4_EENS0_16BuiltinArgumentsE + 504
10  NativeScript                  	0x000000010cfcd5e4 _ZN2v88internalL26Builtin_Impl_HandleApiCallENS0_16BuiltinArgumentsEPNS0_7IsolateE + 240
11  NativeScript                  	0x000000010d84df4c Builtins_CEntry_Return1_DontSaveFPRegs_ArgvOnStack_BuiltinExit + 108
12  NativeScript                  	0x000000010d7dbe58 Builtins_InterpreterEntryTrampoline + 248
13  NativeScript                  	0x000000010d7dbe58 Builtins_InterpreterEntryTrampoline + 248
14  NativeScript                  	0x000000010d7dbe58 Builtins_InterpreterEntryTrampoline + 248
15  NativeScript                  	0x000000010d7dbe58 Builtins_InterpreterEntryTrampoline + 248
16  NativeScript                  	0x000000010d7dbe58 Builtins_InterpreterEntryTrampoline + 248
17  NativeScript                  	0x000000010d7dbe58 Builtins_InterpreterEntryTrampoline + 248
18  NativeScript                  	0x000000010d7dbe58 Builtins_InterpreterEntryTrampoline + 248
19  NativeScript                  	0x000000010d7dbe58 Builtins_InterpreterEntryTrampoline + 248
20  NativeScript                  	0x000000010d7dbe58 Builtins_InterpreterEntryTrampoline + 248
21  NativeScript                  	0x000000010d7dbe58 Builtins_InterpreterEntryTrampoline + 248
22  NativeScript                  	0x000000010d7dbe58 Builtins_InterpreterEntryTrampoline + 248
23  NativeScript                  	0x000000010d7dbe58 Builtins_InterpreterEntryTrampoline + 248
24  NativeScript                  	0x000000010d7dbe58 Builtins_InterpreterEntryTrampoline + 248
25  NativeScript                  	0x000000010d7dbe58 Builtins_InterpreterEntryTrampoline + 248
26  NativeScript                  	0x000000010d7da190 Builtins_JSEntryTrampoline + 176
27  NativeScript                  	0x000000010d7d9e24 Builtins_JSEntry + 164
28  NativeScript                  	0x000000010d19c370 _ZN2v88internal12_GLOBAL__N_16InvokeEPNS0_7IsolateERKNS1_12InvokeParamsE + 2680
29  NativeScript                  	0x000000010d19b8c4 _ZN2v88internal9Execution4CallEPNS0_7IsolateENS0_6HandleINS0_6ObjectEEES6_iPS6_ + 212
30  NativeScript                  	0x000000010cf4f4b4 _ZN2v88Function4CallENS_5LocalINS_7ContextEEENS1_INS_5ValueEEEiPS5_ + 652
31  NativeScript                  	0x000000010ce640f4 _ZN3tns12ArgConverter14MethodCallbackEP7ffi_cifPvPS3_S3_ + 916
32  NativeScript                  	0x000000010cefdb0c ffi_closure_SYSV_inner + 796
33  NativeScript                  	0x000000010cf001b4 .Ldo_closure + 20
34  Foundation                    	0x0000000181156a2c __NSFireTimer + 56
35  CoreFoundation                	0x00000001804235e8 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 28
36  CoreFoundation                	0x00000001804232f8 __CFRunLoopDoTimer + 916
37  CoreFoundation                	0x00000001804229d4 __CFRunLoopDoTimers + 276
38  CoreFoundation                	0x0000000180421bec __CFRunLoopRun + 1756
39  CoreFoundation                	0x000000018041c904 _CFRunLoopRunSpecificWithOptions + 496
40  GraphicsServices              	0x00000001933599c0 GSEventRunModal + 116
41  UIKitCore                     	0x00000001864a54cc -[UIApplication _run] + 776
42  UIKitCore                     	0x0000000186b9f910 UIApplicationMain + 120
43  NativeScript                  	0x000000010cf00044 ffi_call_SYSV + 68
44  NativeScript                  	0x000000010cefd370 ffi_call_int + 972
45  NativeScript                  	0x000000010cee26a8 _ZN3tns7Interop20CallFunctionInternalERNS_10MethodCallE + 376
46  NativeScript                  	0x000000010ce92c4c _ZNSt3__110__function6__funcIZN3tns15MetadataBuilder17CFunctionCallbackERKN2v820FunctionCallbackInfoINS4_5ValueEEEE3$_0FvvEEclEv + 440
47  NativeScript                  	0x000000010ceebdf4 _ZN3tns5Tasks5DrainEv + 100
48  heykiddotalkangular           	0x0000000104870658 main + 736
49  (null)	0x0000000108f570e4 0x0 + 4445270244
50  (null)	0x0000000109217e00 0x0 + 4448157184

Summary by CodeRabbit

  • Bug Fixes
    • Fixed native block ownership and lifecycle handling by correctly copying/releasing blocks and aligning the release method to the block type, preventing crashes or leaks and improving stability after callback disposal.
  • Tests
    • Added a regression test covering ownership of +0 stack blocks returned from native code, including validation across multiple invocations and behavior after release and garbage collection.

@NathanWalker NathanWalker requested a review from edusperoni June 19, 2026 01:59
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8abebbed-cfa2-4036-931f-a13917131783

📥 Commits

Reviewing files that changed from the base of the PR and between f79d470 and 32da495.

📒 Files selected for processing (3)
  • TestFixtures/Api/TNSReturnsRetained.h
  • TestFixtures/Api/TNSReturnsRetained.m
  • TestRunner/app/tests/ApiTests.js

📝 Walkthrough

Walkthrough

The runtime fix replaces CFRetain/CFRelease with Block_copy/Block_release for Objective-C blocks wrapped in the JS bridge. Interop.mm now promotes blocks to the heap via Block_copy before wrapping. ObjectManager.mm adds <Block.h> and releases blocks correctly. Test fixture infrastructure and a regression test verify the fix handles stack-block ownership correctly.

Changes

Block Lifetime Management Fix

Layer / File(s) Summary
Runtime block lifetime fix
NativeScript/runtime/Interop.mm, NativeScript/runtime/ObjectManager.mm
Interop::GetResult's BlockEncoding path replaces CFRetain with Block_copy to promote the block to the heap before wrapping in BlockWrapper. ObjectManager::DisposeValue adds <Block.h> and switches from CFRelease to Block_release in the WrapperType::Block branch, with updated comments documenting the correct pairing.
Test fixture infrastructure
TestFixtures/Api/TNSReturnsRetained.h, TestFixtures/Api/TNSReturnsRetained.m
Adds TNSIntBlock typedef and blockCapturing: class method to expose the scenario of returning a stack block from native code. Implementation uses TNSPassthroughIntBlock passthrough helper to keep the block as a stack block at runtime (preventing compiler optimization), with helper function refactoring.
Regression test
TestRunner/app/tests/ApiTests.js
New test invokes blockCapturing with multiple captured values, verifies each block returns the expected value, nulls references, forces garbage collection, and runs a workload loop to exercise post-collection stability.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 A block on the stack was a slippery friend,
CFRetain was wrong from beginning to end.
Now Block_copy lifts it to heap with great care,
And Block_release cleans up without despair.
The test watches close—hop hop—the memory's fair! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: replacing CFRetain/CFRelease with Block_copy/Block_release for native block memory management.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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.
@NathanWalker NathanWalker force-pushed the fix/block-over-release-on-gc branch from 1c6cbfc to f79d470 Compare June 19, 2026 02:02
@edusperoni edusperoni merged commit e40de8a into main Jun 19, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants