Skip to content

fix: root registered-callback delegates and fix metadata handle ownership#15

Merged
tinysec merged 1 commit into
masterfrom
fix/round19-callback-and-handle-lifetime
Jul 7, 2026
Merged

fix: root registered-callback delegates and fix metadata handle ownership#15
tinysec merged 1 commit into
masterfrom
fix/round19-callback-and-handle-lifetime

Conversation

@tinysec

@tinysec tinysec commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Round-19 audit: delegate lifetime + metadata handle ownership

1:1 comparison against the official C++/Python binaryninja-api found two classes of lifetime bugs where the GC could reclaim a delegate (or strand a native ref) while the core still held the function pointer / handle.

1. Callback delegate lifetime (CallbackOnCollectedDelegate → AccessViolation)

Every site handing a function pointer to a core API that stores it long-term built the adapter delegate inline, leaving no rooted reference. Once the GC collected the temporary, the native pointer dangled and the next callback crashed. Fixed across:

  • 5 Custom* ToNative structs (CustomBinaryView, CustomBinaryViewType, CustomTransform, FileAccessor, LogListener): per-instance thunk fields.
  • BNLogListener.FromNative: copy-paste fix (close/getLogLevel had pointed at log).
  • 11 Custom*Command instance Register*() + 14 static RegisterPluginCommand*: thunk fields / shared UnsafeUtils.PinCallback.
  • Core.RegisterBinaryViewEvent, BinaryViewType.RegisterPlatformRecognizer, FlowGraph.StartLayout (async completion): PinCallback process-lifetime root.
  • Activity.Create/CreateWithEligibility: per-instance delegate fields.
  • 22 UnsafeUtils.WrapProgressDelegate call sites: local + GC.KeepAlive across the synchronous native call.

New UnsafeUtils.PinCallback<T> centralizes the process-lifetime rooting so every registration site routes through one obviously-correct helper.

2. Metadata native-handle ownership

  • Metadata.ToArray + MetadataValueStore.FromNative: MustTakeHandle (no addref on a transferred-owned ref; the core frees only the container).
  • 12 scalar/array Metadata.SetValue overloads: wrap the BNCreate* handle in an owning Metadata (using) and pass a borrowed handle; the core addrefs internally so the value survives the wrapper's disposal. (Previously leaked one BNMetadata ref per call.)

Validation

  • Build: 0 warnings / 0 errors.
  • e2e (outside repo, per project constraint): 1470 passed / 0 failed / 3 skipped (pre-existing absent-export skip). +2 modules: CallbackLifetime (the logListenerSurvivesGc probe would crash the process pre-fix; now passes on all 3 samples) and MetadataSetValueOwnership (all 8 probes pass, incl. survivesGc).

…ship

Round-19 audit (1:1 vs official C++/Python bindings) found two classes of
lifetime bugs where the GC could reclaim a delegate (or strand a native
ref) while the core still held the function pointer / handle.

1. Callback delegate lifetime (CallbackOnCollectedDelegate). Every site that
   hands a function pointer to a core API storing it long-term built the
   adapter delegate inline, leaving no rooted reference. Once the GC
   collected the temporary the native pointer dangled and the next callback
   crashed (AccessViolation). Fixed across:
   - 5 Custom* ToNative structs (CustomBinaryView, CustomBinaryViewType,
     CustomTransform, FileAccessor, LogListener): per-instance thunk fields.
   - BNLogListener.FromNative copy-paste (close/getLogLevel pointed at log).
   - 11 Custom*Command instance Register*() + 14 static RegisterPluginCommand*:
     thunk fields / shared UnsafeUtils.PinCallback root.
   - Core.RegisterBinaryViewEvent, BinaryViewType.RegisterPlatformRecognizer,
     FlowGraph.StartLayout (async completion): PinCallback process root.
   - Activity.Create/CreateWithEligibility: per-instance delegate fields.
   - 22 UnsafeUtils.WrapProgressDelegate call sites: local + GC.KeepAlive
     across the synchronous native call.
   UnsafeUtils.PinCallback<T> centralizes the process-lifetime rooting so
   every registration site routes through one obviously-correct helper.

2. Metadata native-handle ownership. Several paths addref'd a transferred-
   owned handle (MustNewFromHandle) or leaked a caller-owned BNCreate* handle:
   - Metadata.ToArray + MetadataValueStore.FromNative: MustTakeHandle (no
     addref on a transferred-owned ref; container-only free).
   - 12 scalar/array Metadata.SetValue overloads: wrap the BNCreate* handle
     in an owning Metadata (using) and pass a borrowed handle; the core
     addrefs internally so the value survives the wrapper's disposal.

e2e (outside repo, per project constraint): +2 modules (CallbackLifetime,
MetadataSetValueOwnership). 1470 passed / 0 failed / 3 skipped (pre-existing
absent-export skip). CallbackLifetime.logListenerSurvivesGc would crash the
process pre-fix; it now passes on all three samples.
@tinysec tinysec merged commit 8b93cc7 into master Jul 7, 2026
1 check passed
@tinysec tinysec deleted the fix/round19-callback-and-handle-lifetime branch July 7, 2026 03:32
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.

1 participant