[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