https://github.com/atrosinenko updated https://github.com/llvm/llvm-project/pull/220192
>From d9a517724a417b8174de8b4e7167ef4ddc6ecad5 Mon Sep 17 00:00:00 2001 From: Anatoly Trosinenko <[email protected]> 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" } _______________________________________________ llvm-branch-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits
