[llvm] 76cdf60 - [BasicAA] Don't look through llvm.ptrmask in GEP decomposition (#197082)
via llvm-commits
llvm-commits at lists.llvm.org
Tue May 12 08:22:13 PDT 2026
Author: Nick Terrell
Date: 2026-05-12T17:22:07+02:00
New Revision: 76cdf6012b883529e3d608ed4e30d622ba43e10c
URL: https://github.com/llvm/llvm-project/commit/76cdf6012b883529e3d608ed4e30d622ba43e10c
DIFF: https://github.com/llvm/llvm-project/commit/76cdf6012b883529e3d608ed4e30d622ba43e10c.diff
LOG: [BasicAA] Don't look through llvm.ptrmask in GEP decomposition (#197082)
DecomposeGEPExpression() looked through llvm.ptrmask via
getArgumentAliasingToReturnedPointer(Call, MustPreserveNullness=false).
ptrmask preserves the underlying object but can change the byte address
by clearing low bits, so treating its result as having the same symbolic
offset as its argument produces stale offsets and bogus NoAlias answers.
The bug was introduced by 3f2850bc606c847075673554fe49d4a35f525b61.
Rename MustPreserveNullness to MustPreserveOffset, the property
DecomposeGEPExpression actually needs. Offset preservation is strictly
stronger than nullness preservation, so existing callers remain correct
and the accepted intrinsic set is unchanged (ptrmask stays excluded).
switch DecomposeGEPExpression to pass MustPreserveOffset=true. Every
call site is now tagged with MustPreserveOffset=.
Added:
llvm/test/Analysis/BasicAA/ptrmask-gep-decomposition.ll
Modified:
llvm/include/llvm/Analysis/ValueTracking.h
llvm/lib/Analysis/AliasAnalysis.cpp
llvm/lib/Analysis/BasicAliasAnalysis.cpp
llvm/lib/Analysis/CaptureTracking.cpp
llvm/lib/Analysis/Loads.cpp
llvm/lib/Analysis/ValueTracking.cpp
llvm/lib/Transforms/IPO/AttributorAttributes.cpp
llvm/lib/Transforms/IPO/FunctionAttrs.cpp
Removed:
################################################################################
diff --git a/llvm/include/llvm/Analysis/ValueTracking.h b/llvm/include/llvm/Analysis/ValueTracking.h
index 00469421d55b4..b2f664a9c9c0d 100644
--- a/llvm/include/llvm/Analysis/ValueTracking.h
+++ b/llvm/include/llvm/Analysis/ValueTracking.h
@@ -423,25 +423,29 @@ LLVM_ABI uint64_t GetStringLength(const Value *V, unsigned CharSize = 8);
/// This function returns call pointer argument that is considered the same by
/// aliasing rules. You CAN'T use it to replace one value with another. If
-/// \p MustPreserveNullness is true, the call must preserve the nullness of
-/// the pointer.
+/// \p MustPreserveOffset is true, the call must preserve the byte offset of
+/// 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).
LLVM_ABI const Value *
getArgumentAliasingToReturnedPointer(const CallBase *Call,
- bool MustPreserveNullness);
+ bool MustPreserveOffset);
inline Value *getArgumentAliasingToReturnedPointer(CallBase *Call,
- bool MustPreserveNullness) {
+ bool MustPreserveOffset) {
return const_cast<Value *>(getArgumentAliasingToReturnedPointer(
- const_cast<const CallBase *>(Call), MustPreserveNullness));
+ const_cast<const CallBase *>(Call), MustPreserveOffset));
}
/// {launder,strip}.invariant.group returns pointer that aliases its argument,
/// 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 MustPreserveNullness is true,
-/// the intrinsic must preserve the nullness of the pointer.
+/// 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).
LLVM_ABI bool isIntrinsicReturningPointerAliasingArgumentWithoutCapturing(
- const CallBase *Call, bool MustPreserveNullness);
+ const CallBase *Call, bool MustPreserveOffset);
/// This method strips off any GEP address adjustments, pointer casts
/// or `llvm.threadlocal.address` from the specified value \p V, returning the
diff --git a/llvm/lib/Analysis/AliasAnalysis.cpp b/llvm/lib/Analysis/AliasAnalysis.cpp
index 9d89a6d90f706..82f1b7c3a4fd3 100644
--- a/llvm/lib/Analysis/AliasAnalysis.cpp
+++ b/llvm/lib/Analysis/AliasAnalysis.cpp
@@ -928,7 +928,8 @@ bool llvm::isBaseOfObject(const Value *V) {
bool llvm::isEscapeSource(const Value *V) {
if (auto *CB = dyn_cast<CallBase>(V)) {
- if (isIntrinsicReturningPointerAliasingArgumentWithoutCapturing(CB, true))
+ if (isIntrinsicReturningPointerAliasingArgumentWithoutCapturing(
+ CB, /*MustPreserveOffset=*/true))
return false;
// The return value of a function with a captures(ret: address, provenance)
diff --git a/llvm/lib/Analysis/BasicAliasAnalysis.cpp b/llvm/lib/Analysis/BasicAliasAnalysis.cpp
index 8b120f0ad1e11..832749f949aee 100644
--- a/llvm/lib/Analysis/BasicAliasAnalysis.cpp
+++ b/llvm/lib/Analysis/BasicAliasAnalysis.cpp
@@ -652,7 +652,11 @@ BasicAAResult::DecomposeGEPExpression(const Value *V, const DataLayout &DL,
// because it should be in sync with CaptureTracking. Not using it may
// cause weird miscompilations where 2 aliasing pointers are assumed to
// noalias.
- if (auto *RP = getArgumentAliasingToReturnedPointer(Call, false)) {
+ // Pass MustPreserveOffset=true so we exclude llvm.ptrmask, which can
+ // change the byte offset by clearing low bits and would otherwise
+ // corrupt the symbolic offset we are accumulating in `Decomposed`.
+ if (auto *RP = getArgumentAliasingToReturnedPointer(
+ Call, /*MustPreserveOffset=*/true)) {
V = RP;
continue;
}
diff --git a/llvm/lib/Analysis/CaptureTracking.cpp b/llvm/lib/Analysis/CaptureTracking.cpp
index c34fde8d11704..2bea8e2129b4f 100644
--- a/llvm/lib/Analysis/CaptureTracking.cpp
+++ b/llvm/lib/Analysis/CaptureTracking.cpp
@@ -281,7 +281,8 @@ UseCaptureInfo llvm::DetermineUseCaptureKind(const Use &U, const Value *Base) {
// marked with nocapture do not capture. This means that places like
// getUnderlyingObject in ValueTracking or DecomposeGEPExpression
// in BasicAA also need to know about this property.
- if (isIntrinsicReturningPointerAliasingArgumentWithoutCapturing(Call, true))
+ if (isIntrinsicReturningPointerAliasingArgumentWithoutCapturing(
+ Call, /*MustPreserveOffset=*/true))
return UseCaptureInfo::passthrough();
// Volatile operations effectively capture the memory location that they
diff --git a/llvm/lib/Analysis/Loads.cpp b/llvm/lib/Analysis/Loads.cpp
index ac4d37aca7673..797d18325c336 100644
--- a/llvm/lib/Analysis/Loads.cpp
+++ b/llvm/lib/Analysis/Loads.cpp
@@ -161,7 +161,8 @@ static bool isDereferenceableAndAlignedPointer(
if (const auto *Call = dyn_cast<CallBase>(V)) {
- if (auto *RP = getArgumentAliasingToReturnedPointer(Call, true))
+ if (auto *RP = getArgumentAliasingToReturnedPointer(
+ Call, /*MustPreserveOffset=*/true))
return isDereferenceableAndAlignedPointer(RP, Alignment, Size, DL, CtxI,
AC, DT, TLI, Visited, MaxDepth);
diff --git a/llvm/lib/Analysis/ValueTracking.cpp b/llvm/lib/Analysis/ValueTracking.cpp
index 91ad517f194a6..3be0db5ad9c7e 100644
--- a/llvm/lib/Analysis/ValueTracking.cpp
+++ b/llvm/lib/Analysis/ValueTracking.cpp
@@ -3536,7 +3536,8 @@ static bool isKnownNonZeroFromOperator(const Operator *I,
if (I->getType()->isPointerTy()) {
if (Call->isReturnNonNull())
return true;
- if (const auto *RP = getArgumentAliasingToReturnedPointer(Call, true))
+ if (const auto *RP = getArgumentAliasingToReturnedPointer(
+ Call, /*MustPreserveOffset=*/true))
return isKnownNonZero(RP, Q, Depth);
} else {
if (MDNode *Ranges = Q.IIQ.getMetadata(Call, LLVMContext::MD_range))
@@ -6868,38 +6869,38 @@ uint64_t llvm::GetStringLength(const Value *V, unsigned CharSize) {
const Value *
llvm::getArgumentAliasingToReturnedPointer(const CallBase *Call,
- bool MustPreserveNullness) {
+ bool MustPreserveOffset) {
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, MustPreserveNullness))
+ Call, MustPreserveOffset))
return Call->getArgOperand(0);
return nullptr;
}
bool llvm::isIntrinsicReturningPointerAliasingArgumentWithoutCapturing(
- const CallBase *Call, bool MustPreserveNullness) {
+ const CallBase *Call, bool MustPreserveOffset) {
switch (Call->getIntrinsicID()) {
case Intrinsic::launder_invariant_group:
case Intrinsic::strip_invariant_group:
case Intrinsic::aarch64_irg:
case Intrinsic::aarch64_tagp:
// The amdgcn_make_buffer_rsrc function does not alter the address of the
- // input pointer (and thus preserve null-ness for the purposes of escape
- // analysis, which is where the MustPreserveNullness flag comes in to play).
- // However, it will not necessarily map ptr addrspace(N) null to ptr
- // addrspace(8) null, aka the "null descriptor", which has "all loads return
- // 0, all stores are dropped" semantics. Given the context of this intrinsic
- // list, no one should be relying on such a strict interpretation of
- // MustPreserveNullness (and, at time of writing, they are not), but we
- // document this fact out of an abundance of caution.
+ // input pointer (and thus preserves the byte offset, which is the property
+ // the MustPreserveOffset flag selects). However, it will not necessarily
+ // map ptr addrspace(N) null to ptr addrspace(8) null, aka the "null
+ // descriptor", which has "all loads return 0, all stores are dropped"
+ // semantics. Given the context of this intrinsic list, no one should be
+ // relying on such a strict bit-exact null mapping (and, at time of
+ // writing, they are not), but we document this fact out of an abundance
+ // of caution.
case Intrinsic::amdgcn_make_buffer_rsrc:
return true;
case Intrinsic::ptrmask:
- return !MustPreserveNullness;
+ return !MustPreserveOffset;
case Intrinsic::threadlocal_address:
// The underlying variable changes with thread ID. The Thread ID may change
// at coroutine suspend points.
@@ -6970,7 +6971,8 @@ const Value *llvm::getUnderlyingObject(const Value *V, unsigned MaxLookup) {
// because it should be in sync with CaptureTracking. Not using it may
// cause weird miscompilations where 2 aliasing pointers are assumed to
// noalias.
- if (auto *RP = getArgumentAliasingToReturnedPointer(Call, false)) {
+ if (auto *RP = getArgumentAliasingToReturnedPointer(
+ Call, /*MustPreserveOffset=*/false)) {
V = RP;
continue;
}
diff --git a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
index 7c50b9faf3c80..69b479fb41ea4 100644
--- a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
+++ b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
@@ -12768,7 +12768,7 @@ struct AAInvariantLoadPointerImpl
case IRP_CALL_SITE_RETURNED: {
const auto &CB = cast<CallBase>(getAnchorValue());
return !isIntrinsicReturningPointerAliasingArgumentWithoutCapturing(
- &CB, /*MustPreserveNullness=*/false);
+ &CB, /*MustPreserveOffset=*/false);
}
case IRP_ARGUMENT: {
const Function *F = getAssociatedFunction();
@@ -12903,7 +12903,7 @@ struct AAInvariantLoadPointerImpl
if (const auto *CB = dyn_cast<CallBase>(&getAnchorValue())) {
if (isIntrinsicReturningPointerAliasingArgumentWithoutCapturing(
- CB, /*MustPreserveNullness=*/false)) {
+ CB, /*MustPreserveOffset=*/false)) {
for (const Value *Arg : CB->args()) {
if (!IsLocallyInvariantLoadIfPointer(*Arg))
return indicatePessimisticFixpoint();
@@ -12949,7 +12949,7 @@ struct AAInvariantLoadPointerCallSiteReturned final
const auto &CB = cast<CallBase>(getAnchorValue());
if (isIntrinsicReturningPointerAliasingArgumentWithoutCapturing(
- &CB, /*MustPreserveNullness=*/false))
+ &CB, /*MustPreserveOffset=*/false))
return AAInvariantLoadPointerImpl::initialize(A);
if (F->onlyReadsMemory() && F->hasNoSync())
diff --git a/llvm/lib/Transforms/IPO/FunctionAttrs.cpp b/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
index c6113fbb96b3f..1aea1ee301ad5 100644
--- a/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
+++ b/llvm/lib/Transforms/IPO/FunctionAttrs.cpp
@@ -916,7 +916,7 @@ determinePointerAccessAttrs(Argument *A,
// but return results thas alias their pointer argument, and thus should
// be handled like GEP or addrspacecast above.
if (isIntrinsicReturningPointerAliasingArgumentWithoutCapturing(
- &CB, /*MustPreserveNullness=*/false)) {
+ &CB, /*MustPreserveOffset=*/false)) {
for (Use &UU : CB.uses())
if (Visited.insert(&UU).second)
Worklist.push_back(&UU);
diff --git a/llvm/test/Analysis/BasicAA/ptrmask-gep-decomposition.ll b/llvm/test/Analysis/BasicAA/ptrmask-gep-decomposition.ll
new file mode 100644
index 0000000000000..d746c29e90deb
--- /dev/null
+++ b/llvm/test/Analysis/BasicAA/ptrmask-gep-decomposition.ll
@@ -0,0 +1,25 @@
+; RUN: opt -aa-pipeline=basic-aa -passes=aa-eval -print-all-alias-modref-info -disable-output %s 2>&1 | FileCheck %s
+
+; BasicAA must not look through llvm.ptrmask when decomposing a GEP into a
+; symbolic byte offset, because ptrmask preserves the underlying object but
+; can change the byte address. With %base 2-aligned:
+; %p = %base + 1
+; %q = ptrmask(%p, -2) == %base
+; %r = %q + 1 == %p
+; so %p and %r alias.
+
+declare ptr @llvm.ptrmask.p0.i64(ptr, i64)
+
+define i8 @ptrmask_gep_may_alias(ptr align 2 %base) {
+; CHECK-LABEL: Function: ptrmask_gep_may_alias
+; CHECK: MayAlias: i8* %p, i8* %r
+entry:
+ %p = getelementptr i8, ptr %base, i64 1
+ %q = call ptr @llvm.ptrmask.p0.i64(ptr %p, i64 -2)
+ %r = getelementptr i8, ptr %q, i64 1
+
+ store i8 7, ptr %p, align 1
+ store i8 42, ptr %r, align 1
+ %v = load i8, ptr %p, align 1
+ ret i8 %v
+}
More information about the llvm-commits
mailing list