[llvm-branch-commits] [llvm] [AArch64][PAC] Precommit tests on efficient LR checks before tail calls (PR #220192)
Anatoly Trosinenko via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Tue Sep 1 01:28:57 PDT 2026
https://github.com/atrosinenko created https://github.com/llvm/llvm-project/pull/220192
When performing a tail call with pac-ret hardening enabled, depending on
the performance vs. security trade-off, it may be required to insert an
explicit check that the LR register contains a valid address (that is,
the authentication succeeded), as unlike a regular call, a tail call does
not dereference LR right away.
When shrink-wrapping optimization is in effect, this might be expensive
both in terms of time complexity and code size. This commit adds several
tests demonstrating the existing codegen behavior.
>From 8a9eb66233414145677a74be7f17002a67e3ad04 Mon Sep 17 00:00:00 2001
From: Anatoly Trosinenko <atrosinenko at accesssoftek.com>
Date: Sat, 1 Aug 2026 15:05:52 +0300
Subject: [PATCH] [AArch64][PAC] Precommit tests on efficient LR checks before
tail calls
When performing a tail call with pac-ret hardening enabled, depending on
the performance vs. security trade-off, it may be required to insert an
explicit check that the LR register contains a valid address (that is,
the authentication succeeded), as unlike a regular call, a tail call does
not dereference LR right away.
When shrink-wrapping optimization is in effect, this might be expensive
both in terms of time complexity and code size. This commit adds several
tests demonstrating the existing codegen behavior.
---
.../ptrauth-tail-call-shrink-wrapping.ll | 476 ++++++++++++++++++
1 file changed, 476 insertions(+)
create mode 100644 llvm/test/CodeGen/AArch64/ptrauth-tail-call-shrink-wrapping.ll
diff --git a/llvm/test/CodeGen/AArch64/ptrauth-tail-call-shrink-wrapping.ll b/llvm/test/CodeGen/AArch64/ptrauth-tail-call-shrink-wrapping.ll
new file mode 100644
index 0000000000000..6f71719dc1e6f
--- /dev/null
+++ b/llvm/test/CodeGen/AArch64/ptrauth-tail-call-shrink-wrapping.ll
@@ -0,0 +1,476 @@
+; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 6
+; RUN: llc -mtriple aarch64 -o - %s -asm-verbose=0 \
+; RUN: -aarch64-authenticated-lr-check-method=high-bits-notbi \
+; RUN: | FileCheck --check-prefix=ASM %s
+
+;; If the LR register is authenticated after reloading from the stack, then
+;; depending on the security vs. performance trade-off, it may be desired to
+;; check that the authentication succeeded. Without FEAT_FPAC, this requires
+;; special instruction sequence to be emitted by the compiler between the
+;; authentication point and the tail call instruction.
+;;
+;; Originally, this instruction sequence was emitted as part of the expanded
+;; tail call pseudo instruction (one of TCRETURN variants), which may be
+;; sub-optimal in some cases due to shrink-wrapping optimization, like here:
+;;
+;; caller:
+;; cbz x0, .Lother_code
+;; eor x17, x30, x30, lsl #1 ; --- Waste of time:
+;; tbz x17, #62, .Lauth_success_0 ; | Checks the address in LR, even though
+;; brk #0xc471 ; | it was never re-loaded from the stack
+;; .Lauth_success_0: ; --- and authenticated at this point yet.
+;; b callee
+;; .Lother_code:
+;; pacibsp
+;; sub sp, sp, #112
+;; stp x29, x30, [sp, #16]
+;; ...
+;; ldp x29, x30, [sp, #16]
+;; add sp, sp, #112
+;; autibsp
+;; eor x17, x30, x30, lsl #1 ; --- The intended use case of LR check:
+;; tbz x17, #62, .Lauth_success_0 ; | LR was authenticated after reloading
+;; brk #0xc471 ; | from memory but is not used right away
+;; .Lauth_success_0: ; ---
+;; b other_callee
+
+declare i64 @callee()
+declare i64 @callee2()
+declare i64 @callee3()
+
+;; Basic linear examples, no shrink-wrapping is applicable.
+
+define i64 @test_linear_regular_return() #0 {
+; ASM-LABEL: test_linear_regular_return:
+; ASM: paciasp
+; ASM-NEXT: .cfi_negate_ra_state
+; ASM-NEXT: str x30, [sp, #-16]!
+; ASM-NEXT: .cfi_def_cfa_offset 16
+; ASM-NEXT: .cfi_offset w30, -16
+; ASM-NEXT: mov w0, #123
+; ASM-NEXT: //APP
+; ASM-NEXT: //NO_APP
+; ASM-NEXT: ldr x30, [sp], #16
+; ASM-NEXT: retaa
+ tail call void asm sideeffect "", "~{lr}"()
+ ret i64 123
+}
+
+define i64 @test_linear_tailcall_safe_lr() "sign-return-address"="all" "target-features"="+pauth" {
+; ASM-LABEL: test_linear_tailcall_safe_lr:
+; ASM: paciasp
+; ASM-NEXT: .cfi_negate_ra_state
+; ASM-NEXT: autiasp
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_0
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_0:
+; ASM-NEXT: b callee
+ %call = tail call i64 @callee()
+ ret i64 %call
+}
+
+define i64 @test_linear_tailcall_reloaded_lr() #0 {
+; ASM-LABEL: test_linear_tailcall_reloaded_lr:
+; ASM: paciasp
+; ASM-NEXT: .cfi_negate_ra_state
+; ASM-NEXT: str x30, [sp, #-16]!
+; ASM-NEXT: .cfi_def_cfa_offset 16
+; ASM-NEXT: .cfi_offset w30, -16
+; ASM-NEXT: //APP
+; ASM-NEXT: mov x30, #12345
+; ASM-NEXT: //NO_APP
+; ASM-NEXT: ldr x30, [sp], #16
+; ASM-NEXT: autiasp
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_1
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_1:
+; ASM-NEXT: b callee
+ tail call void asm sideeffect "mov x30, 12345", "~{lr}"()
+ %call = tail call i64 @callee()
+ ret i64 %call
+}
+
+;; Depending on the %arg value, it is possible that no stack frame has to be
+;; created, thus checking LR is not always required.
+define i64 @test_single_tailcall_reloaded_lr(i64 %arg) #0 {
+; ASM-LABEL: test_single_tailcall_reloaded_lr:
+; ASM: cbz x0, .LBB3_2
+; ASM-NEXT: paciasp
+; ASM-NEXT: .cfi_negate_ra_state
+; ASM-NEXT: str x30, [sp, #-16]!
+; ASM-NEXT: .cfi_def_cfa_offset 16
+; ASM-NEXT: .cfi_offset w30, -16
+; ASM-NEXT: //APP
+; ASM-NEXT: mov x30, #12345
+; ASM-NEXT: //NO_APP
+; ASM-NEXT: ldr x30, [sp], #16
+; ASM-NEXT: autiasp
+; ASM-NEXT: .LBB3_2:
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_2
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_2:
+; ASM-NEXT: b callee
+ %cond = icmp eq i64 %arg, 0
+ br i1 %cond, label %if.end, label %if.then
+
+if.then:
+ tail call void asm sideeffect "mov x30, 12345", "~{lr}"()
+ br label %if.end
+
+if.end:
+ %res = tail call i64 @callee()
+ ret i64 %res
+}
+
+;; In this example, it is obviously beneficial to perform authentication and
+;; checking of LR at the shrink-wrapped epilogue site, but "sign-return-address"="all"
+;; indicates that the user probably expects LR to be protected wherever possible.
+define i64 @test_optimizable_epilogue_but_sign_all(i64 %arg) "sign-return-address"="all" "target-features"="+pauth" {
+; ASM-LABEL: test_optimizable_epilogue_but_sign_all:
+; ASM: paciasp
+; ASM-NEXT: .cfi_negate_ra_state
+; ASM-NEXT: cbz x0, .LBB4_2
+; ASM-NEXT: str x30, [sp, #-16]!
+; ASM-NEXT: .cfi_def_cfa_offset 16
+; ASM-NEXT: .cfi_offset w30, -16
+; ASM-NEXT: //APP
+; ASM-NEXT: mov x30, #12345
+; ASM-NEXT: //NO_APP
+; ASM-NEXT: ldr x30, [sp], #16
+; ASM-NEXT: .LBB4_2:
+; ASM-NEXT: cmp x0, #999
+; ASM-NEXT: b.hi .LBB4_4
+; ASM-NEXT: autiasp
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_3
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_3:
+; ASM-NEXT: b callee
+; ASM-NEXT: .LBB4_4:
+; ASM-NEXT: autiasp
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_4
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_4:
+; ASM-NEXT: b callee2
+ %cond = icmp eq i64 %arg, 0
+ br i1 %cond, label %common, label %if.then
+
+if.then:
+ tail call void asm sideeffect "mov x30, 12345", "~{lr}"()
+ br label %common
+
+common:
+ %cond2 = icmp ult i64 %arg, 1000
+ br i1 %cond2, label %exit1, label %exit2
+
+exit1:
+ %res = tail call i64 @callee()
+ ret i64 %res
+
+exit2:
+ %res2 = tail call i64 @callee2()
+ ret i64 %res2
+}
+
+;; No need to check LR anywhere:
+;; * at the tail call site, LR is as trusted as at the function entry
+;; * at PAUTH_EPILOGUE site, no tail calls are reachable
+define i64 @test_single_safe_tailcall_single_return(i64 %arg) #0 {
+; ASM-LABEL: test_single_safe_tailcall_single_return:
+; ASM: cbz x0, .LBB5_2
+; ASM-NEXT: paciasp
+; ASM-NEXT: .cfi_negate_ra_state
+; ASM-NEXT: str x30, [sp, #-16]!
+; ASM-NEXT: .cfi_def_cfa_offset 16
+; ASM-NEXT: .cfi_offset w30, -16
+; ASM-NEXT: mov w0, #1
+; ASM-NEXT: //APP
+; ASM-NEXT: mov x30, #12345
+; ASM-NEXT: //NO_APP
+; ASM-NEXT: ldr x30, [sp], #16
+; ASM-NEXT: retaa
+; ASM-NEXT: .LBB5_2:
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_5
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_5:
+; ASM-NEXT: b callee
+ %cond = icmp eq i64 %arg, 0
+ br i1 %cond, label %do.tailcall, label %ret.to.authed.lr
+
+ret.to.authed.lr:
+ tail call void asm sideeffect "mov x30, 12345", "~{lr}"()
+ ret i64 1
+
+do.tailcall:
+ %res = tail call i64 @callee()
+ ret i64 %res
+}
+
+;; One tail call requires checking LR, the other one doesn't - both cases are
+;; obvious.
+define i64 @test_two_tailcalls(i64 %arg) #0 {
+; ASM-LABEL: test_two_tailcalls:
+; ASM: cbz x0, .LBB6_2
+; ASM-NEXT: paciasp
+; ASM-NEXT: .cfi_negate_ra_state
+; ASM-NEXT: str x30, [sp, #-16]!
+; ASM-NEXT: .cfi_def_cfa_offset 16
+; ASM-NEXT: .cfi_offset w30, -16
+; ASM-NEXT: //APP
+; ASM-NEXT: mov x30, #12345
+; ASM-NEXT: //NO_APP
+; ASM-NEXT: ldr x30, [sp], #16
+; ASM-NEXT: autiasp
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_6
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_6:
+; ASM-NEXT: b callee
+; ASM-NEXT: .LBB6_2:
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_7
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_7:
+; ASM-NEXT: b callee2
+ %cond = icmp eq i64 %arg, 0
+ br i1 %cond, label %do.tailcall.safe.lr, label %do.tailcall.authed.lr
+
+do.tailcall.authed.lr:
+ tail call void asm sideeffect "mov x30, 12345", "~{lr}"()
+ %res = tail call i64 @callee()
+ ret i64 %res
+
+do.tailcall.safe.lr:
+ %res2 = tail call i64 @callee2()
+ ret i64 %res2
+}
+
+;; One tail call requires checking LR, the other one doesn't. Replacing two
+;; separate checks before the tail calls with a single check at the insertion
+;; point of the shrink-wrapped epilogue would hurt many regular returns, though.
+;; In this particular example, an optimal solution is to omit the check before
+;; the "safe" tail call and keep the check before the "unsafe" one intact.
+define i64 @test_unlikely_tailcall_after_shrink_wrapped_epilogue(i64 %arg) #0 {
+; ASM-LABEL: test_unlikely_tailcall_after_shrink_wrapped_epilogue:
+; ASM: cbz x0, .LBB7_5
+; ASM-NEXT: paciasp
+; ASM-NEXT: .cfi_negate_ra_state
+; ASM-NEXT: str x30, [sp, #-16]!
+; ASM-NEXT: .cfi_def_cfa_offset 16
+; ASM-NEXT: .cfi_offset w30, -16
+; ASM-NEXT: mov x8, x0
+; ASM-NEXT: //APP
+; ASM-NEXT: mov x30, #12345
+; ASM-NEXT: mov x0, #1
+; ASM-NEXT: //NO_APP
+; ASM-NEXT: ldr x30, [sp], #16
+; ASM-NEXT: autiasp
+; ASM-NEXT: cbz x0, .LBB7_6
+; ASM-NEXT: cmp x8, #1000
+; ASM-NEXT: b.lo .LBB7_7
+; ASM-NEXT: sub x8, x0, x8
+; ASM-NEXT: cmp x8, #11
+; ASM-NEXT: b.lo .LBB7_8
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_8
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_8:
+; ASM-NEXT: b callee
+; ASM-NEXT: .LBB7_5:
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_9
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_9:
+; ASM-NEXT: b callee2
+; ASM-NEXT: .LBB7_6:
+; ASM-NEXT: mov x0, x8
+; ASM-NEXT: .LBB7_7:
+; ASM-NEXT: ret
+; ASM-NEXT: .LBB7_8:
+; ASM-NEXT: mov w0, #123
+; ASM-NEXT: ret
+ %cond = icmp eq i64 %arg, 0
+ br i1 %cond, label %do.tailcall.safe.lr, label %spill.lr
+
+spill.lr:
+ %res = tail call i64 asm sideeffect "mov x30, 12345\0A\09mov $0, 1", "=r,~{lr}"()
+ %cond2 = icmp eq i64 %res, 0
+ br i1 %cond2, label %exit2, label %case3
+
+exit2:
+ ret i64 %arg
+
+case3:
+ %cond3 = icmp ult i64 %arg, 1000
+ br i1 %cond3, label %exit3, label %case4
+
+exit3:
+ ret i64 %res
+
+case4:
+ %diff = sub i64 %res, %arg
+ %cond4 = icmp ugt i64 %diff, 10
+ br i1 %cond4, label %exit4, label %exit5
+
+exit4:
+ ; This is the only tail call that requires checking LR.
+ %res2 = tail call i64 @callee()
+ ret i64 %res2
+
+exit5:
+ ret i64 123
+
+do.tailcall.safe.lr:
+ %res3 = tail call i64 @callee2()
+ ret i64 %res3
+}
+
+;; After the shrink-wrapped epilogue, the function is exited only via tail
+;; calls. Thus, bringing the LR check along with the rest of epilogue code would
+;; be preferable w.r.t. code size, though keeping the check in both %exit1 and
+;; %exit2 would result in the same number of instructions executed in the
+;; LLVM IR terms. In %exit3, the check could obviously be omitted.
+define i64 @test_only_tailcalls_after_shrink_wrapped_epilogue(i64 %arg, i64 %arg2) #0 {
+; ASM-LABEL: test_only_tailcalls_after_shrink_wrapped_epilogue:
+; ASM: cbz x0, .LBB8_3
+; ASM-NEXT: paciasp
+; ASM-NEXT: .cfi_negate_ra_state
+; ASM-NEXT: str x30, [sp, #-16]!
+; ASM-NEXT: .cfi_def_cfa_offset 16
+; ASM-NEXT: .cfi_offset w30, -16
+; ASM-NEXT: //APP
+; ASM-NEXT: mov x30, #12345
+; ASM-NEXT: mov x8, #1
+; ASM-NEXT: //NO_APP
+; ASM-NEXT: ldr x30, [sp], #16
+; ASM-NEXT: autiasp
+; ASM-NEXT: cbz x8, .LBB8_5
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_10
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_10:
+; ASM-NEXT: b callee2
+; ASM-NEXT: .LBB8_3:
+; ASM-NEXT: cmp x1, #999
+; ASM-NEXT: b.hi .LBB8_6
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_11
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_11:
+; ASM-NEXT: b callee3
+; ASM-NEXT: .LBB8_5:
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_12
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_12:
+; ASM-NEXT: b callee
+; ASM-NEXT: .LBB8_6:
+; ASM-NEXT: add x0, x1, #1
+; ASM-NEXT: ret
+ %cond = icmp eq i64 %arg, 0
+ br i1 %cond, label %proceed.without.frame, label %spill.lr
+
+spill.lr:
+ %res = tail call i64 asm sideeffect "mov x30, 12345\0A\09mov $0, 1", "=r,~{lr}"()
+ %cond2 = icmp eq i64 %res, 0
+ br i1 %cond2, label %exit1, label %exit2
+
+exit1:
+ %res1 = tail call i64 @callee()
+ ret i64 %res1
+
+exit2:
+ %res2 = tail call i64 @callee2()
+ ret i64 %res2
+
+proceed.without.frame:
+ %cond3 = icmp ult i64 %arg2, 1000
+ br i1 %cond3, label %exit3, label %exit4
+
+exit3:
+ %res3 = tail call i64 @callee3()
+ ret i64 %res3
+
+exit4:
+ %res4 = add i64 %arg2, 1
+ ret i64 %res4
+}
+
+;; In this example, both tail calls and regular returns are possible after
+;; the shrink-wrapped epilogue. While it seems generally reasonable to keep
+;; LR checks right before the tail calls done in %exit2 and %exit3, it might
+;; be preferable to bring the check along the epilogue, if code size must be
+;; shrunk as much as possible. The check before the tail call performed in
+;; %do.tailcall.clean.lr can be obviously dropped.
+define i64 @test_various_exits_after_shrink_wrapped_epilogue(i64 %arg) #0 {
+; ASM-LABEL: test_various_exits_after_shrink_wrapped_epilogue:
+; ASM: cbz x0, .LBB9_4
+; ASM-NEXT: paciasp
+; ASM-NEXT: .cfi_negate_ra_state
+; ASM-NEXT: str x30, [sp, #-16]!
+; ASM-NEXT: .cfi_def_cfa_offset 16
+; ASM-NEXT: .cfi_offset w30, -16
+; ASM-NEXT: //APP
+; ASM-NEXT: mov x30, #12345
+; ASM-NEXT: mov x8, #1
+; ASM-NEXT: //NO_APP
+; ASM-NEXT: ldr x30, [sp], #16
+; ASM-NEXT: autiasp
+; ASM-NEXT: cbz x8, .LBB9_5
+; ASM-NEXT: cmp x0, #999
+; ASM-NEXT: b.hi .LBB9_6
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_13
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_13:
+; ASM-NEXT: b callee2
+; ASM-NEXT: .LBB9_4:
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_14
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_14:
+; ASM-NEXT: b callee3
+; ASM-NEXT: .LBB9_5:
+; ASM-NEXT: eor x16, x30, x30, lsl #1
+; ASM-NEXT: tbz x16, #62, .Lauth_success_15
+; ASM-NEXT: brk #0xc470
+; ASM-NEXT: .Lauth_success_15:
+; ASM-NEXT: b callee
+; ASM-NEXT: .LBB9_6:
+; ASM-NEXT: add x0, x0, #1
+; ASM-NEXT: ret
+ %cond = icmp eq i64 %arg, 0
+ br i1 %cond, label %do.tailcall.clean.lr, label %spill.lr
+
+spill.lr:
+ %res = tail call i64 asm sideeffect "mov x30, 12345\0A\09mov $0, 1", "=r,~{lr}"()
+ %cond2 = icmp eq i64 %res, 0
+ br i1 %cond2, label %exit2, label %case3
+
+exit2:
+ %res1 = tail call i64 @callee()
+ ret i64 %res1
+
+case3:
+ %cond3 = icmp ult i64 %arg, 1000
+ br i1 %cond3, label %exit3, label %exit4
+
+exit3:
+ %res2 = tail call i64 @callee2()
+ ret i64 %res2
+
+exit4:
+ %res4 = add i64 %arg, 1
+ ret i64 %res4
+
+do.tailcall.clean.lr:
+ %res3 = tail call i64 @callee3()
+ ret i64 %res3
+}
+
+attributes #0 = { "sign-return-address"="non-leaf" "target-features"="+pauth" }
More information about the llvm-branch-commits
mailing list