Skip to content

fix: HLIL member_index masking + operand-count table (root cause)#24

Merged
tinysec merged 1 commit into
masterfrom
fix/hlil-member-index-masking
Jul 8, 2026
Merged

fix: HLIL member_index masking + operand-count table (root cause)#24
tinysec merged 1 commit into
masterfrom
fix/hlil-member-index-masking

Conversation

@tinysec

@tinysec tinysec commented Jul 8, 2026

Copy link
Copy Markdown
Owner

Summary

Python's _get_member_index masks the sentinel (1<<63) and returns None when there is no named member; the binding returned the raw ulong and never returned null.

Investigation surfaced the deeper root cause: the OperationOperands table undercounted the three field instructions by omitting the member_index slot, so RawOperands was one short and any operand access on them (including the existing Offset) threw IndexOutOfRangeException. member_index was therefore never readable at all.

Changes

  • OperationOperands — corrected counts:

    • HLIL_STRUCT_FIELD: 2 → 3 (source, offset, member_index)
    • HLIL_DEREF_FIELD: 2 → 3
    • HLIL_DEREF_FIELD_SSA: 3 → 4 (source, srcMem, offset, member_index)

    Growing the count only adds the member_index slot; existing operand indices are unchanged, so Offset/Source/SourceMemory are unaffected.

  • HighLevelILInstruction.MemberIndexFromRawOperand — masks the (1<<63) sentinel to null, mirroring Python _get_member_index.

  • HLILStructField.MemberIndex, HLILDerefField.MemberIndex, HLILDerefFieldSSA.MemberIndex — now long? via the helper. The two Deref members are renamed from MemoryIndex for parity with StructField (no internal callers reference the old name).

Verification

  • Binding builds clean (0 warnings, 0 errors).
  • New E2E test: traverse cat.bndb HLIL field instructions; assert MemberIndex is null or a non-negative index (never the leaked sentinel) and that cat's no-named-member struct field surfaces as null, matching Python member_index == None.
  • Full harness regression: 38/38 pass (no regressions from the operand-count change).

Python's _get_member_index masks the sentinel (1<<63) and returns None when
there is no named member; the binding returned the raw ulong and never returned
null. Investigation showed the deeper root cause: the OperationOperands table
undercounted the three field instructions by omitting the member_index slot, so
RawOperands was one short and ANY operand access on them (including the
existing Offset) threw IndexOutOfRangeException. member_index was therefore
never readable at all.

- OperationOperands: HLIL_STRUCT_FIELD 2->3, HLIL_DEREF_FIELD 2->3,
  HLIL_DEREF_FIELD_SSA 3->4 (source, offset/srcMem, member_index). Growing the
  count only adds the member_index slot; existing operand indices are unchanged.
- HighLevelILInstruction.MemberIndexFromRawOperand: masks the (1<<63) sentinel
  to null, mirroring Python _get_member_index.
- HLILStructField.MemberIndex, HLILDerefField.MemberIndex,
  HLILDerefFieldSSA.MemberIndex: long? via the helper. The two Deref members
  are renamed from MemoryIndex for parity with StructField (no internal callers).
@tinysec tinysec merged commit 49ea349 into master Jul 8, 2026
1 check passed
@tinysec tinysec deleted the fix/hlil-member-index-masking branch July 8, 2026 06:05
tinysec added a commit that referenced this pull request Jul 8, 2026
)

Closes HIGH missing-capi gap #24. The binding declared the
BNRustSimplifyStrToFQN P/Invoke (which returns a BNQualifiedName by value)
but declined to expose it: two TODO notes warned the struct-return (sret)
ABI made the binding "non-trivial" and redirected callers to the
string-only RustSimplifyStrToStr, so Python's
demangle.simplify_name_to_qualified_name (demangle.py:253) had no managed
equivalent.

The sret concern was unfounded: BNQualifiedName is blittable (two IntPtrs
plus a ulong), so the P/Invoke marshaller handles the struct-return ABI
directly -- the same pattern as BNTypeBuilderGetStructureName. Verified
end-to-end: a templated C++ name collapses identically to Python.

Add Core.SimplifyNameToQualifiedName with a string overload (simplify
flag, defaults true) and a QualifiedName overload (Python forces
simplify=true for qualified-name input). Both wrap via
QualifiedName.TakeNative (reads components eagerly, then frees the native
array) and return null when the simplifier yields no components. Removes
the two stale TODO notes.
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