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

Reply via email to