Issue 177761
Summary [BOLT] Gadget scanner reports false positives due to jump tables
Labels BOLT
Assignees
Reporter atrosinenko
    Pointer Authentication analyzer in `llvm-bolt-binary-analysis` may incorrectly
report valid code as unsafe when function is lowered using jump tables.

## How to reproduce

Compile this source (loosely reproducing the issue observed in
`SingleSource/Regression/C/gcc-c-torture/execute` from llvm-test-suite)

```c
__attribute__((__noreturn__)) void abort(void);

int jump_table_test_func(int in, int arg) {
  switch (in) {
  case 0: case 10: case 20: return 1;
  case 1: case 11: case 21: return 15;
  case 2: case 12: case 22: return -1;
  case 3: case 13: case 23: return arg + 42;
  case 4: case 14: case 24: return 3;
  case 5: case 15: case 25: return arg / 2;
  case 6: case 16: case 26: return 0;
  case 7: case 17: case 27: return 42;
  default: abort();
  }
}
```

with recent version of Clang:

```bash
clang -target aarch64-linux-pauthtest -march=v8.3-a -fuse-ld=lld \
      -O3 --shared -nostdlib -Wl,--emit-relocs \
      -faarch64-jump-table-hardening \
      jt-test.c -o jt-test.so
```

Please note that `-faarch64-jump-table-hardening` does not add any new signing
or authentication instructions to the code being emitted, it merely enforces
the dispatcher code sequence being emitted at once and not separated into
several pseudos possibly intermixed with unrelated instructions.
Furthermore, `--shared` and `-nostdlib` options here are mostly used not to
require custom sysroots to reproduce this bug.

I tested this with system-provided Clang on Ubuntu 25.10:

```
$ clang --version
Ubuntu clang version 20.1.8 (0ubuntu4)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/lib/llvm-20/bin
```

The following assembly code is emitted for `jump_table_test_func` function:

<details>
<summary>Generated by `clang -target aarch64-linux-pauthtest -march=v8.3-a -faarch64-jump-table-hardening -O3 -S jt-test.c -o-`</summary>

```asm
jump_table_test_func: // @jump_table_test_func
        .cfi_startproc
// %bb.0:
        cmp w0, #27
        b.hi    .LBB0_10
// %bb.1:
        mov     w16, w0
 mov     w0, #1                          // =0x1
        cmp     x16, #27
        csel    x16, x16, xzr, ls
        adrp    x17, .LJTI0_0
 add     x17, x17, :lo12:.LJTI0_0
        ldrsw   x16, [x17, x16, lsl #2]
.Ltmp0:
        adr     x17, .Ltmp0
        add     x16, x17, x16
 br      x16
.LBB0_2:
        mov     w0, #15                         // =0xf
.LBB0_3:
        ret
.LBB0_4:
        mov     w0, #42 // =0x2a
        ret
.LBB0_5:
        add     w0, w1, #42
 ret
.LBB0_6:
        add     w8, w1, w1, lsr #31
        asr     w0, w8, #1
        ret
.LBB0_7:
        mov     w0, #3                          // =0x3
        ret
.LBB0_8:
        mov     w0, #-1 // =0xffffffff
        ret
.LBB0_9:
        mov     w0, wzr
 ret
.LBB0_10:
        .cfi_b_key_frame
        pacibsp
 .cfi_negate_ra_state
        stp     x29, x30, [sp, #-16]!           // 16-byte Folded Spill
        .cfi_def_cfa_offset 16
        mov     x29, sp
        .cfi_def_cfa w29, 16
        .cfi_offset w30, -8
 .cfi_offset w29, -16
        bl      abort
.Lfunc_end0:
        .size jump_table_test_func, .Lfunc_end0-jump_table_test_func
 .cfi_endproc
        .section        .rodata,"a",@progbits
 .p2align        2, 0x0
.LJTI0_0:
        .word   .LBB0_3-.Ltmp0
 .word   .LBB0_2-.Ltmp0
        .word   .LBB0_8-.Ltmp0
        .word .LBB0_5-.Ltmp0
        .word   .LBB0_7-.Ltmp0
        .word .LBB0_6-.Ltmp0
        .word   .LBB0_9-.Ltmp0
        .word .LBB0_4-.Ltmp0
        .word   .LBB0_10-.Ltmp0
        .word .LBB0_10-.Ltmp0
        .word   .LBB0_3-.Ltmp0
        .word .LBB0_2-.Ltmp0
        .word   .LBB0_8-.Ltmp0
        .word .LBB0_5-.Ltmp0
        .word   .LBB0_7-.Ltmp0
        .word .LBB0_6-.Ltmp0
        .word   .LBB0_9-.Ltmp0
        .word .LBB0_4-.Ltmp0
        .word   .LBB0_10-.Ltmp0
        .word .LBB0_10-.Ltmp0
        .word   .LBB0_3-.Ltmp0
        .word .LBB0_2-.Ltmp0
        .word   .LBB0_8-.Ltmp0
        .word .LBB0_5-.Ltmp0
        .word   .LBB0_7-.Ltmp0
        .word .LBB0_6-.Ltmp0
        .word   .LBB0_9-.Ltmp0
        .word .LBB0_4-.Ltmp0
```

