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:
When a data variable starts inside a jump table, Binary Ninja ends the table at that variable. The table ends there even when the bounds check on the switch index allows more entries. Binary Ninja's own analysis can define such a variable inside a table.
A stub of the form adrp x16, target@PAGE; add x16, x16, target@PAGEOFF; br x16 loads a page address with adrp. Binary Ninja defines an auto void data variable at that address. The stub uses the page address only to compute the branch target. When the page address falls inside a jump table elsewhere in __text, Binary Ninja ends the table at the variable. Binary Ninja's analysis of the switch then misses the remaining cases. The linear sweep makes a function out of each missed case.
In the attached repro, _main has a 91-case switch:
cmp w16, #0x5a
b.hi default
adrp x17, table@PAGE
add x17, x17, table@PAGEOFF
ldrsw x16, [x17, x16, lsl #2]
adr x17, base
add x16, x17, x16
br x16
Its table runs from 0x100002f6c to 0x1000030d8, across the page boundary at 0x100003000. _target_stub at 0x100001000 branches to _target at 0x1000030d8, whose page is 0x100003000.
Binary Ninja then:
- defines a
voiddata variable at0x100003000, whose only code reference is the stub'sadrpat0x100001000; - types the table at
0x100002f6casuint32_t[0x25], which is 37 entries ending at that variable; - gives the
br x16at0x10000202c37 of its 91 targets; - makes 54 functions out of the missed cases,
sub_1000021ectosub_100002468.
control is built from the same source without _target_stub. Its table is uint32_t[0x5b], the br has all 91 targets, and no other function starts inside _main. repro gave the same result on three runs.
Steps To Reproduce:
- Unzip the attachment and open
reproin Binary Ninja with default settings. Wait for analysis to finish. - Go to
0x100003000. Avoiddata variable is defined there, with one code reference, from_target_stubat0x100001000. - Go to
0x100002f6c. The jump table is typeduint32_t[0x25]. - Go to the
br x16at0x10000202cin_main. It has 37 targets. The functionssub_1000021ectosub_100002468start inside_main's case code. - Open
controlthe same way. Its table isuint32_t[0x5b], and thebrhas 91 targets.
check.py prints the same facts headless: python3 check.py repro, then python3 check.py control.
Expected Behavior:
cmp w16, #0x5a; b.hi limits the table at 0x100002f6c to 91 entries. Binary Ninja should read all 91, and the br x16 in _main should have all 91 targets, as it does in control.
The stub uses the page address 0x100003000 only to compute _target's address. Binary Ninja should not define a data variable there.
Screenshots/Video Recording:
N/A
Binary:
quick space launches inferentially
The sources repro.s and control.s, the binaries built from them with clang -arch arm64 <file>.s -o <file>, and check.py.
Additional Information:
- Measured headless with default settings and user plugins disabled (
BN_DISABLE_USER_PLUGINS=1). - Two changes would each give
reprothe resultcontrolgets: reading as many entries as the bounds check allows, or not defining a variable at the stub's page address. The code states the table's size, and a variable inside the table should not shorten the table. - Workaround: define a data variable, auto or user, over the whole table before analysis. With
int32_t[91]defined at0x100002f6cfirst, Binary Ninja does not define a variable at0x100003000, thebrhas 91 targets, and no function starts inside_main. - Only the
adrp/add/brstub pattern was tested.
Activity
[-]AArch64: a stub’s adrp page address becomes a data variable that truncates an unrelated jump table[/-][+][AArch64] A stub’s adrp page address becomes a data variable that truncates an unrelated jump table[/+]plafosse commentedon Oct 21, 2026
We intentionally clamp jump table entries when we run into another DataVariable as a heuristic to prevent going off the rails. Perhaps we can eliminate that clamping if the DataVariable causing the clamp is also part of the current function and not used in another jump table calculation. This is a tricky edge case that we have to be very careful with.
WeiN76LQh commentedon Oct 21, 2026
Whats the reason for defining a data variable at the adrp page address? That sounds like the easier solution to just drop that.
plafosse commentedon Oct 21, 2026
Its overaggressive DataVariable creation and we just don't have a good mechanism for disambiguating partial pointers from full pointers.
WeiN76LQh commentedon Oct 21, 2026
Given
adrp/adr+addis a common instruction sequence in aarch64 would that not be relatively easy to identify whats a partial vs full pointer? I don't imagine there's a case where you getadrp/adrfollowed by anaddbut the compiler intends to use the result of theadrp/adrand the value after theadd. Even then you could just reduce it to where theaddmust overwrite the register set byadrp/adr.plafosse commentedon Oct 21, 2026
You're right that we could (and probably should) handle the common adrp+add however that isn't complete solution you frequently get things like adrp followed by a conditional branch with a different add on each branch, so its not as easy as just a pattern match. We also do have some unused intermediate value tracking but I'm not fully sure what the conditions on that are.
WeiN76LQh commentedon Oct 21, 2026
There's always an edge case 😅
plafosse commentedon Oct 21, 2026
dealing with edge cases is my entire life
WeiN76LQh commentedon Oct 21, 2026
I felt the pain in that comment 😂