[ARM] Load/store optimizer clobbers a live base register, causing an out-of-bounds load
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
LLVM's ARM load/store optimizer produces an incorrect address when combining double-precision component loads
into `vldmia` instructions. In the reduced example below, the last of six quaternion products reads `rhs[9]`
instead of `rhs[5]`, beyond the end of a six-element array. The input C++ has no out-of-bounds accesses.
Disabling `--arm-load-store-opt` makes the locally reproduced failure disappear. With the attached IR,
assertions-enabled LLVM trunk and `-verify-machineinstrs` report an undefined physical register immediately
after the ARM load/store optimization pass.
**The backend bug is still present in the tested LLVM 24 trunk build. No fixed version has been identified.**
Compiling this C++ source directly with trunk happens to produce a passing executable, but compiling the
known-failing IR with trunk still produces a failing executable. Please use the IR when investigating trunk;
the direct C++ result alone does not establish that the backend bug has been fixed.
## Reduced standalone C++ reproducer
Save this as `repro.cpp`. It requires only C++14 and the standard library.
The integer sequence expands exactly six assignments; all source indices are in bounds.
```cpp
#include
#include
struct Q {
double x, y, z, w;
};
inline Q multiply(const Q& a, const Q& b) {
return {a.w*b.x+a.x*b.w+a.y*b.z-a.z*b.y,
a.w*b.y+a.y*b.w+a.z*b.x-a.x*b.z,
a.w*b.z+a.z*b.w+a.x*b.y-a.y*b.x,
a.w*b.w-a.x*b.x-a.y*b.y-a.z*b.z};
}
template
__attribute__((noinline)) void broadcast(Q* out, const Q* lhs, const Q* rhs,
std::integer_sequence) {
int unused[] = {0, (out[I] = multiply(lhs[I % 2], rhs[I]), 0)...};
(void)unused;
}
int main() {
Q lhs[2], rhs[6], out[6];
for (auto& q : lhs) q = {1, 0, 0, 0};
for (auto& q : rhs) q = {0, 1, 0, 0};
broadcast(out, lhs, rhs, std::make_integer_sequence{});
for (int i = 0; i < 6; ++i) {
if (out[i].x != 0 || out[i].y != 0 || out[i].z != 1 || out[i].w != 0) {
std::printf("FAIL %d: %g %g %g %g\n", i, out[i].x, out[i].y, out[i].z, out[i].w);
return 1;
}
}
std::puts("PASS");
}
```
Expected: `PASS`, exit status 0. Each product is `i*j = k`, represented as `{0,0,1,0}`.
Observed with Clang 18.1.3: `FAIL 5: ...`, exit status 1. The four printed numbers vary because the generated
code reads beyond `rhs`; their particular values are not part of the reproducer.
## Reproduction commands
On ARMv7 Linux with VFPv4:
```sh
clang++-18 -std=c++14 -O2 -march=armv7-a -mfpu=neon-vfpv4 repro.cpp -o repro
./repro
# Control: same source and settings, but disable the offending pass.
clang++-18 -std=c++14 -O2 -march=armv7-a -mfpu=neon-vfpv4 \
-mllvm --arm-load-store-opt=false repro.cpp -o repro-safe
./repro-safe
```
For cross-compilation, add `--target=arm-linux-gnueabihf`, configure an ARMHF GCC toolchain/sysroot/linker,
and run the executable with `qemu-arm -L /path/to/armhf-runtime ./repro`.
Local execution used QEMU, not native ARM hardware. The sysroot contained ARMHF glibc 2.39 and GCC 13
development libraries. No fast-math flags were used.
### Stable backend reproducer for trunk
The exact tested IR is supplied separately as
[`output/llvm_arm_load_store_report/repro.ll`](output/llvm_arm_load_store_report/repro.ll).
Attach that file to the upstream issue. It contains both `main` and the failing multiplication function,
with no third-party library dependency. It was generated from the source above using Clang 18.1.3:
```sh
clang++-18 --target=arm-linux-gnueabihf \
--gcc-toolchain=/path/to/toolchain --sysroot=/path/to/sysroot \
-std=c++14 -O2 -march=armv7-a -mfpu=neon-vfpv4 \
-S -emit-llvm repro.cpp -o repro.ll
# On an x86 host, this needs only an LLVM build with the ARM target.
llc -O2 -mtriple=armv7-unknown-linux-gnueabihf \
-mattr=+vfp4,+neon repro.ll -o repro.s
# Link using the ARMHF toolchain; -no-pie matches llc's static relocation output.
arm-linux-gnueabihf-g++ -march=armv7-a -mfpu=neon-vfpv4 -no-pie repro.s -o repro
qemu-arm -L /path/to/armhf-runtime ./repro
# Detect the invalid machine code without executing an ARM binary.
llc -O2 -mtriple=armv7-unknown-linux-gnueabihf \
-mattr=+vfp4,+neon -verify-machineinstrs repro.ll -o repro.s
```
For the verifier check, use the assertions-enabled trunk build. Adding `--arm-load-store-opt=false` to that
invocation makes it compile successfully with the verifier enabled.
## Incorrect generated code
In the trunk `llc` output, `r2` initially holds the original `rhs` pointer. The relevant instructions are:
```asm
add r3, r2, #96
add r2, r2, #128 @ r2 is now original rhs + 128
...
vldmia r2, {d16, d17, d18, d19} @ rhs[4], correct
add r2, r2, #160 @ computes original rhs + 288, not +160
...
vldmia r2, {d16, d17, d18, d19} @ rhs[9], outside the array
```
`sizeof(Q)` is 32 bytes. The last product requires offset 160 (`rhs[5]`), not offset 288.
The generated code has destroyed the original base address before its last use.
Assertions-enabled trunk with machine verification reports:
```text
# After ARM load / store optimization pass
*** Bad machine code: Using an undefined physical register ***
- instruction: $r2 = ADDri killed $r2, 160, 14, $noreg, $noreg
- operand 1: killed $r2
LLVM ERROR: Found 1 machine code errors.
```
The full diagnostic, including the machine-function dump and stack trace, is preserved in
[`output/llvm_arm_load_store_report/llc-assertions-trunk.json`](output/llvm_arm_load_store_report/llc-assertions-trunk.json).
## Version checks and current status
Compiler Explorer checks were performed on 2026-09-15 using its then-current published trunk build,
dated 2026-09-14. This is a tested snapshot, not a claim that LLVM's moving Git HEAD was built locally.
The Clang trunk compiler reports:
```text
clang version 24.0.0git
https://github.com/llvm/llvm-project.git
dbac421efa19a5901f2511958040e18e8188b112
```
| Compiler / input | Result |
|---|---|
| Local Clang 14.0.6, C++ source, `-O2` | Fails at output element 5 under QEMU |
| Local Clang 18.1.3, C++ source, `-O1`, `-O2`, `-O3` | Fails at output element 5 under QEMU |
| Local Clang 18.1.3, C++ source, `-O2 --arm-load-store-opt=false` | Passes under QEMU |
| LLVM 22.1.0 `llc`, attached IR, `-O2` | Generated executable fails under QEMU |
| LLVM 23.1.0 `llc`, attached IR, `-O2` | Generated executable fails under QEMU |
| LLVM 24.0.0git `llc` trunk, attached IR, `-O2` | Generated executable fails under QEMU |
| Clang 24.0.0git trunk at the revision above, attached IR, `-O2` | Generated executable fails under QEMU |
| Assertions-enabled `llc` trunk, attached IR, `-verify-machineinstrs` | Aborts after ARM load/store optimization |
| Same assertions-enabled invocation with `--arm-load-store-opt=false` | Compiles successfully |
| Clang 24.0.0git trunk, C++ source directly, `-O2` | Generated executable passes under QEMU; does not disprove the IR failure |
Compiler Explorer supplied assembly; it did not execute the ARM program. Successful assembly outputs were
assembled/linked locally and executed with QEMU. The corresponding compiler IDs are `armv7-clang-trunk`,
`irclangtrunk`, `llctrunk`, `llc-assertions-trunk`, `llc2210`, and `llc2310`. Raw responses are preserved beside the IR.
Since the same backend failure remains reproducible in trunk, there is **no confirmed fixed version to report**.
A first-fixed-release search based solely on the direct C++ test would give a misleading result.
## Context
The relevant backend implementation is
[`llvm/lib/Target/ARM/ARMLoadStoreOptimizer.cpp`](https://github.com/llvm/llvm-project/blob/main/llvm/lib/Target/ARM/ARMLoadStoreOptimizer.cpp).
An experimental workaround puts both operand pointers through an empty register-only inline-assembly
barrier before multiplication. That avoids the observed failures without removing test coverage, but does not
fix LLVM and can affect optimization. The backend pass-disable control is preferable for isolating this report.
Contributor guide
Research direction
Start with llvm/lib/Target/ARM/ARMLoadStoreOptimizer.cpp and the attached output/llvm_arm_load_store_report/repro.ll. Run llc with the ARM triple, +vfp4,+neon, and -verify-machineinstrs using the assertions-enabled build, then compare with --arm-load-store-opt=false. Done means the verifier succeeds and the generated reproducer no longer performs the out-of-bounds load.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100