| Issue |
208984
|
| Summary |
[BPF] Miscompile: conditional branch over an unreachable/zero-instruction successor lands on the wrong target
|
| Labels |
new issue
|
| Assignees |
|
| Reporter |
choury
|
## Summary
The BPF backend miscompiles a conditional branch whose successor lowers to **zero instructions** — an `unreachable` block, or a block containing only an inline-asm `barrier()` with no terminator (which `MachineBlockPlacement` can synthesize via tail-duplication). The conditional-branch offset is computed against that zero-instruction block's address, so the branch silently lands on whatever is laid out next: past the function end, or (as in the reproducer below) on the preceding `call`. The generated code does **not** match the IR semantics, and there is **no diagnostic**.
This is a silent miscompile reproducible from ~25 lines of plain C with `clang -target bpf -mcpu=v4 -O1`. Verified still present on LLVM trunk (23, 2026-06-22).
## Reproducer (C)
```c
/* repro.c */
extern void raise_fn(void) __attribute__((noreturn));
static volatile int counter;
static volatile int pending;
#define barrier() asm volatile("" :: : "memory")
static __attribute__((always_inline)) inline void int_on(void) {
barrier();
if (--counter == 0 && pending)
raise_fn(); /* noreturn */
barrier(); /* trailing barrier: returns to caller */
}
__attribute__((noreturn)) void f(int c) {
if (c)
int_on();
else
raise_fn();
__builtin_unreachable();
}
```
Compile + disassemble:
```
clang -target bpf -mcpu=v4 -O1 -c repro.c -o repro.o
bpf-objdump -d repro.o
```
Output:
```
0000000000000000 <f>:
0: 16 01 0a 00 00 00 00 00 jeq32 %r1,0,10 ; c==0 -> raise_fn
8: 18 01 00 00 00 00 00 00 lddw %r1,0
18: 61 12 00 00 00 00 00 00 ldxw %r2,[%r1+0]
20: 04 02 00 00 ff ff ff ff add32 %r2,-1
28: 63 21 00 00 00 00 00 00 stxw [%r1+0],%r2
30: 56 02 05 00 00 00 00 00 jne32 %r2,0,5 ; counter!=0 -> WRONG target
38: 18 01 00 00 04 00 00 00 lddw %r1,4
48: 61 11 00 00 00 00 00 00 ldxw %r1,[%r1+0]
50: 16 01 01 00 00 00 00 00 jeq32 %r1,0,1 ; pending==0 -> WRONG target
58: 85 10 00 00 ff ff ff ff call -1 ; raise_fn (block 10)
60: 85 10 00 00 ff ff ff ff call -1
68: 95 00 00 00 00 00 00 00 exit
```
## Why it is wrong
The IR for `f` is well-formed:
```llvm
3: ; preds = %1
... ; --counter
%6 = icmp eq i32 %5, 0
br i1 %6, label %7, label %11 ; counter!=0 -> %11
7: ; preds = %3
... ; load pending
br i1 %9, label %11, label %10 ; pending==0 -> %11
10: ; preds = %7
tail call void @raise_fn() #3
unreachable
11: ; preds = %3, %7
tail call void asm sideeffect "", "~{memory}"() ; the trailing barrier()
unreachable ; <-- lowers to ZERO instructions
```
`%11` (the "skip `raise_fn`" path — `counter != 0` or `pending == 0`) is a `barrier(); unreachable` block. The `barrier()` is empty inline asm; the block lowers to **no BPF instructions**.
In the disassembly, `%11` is laid out last and emits nothing, so its address sits at/after the end of the real code. The two conditional branches (`0x30`, `0x50`) that target `%11` instead resolve to `0x58` — **the `call raise_fn`** — i.e. the "do not raise" path jumps straight into `raise_fn`. A clear miscompile.
A 12-line hand-written `.ll` reproduces the same defect via `llc` alone (no `opt`, no pipeline), isolating it to the backend's lowering/layout, not the optimizer:
```llvm
; minbug.ll
target triple = "bpf"
define void @f(i1 %c) {
br i1 %c, label %unreachable_taken, label %fall
fall:
store i32 0, ptr null, align 4
unreachable
unreachable_taken:
unreachable
}
define void @next() { ret void }
```
```
0000000000000000 <f>:
0: 54 01 00 00 01 00 00 00 and32 %r1,1
8: 56 01 02 00 00 00 00 00 jne32 %r1,0,2 ; -> 0x20 = start of <next>
10: b7 01 00 00 00 00 00 00 mov %r1,0
18: 62 01 00 00 00 00 00 00 stw [%r1+0],0
0000000000000020 <next>:
20: 95 00 00 00 00 00 00 00 exit
```
## Scope
- Reproduces on Debian LLVM **19.1.7**, **21.1.8**, and the **development trunk** (`Debian clang version 23.0.0`, build `++20260622104938+cd3bfc1f9926`, 2026-06-22). Identical miscompiled offsets (`jne32 off=5`, `jeq32 off=1`) on all three — **unfixed on trunk**.
- Reproduces on **every** `-mcpu` (`generic`/`v1`/`v2` emit `jne`/`jeq`; `v3`/`v4` emit `jne32`/`jeq32`).
- The `noinline` workaround (mark `int_on` `noinline` instead of `always_inline`) makes the trailing barrier block not get inlined and the miscompile disappears — confirming the zero-instruction-successor shape is the trigger.
This is distinct from the known 16-bit jump-offset overflow issues (#48509, #86780, #178523, #134462, #55669): those require very large functions (>32k insns) and produce either a compile-time `LLVM ERROR` or a truncated 16-bit offset. This one triggers in a ~13-instruction function with offsets of `5` and `1` (far from any overflow) and is completely silent.
## Root cause (analysis)
A conditional-branch successor that lowers to zero instructions has an MBB address equal to the end of the real emitted code. The branch offset is computed as `(target_MBB_addr - branch_addr) / 8`; since the target MBB has no body, control falls through past the function boundary (single-function case) or onto whatever precedes it in layout (multi-block case). Other backends typically either fold such a branch (the unreachable successor is trivially dead) or emit an explicit terminator (e.g. `exit`/`trap`) in the unreachable MBB so the offset is always in-bounds. The BPF backend does neither and emits a real conditional jump with an unresolved/wrong target.
## Suggested fix direction
The BPF backend should not emit a conditional jump whose target is a zero-instruction / `unreachable` successor. Either:
1. fold the branch — the live successor is the only real destination, or
2. materialize an explicit terminator (`exit`) in `unreachable`/barrier-only MBBs so every branch target resolves to an in-bounds, in-function address.
## Environment
- Verified on `Debian clang version 19.1.7`, `21.1.8`, and trunk `23.0.0` (2026-06-22). Identical miscompile on all three.
- Reproduce: `clang -target bpf -mcpu=v4 -O1 -c repro.c -o repro.o && bpf-objdump -d repro.o`
_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs