[llvm] [SimplifyCFG] Fix stale call-site nonnull causing false UB on PHI args (PR #222004)
Saiyedul Islam via llvm-commits
llvm-commits at lists.llvm.org
Tue Sep 8 06:51:25 PDT 2026
https://github.com/saiislam updated https://github.com/llvm/llvm-project/pull/222004
>From aa3229d7c0961e1dce0ae1c4a2cafded2db085b7 Mon Sep 17 00:00:00 2001
From: Saiyedul Islam <Saiyedul.Islam at amd.com>
Date: Thu, 3 Sep 2026 10:39:11 -0500
Subject: [PATCH] [SimplifyCFG] Fix stale call-site nonnull causing false UB on
PHI args
Inlining can leave a stale call-site nonnull on a PHI that has a null
incoming. passingValueIsAlwaysUndefined() treated that as UB and dropped
the predecessor; #200164 made those call uses reliably visible.
Only treat null as UB when the callee declares nonnull or
dereferenceable (including after looking through GEPs). Call-site-only
attributes must not be used to eliminate predecessors.
---
llvm/lib/Transforms/Utils/SimplifyCFG.cpp | 24 +++++-
.../SimplifyCFG/UnreachableEliminate.ll | 85 +++++++++++++++++++
2 files changed, 107 insertions(+), 2 deletions(-)
diff --git a/llvm/lib/Transforms/Utils/SimplifyCFG.cpp b/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
index ca96f2e70d810..871a8dafc72cf 100644
--- a/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
+++ b/llvm/lib/Transforms/Utils/SimplifyCFG.cpp
@@ -9150,10 +9150,30 @@ static bool passingValueIsAlwaysUndefined(Value *V, Instruction *I, bool PtrValu
if (CB->isArgOperand(&Use)) {
unsigned ArgIdx = CB->getArgOperandNo(&Use);
- // Passing null to a nonnnull+noundef argument is undefined.
+ // Passing null to a nonnull+noundef argument is undefined.
if (isa<ConstantPointerNull>(C) && C->getType()->isPointerTy() &&
- CB->paramHasNonNullAttr(ArgIdx, /*AllowUndefOrPoison=*/false))
+ CB->paramHasNonNullAttr(ArgIdx, /*AllowUndefOrPoison=*/false)) {
+ // Call-site nonnull/dereferenceable is not a stable ABI contract:
+ // it may have been inferred when the argument was provably
+ // non-null and then gone stale (e.g. after inlining introduced a
+ // PHI with a null incoming). Treat nonnull as UB only when the
+ // callee itself declares it:
+ // (a) an explicit 'nonnull' parameter attribute, or
+ // (b) 'dereferenceable' (implies nonnull unless null pointers
+ // are defined in the callee's address space).
+ const Function *Callee = CB->getCalledFunction();
+ if (!Callee || ArgIdx >= Callee->arg_size())
+ return false;
+ unsigned ArgAS =
+ CB->getArgOperand(ArgIdx)->getType()->getPointerAddressSpace();
+ bool CalleeImpliesNonNull =
+ Callee->hasParamAttribute(ArgIdx, Attribute::NonNull) ||
+ (Callee->hasParamAttribute(ArgIdx, Attribute::Dereferenceable) &&
+ !NullPointerIsDefined(Callee, ArgAS));
+ if (!CalleeImpliesNonNull)
+ return false;
return !PtrValueMayBeModified;
+ }
// Passing undef to a noundef argument is undefined.
if (isa<UndefValue>(C) && CB->isPassingUndefUB(ArgIdx))
return true;
diff --git a/llvm/test/Transforms/SimplifyCFG/UnreachableEliminate.ll b/llvm/test/Transforms/SimplifyCFG/UnreachableEliminate.ll
index b89619d89f014..77be787d0feec 100644
--- a/llvm/test/Transforms/SimplifyCFG/UnreachableEliminate.ll
+++ b/llvm/test/Transforms/SimplifyCFG/UnreachableEliminate.ll
@@ -363,6 +363,91 @@ else:
ret void
}
+; Call-site-only nonnull+noundef on a PHI argument is not a stable ABI
+; contract (it can go stale after inlining) and must not be treated as UB.
+; The callee does not declare the parameter nonnull.
+define void @test9_stale_callsite_nonnull(i1 %X, ptr %Y) {
+; CHECK-LABEL: @test9_stale_callsite_nonnull(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: [[SPEC_SELECT:%.*]] = select i1 [[X:%.*]], ptr null, ptr [[Y:%.*]]
+; CHECK-NEXT: [[TMP0:%.*]] = call ptr @fn_ptr_arg(ptr noundef nonnull [[SPEC_SELECT]])
+; CHECK-NEXT: ret void
+;
+entry:
+ br i1 %X, label %if, label %else
+
+if:
+ br label %else
+
+else:
+ %phi = phi ptr [ %Y, %entry ], [ null, %if ]
+ call ptr @fn_ptr_arg(ptr nonnull noundef %phi)
+ ret void
+}
+
+; Same as above for call-site-only dereferenceable, which paramHasNonNullAttr
+; treats as implying nonnull.
+define void @test9_stale_callsite_deref(i1 %X, ptr %Y) {
+; CHECK-LABEL: @test9_stale_callsite_deref(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: [[SPEC_SELECT:%.*]] = select i1 [[X:%.*]], ptr null, ptr [[Y:%.*]]
+; CHECK-NEXT: [[TMP0:%.*]] = call ptr @fn_ptr_arg(ptr dereferenceable(4) [[SPEC_SELECT]])
+; CHECK-NEXT: ret void
+;
+entry:
+ br i1 %X, label %if, label %else
+
+if:
+ br label %else
+
+else:
+ %phi = phi ptr [ %Y, %entry ], [ null, %if ]
+ call ptr @fn_ptr_arg(ptr dereferenceable(4) %phi)
+ ret void
+}
+
+; Indirect call: there is no callee declaration to consult.
+define void @test9_stale_callsite_nonnull_indirect(i1 %X, ptr %Y, ptr %fn) {
+; CHECK-LABEL: @test9_stale_callsite_nonnull_indirect(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: [[SPEC_SELECT:%.*]] = select i1 [[X:%.*]], ptr null, ptr [[Y:%.*]]
+; CHECK-NEXT: [[TMP0:%.*]] = call ptr [[FN:%.*]](ptr noundef nonnull [[SPEC_SELECT]])
+; CHECK-NEXT: ret void
+;
+entry:
+ br i1 %X, label %if, label %else
+
+if:
+ br label %else
+
+else:
+ %phi = phi ptr [ %Y, %entry ], [ null, %if ]
+ call ptr %fn(ptr nonnull noundef %phi)
+ ret void
+}
+
+; Looking through a zero-offset GEP of the PHI must use the same rule.
+define void @test9_stale_callsite_nonnull_gep(i1 %X, ptr %Y) {
+; CHECK-LABEL: @test9_stale_callsite_nonnull_gep(
+; CHECK-NEXT: entry:
+; CHECK-NEXT: [[SPEC_SELECT:%.*]] = select i1 [[X:%.*]], ptr null, ptr [[Y:%.*]]
+; CHECK-NEXT: [[GEP:%.*]] = getelementptr i8, ptr [[SPEC_SELECT]], i64 0
+; CHECK-NEXT: [[TMP0:%.*]] = call ptr @fn_ptr_arg(ptr noundef nonnull [[GEP]])
+; CHECK-NEXT: ret void
+;
+entry:
+ br i1 %X, label %if, label %else
+
+if:
+ br label %else
+
+else:
+ %phi = phi ptr [ %Y, %entry ], [ null, %if ]
+ %gep = getelementptr i8, ptr %phi, i64 0
+ call ptr @fn_ptr_arg(ptr nonnull noundef %gep)
+ ret void
+}
+
define void @test9_gep_mismatch(i1 %X, ptr %Y, ptr %P) {
; CHECK-LABEL: @test9_gep_mismatch(
; CHECK-NEXT: entry:
More information about the llvm-commits
mailing list