Repository navigation
Description
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:
- Open
/bin/lsfrom macOS 26.6.2 (SHA-25624752389e1d97c9555dd153b644902fadd460dfbe1a166251876c67bbacb0810), and choose the arm64e slice. - Wait for analysis to finish.
- 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_strlenasbl <variable>, and the first instruction of the stub asb <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)- 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
__constof__DATA_CONSTat0x1000084d0, in__bssat0x10000c1d0, or at the extern_strlenin.externat0x100018240, 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. Settargetto the address of the extern_strlenin.extern,0x100018240. Leave out the write ofbl <variable>, keep the write ofb <variable>at the stub, and setstub.inline_during_analysis = Truebeforebv.update_analysis_and_wait(). The inlined call insub_100001a78at0x100001e48readsx0_10 = 0x100018240()and does not pass any arguments. The tail call in_strlenstill passes five arguments. - After
call.function.reanalyze(), the call still readsx0_10 = 0x10000c010(x0_9).
Activity