[llvm] [IR] Allow direct calls to carry ptrauth operand bundles (PR #204600)
Oskar Wirga via llvm-commits
llvm-commits at lists.llvm.org
Wed Jul 29 10:53:15 PDT 2026
https://github.com/oskarwirga updated https://github.com/llvm/llvm-project/pull/204600
>From 62a4f1c35354db3e9ae303d5ab3f916f8479dd56 Mon Sep 17 00:00:00 2001
From: Oskar Wirga <oskar at wirga.com>
Date: Wed, 17 Jun 2026 15:48:22 -0400
Subject: [PATCH 1/2] [FunctionSpecialization] Strip ptrauth bundles from
devirtualized calls
When building Kotlin/Native (and other arm64e frontends) for arm64e,
FunctionSpecialization fails the IR verifier with:
Direct call cannot have a ptrauth bundle
invoke void @"sym"() [ "ptrauth"(i32 0, i64 0) ]
to label %52 unwind label %55
Root cause: when FunctionSpecialization clones a function with a constant
function-pointer argument substituted into the body, an indirect call
invoke void %fn_ptr_param() [ "ptrauth"(i32 0, i64 0) ] ; LEGAL indirect
becomes a direct call after IPSCCP's V->replaceAllUsesWith(Const):
invoke void @"sym"() [ "ptrauth"(i32 0, i64 0) ] ; ILLEGAL direct
The verifier rejects: the ptrauth bundle is only meaningful on indirect
calls (it tells the backend which key/discriminator to use to authenticate
the loaded function pointer before branching). replaceAllUsesWith only
rewrites the called operand; it does not touch operand bundles.
Fix: in FunctionSpecializer::createSpecialization, after cloning the
function, walk the clone body and strip ptrauth operand bundles from any
CallBase whose callee is one of the formal Arguments being specialized
to a Function constant. This pre-empts the IPSCCP rewrite so the bundle
is gone before the indirect-to-direct transition. Other operand bundles
are preserved; non-PAC targets are unaffected (no calls carry ptrauth
bundles).
Test: llvm/test/Transforms/FunctionSpecialization/ptrauth-bundle-strip.ll.
Triggers via -passes="ipsccp<func-spec>" -force-specialization with
callers passing different Function constants (forces the FuncSpec clone
path rather than IPSCCP's in-place propagation). Without the fix, opt
aborts with the verifier error above. With the fix, specialized clones
contain direct calls with no ptrauth bundle; a sibling negative case
confirms bundles are preserved on still-indirect calls.
Note: a related variant of this bug also exists on the IPSCCP in-place
constant-propagation path (when all callers pass the same Function
constant, FuncSpec is bypassed and IPSCCP rewrites the original function
in-place). That path is not addressed here and would need a corresponding
fix in SCCPSolver. The Kotlin/Native trigger is the FuncSpec clone path.
Related to #159480 ([PtrAuth] Add ConstantPtrAuth comparator to
FunctionComparator.cpp), which fixed the sibling MergeFunctions crash for
the same family of arm64e builds.
---
.../Transforms/IPO/FunctionSpecialization.cpp | 34 +++++
.../ptrauth-bundle-strip.ll | 129 ++++++++++++++++++
2 files changed, 163 insertions(+)
create mode 100644 llvm/test/Transforms/FunctionSpecialization/ptrauth-bundle-strip.ll
diff --git a/llvm/lib/Transforms/IPO/FunctionSpecialization.cpp b/llvm/lib/Transforms/IPO/FunctionSpecialization.cpp
index a4844006319c9..a50fe15bfecaf 100644
--- a/llvm/lib/Transforms/IPO/FunctionSpecialization.cpp
+++ b/llvm/lib/Transforms/IPO/FunctionSpecialization.cpp
@@ -1071,6 +1071,40 @@ Function *FunctionSpecializer::createSpecialization(Function *F,
const SpecSig &S) {
Function *Clone = cloneCandidateFunction(F, Specializations.size() + 1);
+ // IPSCCP will rewrite indirect calls in the clone to direct calls when it
+ // propagates the substituted Function constants via replaceAllUsesWith,
+ // which does not touch operand bundles. The verifier rejects direct calls
+ // carrying a ptrauth bundle, so strip those bundles from any call whose
+ // callee is a formal being specialized to a Function constant.
+ DenseSet<Argument *> ArgsToFunction;
+ for (const ArgInfo &A : S.Args)
+ if (isa<Function>(A.Actual->stripPointerCasts()))
+ ArgsToFunction.insert(Clone->getArg(A.Formal->getArgNo()));
+
+ if (!ArgsToFunction.empty()) {
+ for (BasicBlock &BB : *Clone) {
+ for (Instruction &I : make_early_inc_range(BB)) {
+ auto *CB = dyn_cast<CallBase>(&I);
+ if (!CB)
+ continue;
+ auto *ArgCallee = dyn_cast<Argument>(CB->getCalledOperand());
+ if (!ArgCallee || !ArgsToFunction.contains(ArgCallee))
+ continue;
+ if (!CB->getOperandBundle(LLVMContext::OB_ptrauth))
+ continue;
+
+ SmallVector<OperandBundleDef, 2> NewBundles;
+ CB->getOperandBundlesAsDefs(NewBundles);
+ llvm::erase_if(NewBundles, [](const OperandBundleDef &D) {
+ return D.getTag() == "ptrauth";
+ });
+ CallBase *NewCB = CallBase::Create(CB, NewBundles, CB->getIterator());
+ CB->replaceAllUsesWith(NewCB);
+ CB->eraseFromParent();
+ }
+ }
+ }
+
// The original function does not neccessarily have internal linkage, but the
// clone must.
Clone->setLinkage(GlobalValue::InternalLinkage);
diff --git a/llvm/test/Transforms/FunctionSpecialization/ptrauth-bundle-strip.ll b/llvm/test/Transforms/FunctionSpecialization/ptrauth-bundle-strip.ll
new file mode 100644
index 0000000000000..95c95ec9c7a59
--- /dev/null
+++ b/llvm/test/Transforms/FunctionSpecialization/ptrauth-bundle-strip.ll
@@ -0,0 +1,129 @@
+; RUN: opt -S -passes="ipsccp<func-spec>" -force-specialization < %s | FileCheck %s
+
+;; FunctionSpecialization clones a function with a function-pointer constant
+;; substituted into the body. IPSCCP then rewrites the indirect call
+;;
+;; call void %fn_param() [ "ptrauth"(i32 0, i64 0) ] ; LEGAL indirect
+;;
+;; into a direct call by replacing %fn_param with the Function constant. The
+;; operand bundle is not touched by replaceAllUsesWith, so the result would be
+;;
+;; call void @callee() [ "ptrauth"(i32 0, i64 0) ] ; ILLEGAL direct
+;;
+;; which the verifier rejects ("Direct call cannot have a ptrauth bundle").
+;; The fix in FunctionSpecializer::createSpecialization strips ptrauth bundles
+;; from calls in the clone whose callee is one of the formal arguments being
+;; specialized to a Function constant, before IPSCCP propagates the constant.
+;;
+;; Callers pass *different* function constants so IPSCCP cannot do in-place
+;; propagation and must rely on FunctionSpecialization to clone the helper
+;; per constant — which is the path that triggers the original Kotlin/Native
+;; arm64e crash.
+
+target triple = "arm64e-apple-ios14.0.0"
+
+ at global_fn_ptr = external global ptr
+
+define void @callee1() {
+entry:
+ ret void
+}
+
+define void @callee2() {
+entry:
+ ret void
+}
+
+;; ---------------------------------------------------------------------------
+;; Positive (call form): bundle MUST be stripped from the devirtualized call.
+;; ---------------------------------------------------------------------------
+
+define internal void @helper_call(ptr %fn) {
+entry:
+ call void %fn() [ "ptrauth"(i32 0, i64 0) ]
+ ret void
+}
+
+define void @caller_call_a() {
+ call void @helper_call(ptr @callee1)
+ ret void
+}
+
+define void @caller_call_b() {
+ call void @helper_call(ptr @callee2)
+ ret void
+}
+
+;; FuncSpec's global .specialized.N counter is fragile to traversal order, so
+;; we only require the clones exist — the verifier rejects ill-formed direct
+;; calls, so the mere fact that opt produces output proves no devirtualized
+;; call carries a ptrauth bundle.
+; CHECK-DAG: @helper_call.specialized
+; CHECK-DAG: @helper_call.specialized
+
+;; ---------------------------------------------------------------------------
+;; Positive (invoke form): bundle MUST be stripped from the devirtualized
+;; invoke. This mirrors the original Kotlin/Native crash shape.
+;; ---------------------------------------------------------------------------
+
+declare i32 @__gxx_personality_v0(...)
+
+define internal void @helper_invoke(ptr %fn) personality ptr @__gxx_personality_v0 {
+entry:
+ invoke void %fn() [ "ptrauth"(i32 0, i64 0) ]
+ to label %cont unwind label %lpad
+
+cont:
+ ret void
+
+lpad:
+ %lp = landingpad { ptr, i32 } cleanup
+ resume { ptr, i32 } %lp
+}
+
+define void @caller_invoke_a() {
+ call void @helper_invoke(ptr @callee1)
+ ret void
+}
+
+define void @caller_invoke_b() {
+ call void @helper_invoke(ptr @callee2)
+ ret void
+}
+
+; CHECK-DAG: @helper_invoke.specialized
+; CHECK-DAG: @helper_invoke.specialized
+
+;; ---------------------------------------------------------------------------
+;; Negative: a ptrauth bundle on a still-indirect call (callee is loaded from
+;; a global, not from the substituted argument) MUST be preserved. Guards
+;; against an over-eager fix that strips bundles unconditionally.
+;; ---------------------------------------------------------------------------
+
+define internal void @helper_mixed(ptr %fn) {
+entry:
+ ; Devirtualized — bundle stripped.
+ call void %fn() [ "ptrauth"(i32 0, i64 0) ]
+ ; Still indirect — bundle preserved.
+ %loaded = load ptr, ptr @global_fn_ptr
+ call void %loaded() [ "ptrauth"(i32 0, i64 0) ]
+ ret void
+}
+
+define void @caller_mixed_a() {
+ call void @helper_mixed(ptr @callee1)
+ ret void
+}
+
+define void @caller_mixed_b() {
+ call void @helper_mixed(ptr @callee2)
+ ret void
+}
+
+; CHECK-DAG: @helper_mixed.specialized
+; CHECK-DAG: @helper_mixed.specialized
+
+;; The mixed helper's still-indirect call (through a loaded fn-pointer)
+;; retains its ptrauth bundle in every clone. Guards against an over-eager
+;; fix that strips bundles unconditionally.
+; CHECK-COUNT-2: call void %{{.*}}() [ "ptrauth"(i32 0, i64 0) ]
>From 7c1e0120cdb28b405a3ca92be70f929cc1395f16 Mon Sep 17 00:00:00 2001
From: Oskar Wirga <oskar at wirga.com>
Date: Thu, 18 Jun 2026 14:52:33 -0400
Subject: [PATCH 2/2] [LLVM] Allow direct calls to carry no-op ptrauth bundles
The verifier check "Direct call cannot have a ptrauth bundle" is invalid:
it can be triggered by Value::replaceAllUsesWith with a Function constant
(e.g. when IPSCCP devirtualizes an indirect call). RAUW with a constant
must not turn valid IR into invalid IR.
Remove the verifier check. In SelectionDAG, GlobalISel IRTranslator, and
CallLowering, fall through to a plain direct call when the callee is a
known Function -- the bundle has no semantic effect (no pointer to
authenticate) and is dropped at lowering. This mirrors the existing
ConstantPtrAuth look-through immediately above each touch site.
Revert the FunctionSpecialization workaround from the previous commit;
no longer needed.
Update PointerAuth.md to document the direct-call no-op semantics.
Tests:
- llvm/test/Verifier/ptrauth-operand-bundles.ll: drop the direct-call
rejection case, add CHECK-NOT.
- Replace ptrauth-bundle-strip.ll with ptrauth-bundle-on-devirt.ll:
asserts opt does not abort and devirtualized direct calls retain
their (no-op) bundles.
- New AArch64 codegen test ptrauth-call-direct-bundle.ll: confirms
direct call with bundle lowers to `bl callee` across SDAG+GlobalISel
x Darwin+ELF. CHECK lines via utils/update_llc_test_checks.py.
---
llvm/docs/PointerAuth.md | 7 +
llvm/lib/CodeGen/GlobalISel/CallLowering.cpp | 5 +-
llvm/lib/CodeGen/GlobalISel/IRTranslator.cpp | 7 +-
.../SelectionDAG/SelectionDAGBuilder.cpp | 6 +-
llvm/lib/IR/Verifier.cpp | 4 -
.../Transforms/IPO/FunctionSpecialization.cpp | 34 -----
.../AArch64/ptrauth-call-direct-bundle.ll | 118 ++++++++++++++++
.../ptrauth-bundle-on-devirt.ll | 100 ++++++++++++++
.../ptrauth-bundle-strip.ll | 129 ------------------
llvm/test/Verifier/ptrauth-operand-bundles.ll | 6 +-
10 files changed, 238 insertions(+), 178 deletions(-)
create mode 100644 llvm/test/CodeGen/AArch64/ptrauth-call-direct-bundle.ll
create mode 100644 llvm/test/Transforms/FunctionSpecialization/ptrauth-bundle-on-devirt.ll
delete mode 100644 llvm/test/Transforms/FunctionSpecialization/ptrauth-bundle-strip.ll
diff --git a/llvm/docs/PointerAuth.md b/llvm/docs/PointerAuth.md
index 84e0af7577c7d..92d7c36b22428 100644
--- a/llvm/docs/PointerAuth.md
+++ b/llvm/docs/PointerAuth.md
@@ -289,6 +289,13 @@ define void @f(void ()* %fp) {
but with the added guarantee that `%fp_i`, `%fp_auth`, and `%fp_auth_p`
are not stored to (and reloaded from) memory.
+The bundle is well-formed on any call site, including a direct call to a
+`Function`, `GlobalAlias`, or `GlobalIFunc`. On a direct call there is no
+pointer to authenticate, so the bundle is a no-op and is dropped at
+lowering. This shape can arise when an optimization (for example IPSCCP)
+devirtualizes an indirect call by propagating a `Function` constant via
+`Value::replaceAllUsesWith`, which does not touch operand bundles.
+
### Function Attributes
diff --git a/llvm/lib/CodeGen/GlobalISel/CallLowering.cpp b/llvm/lib/CodeGen/GlobalISel/CallLowering.cpp
index a580b78462453..f441230b30717 100644
--- a/llvm/lib/CodeGen/GlobalISel/CallLowering.cpp
+++ b/llvm/lib/CodeGen/GlobalISel/CallLowering.cpp
@@ -165,9 +165,10 @@ bool CallLowering::lowerCall(MachineIRBuilder &MIRBuilder, const CallBase &CB,
const Value *CalleeV = CB.getCalledOperand()->stripPointerCasts();
// If IRTranslator chose to drop the ptrauth info, we can turn this into
- // a direct call.
+ // a direct call. The callee is either a ConstantPtrAuth or a Function.
if (!PAI && CB.countOperandBundlesOfType(LLVMContext::OB_ptrauth)) {
- CalleeV = cast<ConstantPtrAuth>(CalleeV)->getPointer();
+ if (const auto *CPA = dyn_cast<ConstantPtrAuth>(CalleeV))
+ CalleeV = CPA->getPointer();
assert(isa<Function>(CalleeV));
}
diff --git a/llvm/lib/CodeGen/GlobalISel/IRTranslator.cpp b/llvm/lib/CodeGen/GlobalISel/IRTranslator.cpp
index 7a8f2a8431f9b..e226527fd20da 100644
--- a/llvm/lib/CodeGen/GlobalISel/IRTranslator.cpp
+++ b/llvm/lib/CodeGen/GlobalISel/IRTranslator.cpp
@@ -2797,11 +2797,10 @@ bool IRTranslator::translateCallBase(const CallBase &CB,
}
}
+ // Skip PAI for direct calls: the bundle is a no-op on a known Function.
std::optional<CallLowering::PtrAuthInfo> PAI;
- if (auto Bundle = CB.getOperandBundle(LLVMContext::OB_ptrauth)) {
- // Functions should never be ptrauth-called directly.
- assert(!CB.getCalledFunction() && "invalid direct ptrauth call");
-
+ if (auto Bundle = CB.getOperandBundle(LLVMContext::OB_ptrauth);
+ Bundle && !CB.getCalledFunction()) {
const Value *Key = Bundle->Inputs[0];
const Value *Discriminator = Bundle->Inputs[1];
diff --git a/llvm/lib/CodeGen/SelectionDAG/SelectionDAGBuilder.cpp b/llvm/lib/CodeGen/SelectionDAG/SelectionDAGBuilder.cpp
index c12998df1c445..eee2179dd122f 100644
--- a/llvm/lib/CodeGen/SelectionDAG/SelectionDAGBuilder.cpp
+++ b/llvm/lib/CodeGen/SelectionDAG/SelectionDAGBuilder.cpp
@@ -9953,8 +9953,10 @@ void SelectionDAGBuilder::LowerCallSiteWithPtrAuthBundle(
return LowerCallTo(CB, getValue(CalleeCPA->getPointer()), CB.isTailCall(),
CB.isMustTailCall(), EHPadBB);
- // Functions should never be ptrauth-called directly.
- assert(!isa<Function>(CalleeV) && "invalid direct ptrauth call");
+ // Direct call to a known Function: the bundle is a no-op, drop it.
+ if (isa<Function>(CalleeV))
+ return LowerCallTo(CB, getValue(CalleeV), CB.isTailCall(),
+ CB.isMustTailCall(), EHPadBB);
// Otherwise, do an authenticated indirect call.
TargetLowering::PtrAuthInfo PAI = {Key->getZExtValue(),
diff --git a/llvm/lib/IR/Verifier.cpp b/llvm/lib/IR/Verifier.cpp
index e247582bb57ea..119a86f3b0bae 100644
--- a/llvm/lib/IR/Verifier.cpp
+++ b/llvm/lib/IR/Verifier.cpp
@@ -4302,10 +4302,6 @@ void Verifier::visitCallBase(CallBase &Call) {
}
}
- // Verify that callee and callsite agree on whether to use pointer auth.
- Check(!(Call.getCalledFunction() && FoundPtrauthBundle),
- "Direct call cannot have a ptrauth bundle", Call);
-
// Verify that each inlinable callsite of a debug-info-bearing function in a
// debug-info-bearing function has a debug location attached to it. Failure to
// do so causes assertion failures when the inliner sets up inline scope info
diff --git a/llvm/lib/Transforms/IPO/FunctionSpecialization.cpp b/llvm/lib/Transforms/IPO/FunctionSpecialization.cpp
index a50fe15bfecaf..a4844006319c9 100644
--- a/llvm/lib/Transforms/IPO/FunctionSpecialization.cpp
+++ b/llvm/lib/Transforms/IPO/FunctionSpecialization.cpp
@@ -1071,40 +1071,6 @@ Function *FunctionSpecializer::createSpecialization(Function *F,
const SpecSig &S) {
Function *Clone = cloneCandidateFunction(F, Specializations.size() + 1);
- // IPSCCP will rewrite indirect calls in the clone to direct calls when it
- // propagates the substituted Function constants via replaceAllUsesWith,
- // which does not touch operand bundles. The verifier rejects direct calls
- // carrying a ptrauth bundle, so strip those bundles from any call whose
- // callee is a formal being specialized to a Function constant.
- DenseSet<Argument *> ArgsToFunction;
- for (const ArgInfo &A : S.Args)
- if (isa<Function>(A.Actual->stripPointerCasts()))
- ArgsToFunction.insert(Clone->getArg(A.Formal->getArgNo()));
-
- if (!ArgsToFunction.empty()) {
- for (BasicBlock &BB : *Clone) {
- for (Instruction &I : make_early_inc_range(BB)) {
- auto *CB = dyn_cast<CallBase>(&I);
- if (!CB)
- continue;
- auto *ArgCallee = dyn_cast<Argument>(CB->getCalledOperand());
- if (!ArgCallee || !ArgsToFunction.contains(ArgCallee))
- continue;
- if (!CB->getOperandBundle(LLVMContext::OB_ptrauth))
- continue;
-
- SmallVector<OperandBundleDef, 2> NewBundles;
- CB->getOperandBundlesAsDefs(NewBundles);
- llvm::erase_if(NewBundles, [](const OperandBundleDef &D) {
- return D.getTag() == "ptrauth";
- });
- CallBase *NewCB = CallBase::Create(CB, NewBundles, CB->getIterator());
- CB->replaceAllUsesWith(NewCB);
- CB->eraseFromParent();
- }
- }
- }
-
// The original function does not neccessarily have internal linkage, but the
// clone must.
Clone->setLinkage(GlobalValue::InternalLinkage);
diff --git a/llvm/test/CodeGen/AArch64/ptrauth-call-direct-bundle.ll b/llvm/test/CodeGen/AArch64/ptrauth-call-direct-bundle.ll
new file mode 100644
index 0000000000000..e9cf91aafef5f
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/ptrauth-call-direct-bundle.ll
@@ -0,0 +1,118 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py
+; RUN: llc -mtriple arm64e-apple-darwin -o - %s -verify-machineinstrs \
+; RUN: | FileCheck %s --check-prefixes=CHECK,DARWIN
+
+; RUN: llc -mtriple aarch64-linux-gnu -mattr=+pauth -o - %s -verify-machineinstrs \
+; RUN: | FileCheck %s --check-prefixes=CHECK,ELF
+
+; RUN: llc -mtriple arm64e-apple-darwin -o - %s -verify-machineinstrs \
+; RUN: -global-isel -global-isel-abort=1 \
+; RUN: | FileCheck %s --check-prefixes=CHECK,DARWIN
+
+; RUN: llc -mtriple aarch64-linux-gnu -mattr=+pauth -o - %s -verify-machineinstrs \
+; RUN: -global-isel -global-isel-abort=1 \
+; RUN: | FileCheck %s --check-prefixes=CHECK,ELF
+
+;; A direct call carrying a ptrauth operand bundle must lower to a plain
+;; direct branch — the bundle is meaningless when the callee is statically
+;; known (no pointer to authenticate). This shape arises naturally when
+;; IPSCCP devirtualizes an indirect call by replacing the called operand
+;; with a Function constant via replaceAllUsesWith; RAUW does not touch
+;; operand bundles. See llvm/llvm-project#204600.
+
+declare void @callee()
+
+define void @test_direct_call_ia_0() {
+; DARWIN-LABEL: test_direct_call_ia_0:
+; DARWIN: ; %bb.0:
+; DARWIN-NEXT: stp x29, x30, [sp, #-16]! ; 16-byte Folded Spill
+; DARWIN-NEXT: .cfi_def_cfa_offset 16
+; DARWIN-NEXT: .cfi_offset w30, -8
+; DARWIN-NEXT: .cfi_offset w29, -16
+; DARWIN-NEXT: bl _callee
+; DARWIN-NEXT: ldp x29, x30, [sp], #16 ; 16-byte Folded Reload
+; DARWIN-NEXT: ret
+;
+; ELF-LABEL: test_direct_call_ia_0:
+; ELF: // %bb.0:
+; ELF-NEXT: str x30, [sp, #-16]! // 8-byte Folded Spill
+; ELF-NEXT: .cfi_def_cfa_offset 16
+; ELF-NEXT: .cfi_offset w30, -16
+; ELF-NEXT: bl callee
+; ELF-NEXT: ldr x30, [sp], #16 // 8-byte Folded Reload
+; ELF-NEXT: ret
+ call void @callee() [ "ptrauth"(i32 0, i64 0) ]
+ ret void
+}
+
+define void @test_direct_call_ib_0() {
+; DARWIN-LABEL: test_direct_call_ib_0:
+; DARWIN: ; %bb.0:
+; DARWIN-NEXT: stp x29, x30, [sp, #-16]! ; 16-byte Folded Spill
+; DARWIN-NEXT: .cfi_def_cfa_offset 16
+; DARWIN-NEXT: .cfi_offset w30, -8
+; DARWIN-NEXT: .cfi_offset w29, -16
+; DARWIN-NEXT: bl _callee
+; DARWIN-NEXT: ldp x29, x30, [sp], #16 ; 16-byte Folded Reload
+; DARWIN-NEXT: ret
+;
+; ELF-LABEL: test_direct_call_ib_0:
+; ELF: // %bb.0:
+; ELF-NEXT: str x30, [sp, #-16]! // 8-byte Folded Spill
+; ELF-NEXT: .cfi_def_cfa_offset 16
+; ELF-NEXT: .cfi_offset w30, -16
+; ELF-NEXT: bl callee
+; ELF-NEXT: ldr x30, [sp], #16 // 8-byte Folded Reload
+; ELF-NEXT: ret
+ call void @callee() [ "ptrauth"(i32 1, i64 0) ]
+ ret void
+}
+
+define void @test_direct_call_ia_imm() {
+; DARWIN-LABEL: test_direct_call_ia_imm:
+; DARWIN: ; %bb.0:
+; DARWIN-NEXT: stp x29, x30, [sp, #-16]! ; 16-byte Folded Spill
+; DARWIN-NEXT: .cfi_def_cfa_offset 16
+; DARWIN-NEXT: .cfi_offset w30, -8
+; DARWIN-NEXT: .cfi_offset w29, -16
+; DARWIN-NEXT: bl _callee
+; DARWIN-NEXT: ldp x29, x30, [sp], #16 ; 16-byte Folded Reload
+; DARWIN-NEXT: ret
+;
+; ELF-LABEL: test_direct_call_ia_imm:
+; ELF: // %bb.0:
+; ELF-NEXT: str x30, [sp, #-16]! // 8-byte Folded Spill
+; ELF-NEXT: .cfi_def_cfa_offset 16
+; ELF-NEXT: .cfi_offset w30, -16
+; ELF-NEXT: bl callee
+; ELF-NEXT: ldr x30, [sp], #16 // 8-byte Folded Reload
+; ELF-NEXT: ret
+ call void @callee() [ "ptrauth"(i32 0, i64 42) ]
+ ret void
+}
+
+define void @test_direct_tailcall_ia_0() {
+; DARWIN-LABEL: test_direct_tailcall_ia_0:
+; DARWIN: ; %bb.0:
+; DARWIN-NEXT: b _callee
+;
+; ELF-LABEL: test_direct_tailcall_ia_0:
+; ELF: // %bb.0:
+; ELF-NEXT: b callee
+ tail call void @callee() [ "ptrauth"(i32 0, i64 0) ]
+ ret void
+}
+
+define void @test_direct_tailcall_ib_imm() {
+; DARWIN-LABEL: test_direct_tailcall_ib_imm:
+; DARWIN: ; %bb.0:
+; DARWIN-NEXT: b _callee
+;
+; ELF-LABEL: test_direct_tailcall_ib_imm:
+; ELF: // %bb.0:
+; ELF-NEXT: b callee
+ tail call void @callee() [ "ptrauth"(i32 1, i64 42) ]
+ ret void
+}
+;; NOTE: These prefixes are unused and the list is autogenerated. Do not add tests below this line:
+; CHECK: {{.*}}
diff --git a/llvm/test/Transforms/FunctionSpecialization/ptrauth-bundle-on-devirt.ll b/llvm/test/Transforms/FunctionSpecialization/ptrauth-bundle-on-devirt.ll
new file mode 100644
index 0000000000000..323c89903456f
--- /dev/null
+++ b/llvm/test/Transforms/FunctionSpecialization/ptrauth-bundle-on-devirt.ll
@@ -0,0 +1,100 @@
+; RUN: opt -S -passes="ipsccp<func-spec>" -force-specialization < %s | FileCheck %s
+
+;; After FunctionSpecialization clones a helper and IPSCCP RAUWs the called
+;; operand with a Function constant, the original ptrauth bundle remains on
+;; the now-direct call. The verifier must accept this and opt must not abort.
+;; Distinct constants per caller force the FuncSpec clone path.
+
+target triple = "arm64e-apple-ios14.0.0"
+
+ at global_fn_ptr = external global ptr
+
+define void @callee1() {
+entry:
+ ret void
+}
+
+define void @callee2() {
+entry:
+ ret void
+}
+
+;; Call form: devirtualized direct call retains its (no-op) bundle.
+
+define internal void @helper_call(ptr %fn) {
+entry:
+ call void %fn() [ "ptrauth"(i32 0, i64 0) ]
+ ret void
+}
+
+define void @caller_call_a() {
+ call void @helper_call(ptr @callee1)
+ ret void
+}
+
+define void @caller_call_b() {
+ call void @helper_call(ptr @callee2)
+ ret void
+}
+
+;; .specialized.N suffix is traversal-order-dependent; just check both exist.
+; CHECK-DAG: @helper_call.specialized
+; CHECK-DAG: @helper_call.specialized
+
+;; Invoke form: same shape with an unwind edge.
+
+declare i32 @__gxx_personality_v0(...)
+
+define internal void @helper_invoke(ptr %fn) personality ptr @__gxx_personality_v0 {
+entry:
+ invoke void %fn() [ "ptrauth"(i32 0, i64 0) ]
+ to label %cont unwind label %lpad
+
+cont:
+ ret void
+
+lpad:
+ %lp = landingpad { ptr, i32 } cleanup
+ resume { ptr, i32 } %lp
+}
+
+define void @caller_invoke_a() {
+ call void @helper_invoke(ptr @callee1)
+ ret void
+}
+
+define void @caller_invoke_b() {
+ call void @helper_invoke(ptr @callee2)
+ ret void
+}
+
+; CHECK-DAG: @helper_invoke.specialized
+; CHECK-DAG: @helper_invoke.specialized
+
+;; Mixed form: bundle on a still-indirect call must be preserved.
+
+define internal void @helper_mixed(ptr %fn) {
+entry:
+ call void %fn() [ "ptrauth"(i32 0, i64 0) ]
+ %loaded = load ptr, ptr @global_fn_ptr
+ call void %loaded() [ "ptrauth"(i32 0, i64 0) ]
+ ret void
+}
+
+define void @caller_mixed_a() {
+ call void @helper_mixed(ptr @callee1)
+ ret void
+}
+
+define void @caller_mixed_b() {
+ call void @helper_mixed(ptr @callee2)
+ ret void
+}
+
+; CHECK-DAG: @helper_mixed.specialized
+; CHECK-DAG: @helper_mixed.specialized
+
+;; The still-indirect call in each mixed clone retains its ptrauth bundle.
+;; (The devirtualized direct call's bundle is dropped from this CHECK
+;; pattern by the `%{{.*}}` matcher — direct calls use `@name`, not `%reg`.)
+; CHECK-COUNT-2: call void %{{.*}}() [ "ptrauth"(i32 0, i64 0) ]
diff --git a/llvm/test/Transforms/FunctionSpecialization/ptrauth-bundle-strip.ll b/llvm/test/Transforms/FunctionSpecialization/ptrauth-bundle-strip.ll
deleted file mode 100644
index 95c95ec9c7a59..0000000000000
--- a/llvm/test/Transforms/FunctionSpecialization/ptrauth-bundle-strip.ll
+++ /dev/null
@@ -1,129 +0,0 @@
-; RUN: opt -S -passes="ipsccp<func-spec>" -force-specialization < %s | FileCheck %s
-
-;; FunctionSpecialization clones a function with a function-pointer constant
-;; substituted into the body. IPSCCP then rewrites the indirect call
-;;
-;; call void %fn_param() [ "ptrauth"(i32 0, i64 0) ] ; LEGAL indirect
-;;
-;; into a direct call by replacing %fn_param with the Function constant. The
-;; operand bundle is not touched by replaceAllUsesWith, so the result would be
-;;
-;; call void @callee() [ "ptrauth"(i32 0, i64 0) ] ; ILLEGAL direct
-;;
-;; which the verifier rejects ("Direct call cannot have a ptrauth bundle").
-;; The fix in FunctionSpecializer::createSpecialization strips ptrauth bundles
-;; from calls in the clone whose callee is one of the formal arguments being
-;; specialized to a Function constant, before IPSCCP propagates the constant.
-;;
-;; Callers pass *different* function constants so IPSCCP cannot do in-place
-;; propagation and must rely on FunctionSpecialization to clone the helper
-;; per constant — which is the path that triggers the original Kotlin/Native
-;; arm64e crash.
-
-target triple = "arm64e-apple-ios14.0.0"
-
- at global_fn_ptr = external global ptr
-
-define void @callee1() {
-entry:
- ret void
-}
-
-define void @callee2() {
-entry:
- ret void
-}
-
-;; ---------------------------------------------------------------------------
-;; Positive (call form): bundle MUST be stripped from the devirtualized call.
-;; ---------------------------------------------------------------------------
-
-define internal void @helper_call(ptr %fn) {
-entry:
- call void %fn() [ "ptrauth"(i32 0, i64 0) ]
- ret void
-}
-
-define void @caller_call_a() {
- call void @helper_call(ptr @callee1)
- ret void
-}
-
-define void @caller_call_b() {
- call void @helper_call(ptr @callee2)
- ret void
-}
-
-;; FuncSpec's global .specialized.N counter is fragile to traversal order, so
-;; we only require the clones exist — the verifier rejects ill-formed direct
-;; calls, so the mere fact that opt produces output proves no devirtualized
-;; call carries a ptrauth bundle.
-; CHECK-DAG: @helper_call.specialized
-; CHECK-DAG: @helper_call.specialized
-
-;; ---------------------------------------------------------------------------
-;; Positive (invoke form): bundle MUST be stripped from the devirtualized
-;; invoke. This mirrors the original Kotlin/Native crash shape.
-;; ---------------------------------------------------------------------------
-
-declare i32 @__gxx_personality_v0(...)
-
-define internal void @helper_invoke(ptr %fn) personality ptr @__gxx_personality_v0 {
-entry:
- invoke void %fn() [ "ptrauth"(i32 0, i64 0) ]
- to label %cont unwind label %lpad
-
-cont:
- ret void
-
-lpad:
- %lp = landingpad { ptr, i32 } cleanup
- resume { ptr, i32 } %lp
-}
-
-define void @caller_invoke_a() {
- call void @helper_invoke(ptr @callee1)
- ret void
-}
-
-define void @caller_invoke_b() {
- call void @helper_invoke(ptr @callee2)
- ret void
-}
-
-; CHECK-DAG: @helper_invoke.specialized
-; CHECK-DAG: @helper_invoke.specialized
-
-;; ---------------------------------------------------------------------------
-;; Negative: a ptrauth bundle on a still-indirect call (callee is loaded from
-;; a global, not from the substituted argument) MUST be preserved. Guards
-;; against an over-eager fix that strips bundles unconditionally.
-;; ---------------------------------------------------------------------------
-
-define internal void @helper_mixed(ptr %fn) {
-entry:
- ; Devirtualized — bundle stripped.
- call void %fn() [ "ptrauth"(i32 0, i64 0) ]
- ; Still indirect — bundle preserved.
- %loaded = load ptr, ptr @global_fn_ptr
- call void %loaded() [ "ptrauth"(i32 0, i64 0) ]
- ret void
-}
-
-define void @caller_mixed_a() {
- call void @helper_mixed(ptr @callee1)
- ret void
-}
-
-define void @caller_mixed_b() {
- call void @helper_mixed(ptr @callee2)
- ret void
-}
-
-; CHECK-DAG: @helper_mixed.specialized
-; CHECK-DAG: @helper_mixed.specialized
-
-;; The mixed helper's still-indirect call (through a loaded fn-pointer)
-;; retains its ptrauth bundle in every clone. Guards against an over-eager
-;; fix that strips bundles unconditionally.
-; CHECK-COUNT-2: call void %{{.*}}() [ "ptrauth"(i32 0, i64 0) ]
diff --git a/llvm/test/Verifier/ptrauth-operand-bundles.ll b/llvm/test/Verifier/ptrauth-operand-bundles.ll
index 7aa5a22f7816f..bb92f2947df5f 100644
--- a/llvm/test/Verifier/ptrauth-operand-bundles.ll
+++ b/llvm/test/Verifier/ptrauth-operand-bundles.ll
@@ -20,9 +20,9 @@ define void @test_ptrauth_bundle(i64 %arg0, i32 %arg1, ptr %arg2) {
; CHECK-NEXT: call void %arg2() [ "ptrauth"(i32 42, i32 120) ]
call void %arg2() [ "ptrauth"(i32 42, i32 120) ]
-; CHECK: Direct call cannot have a ptrauth bundle
-; CHECK-NEXT: call void @g() [ "ptrauth"(i32 42, i64 120) ]
- call void @g() [ "ptrauth"(i32 42, i64 120) ]
+; Direct call carrying a ptrauth bundle is legal IR; the backend drops it.
+; CHECK-NOT: Direct call cannot have a ptrauth bundle
+ call void @g() [ "ptrauth"(i32 42, i64 120) ] ; OK
; CHECK-NOT: call void
call void %arg2() [ "ptrauth"(i32 42, i64 120) ] ; OK
More information about the llvm-commits
mailing list