Skip to content

Binary Ninja applies the type of a data variable of a function type to a tail call to the variable, but not to a call #8675

Open

Description

@WeiN76LQh

The following issue was identified, triaged and written by Claude Opus 5.5, I have read through it to make sure the information is coherent and useful.


Version and Platform (required):

  • Binary Ninja Version: 6.1.10957-dev (build 2899315201)
  • Edition: Ultimate
  • OS: macOS
  • OS Version: 26.6.2 (25G83)
  • CPU Architecture: Apple M2 Ultra

Bug Description:
In the steps below, a call (bl) and a tail call (b) both branch to 0x10000c010 in __data. A user data variable at that address has a function type with five parameters, and no function starts there. Binary Ninja applies the variable's type to the tail call, and the tail call passes five arguments. Binary Ninja does not apply the type to the call. In the medium level IL, the call passes one argument, x0_9. The call's destination has the type int64_t (*)(…), a pointer to the variable's function type.

Steps To Reproduce:

  1. Open /bin/ls from macOS 26.6.2 (SHA-256 24752389e1d97c9555dd153b644902fadd460dfbe1a166251876c67bbacb0810), and choose the arm64e slice.
  2. Wait for analysis to finish.
  3. Run the following script in the Python console. The script defines a user data variable in __data, with a function type that has five parameters. The script then rewrites one call to the stub of _strlen as bl <variable>, and the first instruction of the stub as b <variable>. The script writes to the binary view, not to the file on disk.
import struct

# A variable of a function type of five parameters, in `__data`. No function starts at its address.
target = bv.get_section_by_name("__data").start + 0x10
bv.define_user_data_var(target, bv.parse_type_string("int64_t (int64_t a, int64_t b, int64_t c, int64_t d, int64_t e)")[0])
print("variable:", hex(target), bv.get_data_var_at(target).type)

# The stub of `_strlen`, and the call to the stub at the lowest address.
stub = next(f for f in bv.functions if f.name == "_strlen" and f.start in bv.get_section_by_name("__auth_stubs"))
call = min((r for r in bv.get_code_refs(stub.start) if r.function.start != stub.start), key=lambda r: r.address)

# Rewrite the call as `bl <variable>`, and the stub's first instruction as `b <variable>`.
def branch(pc, destination, link):
    return struct.pack("<I", (0x94000000 if link else 0x14000000) | (((destination - pc) >> 2) & 0x3FFFFFF))
bv.write(call.address, branch(call.address, target, True))
bv.write(stub.start, branch(stub.start, target, False))
bv.update_analysis_and_wait()

def call_at(function, address):
    calls = ("MLIL_CALL", "MLIL_TAILCALL")
    return next(i for i in function.mlil.instructions if i.address == address and i.operation.name.startswith(calls))
tail_call = call_at(stub, stub.start)
print("tail call in", stub.name, "@", hex(stub.start), ":", tail_call)
call_ins = call_at(call.function, call.address)
print("call in", call.function.name, "@", hex(call.address), ":", call_ins)
print("type of the call's destination:", call_ins.dest.expr_type)
  1. The script prints:
variable: 0x10000c010 int64_t(int64_t a, int64_t b, int64_t c, int64_t d, int64_t e)
tail call in _strlen @ 0x1000046d4 : return 0x10000c010(__s, b, c, d, e) __tailcall
call in sub_100001a78 @ 0x100001e48 : x0_10 = 0x10000c010(x0_9)
type of the call's destination: int64_t (*)(int64_t a, int64_t b, int64_t c, int64_t d, int64_t e)

The tail call passes five arguments, and the call passes one. The high level IL of sub_100001a78 reads void* x0_10 = (&data_10000c010)(a).

Expected Behavior:
Binary Ninja applies the type of the variable at the call's destination to the call, as it does to the tail call. The call in sub_100001a78 then passes five arguments and returns an int64_t.

Screenshots/Video Recording:
Not applicable. The script prints the medium level IL of both instructions.

Binary:
/bin/ls from macOS 26.6.2 (SHA-256 above). The steps use a binary that ships with macOS.

Additional Information:

  • The section that contains the variable does not change the result. With the variable in __const of __DATA_CONST at 0x1000084d0, in __bss at 0x10000c1d0, or at the extern _strlen in .extern at 0x100018240, the tail call passes five arguments and the call passes one.
  • With the variable at the extern _strlen, Binary Ninja does not apply the type to a call that it inlines from the stub either. Set target to the address of the extern _strlen in .extern, 0x100018240. Leave out the write of bl <variable>, keep the write of b <variable> at the stub, and set stub.inline_during_analysis = True before bv.update_analysis_and_wait(). The inlined call in sub_100001a78 at 0x100001e48 reads x0_10 = 0x100018240() and does not pass any arguments. The tail call in _strlen still passes five arguments.
  • After call.function.reanalyze(), the call still reads x0_10 = 0x10000c010(x0_9).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions