llvm / llvm/llvm-project

[BPF] Trailing DBG_VALUE suppresses exit after __bpf_trap

Open
#221,150 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

A trailing debug record changes the ordinary BPF instructions emitted for an
LLVM IR module accepted by `llvm-as` and LLVM's IR verifier. Without the
record, LLVM emits
`call __bpf_trap; exit`. With it, the record lowers to a trailing `DBG_VALUE`,
and LLVM omits the `exit` that `addExitAfterUnreachable()` is intended to add.

This reproduces with LLVM 24.0.0git at revision
`9e3a7335e77e92cf4d739d78592c6c31900c4e97` on x86-64 Linux, targeting
`bpfel` with `-mcpu=v3`.

## Reproducer

Attach these two files individually:

- `reduced.ll.txt` (the `.txt` suffix is only for GitHub uploads); and
- `reproduce.sh`.

[reduced.ll.txt](https://github.com/user-attachments/files/31823351/reduced.ll.txt)

[reproduce.sh](https://github.com/user-attachments/files/31823354/reproduce.sh)

Run:

```bash
bash reproduce.sh
```

The script checks the IR with `llvm-as` and `opt -passes=verify`, enables
`-verify-machineinstrs`, and compares the input with a copy that differs only
by deletion of the `#dbg_value` record.

## Actual behavior

```text
without #dbg_value:
call __bpf_trap
exit
with #dbg_value:
call __bpf_trap
without #dbg_value exit count: 1
with #dbg_value exit count: 0
```

The same difference is present in the encoded objects: the version without
the debug record contains a `call` relocation for `__bpf_trap` followed by an
`exit`, whereas the version with the record contains only the call.

Immediately before `bpf-mi-pre-emit-peephole`, the ordinary Machine IR is
identical. The debug variant has one additional `DBG_VALUE` after
`JAL @__bpf_trap`.

## Cause

`BPFMIPreEmitPeepholeImpl::addExitAfterUnreachable()` examines the physical
last instruction in the block:

```cpp
MachineInstr &MI = MBB.back();
if (MI.getOpcode() != BPF::JAL || !MI.getOperand(0).isGlobal() ||
MI.getOperand(0).getGlobal()->getName() != BPF_TRAP)
return false;
```

The trailing `DBG_VALUE` therefore hides the final ordinary
`JAL @__bpf_trap`, and the pass returns without inserting `BPF::RET`.

The exit-insertion logic was introduced in
https://github.com/llvm/llvm-project/pull/131731 to prevent fallthrough after a
terminal trap call and allow the kernel verifier to report the dedicated
`__bpf_trap` diagnostic.

## Expected behavior

Debug-only instructions should not affect exit insertion. Both variants
should emit `call __bpf_trap; exit`.

One focused fix is to inspect the last non-debug instruction, while retaining
pseudo probes as barriers, rather than using `MBB.back()`.

## Impact boundary

For a reachable `__bpf_trap`, the current Linux verifier rejects both
variants. With the exit, verification can reach the dedicated
`unexpected __bpf_trap() due to uninitialized variable?` diagnostic. Without
it, structural verification instead rejects the subprogram as
`last insn is not an exit or jmp`.

Contributor guide

Open the contributing guide

Research direction

Start with BPFMIPreEmitPeepholeImpl::addExitAfterUnreachable() and inspect how it uses MBB.back() around trailing DBG_VALUE instructions. Run reproduce.sh with reduced.ll.txt to confirm the differing instruction and exit counts. Done means debug-only instructions no longer suppress the exit after __bpf_trap, while pseudo probes remain barriers.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, linux
Domain
compilers
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.