[llvm] [LAA] Fix off-by-EltSize in negative-step deref bounds check (PR #211964)
via llvm-commits
llvm-commits at lists.llvm.org
Fri Jul 24 16:46:24 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-llvm-analysis
Author: Aleksandr Popov (aleks-tmb)
<details>
<summary>Changes</summary>
Negative-step branch of evaluatePtrAddRecAtMaxBTCWillNotWrap fixes:
* Lower bound: compare FirstIterOffset against WalkBytes instead of SpanBytes (= WalkBytes + EltSize). The lowest accessed byte is itself the start of an EltSize-sized access, so no extra slack is needed.
* Upper bound: MaxOffset is the exclusive upper offset of the accessed range, so it must be FirstIterOffset + EltSize, not
FirstIterOffset alone. This closes the gap that let the top access spill past DerefBytes without being noticed.
Fixes https://github.com/llvm/llvm-project/issues/211962
Built on top of https://github.com/llvm/llvm-project/pull/211960
---
Full diff: https://github.com/llvm/llvm-project/pull/211964.diff
2 Files Affected:
- (modified) llvm/lib/Analysis/LoopAccessAnalysis.cpp (+33-27)
- (added) llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll (+89)
``````````diff
diff --git a/llvm/lib/Analysis/LoopAccessAnalysis.cpp b/llvm/lib/Analysis/LoopAccessAnalysis.cpp
index 1f13106214ea9..99057012b8dba 100644
--- a/llvm/lib/Analysis/LoopAccessAnalysis.cpp
+++ b/llvm/lib/Analysis/LoopAccessAnalysis.cpp
@@ -266,56 +266,62 @@ static bool evaluatePtrAddRecAtMaxBTCWillNotWrap(
Step = SE.getNoopOrSignExtend(Step, WiderTy);
MaxBTC = SE.getNoopOrZeroExtend(MaxBTC, WiderTy);
+ EltSize = SE.getNoopOrZeroExtend(EltSize, WiderTy);
// For the computations below, make sure they don't unsigned wrap.
if (!SE.isKnownPredicate(CmpInst::ICMP_UGE, AR->getStart(), StartPtr))
return false;
- const SCEV *StartOffset = SE.getNoopOrZeroExtend(
+ // Byte offset of the pointer at iteration 0 from the base pointer.
+ const SCEV *FirstIterOffset = SE.getNoopOrZeroExtend(
SE.getMinusSCEV(AR->getStart(), StartPtr), WiderTy);
if (!LoopGuards)
LoopGuards.emplace(ScalarEvolution::LoopGuards::collect(AR->getLoop(), SE));
MaxBTC = SE.applyLoopGuards(MaxBTC, *LoopGuards);
- const SCEV *OffsetAtLastIter =
- mulSCEVNoOverflow(MaxBTC, SE.getAbsExpr(Step, /*IsNSW=*/false), SE);
- if (!OffsetAtLastIter) {
+ const SCEV *AbsStep = SE.getAbsExpr(Step, /*IsNSW=*/false);
+ // Total distance (in bytes) walked between the first and the last
+ // accessed pointer; MaxBTC * |Step|.
+ const SCEV *WalkBytes = mulSCEVNoOverflow(MaxBTC, AbsStep, SE);
+ if (!WalkBytes) {
// Re-try with constant max backedge-taken count if using the symbolic one
// failed.
MaxBTC = SE.getConstantMaxBackedgeTakenCount(AR->getLoop());
if (isa<SCEVCouldNotCompute>(MaxBTC))
return false;
- MaxBTC = SE.getNoopOrZeroExtend(
- MaxBTC, WiderTy);
- OffsetAtLastIter =
- mulSCEVNoOverflow(MaxBTC, SE.getAbsExpr(Step, /*IsNSW=*/false), SE);
- if (!OffsetAtLastIter)
+ MaxBTC = SE.getNoopOrZeroExtend(MaxBTC, WiderTy);
+ WalkBytes = mulSCEVNoOverflow(MaxBTC, AbsStep, SE);
+ if (!WalkBytes)
return false;
}
- const SCEV *OffsetEndBytes = addSCEVNoOverflow(
- OffsetAtLastIter, SE.getNoopOrZeroExtend(EltSize, WiderTy), SE);
- if (!OffsetEndBytes)
+ // Total length in bytes of the accessed range (from the first accessed
+ // byte through the end of the last access); WalkBytes + EltSize.
+ const SCEV *SpanBytes = addSCEVNoOverflow(WalkBytes, EltSize, SE);
+ if (!SpanBytes)
return false;
+ // Compute MaxOffset per direction: exclusive upper offset of the
+ // accessed range.
+ const SCEV *MaxOffset;
if (IsKnownNonNegative) {
- // For positive steps, check if
- // (AR->getStart() - StartPtr) + (MaxBTC * Step) + EltSize <= DerefBytes,
- // while making sure none of the computations unsigned wrap themselves.
- const SCEV *EndBytes = addSCEVNoOverflow(StartOffset, OffsetEndBytes, SE);
- if (!EndBytes)
+ // Range = [FirstIterOffset, FirstIterOffset + SpanBytes).
+ MaxOffset = addSCEVNoOverflow(FirstIterOffset, SpanBytes, SE);
+ if (!MaxOffset)
+ return false;
+ } else {
+ // Range = [FirstIterOffset - WalkBytes, FirstIterOffset + EltSize).
+ // Lower end must not underflow past StartPtr.
+ assert(SE.isKnownNegative(Step) && "must be known negative");
+ if (!SE.isKnownPredicate(CmpInst::ICMP_SGE, FirstIterOffset, WalkBytes))
+ return false;
+ MaxOffset = addSCEVNoOverflow(FirstIterOffset, EltSize, SE);
+ if (!MaxOffset)
return false;
-
- DerefBytesSCEV = SE.applyLoopGuards(DerefBytesSCEV, *LoopGuards);
- return SE.isKnownPredicate(CmpInst::ICMP_ULE, EndBytes, DerefBytesSCEV);
}
-
- // For negative steps check if
- // * StartOffset >= (MaxBTC * Step + EltSize)
- // * StartOffset <= DerefBytes.
- assert(SE.isKnownNegative(Step) && "must be known negative");
- return SE.isKnownPredicate(CmpInst::ICMP_SGE, StartOffset, OffsetEndBytes) &&
- SE.isKnownPredicate(CmpInst::ICMP_ULE, StartOffset, DerefBytesSCEV);
+ // MaxOffset must not exceed the deref-region end.
+ DerefBytesSCEV = SE.applyLoopGuards(DerefBytesSCEV, *LoopGuards);
+ return SE.isKnownPredicate(CmpInst::ICMP_ULE, MaxOffset, DerefBytesSCEV);
}
std::pair<const SCEV *, const SCEV *> llvm::getStartAndEndForAccess(
diff --git a/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll b/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll
new file mode 100644
index 0000000000000..db8670bf896b0
--- /dev/null
+++ b/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll
@@ -0,0 +1,89 @@
+; RUN: opt -passes='print<access-info>' -disable-output %s 2>&1 | FileCheck %s
+
+; Reverse i32 loop over 4 elements whose access range exactly fills the
+; dereferenceable region (deref(16), reads bytes [0, 16)). LAA proves
+; no-wrap and produces the tight (%A .. %A + 16) bound.
+;
+; Pseudocode:
+; // A, B: at least 4 i32s dereferenceable each
+; for (i64 i = 3; i >= 0; --i) {
+; i32 l = A[i]; // A[3], A[2], A[1], A[0]
+; B[i] = 0;
+; if (l == 0) break;
+; }
+
+define void @reverse_reaches_base(ptr dereferenceable(16) %A, ptr dereferenceable(16) %B) {
+; CHECK-LABEL: 'reverse_reaches_base'
+; CHECK: Group GRP0:
+; CHECK-NEXT: (Low: %B High: (16 + %B)<nuw>)
+; CHECK-NEXT: Member: {(12 + %B)<nuw>,+,-4}<nw><%loop>
+; CHECK: Group GRP1:
+; CHECK-NEXT: (Low: %A High: (16 + %A)<nuw>)
+; CHECK-NEXT: Member: {(12 + %A)<nuw>,+,-4}<nw><%loop>
+entry:
+ br label %loop
+
+loop:
+ %iv = phi i64 [ 3, %entry ], [ %iv.dec, %latch ]
+ %gep.A = getelementptr inbounds i32, ptr %A, i64 %iv
+ %gep.B = getelementptr inbounds i32, ptr %B, i64 %iv
+ %l = load i32, ptr %gep.A, align 4
+ store i32 0, ptr %gep.B, align 4
+ %uncntable = icmp eq i32 %l, 0
+ br i1 %uncntable, label %exit.early, label %latch
+
+latch:
+ %iv.dec = add nsw i64 %iv, -1
+ %ec = icmp eq i64 %iv, 0
+ br i1 %ec, label %exit.done, label %loop
+
+exit.early:
+ ret void
+
+exit.done:
+ ret void
+}
+
+; Reverse i32 loop whose top read spills one byte past the deref end.
+; The IR is UB by construction: top i32 read at byte 13 covers [13, 17),
+; but deref(16) only guarantees [0, 16). LAA rejects the AR and
+; falls back to the wide low bound.
+;
+; Pseudocode:
+; for (i64 i = 13; i > 1; i -= 4) {
+; i32 l = *(i32*)((char*)A + i); // reads [i, i+4)
+; *(i32*)((char*)B + i) = 0;
+; if (l == 0) break;
+; }
+
+define void @reverse_top_spills(ptr dereferenceable(16) %A, ptr dereferenceable(16) %B) {
+; CHECK-LABEL: 'reverse_top_spills'
+; CHECK: Group GRP0:
+; CHECK-NEXT: (Low: (-4 + inttoptr (i64 -1 to ptr))<nsw> High: (17 + %B))
+; CHECK-NEXT: Member: {(13 + %B)<nuw>,+,-4}<nw><%loop2>
+; CHECK: Group GRP1:
+; CHECK-NEXT: (Low: (-4 + inttoptr (i64 -1 to ptr))<nsw> High: (17 + %A))
+; CHECK-NEXT: Member: {(13 + %A)<nuw>,+,-4}<nw><%loop2>
+entry:
+ br label %loop2
+
+loop2:
+ %iv = phi i64 [ 13, %entry ], [ %iv.dec, %latch ]
+ %gep.A = getelementptr inbounds i8, ptr %A, i64 %iv
+ %gep.B = getelementptr inbounds i8, ptr %B, i64 %iv
+ %l = load i32, ptr %gep.A, align 1
+ store i32 0, ptr %gep.B, align 1
+ %uncntable = icmp eq i32 %l, 0
+ br i1 %uncntable, label %exit.early, label %latch
+
+latch:
+ %iv.dec = add nsw i64 %iv, -4
+ %ec = icmp eq i64 %iv, 5
+ br i1 %ec, label %exit.done, label %loop2
+
+exit.early:
+ ret void
+
+exit.done:
+ ret void
+}
``````````
</details>
https://github.com/llvm/llvm-project/pull/211964
More information about the llvm-commits
mailing list