llvm / llvm/llvm-project

# [LAA] Forked-pointer grouping can produce wrapping bounds and allow incorrect vectorization

Open
#223,668 0 comments 0 reactions 0 assignees View on GitHub
confirmed miscompilation vectorizers
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

While investigating https://github.com/llvm/llvm-project/pull/187252, I found a pre-existing correctness issue in LAA's runtime pointer-check grouping, reproducible on unpatched `main`.

`RuntimeCheckingPtrGroup::addPointer`, called by `groupChecks`, can include the range of an unselected fork arm when merging read ranges. A large negative offset in that arm can make the merged lower bound wrap, causing the runtime overlap check to miss a real dependency and allow incorrect vectorization.

Here is a reproducer. `choose_near` is true, so all actual reads stay inside `stencil`. The unused far offset is never used to form a pointer in the source:

```cpp
#include
#include
#include
#include

#ifndef FAR_SHIFT
#define FAR_SHIFT 24
#endif

alignas(64) unsigned char stencil[1024];
constexpr std::ptrdiff_t Far = std::ptrdiff_t{1} << FAR_SHIFT;

extern "C" __attribute__((noinline)) void
stencil_step(bool choose_near, unsigned char *out, std::ptrdiff_t n) {
#pragma clang loop vectorize_width(4) interleave_count(1)
for (std::ptrdiff_t i = 0; i < n; ++i) {
const bool take_near = choose_near || ((i & 1) == 0);
const std::ptrdiff_t offset = take_near ? 32 : -Far;
const unsigned char *p = stencil + (i + offset);
out[i] = static_cast(stencil[i] + stencil[i + 16] + *p);
}
}

int main() {
constexpr std::ptrdiff_t N = 64;
unsigned char reference[1024];
for (std::size_t i = 0; i < sizeof(stencil); ++i)
stencil[i] = reference[i] = static_cast(i * 7 + 3);
#pragma clang loop vectorize(disable) interleave(disable)
for (std::ptrdiff_t i = 0; i < N; ++i)
reference[33 + i] = static_cast(
reference[i] + reference[i + 16] + reference[i + 32]);
const auto base = reinterpret_cast(stencil);
std::printf("base=%#018" PRIxPTR " far=%#018" PRIxPTR
" grouped_low=%#018" PRIxPTR " grouped_high=%#018" PRIxPTR
" writes=[%#018" PRIxPTR ",%#018" PRIxPTR ")\n",
base, static_cast(Far),
base - static_cast(Far), base + 32 + N,
base + 33, base + 33 + N);
stencil_step(true, stencil + 33, N);
for (std::size_t i = 0; i < sizeof(stencil); ++i) {
if (stencil[i] != reference[i]) {
std::printf("FAIL at index %zu: got %u, expected %u\n", i,
static_cast(stencil[i]),
static_cast(reference[i]));
return 1;
}
}
std::puts("PASS");
}
```

I reproduced this on AArch64 Linux with Clang `24.0.0git`, built from unpatched `main` at `73344b523c93f25c9dafcabf803295e92bf5cb5f` (target: `aarch64-unknown-linux-gnu`). Save the source above as `constant-fork.cpp` and run:

```sh
clang -mcpu=native -O3 constant-fork.cpp \
-mllvm -unswitch-threshold=1 -fno-pie -no-pie \
-Wl,-Ttext-segment=0x10000 -o constant-fork
./constant-fork
```

The low link address places `stencil` below `Far`, so computing `stencil - Far` underflows. `-unswitch-threshold=1` keeps the conditional selection in the loop through vectorization.

Output:

```text
base=0x0000000000030080 far=0x0000000001000000 grouped_low=0xffffffffff030080 grouped_high=0x00000000000300e0 writes=[0x00000000000300a1,0x00000000000300e1)
FAIL at index 34: got 110, expected 221
```

The same source compiled with `-O0` passes. In the optimized IR, the merged read range is `[stencil - Far, stencil + n + 32)`. Its lower bound wraps to a large address, so the runtime check incorrectly reports no overlap with the writes.

Contributor guide

Open the contributing guide

Research direction

Start with LAA's RuntimeCheckingPtrGroup::addPointer and groupChecks, then reproduce the failure using constant-fork.cpp and the provided clang command. Trace how the fork-arm read ranges are merged and verify that the runtime overlap check catches the dependency; the reproducer should finish with PASS rather than a FAIL line.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.