</details>

This results in the following reports generated by gadget scanner (built from
recent mainline commit `3de4d32a7228519312a844bcd48a34124b1a718b`):

```
$ ./bin/llvm-bolt-binary-analysis --scanners=pauth ./jt-test.so
BOLT-INFO: shared object or position-independent executable detected
BOLT-INFO: Target architecture: aarch64
BOLT-INFO: BOLT version: 3de4d32a7228519312a844bcd48a34124b1a718b
BOLT-INFO: first alloc address is 0x0
BOLT-INFO: creating new program header table at address 0x200000, offset 0x200000
BOLT-INFO: enabling relocation mode

GS-PAUTH: Warning: possibly imprecise CFG, the analysis quality may be degraded in this function. According to BOLT, unreachable code is found in function jump_table_test_func, basic block .LFT1, at address 104c0
  The instruction is     000104c0:      mov     w0, #0xf

GS-PAUTH: non-protected call found in function jump_table_test_func, basic block .Ltmp1, at address 104bc
  The instruction is     000104bc:      br      x16 # UNKNOWN CONTROL FLOW
  The 1 instructions that write to the affected registers after any authentication are:
  1.     000104b8:      add     x16, x17, x16
  This happens in the following basic block:
    000104b4:   adr     x17, __ENTRY_jump_table_test_func@0x104b4
    000104b8:   add     x16, x17, x16
 000104bc:   br      x16 # UNKNOWN CONTROL FLOW

GS-PAUTH: non-protected ret found in function jump_table_test_func, basic block .LFT1, at address 104c4
  The instruction is     000104c4:      ret
  The 0 instructions that write to the affected registers after any authentication are:

GS-PAUTH: non-protected ret found in function jump_table_test_func, basic block .LFT2, at address 104cc
  The instruction is     000104cc:      ret
  The 0 instructions that write to the affected registers after any authentication are:

GS-PAUTH: non-protected ret found in function jump_table_test_func, basic block .LFT3, at address 104d4
  The instruction is     000104d4: ret
  The 0 instructions that write to the affected registers after any authentication are:

GS-PAUTH: non-protected ret found in function jump_table_test_func, basic block .LFT4, at address 104e0
  The instruction is     000104e0:      ret
  The 0 instructions that write to the affected registers after any authentication are:

GS-PAUTH: non-protected ret found in function jump_table_test_func, basic block .LFT5, at address 104e8
  The instruction is     000104e8:      ret
  The 0 instructions that write to the affected registers after any authentication are:

GS-PAUTH: non-protected ret found in function jump_table_test_func, basic block .LFT6, at address 104f0
  The instruction is     000104f0:      ret
  The 0 instructions that write to the affected registers after any authentication are:

GS-PAUTH: non-protected ret found in function jump_table_test_func, basic block .LFT7, at address 104f8
  The instruction is     000104f8:      ret
  The 0 instructions that write to the affected registers after any authentication are:
```

## Analysis

Back in the day I tried implementing a fix in #138884, which was
intended to eliminate "non-protected call" false positive reported for the
jump table dispatch instruction itself.

Nevertheless, even with #138884, BOLT is still unable to properly reconstruct
CFG for `jump_table_test_func`:

<details>
<summary>Disassembled `jump_table_test_func` dumped with `-debug-_only_=bolt-pauth-scanner`</summary>

