| 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