[llvm] [Loads] Fix incorrect replacement of pointers with different provenance (PR #224281)
Nikita Popov via llvm-commits
llvm-commits at lists.llvm.org
Thu Sep 17 05:32:53 PDT 2026
https://github.com/nikic created https://github.com/llvm/llvm-project/pull/224281
isPointerAlwaysReplaceable() uses getUnderlyingObject() to check whether the two pointers have the same provenance, because in that case the replacement is always legal. However, getUnderlyingObject() does not actually guarantee that the provenance is the same: In particular, it can look through some intrinsics like strip.invariant.group and launder.invariant.group, which do change the provenance of the pointer. And replacing the result of those intrinsics with their argument is indeed incorrect.
Fix this by adding a MustPreserveProvenance argument to the relevant APIs, and enable it in isPointerAlwaysReplaceable().
Noticed while working on https://github.com/llvm/llvm-project/pull/224222.
>From 29bba4ccfab14ec18228beeb1c08b734fee211ac Mon Sep 17 00:00:00 2001
From: Nikita Popov <npopov at redhat.com>
Date: Thu, 17 Sep 2026 12:41:03 +0200
Subject: [PATCH 1/2] Add test for miscompile
---
llvm/test/Transforms/GVN/assume-equal.ll | 28 ++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/llvm/test/Transforms/GVN/assume-equal.ll b/llvm/test/Transforms/GVN/assume-equal.ll
index d2a584e891703..362f61a7eb961 100644
--- a/llvm/test/Transforms/GVN/assume-equal.ll
+++ b/llvm/test/Transforms/GVN/assume-equal.ll
@@ -464,6 +464,34 @@ define i8 @assume_ptr_eq_same_prov(ptr %p, i64 %x) {
ret i8 %v
}
+define ptr @test_strip_invariant(ptr %x) {
+; CHECK-LABEL: define ptr @test_strip_invariant(
+; CHECK-SAME: ptr [[X:%.*]]) {
+; CHECK-NEXT: [[X_STRIP:%.*]] = call ptr @llvm.strip.invariant.group.p0(ptr [[X]])
+; CHECK-NEXT: [[CMP:%.*]] = icmp eq ptr [[X]], [[X_STRIP]]
+; CHECK-NEXT: call void @llvm.assume(i1 [[CMP]])
+; CHECK-NEXT: ret ptr [[X]]
+;
+ %x.strip = call ptr @llvm.strip.invariant.group(ptr %x)
+ %cmp = icmp eq ptr %x, %x.strip
+ call void @llvm.assume(i1 %cmp)
+ ret ptr %x.strip
+}
+
+define ptr @test_launder_invariant(ptr %x) {
+; CHECK-LABEL: define ptr @test_launder_invariant(
+; CHECK-SAME: ptr [[X:%.*]]) {
+; CHECK-NEXT: [[X_LAUNDER:%.*]] = call ptr @llvm.launder.invariant.group.p0(ptr [[X]])
+; CHECK-NEXT: [[CMP:%.*]] = icmp eq ptr [[X]], [[X_LAUNDER]]
+; CHECK-NEXT: call void @llvm.assume(i1 [[CMP]])
+; CHECK-NEXT: ret ptr [[X]]
+;
+ %x.launder = call ptr @llvm.launder.invariant.group(ptr %x)
+ %cmp = icmp eq ptr %x, %x.launder
+ call void @llvm.assume(i1 %cmp)
+ ret ptr %x.launder
+}
+
declare noalias ptr @_Znwm(i64)
declare void @_ZN1AC1Ev(ptr)
declare void @llvm.assume(i1)
>From 821bca141fc73b92e0c85386c4358ccfc7713891 Mon Sep 17 00:00:00 2001
From: Nikita Popov <npopov at redhat.com>
Date: Thu, 17 Sep 2026 12:54:26 +0200
Subject: [PATCH 2/2] Fix it
---
llvm/include/llvm/Analysis/ValueTracking.h | 30 ++++++++++++++--------
llvm/lib/Analysis/Loads.cpp | 4 +--
llvm/lib/Analysis/ValueTracking.cpp | 25 +++++++++++-------
llvm/test/Transforms/GVN/assume-equal.ll | 4 +--
4 files changed, 40 insertions(+), 23 deletions(-)
diff --git a/llvm/include/llvm/Analysis/ValueTracking.h b/llvm/include/llvm/Analysis/ValueTracking.h
index 9a1fdd0ca05a9..42ee2b317be21 100644
--- a/llvm/include/llvm/Analysis/ValueTracking.h
+++ b/llvm/include/llvm/Analysis/ValueTracking.h
@@ -446,9 +446,12 @@ LLVM_ABI uint64_t GetStringLength(const Value *V, unsigned CharSize = 8);
/// the pointer within its underlying object. Offset preservation implies
/// nullness preservation; pass true when callers reason about either offset or
/// null equality (e.g. GEP decomposition, dereferenceability, isKnownNonZero).
+/// If \p MustPreserveProvenance is true, the call must preserve the provenance
+/// exactly, as opposed to being only based-on the argument.
LLVM_ABI const Value *
getArgumentAliasingToReturnedPointer(const CallBase *Call,
- bool MustPreserveOffset);
+ bool MustPreserveOffset,
+ bool MustPreserveProvenance = false);
inline Value *getArgumentAliasingToReturnedPointer(CallBase *Call,
bool MustPreserveOffset) {
return const_cast<Value *>(getArgumentAliasingToReturnedPointer(
@@ -459,30 +462,37 @@ inline Value *getArgumentAliasingToReturnedPointer(CallBase *Call,
/// and it only captures pointer by returning it.
/// These intrinsics are not marked as nocapture, because returning is
/// considered as capture. The arguments are not marked as returned neither,
-/// because it would make it useless. If \p MustPreserveOffset is true, the
-/// intrinsic must preserve the byte offset of the pointer within its
-/// underlying object (which excludes `llvm.ptrmask`, since masking off low
-/// bits changes the byte offset while still aliasing the same object).
+/// because it would make it useless. See getArgumentAliasingToReturnedPointer()
+/// for the meaning of \p MustPreserveOffset and \p MustPreserveProvenance.
LLVM_ABI bool isIntrinsicReturningPointerAliasingArgumentWithoutCapturing(
- const CallBase *Call, bool MustPreserveOffset);
+ const CallBase *Call, bool MustPreserveOffset,
+ bool MustPreserveProvenance = false);
/// This method strips off any GEP address adjustments, pointer casts
/// or `llvm.threadlocal.address` from the specified value \p V, returning the
/// original object being addressed. Note that the returned value has pointer
/// type if the specified value does. If the \p MaxLookup value is non-zero, it
/// limits the number of instructions to be stripped off.
+/// If \p MustPreserveProvenance is true, return a pointer with the exactly
+/// same provenance as \p V, as opposed to \p V only being based-on the
+/// underlying object.
LLVM_ABI const Value *
-getUnderlyingObject(const Value *V, unsigned MaxLookup = MaxLookupSearchDepth);
+getUnderlyingObject(const Value *V, unsigned MaxLookup = MaxLookupSearchDepth,
+ bool MustPreserveProvenance = false);
inline Value *getUnderlyingObject(Value *V,
- unsigned MaxLookup = MaxLookupSearchDepth) {
+ unsigned MaxLookup = MaxLookupSearchDepth,
+ bool MustPreserveProvenance = false) {
// Force const to avoid infinite recursion.
const Value *VConst = V;
- return const_cast<Value *>(getUnderlyingObject(VConst, MaxLookup));
+ return const_cast<Value *>(
+ getUnderlyingObject(VConst, MaxLookup, MustPreserveProvenance));
}
/// Like getUnderlyingObject(), but will try harder to find a single underlying
/// object. In particular, this function also looks through selects and phis.
-LLVM_ABI const Value *getUnderlyingObjectAggressive(const Value *V);
+LLVM_ABI const Value *
+getUnderlyingObjectAggressive(const Value *V,
+ bool MustPreserveProvenance = false);
/// This method is similar to getUnderlyingObject except that it can
/// look through phi and select instructions and return multiple objects.
diff --git a/llvm/lib/Analysis/Loads.cpp b/llvm/lib/Analysis/Loads.cpp
index de9022c540d42..394f7add60c02 100644
--- a/llvm/lib/Analysis/Loads.cpp
+++ b/llvm/lib/Analysis/Loads.cpp
@@ -860,8 +860,8 @@ static bool isPointerAlwaysReplaceable(const Value *From, const Value *To,
isDereferenceablePointer(To, Type::getInt8Ty(To->getContext()), DL) &&
IsBasedOnConstantGlobal(To))
return true;
- return getUnderlyingObjectAggressive(From) ==
- getUnderlyingObjectAggressive(To);
+ return getUnderlyingObjectAggressive(From, /*MustPreserveProvenance=*/true) ==
+ getUnderlyingObjectAggressive(To, /*MustPreserveProvenance=*/true);
}
bool llvm::canReplacePointersInUseIfEqual(const Use &U, const Value *To,
diff --git a/llvm/lib/Analysis/ValueTracking.cpp b/llvm/lib/Analysis/ValueTracking.cpp
index dce213534d11f..a953b5b2386df 100644
--- a/llvm/lib/Analysis/ValueTracking.cpp
+++ b/llvm/lib/Analysis/ValueTracking.cpp
@@ -7195,20 +7195,22 @@ uint64_t llvm::GetStringLength(const Value *V, unsigned CharSize) {
const Value *
llvm::getArgumentAliasingToReturnedPointer(const CallBase *Call,
- bool MustPreserveOffset) {
+ bool MustPreserveOffset,
+ bool MustPreserveProvenance) {
assert(Call &&
"getArgumentAliasingToReturnedPointer only works on nonnull calls");
if (const Value *RV = Call->getReturnedArgOperand())
return RV;
// This can be used only as a aliasing property.
if (isIntrinsicReturningPointerAliasingArgumentWithoutCapturing(
- Call, MustPreserveOffset))
+ Call, MustPreserveOffset, MustPreserveProvenance))
return Call->getArgOperand(0);
return nullptr;
}
bool llvm::isIntrinsicReturningPointerAliasingArgumentWithoutCapturing(
- const CallBase *Call, bool MustPreserveOffset) {
+ const CallBase *Call, bool MustPreserveOffset,
+ bool MustPreserveProvenance) {
switch (Call->getIntrinsicID()) {
case Intrinsic::launder_invariant_group:
case Intrinsic::strip_invariant_group:
@@ -7224,7 +7226,7 @@ bool llvm::isIntrinsicReturningPointerAliasingArgumentWithoutCapturing(
// writing, they are not), but we document this fact out of an abundance
// of caution.
case Intrinsic::amdgcn_make_buffer_rsrc:
- return true;
+ return !MustPreserveProvenance;
case Intrinsic::ptrmask:
return !MustPreserveOffset;
case Intrinsic::threadlocal_address:
@@ -7263,7 +7265,8 @@ static bool isSameUnderlyingObjectInLoop(const PHINode *PN,
return true;
}
-const Value *llvm::getUnderlyingObject(const Value *V, unsigned MaxLookup) {
+const Value *llvm::getUnderlyingObject(const Value *V, unsigned MaxLookup,
+ bool MustPreserveProvenance) {
for (unsigned Count = 0; MaxLookup == 0 || Count < MaxLookup; ++Count) {
if (auto *GEP = dyn_cast<GEPOperator>(V)) {
const Value *PtrOp = GEP->getPointerOperand();
@@ -7298,7 +7301,7 @@ const Value *llvm::getUnderlyingObject(const Value *V, unsigned MaxLookup) {
// cause weird miscompilations where 2 aliasing pointers are assumed to
// noalias.
if (auto *RP = getArgumentAliasingToReturnedPointer(
- Call, /*MustPreserveOffset=*/false)) {
+ Call, /*MustPreserveOffset=*/false, MustPreserveProvenance)) {
V = RP;
continue;
}
@@ -7353,7 +7356,8 @@ void llvm::getUnderlyingObjects(const Value *V,
} while (!Worklist.empty());
}
-const Value *llvm::getUnderlyingObjectAggressive(const Value *V) {
+const Value *llvm::getUnderlyingObjectAggressive(const Value *V,
+ bool MustPreserveProvenance) {
const unsigned MaxVisited = 8;
SmallPtrSet<const Value *, 8> Visited;
@@ -7363,10 +7367,13 @@ const Value *llvm::getUnderlyingObjectAggressive(const Value *V) {
// Used as fallback if we can't find a common underlying object through
// recursion.
bool First = true;
- const Value *FirstObject = getUnderlyingObject(V);
+ const Value *FirstObject =
+ getUnderlyingObject(V, MaxLookupSearchDepth, MustPreserveProvenance);
do {
const Value *P = Worklist.pop_back_val();
- P = First ? FirstObject : getUnderlyingObject(P);
+ P = First ? FirstObject
+ : getUnderlyingObject(P, MaxLookupSearchDepth,
+ MustPreserveProvenance);
First = false;
if (!Visited.insert(P).second)
diff --git a/llvm/test/Transforms/GVN/assume-equal.ll b/llvm/test/Transforms/GVN/assume-equal.ll
index 362f61a7eb961..f2ff8c8d411f5 100644
--- a/llvm/test/Transforms/GVN/assume-equal.ll
+++ b/llvm/test/Transforms/GVN/assume-equal.ll
@@ -470,7 +470,7 @@ define ptr @test_strip_invariant(ptr %x) {
; CHECK-NEXT: [[X_STRIP:%.*]] = call ptr @llvm.strip.invariant.group.p0(ptr [[X]])
; CHECK-NEXT: [[CMP:%.*]] = icmp eq ptr [[X]], [[X_STRIP]]
; CHECK-NEXT: call void @llvm.assume(i1 [[CMP]])
-; CHECK-NEXT: ret ptr [[X]]
+; CHECK-NEXT: ret ptr [[X_STRIP]]
;
%x.strip = call ptr @llvm.strip.invariant.group(ptr %x)
%cmp = icmp eq ptr %x, %x.strip
@@ -484,7 +484,7 @@ define ptr @test_launder_invariant(ptr %x) {
; CHECK-NEXT: [[X_LAUNDER:%.*]] = call ptr @llvm.launder.invariant.group.p0(ptr [[X]])
; CHECK-NEXT: [[CMP:%.*]] = icmp eq ptr [[X]], [[X_LAUNDER]]
; CHECK-NEXT: call void @llvm.assume(i1 [[CMP]])
-; CHECK-NEXT: ret ptr [[X]]
+; CHECK-NEXT: ret ptr [[X_LAUNDER]]
;
%x.launder = call ptr @llvm.launder.invariant.group(ptr %x)
%cmp = icmp eq ptr %x, %x.launder
More information about the llvm-commits
mailing list