```
.LBB00 (2 instructions, align : 1)
  Entry Point
  CFI State : 0
    00000000:   cmp     w0, #0x1b
    00000004:   b.hi    .Ltmp0
  Successors: .Ltmp0, .LFT0
  CFI State: 0

.LFT0 (7 instructions, align : 1)
  CFI State : 0
 Predecessors: .LBB00
    00000008:   mov     w16, w0
    0000000c:   mov w0, #0x1
    00000010:   cmp     x16, #0x1b
    00000014:   csel    x16, x16, xzr, ls
    00000018:   nop # NOP: 1
    0000001c:   adr     x17, ".rodata/1"
    00000020:   ldrsw   x16, [x17, x16, lsl #2]
  Successors: .Ltmp1
  CFI State: 0

.Ltmp1 (3 instructions, align : 1)
  Secondary Entry Point: __ENTRY_jump_table_test_func@0x104b4
  CFI State : 0
 Predecessors: .LFT0
    00000024:   adr     x17, __ENTRY_jump_table_test_func@0x104b4
    00000028:   add     x16, x17, x16
 0000002c:   br      x16 # UNKNOWN CONTROL FLOW
  CFI State: 0

.LFT1 (2 instructions, align : 1)
  CFI State : 0
    00000030:   mov     w0, #0xf
 00000034:   ret
  CFI State: 0

.LFT2 (2 instructions, align : 1)
 CFI State : 0
    00000038:   mov     w0, #0x2a
    0000003c:   ret
  CFI State: 0

.LFT3 (2 instructions, align : 1)
  CFI State : 0
    00000040: add     w0, w1, #0x2a
    00000044:   ret
  CFI State: 0

.LFT4 (3 instructions, align : 1)
  CFI State : 0
    00000048:   add     w8, w1, w1, lsr #31
    0000004c:   asr     w0, w8, #1
    00000050:   ret
  CFI State: 0

.LFT5 (2 instructions, align : 1)
  CFI State : 0
    00000054: mov     w0, #0x3
    00000058:   ret
  CFI State: 0

.LFT6 (2 instructions, align : 1)
  CFI State : 0
    0000005c:   mov     w0, #-0x1
    00000060:   ret
  CFI State: 0

.LFT7 (2 instructions, align : 1)
  CFI State : 0
    00000064:   mov     w0, wzr
    00000068:   ret
 CFI State: 0

.Ltmp0 (8 instructions, align : 1)
  CFI State : 0
 Predecessors: .LBB00
    0000006c:   pacibsp
    00000070:   stp     x29, x30, [sp, #-0x10]!
    00000074:   !CFI    $0      ; OpDefCfaOffset 16
 00000074:   mov     x29, sp
    00000078:   !CFI    $1      ; OpDefCfa Reg29 16
    00000078:   !CFI    $2      ; OpOffset Reg30 -8
    00000078:   !CFI $3      ; OpOffset Reg29 -16
    00000078:   bl      abort@PLT
  CFI State: 4

DWARF CFI Instructions:
    0:  OpDefCfaOffset 16
    1: OpDefCfa Reg29 16
    2:  OpOffset Reg30 -8
    3:  OpOffset Reg29 -16
End of Function "jump_table_test_func"
```

</details>

For this particular binary, incorrect CFG results in many basic blocks ending
with `ret` instruction being unreachable from BOLT's point of view (note
absence of `Predecessors:` field), resulting in incorrect propagation of
register safety state:

```
.LFT1 (2 instructions, align : 1)
  CFI State : 0
 00000030:   mov     w0, #0xf
    00000034:   ret
  CFI State: 0
```

## Possible solution

It is probably reasonable to consider this issue from two separate points of view:
* CFG reconstruction in BOLT's core
* Preventing false-positive reports in gadget scanner on indirect branch to the
  computed jump table target

These subtasks look related but are somewhat different:
* in BOLT's core we don't have to validate extra hardening available on AArch64,
  we just have to recognize what control flow is possible under normal conditions
* even if CFG is reconstructed perfectly, we still need to understand that this
  particular seemingly unsafe indirect branch is part of a jump table dispatch
  sequence and ignore it as long as it can be matched against a known-good pattern.
 Ideally, we would recognize this pattern as safe according to general rules,
  but not sure it is feasible to implement without a lot of effort.
 The approach of "ignore known-safe sequence", on the other hand, may probably
  introduce some amount of false negatives if constant offsets turn out to
  reside in writable memory, for example.
_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs

Reply via email to