Skip to content

[AArch64] A stub’s adrp page address becomes a data variable that truncates an unrelated jump table #8672

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:
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 void data variable at 0x100003000, whose only code reference is the stub's adrp at 0x100001000;
  • types the table at 0x100002f6c as uint32_t[0x25], which is 37 entries ending at that variable;
  • gives the br x16 at 0x10000202c 37 of its 91 targets;
  • makes 54 functions out of the missed cases, sub_1000021ec to sub_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:

  1. Unzip the attachment and open repro in Binary Ninja with default settings. Wait for analysis to finish.
  2. Go to 0x100003000. A void data variable is defined there, with one code reference, from _target_stub at 0x100001000.
  3. Go to 0x100002f6c. The jump table is typed uint32_t[0x25].
  4. Go to the br x16 at 0x10000202c in _main. It has 37 targets. The functions sub_1000021ec to sub_100002468 start inside _main's case code.
  5. Open control the same way. Its table is uint32_t[0x5b], and the br has 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 repro the result control gets: 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 at 0x100002f6c first, Binary Ninja does not define a variable at 0x100003000, the br has 91 targets, and no function starts inside _main.
  • Only the adrp/add/br stub pattern was tested.

Activity

  1. changed the title [-]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[/+] on Oct 19, 2026
  2. plafosse commented on Oct 21, 2026

    @plafosse
    Member

    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.

  3. WeiN76LQh commented on Oct 21, 2026

    @WeiN76LQh
    Author

    Whats the reason for defining a data variable at the adrp page address? That sounds like the easier solution to just drop that.

  4. plafosse commented on Oct 21, 2026

    @plafosse
    Member

    Its overaggressive DataVariable creation and we just don't have a good mechanism for disambiguating partial pointers from full pointers.

  5. WeiN76LQh commented on Oct 21, 2026

    @WeiN76LQh
    Author

    Given adrp/adr + add is 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 get adrp/adr followed by an add but the compiler intends to use the result of the adrp/adr and the value after the add. Even then you could just reduce it to where the add must overwrite the register set by adrp/adr.

  6. plafosse commented on Oct 21, 2026

    @plafosse
    Member

    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.

  7. WeiN76LQh commented on Oct 21, 2026

    @WeiN76LQh
    Author

    There's always an edge case 😅

  8. plafosse commented on Oct 21, 2026

    @plafosse
    Member

    dealing with edge cases is my entire life

  9. WeiN76LQh commented on Oct 21, 2026

    @WeiN76LQh
    Author

    I felt the pain in that comment 😂

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

Metadata

Metadata

Assignees

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