llvm / llvm/llvm-project

[X86] A debug instruction changes LVI hardening fence placement

Open
#224,484 0 comments 0 reactions 0 assignees View on GitHub
backend:X86
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

## Summary

One `DBG_INSTR_REF` at the start of a join block changes the gadget graph built
by X86 LVI load hardening. The greedy graph cut consequently moves an
`LFENCE` between two ordinary loads. Removing debug information restores the
other placement.

This reproduces with an assertions-enabled LLVM 24.0.0git build at revision
`bc79eb547e6ef6a9bceef4968df0e0a150b96a8d` on x86-64 Linux. Both variants
pass `-verify-machineinstrs`.

## Reproducer

Attach `debug.ll.txt` and `issue-reproducer.sh`, then run:

[debug.ll.txt](https://github.com/user-attachments/files/32361467/debug.ll.txt)
[issue-reproducer.sh](https://github.com/user-attachments/files/32361466/issue-reproducer.sh)

```console
$ bash issue-reproducer.sh
```

The input uses the `+lvi-load-hardening` function attribute. The script runs
the input both as written and after `opt --strip-debug`, prints the locations
of both fences, and compares the final `.text` sections.

## Actual behavior

The input with the debug record places the second fence immediately before the
pointer-consuming load at the outer-loop join. The strip-debug control places
it immediately after the pointer-producing load in the inner loop.

The unmodified compiler produces these `.text` hashes:

```text
with debug: bf285285242f1568f1601ffa49d7e6eb1c380cafc82b564a57f24c2d500b7a45
strip-debug: bb85cdb5db461aa59b0994cefc3e5da5fb848c839ad45c38588383e6bbeac0cc
```

The total number of fences remains two. This report concerns debug-dependent
behavior in a security-hardening pass; it does not claim an exploitable change
or a runtime failure.

## Cause

`X86LoadValueInjectionLoadHardeningImpl::getGadgetGraph()` uses
`MBB->begin()` as each block's graph representative. A leading
`DBG_INSTR_REF` therefore becomes an ordinary graph vertex. The two predecessor
edges enter that debug vertex, while one edge connects it to the first
ordinary instruction, changing the ingress-versus-egress cost used by the
greedy cut.

I have a candidate patch that chooses
`MBB->getFirstNonDebugInstr(/*SkipPseudoOp=*/false)` instead. The explicit
`false` preserves pseudo probes as graph vertices while excluding debug-only
instructions.

## Expected behavior

Debug-only instructions should not be structural vertices in the LVI gadget
graph. Adding or removing debug information should not relocate an `LFENCE`
between ordinary instructions.

Contributor guide

Open the contributing guide

Research direction

Start in X86LoadValueInjectionLoadHardeningImpl::getGadgetGraph() and inspect how each block's graph representative is selected. Run issue-reproducer.sh with the attached debug.ll.txt, compare the fence locations and .text hashes for the debug and strip-debug variants, and confirm that debug-only instructions no longer alter the LFENCE placement.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers, security
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.