llvm / llvm/llvm-project

[BPF] Miscompile: conditional branch over an unreachable/zero-instruction successor lands on the wrong target

Open
#208,984 0 comments 0 reactions 0 assignees View on GitHub
backend:BPF miscompilation
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

## 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 :
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 :
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
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 :
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`

Contributor guide

Open the contributing guide

Research direction

Start with repro.c and minbug.ll, compile them with clang or llc, and inspect the BPF backend's conditional-branch lowering and block layout. Add a regression test for a zero-instruction or unreachable successor, then verify the generated code keeps branch targets in bounds and matches the IR semantics.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.