LiveRangeEdit::foldAsLoad erases folded instruction without notifying delegate; stale MachineInstr* on RegisterCoalescer worklist
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Component
`llvm/lib/CodeGen` — Register allocation / RegisterCoalescer / LiveRangeEdit
---
## Description
`LiveRangeEdit::foldAsLoad()` can erase the folded **use** instruction
(`UseMI`) without calling `Delegate::LRE_WillEraseInstruction()`. When the
register coalescer is the delegate, that instruction may still be referenced
from the coalescer worklist. A subsequent `RegisterCoalescer::joinCopy()` then
dereferences freed `MachineInstr` storage (observed as ASan
`use-after-poison` in builds with a bump-pointer allocator that poisons freed
slots).
`eliminateDeadDef()` already notifies the delegate before erasing an
instruction:
```cpp
if (TheDelegate)
TheDelegate->LRE_WillEraseInstruction(MI);
LIS.RemoveMachineInstrFromMaps(*MI);
MI->eraseFromParent();
```
`foldAsLoad()` replaces `UseMI` with a folded instruction and erases `UseMI`
without the same notification:
```cpp
LIS.ReplaceMachineInstrInMaps(*UseMI, *FoldMI);
// ...
UseMI->eraseFromParent(); // no LRE_WillEraseInstruction
```
`RegisterCoalescer::LRE_WillEraseInstruction()` inserts the MI into
`ErasedInstrs`, and `copyCoalesceWorkList()` (since #79820) skips entries in
`ErasedInstrs` / `CurrentErasedInstrs` before calling `joinCopy()`.
### When does this trigger?
`foldAsLoad()` only succeeds when `TargetInstrInfo::foldMemoryOperand()` /
`foldMemoryOperandImpl(MI, LoadMI, …)` returns a new instruction. In upstream
**release/21.x**, in-tree RISC-V does not implement the `LoadMI` overload (it
returns `nullptr`), so the attached IR does **not** crash vanilla upstream
`llc`.
The bug becomes reachable when a target backend implements regalloc-time
`foldMemoryOperandImpl` that rematerializes immediates through non-load
instructions (e.g. folding a `COPY` use into an `ADDI $x0, imm` when the def is
another immediate producer). We hit this in a downstream RISC-V tree; the
failure mode matches the missing delegate notification, not a target-specific
coalescer change.
### Observed failure (downstream RISC-V, ASan build)
With `-debug-only=regalloc`, immediately before the crash:
```
Try to fold single def: %4:gpr = ADDI $x0, 3
into single use: %6:gpr = COPY %4:gpr
folded: %6:gpr = PseudoMovImm 3
RegisterCoalescer::joinCopy …
==ERROR: AddressSanitizer: use-after-poison …
RegisterCoalescer::joinCopy … RegisterCoalescer.cpp
LiveIntervals::getInstructionIndex …
```
A dead `COPY` on the coalescer worklist was erased by `foldAsLoad()` while
coalescing locals continued in the same `copyCoalesceWorkList()` pass.
---
## Steps to reproduce
**Upstream note:** vanilla upstream `llc` on the IR below completes
successfully (no in-tree RISC-V `foldMemoryOperandImpl` hook). Reproduction
requires a backend where `foldMemoryOperand(MI, Ops, LoadMI)` can fold a
single-use `COPY` when `LoadMI` materializes a constant (or an ASan/instrumented
build of such a downstream tree).
1. Build `llc` with assertions and preferably ASan.
2. Compile the attached `foldasload-coalescer-uaf.ll` at `-O3` for a 64-bit
RISC-V triple (or any target with the regalloc fold hook described above).
3. Observe ASan abort in `RegisterCoalescer::joinCopy` after a successful
`foldAsLoad` during `eliminateDeadDefs()`.
```bash
llc -O3 -mtriple=riscv64 foldasload-coalescer-uaf.ll -o /dev/null
```
---
## Minimal IR (21 lines)
Save as `foldasload-coalescer-uaf.ll`:
```llvm
; RUN: llc -O3 -mtriple=riscv64 %s -o /dev/null
target triple = "riscv64-pc-unknown-gnu"
define void @f() {
entry:
br label %loop
loop:
%i32 = phi i32 [ %and, %inc ], [ 3, %entry ]
%i8 = phi i8 [ %tr, %inc ], [ 3, %entry ]
%c = icmp eq i8 %i8, 0
br i1 %c, label %exit, label %inc
exit:
ret void
inc:
%tr = trunc i32 %i32 to i8
%and = and i32 %i32, 255
br label %loop
}
```
The loop shape keeps a dead-copy / remat opportunity alive through register
coalescing and DCE inside the coalescer.
---
## Proposed fix
Notify the delegate before erasing `UseMI`, mirroring `eliminateDeadDef()`:
```diff
--- a/llvm/lib/CodeGen/LiveRangeEdit.cpp
+++ b/llvm/lib/CodeGen/LiveRangeEdit.cpp
@@ -252,6 +252,8 @@ bool LiveRangeEdit::foldAsLoad(LiveInterval *LI,
if (UseMI->shouldUpdateAdditionalCallInfo())
UseMI->getMF()->moveAdditionalCallInfo(UseMI, FoldMI);
+ if (TheDelegate)
+ TheDelegate->LRE_WillEraseInstruction(UseMI);
UseMI->eraseFromParent();
DefMI->addRegisterDead(LI->reg(), nullptr);
Dead.push_back(DefMI);
```
Verified on a downstream tree with the RISC-V regalloc fold hook: the ASan
failure disappears and codegen completes for the IR above.
---
## Related work
- #79820 / `95b14da678f4` — RegisterCoalescer clears worklist entries recorded
in `ErasedInstrs` / `CurrentErasedInstrs`; this fix ensures `foldAsLoad`
actually records erased uses.
---
## Environment
- LLVM version: 21.1.8 (`release/21.x`, commit `2078da43e25a`)
- Host: Linux x86_64
- Target tested: `riscv64` (`llc -O3`)
- Failure only observed when regalloc `foldMemoryOperandImpl(MI, LoadMI)` is
implemented for the target; vanilla upstream RISC-V does not trigger it today.
Contributor guide
Research direction
Start in llvm/lib/CodeGen/LiveRangeEdit.cpp, comparing foldAsLoad() with eliminateDeadDef() and the delegate behavior in RegisterCoalescer. Run the issue's llc command with the provided IR in a backend that supports the described fold hook; done means UseMI is recorded before erasure and the reproducer completes without the stale-worklist failure.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100