Skip to content

fix: marshal every bool-returning P/Invoke return as 1-byte (I1)#13

Merged
tinysec merged 1 commit into
masterfrom
fix/bool-return-i1-marshaling
Jul 6, 2026
Merged

fix: marshal every bool-returning P/Invoke return as 1-byte (I1)#13
tinysec merged 1 commit into
masterfrom
fix/bool-return-i1-marshaling

Conversation

@tinysec

@tinysec tinysec commented Jul 6, 2026

Copy link
Copy Markdown
Owner

.NET's default bool P/Invoke return marshals as UnmanagedType.Bool (reads 4 bytes / EAX). The Binary Ninja core ABI returns a 1-byte C++ bool in AL, and the x64 ABI leaves bits 8..63 of the return register UNDEFINED for a sub-64-bit return. A default bool return therefore reads 3 garbage bytes and can flip a false to true.

Proven empirically with an isolation probe ? a native function returning 0xFFFFFF00 (AL=0, upper bytes 0xFF):

  • default bool return ? True (wrong)
  • [return: MarshalAs(UnmanagedType.I1)] ? False (correct)

It is latent on the installed core (it happens to zero-extend AL ? e2e passes), but the binding targets the upstream ABI, not this install; a different core build (MSVC version/flags) could leave garbage and surface it across all 700 bool returns.

Fix

Add [return: MarshalAs(UnmanagedType.I1)] to every bool-returning P/Invoke stub (all 700, in Function/). Applied via a byte-exact transform that preserves LF line endings ? diff is 700 insertions, 0 deletions. I1 matches the existing codebase convention for 1-byte bool (struct fields use [MarshalAs(UnmanagedType.I1)]).

Like the round-12 scalar-UTF8 fix, these are generated stubs ? the durable fix belongs in the upstream generator.

Validation

  • Build: 0 warnings / 0 errors.
  • e2e: 1428 passed / 0 failed / 3 skipped (was 1413; +15 from a new BoolReturnMarshaling guard with deterministic Metadata.IsSigned/IsUnsignedInteger + Function/BasicBlock bool assertions).

Marked as a fix (functional correctness: non-deterministic bool returns), not a refactor.

.NET's default `bool` P/Invoke return marshals as UnmanagedType.Bool, reading
4 bytes (EAX). The Binary Ninja core ABI returns a 1-byte C++ `bool` in AL, and
the x64 ABI leaves bits 8..63 of the return register UNDEFINED for a sub-64-bit
return. A default bool return therefore reads 3 garbage bytes and can flip a
`false` to `true`.

Proven empirically with an isolation probe: a native function returning
0xFFFFFF00 (AL=0, upper bytes 0xFF) reads `True` under the default and `False`
under UnmanagedType.I1. Latent on the installed core (it happens to zero-extend
AL), but the binding targets the upstream ABI, not this install, so a different
core build (MSVC version/flags) could leave garbage and surface the bug.

Fix: add `[return: MarshalAs(UnmanagedType.I1)]` to all 700 bool-returning
P/Invoke stubs (applied via a byte-exact transform that preserves LF line
endings; 700 insertions, 0 deletions). I1 matches the existing codebase
convention for 1-byte bool (struct fields use [MarshalAs(UnmanagedType.I1)]).

Like the round-12 scalar-UTF8 fix, these are generated stubs; the durable fix
belongs in the upstream generator (emit the return marshal). Build 0/0; e2e
1428 passed / 0 failed / 3 skipped (new BoolReturnMarshaling guard with
deterministic Metadata.IsSigned/IsUnsignedInteger + Function/BasicBlock bool
assertions).
@tinysec tinysec merged commit 2926870 into master Jul 6, 2026
1 check passed
@tinysec tinysec deleted the fix/bool-return-i1-marshaling branch July 6, 2026 13:23
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