Skip to content

fix: FindAll*/Search match-callback marshaling (crash, leak, struct-by-value)#19

Merged
tinysec merged 1 commit into
masterfrom
fix/findall-match-delegate-marshal
Jul 8, 2026
Merged

fix: FindAll*/Search match-callback marshaling (crash, leak, struct-by-value)#19
tinysec merged 1 commit into
masterfrom
fix/findall-match-delegate-marshal

Conversation

@tinysec

@tinysec tinysec commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

Three correctness bugs in the BinaryView search family, all surfaced by the exhaustive parity audit against the official Python binding. Each one crashed the process (AccessViolation) on the first match, so FindAllText / FindAllConstant were entirely unusable from C#.

1. Wrong delegate type — crash on first match

FindAllText and FindAllConstant both passed a 3-arg MatchDataDelegate thunk to core APIs whose native match callback has a different argument shape:

  • BNFindAllTextWithProgressbool(*)(ctxt, addr, const char* text, BNLinearDisassemblyLine* line) (4 args)
  • BNFindAllConstantWithProgressbool(*)(ctxt, addr, BNLinearDisassemblyLine* line) (3 args)

The thunk reinterpreted the wrong native pointer (text* or line* read as a BNDataBuffer*) and crashed. Each call is now wired to its matching MatchTextDelegate / MatchConstantDelegate. (Verified against binaryninjacore.h:4917.)

2. Struct passed by pointer instead of by value — crash while rendering

The generated P/Invokes declared their BNFunctionViewType parameter as in (by-ref), which marshals the by-value C struct as a pointer. The core read garbage type/name fields and crashed during linear disassembly. FindAllData was unaffected only because it takes no viewType. Dropped in across all 8 sites so the blittable struct is passed by value.

3. Null DisassemblySettings — crash

FindAllText/FindAllConstant forwarded a null settings as IntPtr.Zero, but the core dereferences the settings pointer while rendering. The official Python binding always builds a default DisassemblySettings; this now does the same (disposing only the one it allocates, never a caller-owned instance).

Plus: per-match ownership & GC rooting

  • The match callback owns each matched BNDataBuffer / BNLinearDisassemblyLine (per Python's DataBuffer(handle=) __del__ and _LinearDisassemblyLine_convertorBNFreeLinearDisassemblyLines). The MatchData thunk now takes (owns) the buffer; MatchText/MatchConstant thunks free the line after MustFromNativePointer copies it eagerly (freeing is safe). The earlier borrow / never-free forms leaked one object per match.
  • Every match thunk is now rooted with GC.KeepAlive so a mid-scan GC cannot free it.
  • The wrappers were converted from lambdas to named holder methods (no-lambda rule).

Verification

  • Binding builds clean (0 warnings, 0 errors).
  • New E2E parity tests in sharp-binaryninja-tests: FindAllData / FindAllText / FindAllConstant each drive the callback through ≥1 real match. All 28 harness tests pass (25 prior + 3 new), no regressions.
  • Ground-truth checked against the official Python binding (find_all_text / find_all_constant succeed on the same cat.bndb).

Notes

  • The in → by-value fix is in 8 generated Function/BN*.cs files. The generator should be updated to emit by-value struct params for BNFunctionViewType (and any other by-value structs) so this doesn't regress on regen.
  • DisassemblySettings option parity (Python sets ShowAddress=False, ShowOpcode=False, etc.) is intentionally deferred — the core-default settings work; that's a rendering-preference refinement, not a correctness fix.

…y-value)

Three correctness bugs in the BinaryView search family, surfaced by the parity
audit against the official Python binding:

1. Wrong delegate type (crash on first match). FindAllText and FindAllConstant
   both passed a 3-arg MatchDataDelegate thunk to core APIs whose native match
   callback has a different shape (const char* text + BNLinearDisassemblyLine*,
   or a bare line*). The thunk reinterpreted the wrong native pointer and
   AccessViolation-crashed on the first match. Each call is now wired to its
   matching MatchTextDelegate / MatchConstantDelegate.

2. Struct passed by pointer instead of by value (crash while rendering). The
   generated P/Invokes declared their BNFunctionViewType parameter as `in`
   (by-ref), which marshals the by-value C struct as a pointer; the core read
   garbage fields and crashed. Dropped `in` across all 8 sites so the blittable
   struct is passed by value (BNFindAll/Next Text/Constant*, BNCreate*Graph).

3. Null DisassemblySettings (crash). FindAllText/FindAllConstant forwarded a
   null settings as IntPtr.Zero, but the core dereferences it while rendering.
   Default to a constructed DisassemblySettings like the Python binding, and
   dispose only the one allocated here, never a caller-owned instance.

Also fixed per-match ownership (the callback owns each matched BNDataBuffer /
line per Python): the MatchData thunk now takes (owns) the buffer, the
MatchText/MatchConstant thunks free the BNLinearDisassemblyLine after copying
it (MustFromNativePointer copies eagerly, so freeing is safe); and every match
thunk is rooted with GC.KeepAlive so a mid-scan GC cannot free it. The wrappers
were converted from lambdas to named holder methods to match the no-lambda rule.
@tinysec tinysec merged commit ba6306b into master Jul 8, 2026
1 check passed
@tinysec tinysec deleted the fix/findall-match-delegate-marshal branch July 8, 2026 05:06
tinysec added a commit that referenced this pull request Jul 8, 2026
…#35)

Closes HIGH missing-capi gap #19. The binding declared the five
P/Invokes (BNDuplicateTypeLibrary, BNRegisterTypeLibrary,
BNRemoveTypeLibraryNamedObject, BNRemoveTypeLibraryNamedType,
BNLookupTypeLibraryByGuid) but exposed no managed wrappers, so the
TypeLibrary lifecycle and named-store mutation surface was incomplete
vs Python (typelibrary.py duplicate/register/remove_named_object/
remove_named_type/lookup_by_guid).

Add:
- TypeLibrary.Duplicate() (fresh-GUID copy),
- TypeLibrary.Register() (method form of Python's register property),
- TypeLibrary.RemoveNamedObject(QualifiedName) / RemoveNamedType,
  pinning the BNQualifiedName via ScopedAllocator.AllocStruct because
  the Remove P/Invokes take BNQualifiedName* (a pointer) whereas Add
  takes it by reference,
- static TypeLibrary.LookupByGuid(Architecture, string).
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