i#5623 AArch64: Improve PAUTH transparency (#6856)

Previously direct/indirect branches with pointer authentication would be
mangled to strip the pointer authentication code from the address using
an xpaci instruction.

This means that code that might fail when running natively (because the
pointer authentication doesn't pass) could succeed under DynamoRIO
because the pointer was not being authenticated.

This commit changes the mangling code to use auti* instructions to
authenticate the pointer instead, and adds tests to check that it
behaves correctly for all the branch and authenticate instructions.

Issue: #5623
Fixes: #5623
Co-authored-by: Phil Ramsey <phil.ramsey@arm.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8ece8b7..ba527fa 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -630,13 +630,16 @@
 
 set(proc_supports_sve OFF)
 set(proc_supports_sve2 OFF)
+set(proc_supports_pauth OFF)
 if (AARCH64 AND UNIX)
   set(CFLAGS_SVE "-march=armv8-a+sve")
   set(CFLAGS_SVE2 "-march=armv8-a+sve2")
+  set(CFLAGS_PAUTH "-march=armv8.3-a -mbranch-protection=standard")
   set(ASMFLAGS_SVE "-march=armv8-a+sve")
   set(ASMFLAGS_SVE2 "-march=armv8-a+sve2")
   check_sve_processor_and_compiler_support(proc_supports_sve proc_sve_vl)
   check_sve2_processor_and_compiler_support(proc_supports_sve2)
+  check_pauth_processor_and_compiler_support(proc_supports_pauth)
 endif ()
 
 # Ensure that _AMD64_ or _X86_ are defined on Microsoft Windows, as otherwise
diff --git a/api/samples/opcode_count.cpp b/api/samples/opcode_count.cpp
index 6629f8d..3d22aae 100644
--- a/api/samples/opcode_count.cpp
+++ b/api/samples/opcode_count.cpp
@@ -67,21 +67,32 @@
            "The opcode to consider when counting the number of times "
            "the instruction is executed. Default opcode is set to add.");
 
+#ifdef SHOW_RESULTS
+static bool show_results_default = true;
+#else
+static bool show_results_default = false;
+#endif
+
+static droption_t<bool> show_results(DROPTION_SCOPE_CLIENT, "show_results",
+                                     show_results_default, "Print results to STDOUT",
+                                     "Print results to STDOUT.");
+
 static uintptr_t global_opcode_count = 0;
 static uintptr_t global_total_count = 0;
 
 static void
 event_exit(void)
 {
-#ifdef SHOW_RESULTS
-    char msg[512];
-    int len;
-    len = dr_snprintf(msg, sizeof(msg) / sizeof(msg[0]), "%u/%u instructions executed.",
-                      global_opcode_count, global_total_count);
-    DR_ASSERT(len > 0);
-    NULL_TERMINATE(msg);
-    DISPLAY_STRING(msg);
-#endif /* SHOW_RESULTS */
+    if (show_results.get_value()) {
+        char msg[512];
+        int len;
+        len =
+            dr_snprintf(msg, sizeof(msg) / sizeof(msg[0]), "%u/%u instructions executed.",
+                        global_opcode_count, global_total_count);
+        DR_ASSERT(len > 0);
+        NULL_TERMINATE(msg);
+        DISPLAY_STRING(msg);
+    }
     drx_exit();
     drreg_exit();
     drmgr_exit();
@@ -163,10 +174,8 @@
     /* Get opcode and check if valid. */
     int valid_opcode = dynamorio::samples::opcode.get_value();
     if (valid_opcode < OP_FIRST || valid_opcode > OP_LAST) {
-#ifdef SHOW_RESULTS
         dr_fprintf(STDERR, "Error: give a valid opcode as a parameter.\n");
         dr_abort();
-#endif
     }
 
     drreg_options_t ops = { sizeof(ops), 1 /*max slots needed: aflags*/, false };
@@ -186,15 +195,16 @@
 
     /* Make it easy to tell, by looking at log file, which client executed. */
     dr_log(NULL, DR_LOG_ALL, 1, "Client 'opcode_count' initializing\n");
-#ifdef SHOW_RESULTS
-    /* also give notification to stderr */
-    if (dr_is_notify_on()) {
-#    ifdef WINDOWS
-        /* Ask for best-effort printing to cmd window. This must be called at init. */
-        dr_enable_console_printing();
-#    endif
-        dr_fprintf(STDERR, "Client opcode_count is running and considering opcode: %d.\n",
-                   valid_opcode);
-    }
+    if (dynamorio::samples::show_results.get_value()) {
+        /* also give notification to stderr */
+        if (dr_is_notify_on()) {
+#ifdef WINDOWS
+            /* Ask for best-effort printing to cmd window. This must be called at init. */
+            dr_enable_console_printing();
 #endif
+            dr_fprintf(STDERR,
+                       "Client opcode_count is running and considering opcode: %d.\n",
+                       valid_opcode);
+        }
+    }
 }
diff --git a/core/arch/aarch64/proc.c b/core/arch/aarch64/proc.c
index dea88fe..1a282d1 100644
--- a/core/arch/aarch64/proc.c
+++ b/core/arch/aarch64/proc.c
@@ -233,6 +233,8 @@
         LOG(GLOBAL, LOG_TOP, 1, "ID_AA64ISAR2_EL1 = 0x%016lx\n",
             cpu_info.features.isa_features[AA64ISAR2]);
         LOG_FEATURE(FEATURE_PAUTH2);
+        LOG_FEATURE(FEATURE_FPAC);
+        LOG_FEATURE(FEATURE_FPACCOMBINE);
         LOG_FEATURE(FEATURE_CONSTPACFIELD);
         LOG_FEATURE(FEATURE_WFxT);
 
@@ -276,16 +278,17 @@
 enable_all_test_cpu_features()
 {
     const feature_bit_t features[] = {
-        FEATURE_LSE,    FEATURE_RDM,        FEATURE_FP16,          FEATURE_DotProd,
-        FEATURE_SVE,    FEATURE_LOR,        FEATURE_FHM,           FEATURE_SM3,
-        FEATURE_SM4,    FEATURE_SHA512,     FEATURE_SHA3,          FEATURE_RAS,
-        FEATURE_SPE,    FEATURE_PAUTH,      FEATURE_LRCPC,         FEATURE_LRCPC2,
-        FEATURE_BF16,   FEATURE_I8MM,       FEATURE_F64MM,         FEATURE_FlagM,
-        FEATURE_JSCVT,  FEATURE_DPB,        FEATURE_DPB2,          FEATURE_SVE2,
-        FEATURE_SVEAES, FEATURE_SVEBitPerm, FEATURE_SVESHA3,       FEATURE_SVESM4,
-        FEATURE_MTE,    FEATURE_BTI,        FEATURE_FRINTTS,       FEATURE_PAUTH2,
-        FEATURE_MTE2,   FEATURE_FlagM2,     FEATURE_CONSTPACFIELD, FEATURE_SSBS,
-        FEATURE_SSBS2,  FEATURE_DIT,        FEATURE_LSE2,          FEATURE_WFxT
+        FEATURE_LSE,    FEATURE_RDM,         FEATURE_FP16,          FEATURE_DotProd,
+        FEATURE_SVE,    FEATURE_LOR,         FEATURE_FHM,           FEATURE_SM3,
+        FEATURE_SM4,    FEATURE_SHA512,      FEATURE_SHA3,          FEATURE_RAS,
+        FEATURE_SPE,    FEATURE_PAUTH,       FEATURE_LRCPC,         FEATURE_LRCPC2,
+        FEATURE_BF16,   FEATURE_I8MM,        FEATURE_F64MM,         FEATURE_FlagM,
+        FEATURE_JSCVT,  FEATURE_DPB,         FEATURE_DPB2,          FEATURE_SVE2,
+        FEATURE_SVEAES, FEATURE_SVEBitPerm,  FEATURE_SVESHA3,       FEATURE_SVESM4,
+        FEATURE_MTE,    FEATURE_BTI,         FEATURE_FRINTTS,       FEATURE_PAUTH2,
+        FEATURE_MTE2,   FEATURE_FlagM2,      FEATURE_CONSTPACFIELD, FEATURE_SSBS,
+        FEATURE_SSBS2,  FEATURE_DIT,         FEATURE_LSE2,          FEATURE_WFxT,
+        FEATURE_FPAC,   FEATURE_FPACCOMBINE,
     };
     for (int i = 0; i < BUFFER_SIZE_ELEMENTS(features); ++i) {
         proc_set_feature(features[i], true);
@@ -353,6 +356,16 @@
         DEF_FEAT(AA64ISAR1, 2, 3, FEAT_GR_EQ), /* API (IMP DEF algorithm) */
     (FEATURE_I8MM << 16) |
         DEF_FEAT(AA64ISAR1, 13, 1, FEAT_EQ), /* I8MM (Int8 Matrix mul.) */
+
+    (FEATURE_FPAC << 16) |
+        DEF_FEAT(AA64ISAR1, 1, 4, FEAT_GR_EQ), /* APA (QARMA5 - FPAC) */
+    (FEATURE_FPAC << 16) |
+        DEF_FEAT(AA64ISAR1, 2, 4, FEAT_GR_EQ), /* API (IMP DEF algorithm - FPAC) */
+
+    (FEATURE_FPACCOMBINE << 16) |
+        DEF_FEAT(AA64ISAR1, 1, 5, FEAT_GR_EQ), /* APA (QARMA5 - FPACCOMBINE) */
+    (FEATURE_FPACCOMBINE << 16) |
+        DEF_FEAT(AA64ISAR1, 2, 5, FEAT_GR_EQ), /* API (IMP DEF algorithm - FPACCOMBINE) */
 };
 
 static bool
diff --git a/core/arch/aarchxx/mangle.c b/core/arch/aarchxx/mangle.c
index 1c1aae4..2c1ab93 100644
--- a/core/arch/aarchxx/mangle.c
+++ b/core/arch/aarchxx/mangle.c
@@ -1,6 +1,6 @@
 /* **********************************************************
  * Copyright (c) 2014-2022 Google, Inc.  All rights reserved.
- * Copyright (c) 2016 ARM Limited. All rights reserved.
+ * Copyright (c) 2016-2024 ARM Limited. All rights reserved.
  * **********************************************************/
 
 /*
@@ -44,6 +44,11 @@
 #define POST instrlist_meta_postinsert
 #define PRE instrlist_meta_preinsert
 
+static reg_id_t
+pick_scratch_reg(dcontext_t *dcontext, instr_t *instr, reg_id_t do_not_pick_a,
+                 reg_id_t do_not_pick_b, reg_id_t do_not_pick_c, bool dead_reg_ok,
+                 ushort *scratch_slot DR_PARAM_OUT, bool *should_restore DR_PARAM_OUT);
+
 /* For ARM and AArch64, we always use TLS and never use hardcoded
  * dcontext (xref USE_SHARED_GENCODE_ALWAYS() and -private_ib_in_tls).
  * Thus we use instr_create_{save_to,restore_from}_tls() directly.
@@ -1346,6 +1351,207 @@
 #endif
 }
 
+#if defined(AARCH64)
+/* Insert instructions to perform pointer authentication on the target of a combined
+ * branch and authenticate instruction.
+ * `instr` must be a direct/indirect branch instruction with pointer authentication.
+ */
+static void
+insert_authenticate_pointer(dcontext_t *dcontext, instrlist_t *ilist, instr_t *instr)
+{
+    /* There are three scenarios here depending on which architecture features are
+     * implemented.
+     *
+     * FEAT_PAUTH:
+     *     None of the pauth instructions fault if authenication fails.
+     *     Instead AUTI* inserts an error code in to the pointer to leave it with a
+     *     non-canonical value so a subsequent branch to the address will fault.
+     *     On Linux SIGSEGV is raised with the non-canonical target address as the PC.
+     *
+     * FEAT_FPAC: (implies FEAT_PAUTH is also implemented)
+     *     AUTI* instructions generate a fault when authentication fails.
+     *     On Linux SIGILL will be raised with the faulting AUTI* instruction as the PC.
+     *     Combined authenticate and branch instructions (RETA*, BLRA*, BRA*) behaviour
+     *     is the same as FEAT_PAUTH.
+     *
+     * FEAT_FPACCOMBINE: (implies FEAT_PAUTH and FEAT_FPAC are also implemented)
+     *     AUTI* instructions behave the same as with FEAT_FPAC.
+     *     Combined authenicate and branch instructions behave the same way as AUTI*
+     *     when authentication fails.
+     *     On Linux SIGILL will be raised with the faulting instruction as the PC.
+     *
+     * For the FEAT_PAUTH and FEAT_PAUTHCOMBINE cases AUTI* and the combined auth+branch
+     * instructions we are mangling behave the same way so we can just insert an AUTI*
+     * to authenticate the pointer in IBL_TARGET_REG.
+     * For the FEAT_FPAC case AUTI* and the combined auth+branch instructions behave
+     * differently so we need to perform the authentication ourselves (see below).
+     */
+    ASSERT(proc_has_feature(FEATURE_PAUTH));
+    if (proc_has_feature(FEATURE_FPACCOMBINE) || !proc_has_feature(FEATURE_FPAC)) {
+        switch (instr_get_opcode(instr)) {
+        case OP_retaa:
+            PRE(ilist, instr,
+                INSTR_XL8(INSTR_CREATE_autia(dcontext, opnd_create_reg(IBL_TARGET_REG),
+                                             opnd_create_reg(DR_REG_SP)),
+                          instr_get_app_pc(instr)));
+            break;
+        case OP_retab:
+            PRE(ilist, instr,
+                INSTR_XL8(INSTR_CREATE_autib(dcontext, opnd_create_reg(IBL_TARGET_REG),
+                                             opnd_create_reg(DR_REG_SP)),
+                          instr_get_app_pc(instr)));
+            break;
+        case OP_braa:
+        case OP_blraa:
+            PRE(ilist, instr,
+                INSTR_XL8(INSTR_CREATE_autia(dcontext, opnd_create_reg(IBL_TARGET_REG),
+                                             instr_get_src(instr, 1)),
+                          instr_get_app_pc(instr)));
+            break;
+        case OP_brab:
+        case OP_blrab:
+            PRE(ilist, instr,
+                INSTR_XL8(INSTR_CREATE_autib(dcontext, opnd_create_reg(IBL_TARGET_REG),
+                                             instr_get_src(instr, 1)),
+                          instr_get_app_pc(instr)));
+            break;
+        case OP_braaz:
+        case OP_blraaz:
+            PRE(ilist, instr,
+                INSTR_XL8(INSTR_CREATE_autiza(dcontext, opnd_create_reg(IBL_TARGET_REG)),
+                          instr_get_app_pc(instr)));
+            break;
+        case OP_brabz:
+        case OP_blrabz:
+            PRE(ilist, instr,
+                INSTR_XL8(INSTR_CREATE_autizb(dcontext, opnd_create_reg(IBL_TARGET_REG)),
+                          instr_get_app_pc(instr)));
+            break;
+        default: ASSERT_NOT_REACHED();
+        }
+    } else {
+        /* If FEAT_FPAC is implemented, auti* instructions will generate a fault when
+         * authentication fails, but combined auth+branch instructions won't.
+         * We need to perform the authentication ourselves by stripping and re-signing
+         * the target pointer and comparing that to the original value.
+         * If they match the authentication passes. If they don't match the
+         * authentication has failed and we need to insert a 2-bit error code into the
+         * address in IBL_TARGET_REG to leave it with a non-canonical value that should
+         * fault when the app tries to branch to it.
+         *
+         *     mov      scratch, IBL_TARGET_REG
+         *     xpaci    scratch                          ; Remove PAC
+         *     paci*    scratch                          ; Re-sign pointer
+         *     sub      scratch, scratch, IBL_TARGET_REG ; Check addresses match
+         *                                               ; (avoiding CMP so we don't
+         *                                               ; clobber the app's flags)
+         *     xpaci    IBL_TARGET_REG                   ; Remove PAC from target address
+         *     cbz      end                              ; Skip inserting error code if
+         *                                               ; addresses match
+         * insert_error_code:
+         *     and      IBL_TARGET_REG, IBL_TARGET_REG, #error_code_mask
+         *     orr      IBL_TARGET_REG, IBL_TARGET_REG, #error_code
+         * end:
+         *     ...
+         */
+
+        reg_id_t modifier_reg = DR_REG_NULL;
+        switch (instr_get_opcode(instr)) {
+        case OP_braa:
+        case OP_blraa:
+        case OP_brab:
+        case OP_blrab: modifier_reg = opnd_get_reg(instr_get_src(instr, 1));
+        }
+
+        ushort slot;
+        bool should_restore;
+        reg_id_t scratch = pick_scratch_reg(dcontext, instr, IBL_TARGET_REG, modifier_reg,
+                                            DR_REG_NULL, true, &slot, &should_restore);
+        if (should_restore)
+            insert_save_to_tls_if_necessary(dcontext, ilist, instr, scratch, slot);
+
+        PRE(ilist, instr,
+            XINST_CREATE_move(dcontext, opnd_create_reg(scratch),
+                              opnd_create_reg(IBL_TARGET_REG)));
+        PRE(ilist, instr, INSTR_CREATE_xpaci(dcontext, opnd_create_reg(scratch)));
+
+        bool key_a;
+        switch (instr_get_opcode(instr)) {
+        case OP_retaa:
+            key_a = true;
+            PRE(ilist, instr,
+                INSTR_CREATE_pacia(dcontext, opnd_create_reg(scratch),
+                                   opnd_create_reg(DR_REG_SP)));
+            break;
+        case OP_retab:
+            key_a = false;
+            PRE(ilist, instr,
+                INSTR_CREATE_pacib(dcontext, opnd_create_reg(scratch),
+                                   opnd_create_reg(DR_REG_SP)));
+            break;
+        case OP_braa:
+        case OP_blraa:
+            key_a = true;
+            PRE(ilist, instr,
+                INSTR_CREATE_pacia(dcontext, opnd_create_reg(scratch),
+                                   instr_get_src(instr, 1)));
+            break;
+        case OP_brab:
+        case OP_blrab:
+            key_a = false;
+            PRE(ilist, instr,
+                INSTR_CREATE_pacib(dcontext, opnd_create_reg(scratch),
+                                   instr_get_src(instr, 1)));
+            break;
+        case OP_braaz:
+        case OP_blraaz:
+            key_a = true;
+            PRE(ilist, instr, INSTR_CREATE_paciza(dcontext, opnd_create_reg(scratch)));
+            break;
+        case OP_brabz:
+        case OP_blrabz:
+            key_a = false;
+            PRE(ilist, instr, INSTR_CREATE_pacizb(dcontext, opnd_create_reg(scratch)));
+            break;
+        default:
+            ASSERT_NOT_REACHED();
+            key_a = true; /* Quell compiler maybe-uninitialized warning. */
+        }
+
+        PRE(ilist, instr,
+            INSTR_CREATE_sub(dcontext, opnd_create_reg(scratch), opnd_create_reg(scratch),
+                             opnd_create_reg(IBL_TARGET_REG)));
+
+        PRE(ilist, instr, INSTR_CREATE_xpaci(dcontext, opnd_create_reg(IBL_TARGET_REG)));
+
+        instr_t *end_label = INSTR_CREATE_label(dcontext);
+        PRE(ilist, instr,
+            INSTR_CREATE_cbz(dcontext, opnd_create_instr(end_label),
+                             opnd_create_reg(scratch)));
+
+        /* Insert the error code to make the address fault.
+         * To match the hardware behaviour we use a 2-bit code:
+         *  0b01 if the instruction used key A,
+         *  0b10 if the instruction used key B.
+         */
+        const uint64 error_code = key_a ? 1 : 2;
+        PRE(ilist, instr,
+            INSTR_CREATE_and(dcontext, opnd_create_reg(IBL_TARGET_REG),
+                             opnd_create_reg(IBL_TARGET_REG),
+                             OPND_CREATE_INT(~(3ULL << 53))));
+        PRE(ilist, instr,
+            INSTR_CREATE_orr(dcontext, opnd_create_reg(IBL_TARGET_REG),
+                             opnd_create_reg(IBL_TARGET_REG),
+                             OPND_CREATE_INT(error_code << 53)));
+
+        PRE(ilist, instr, end_label);
+
+        if (should_restore)
+            PRE(ilist, instr, instr_create_restore_from_tls(dcontext, scratch, slot));
+    }
+}
+#endif
+
 instr_t *
 mangle_indirect_call(dcontext_t *dcontext, instrlist_t *ilist, instr_t *instr,
                      instr_t *next_instr, bool mangle_calls, uint flags)
@@ -1365,12 +1571,14 @@
             XINST_CREATE_move(dcontext, opnd_create_reg(IBL_TARGET_REG),
                               instr_get_target(instr)));
     }
+
+    /* If the instruction is a branch with pointer authentication we need to authenticate
+     * the target pointer and restore its canonical value. */
     switch (opc) {
     case OP_blraa:
     case OP_blrab:
     case OP_blraaz:
-    case OP_blrabz:
-        PRE(ilist, instr, INSTR_CREATE_xpaci(dcontext, opnd_create_reg(IBL_TARGET_REG)));
+    case OP_blrabz: insert_authenticate_pointer(dcontext, ilist, instr);
     }
     insert_mov_immed_ptrsz(dcontext, get_call_return_address(dcontext, ilist, instr),
                            opnd_create_reg(DR_REG_X30), ilist, next_instr, NULL, NULL);
@@ -1457,8 +1665,7 @@
     case OP_braa:
     case OP_brab:
     case OP_braaz:
-    case OP_brabz:
-        PRE(ilist, instr, INSTR_CREATE_xpaci(dcontext, opnd_create_reg(IBL_TARGET_REG)));
+    case OP_brabz: insert_authenticate_pointer(dcontext, ilist, instr);
     }
 
     instrlist_remove(ilist, instr); /* remove OP_br or OP_ret */
@@ -3072,6 +3279,36 @@
     return false;
 }
 
+#if defined(AARCH64)
+bool
+instr_is_pauth_branch_mangling(dcontext_t *dcontext, instr_t *inst)
+{
+    /*
+     * Look for a mov to IBL_TARGET_REG followed by an auti* instruction.
+     */
+    if (!instr_is_our_mangling(inst))
+        return false;
+
+    /* mov is an alias of orr so we actually look for OP_orr. */
+    if (!(instr_get_opcode(inst) == OP_orr &&
+          opnd_get_reg(instr_get_dst(inst, 0)) == IBL_TARGET_REG))
+        return false;
+
+    inst = instr_get_next(inst);
+    if (!instr_is_our_mangling(inst))
+        return false;
+
+    const int op = instr_get_opcode(inst);
+    switch (op) {
+    case OP_autia:
+    case OP_autib:
+    case OP_autiza:
+    case OP_autizb: return true;
+    default: return false;
+    }
+}
+#endif
+
 static bool
 is_cbnz_available(dcontext_t *dcontext, reg_id_t reg_strex_dst)
 {
diff --git a/core/arch/arch.h b/core/arch/arch.h
index d2c2eea..bdafa02 100644
--- a/core/arch/arch.h
+++ b/core/arch/arch.h
@@ -1583,6 +1583,10 @@
 bool
 instr_is_ldstex_mangling(dcontext_t *dcontext, instr_t *inst);
 #endif
+#if defined(AARCHXX)
+bool
+instr_is_pauth_branch_mangling(dcontext_t *dcontext, instr_t *inst);
+#endif
 
 /****************************************************************************
  * Platform-independent emit_utils_shared.c
diff --git a/core/arch/asm_defines.asm b/core/arch/asm_defines.asm
index 042d633..a005c5f 100644
--- a/core/arch/asm_defines.asm
+++ b/core/arch/asm_defines.asm
@@ -1083,4 +1083,12 @@
 #define HIDDEN(x) .hidden x
 #endif
 
+#define PASTE(a, b) a##b
+
+/* Use inside a function to declare or reference a function-local label.
+ * Expects FUNCNAME to be defined.
+ */
+#define _LOCAL_LABEL(label, unique_id) PASTE(unique_id, label)
+#define LOCAL_LABEL(label) _LOCAL_LABEL(label, FUNCNAME)
+
 #endif /* _ASM_DEFINES_ASM_ */
diff --git a/core/arch/proc_api.h b/core/arch/proc_api.h
index 0b7373e..441c777 100644
--- a/core/arch/proc_api.h
+++ b/core/arch/proc_api.h
@@ -406,6 +406,11 @@
     FEATURE_WFxT =
         DEF_FEAT(AA64ISAR2, 0, 2,
                  FEAT_EQ), /**< Wait for event / interrupt with timeout (AArch64) */
+    FEATURE_FPAC = DEF_FEAT(AA64ISAR2, 3, 4,
+                            FEAT_GR_EQ), /**< Faulting on AUTI* instructions (AArch64) */
+    FEATURE_FPACCOMBINE =
+        DEF_FEAT(AA64ISAR2, 3, 5, FEAT_GR_EQ), /**< Faulting on combined branch pointer
+                                                  authentication instructions (AArch64) */
 } feature_bit_t;
 
 #endif
diff --git a/core/translate.c b/core/translate.c
index 0fa7de7..67af93b 100644
--- a/core/translate.c
+++ b/core/translate.c
@@ -559,6 +559,11 @@
             /* nothing to do */
         }
 #endif
+#if defined(AARCH64)
+        else if (instr_is_pauth_branch_mangling(tdcontext, inst)) {
+            /* nothing to do. */
+        }
+#endif
         /* Single step mangling adds a nop. */
         else if (instr_is_nop(inst)) {
             /* nothing to do */
diff --git a/make/utils.cmake b/make/utils.cmake
index 720c152..f561468 100644
--- a/make/utils.cmake
+++ b/make/utils.cmake
@@ -355,31 +355,51 @@
   endif ()
 endfunction (check_sve_processor_and_compiler_support)
 
-function (check_sve2_processor_and_compiler_support out)
+function (check_feature_processor_and_compiler_support
+          feat_name c_flags test_prog out)
   include(CheckCSourceRuns)
-  set(sve2_prog "int main() {
-                    asm(\"histcnt z0.d, p0/z, z0.d, z0.d\");
-                    return 0;
-                 }")
-  set(CMAKE_REQUIRED_FLAGS ${CFLAGS_SVE2})
+  set(CMAKE_REQUIRED_FLAGS ${c_flags})
   if (CMAKE_CROSSCOMPILING)
     # If we are cross-compiling check_c_source_runs() can't run the executable on the
-    # host to find out whether the target processor supports SVE2, so we assume it
+    # host to find out whether the target processor supports the feature, so we assume it
     # doesn't.
-    set(proc_found_sve2_EXITCODE 1 CACHE STRING
-        "Set to 0 if target processor/emulator supports SVE2 to enable SVE2 tests"
+    set(proc_found_${feat_name}_EXITCODE 1 CACHE STRING
+        "Set to 0 if target processor/emulator supports ${feat_name} to enable ${feat_name} tests"
         FORCE)
   else ()
-    check_c_source_runs("${sve2_prog}" proc_found_sve2)
+    check_c_source_runs("${test_prog}" proc_found_${feat_name})
   endif ()
-  if (proc_found_sve2)
-    message(STATUS "Compiler and processor support SVE2.")
+  if (proc_found_${feat_name})
+    message(STATUS "Compiler and processor support ${feat_name}.")
   else ()
-    message(STATUS "WARNING: Compiler or processor do not support SVE2. "
+    message(STATUS "WARNING: Compiler or processor do not support ${feat_name}. "
                    "Skipping tests")
   endif ()
-  set(${out} ${proc_found_sve2} PARENT_SCOPE)
-endfunction (check_sve2_processor_and_compiler_support)
+  set(${out} ${proc_found_${feat_name}} PARENT_SCOPE)
+endfunction (check_feature_processor_and_compiler_support)
+
+macro (check_sve2_processor_and_compiler_support out)
+  check_feature_processor_and_compiler_support(sve2
+    ${CFLAGS_SVE2}
+    "int main() {
+        asm(\"histcnt z0.d, p0/z, z0.d, z0.d\");
+        return 0;
+    }"
+    ${out}
+  )
+endmacro (check_sve2_processor_and_compiler_support)
+
+macro (check_pauth_processor_and_compiler_support out)
+  check_feature_processor_and_compiler_support(pauth
+    ${CFLAGS_PAUTH}
+    "int main() {
+        void *addr = 0;
+        asm(\"paciza %[ptr]\" : [ptr] \"+r\" (addr) : :);
+        return 0;
+    }"
+    ${out}
+  )
+endmacro (check_pauth_processor_and_compiler_support)
 
 function (get_processor_vendor out)
   set(cpu_vendor "<unknown>")
diff --git a/suite/tests/CMakeLists.txt b/suite/tests/CMakeLists.txt
index 37e32fe..ad385e4 100644
--- a/suite/tests/CMakeLists.txt
+++ b/suite/tests/CMakeLists.txt
@@ -1125,6 +1125,8 @@
   elseif (DEFINED ${key}_runsve)
     set(rundefs "${rundefs} -D__ARM_FEATURE_SVE")
     set(rundefs "${rundefs} -D__ARM_FEATURE_SVE_BITS=${proc_sve_vl}")
+  elseif (DEFINED ${key}_runpauth)
+      set(rundefs "${rundefs} -D__ARM_FEATURE_PAUTH")
   endif ()
   if (CPU_AMD)
     set(rundefs "${rundefs} -DCPU_AMD")
@@ -1830,6 +1832,15 @@
   endif ()
 endmacro(set_sve_flags)
 
+macro(set_pauth_flags target)
+  if (proc_supports_pauth)
+    if (TARGET ${target}) # Support calling on non-exe target.
+        append_property_string(TARGET ${target} COMPILE_FLAGS "${CFLAGS_PAUTH}")
+    endif ()
+    set(${target}_runpauth 1)
+  endif ()
+endmacro(set_pauth_flags)
+
 ###########################################################################
 
 # We'll want the latest CTest (2.6.4) so we can use the -W parameter
@@ -3579,6 +3590,17 @@
           torunonly_ci(sample.${sample} sample.opcode_count-test ${sample}_test
             samples/opcode_count-test.asm "-opcode 5" "" "")
         endif (X86 AND (NOT X64) AND UNIX)
+        if (AARCH64 AND proc_supports_pauth)
+          # Build a simple app with pauth support and make sure it runs okay under DR and
+          # we see RETAA instructions being traced.
+          add_exe(common.pauth ${ci_shared_app_src})
+          set_pauth_flags(common.pauth)
+
+          # Opcode 679 is OP_retaa.
+          # Set the source to non-existent "common/pauth.c" so that we use our expect file.
+          torunonly_ci(sample.${sample}_pauth common.pauth ${sample} common/pauth.c
+              "-opcode 679 -show_results" "" "")
+        endif ()
       elseif (sample STREQUAL "memtrace_simple")
         if (X86 AND X64 AND UNIX)
           torunonly_ci(sample.${sample}_repstr allasm_repstr ${sample}_test
@@ -5364,6 +5386,14 @@
         link_with_pthread(linux.mangle_asynch)
       endif ()
     endif ()
+
+    if (AARCH64 AND proc_supports_pauth)
+      tobuild(linux.mangle_pauth linux/mangle_pauth.c)
+      # Also run the test natively to prove that the behaviour the test checks for
+      # matches the behaviour running natively.
+      torunonly_native(
+        linux.mangle_pauth_native linux.mangle_pauth mangle_pauth linux/mangle_pauth.c "")
+    endif ()
   endif ()
 
   if (NOT RISCV64)
diff --git a/suite/tests/common/pauth.templatex b/suite/tests/common/pauth.templatex
new file mode 100644
index 0000000..88ca594
--- /dev/null
+++ b/suite/tests/common/pauth.templatex
@@ -0,0 +1,3 @@
+Client opcode_count is running and considering opcode: 679\.
+Hello, world!
+[1-9].*
diff --git a/suite/tests/linux/mangle_pauth.c b/suite/tests/linux/mangle_pauth.c
new file mode 100644
index 0000000..ad530a5
--- /dev/null
+++ b/suite/tests/linux/mangle_pauth.c
@@ -0,0 +1,382 @@
+/* **********************************************************
+ * Copyright (c) 2024 Arm Limited.  All rights reserved.
+ * **********************************************************/
+
+/*
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice,
+ *   this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ *   this list of conditions and the following disclaimer in the documentation
+ *   and/or other materials provided with the distribution.
+ *
+ * * Neither the name of Arm Limited nor the names of its contributors may be
+ *   used to endorse or promote products derived from this software without
+ *   specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE, INC. OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ */
+
+#ifndef ASM_CODE_ONLY /* C code */
+#    include "configure.h"
+#    ifndef UNIX
+#        error UNIX-only
+#    endif
+#    ifndef AARCH64
+#        error AARCH64-only
+#    endif
+#    include "tools.h"
+#    include <setjmp.h>
+#    include <signal.h>
+#    include <stdint.h>
+
+#    define ENABLE_LOGGING false
+#    define LOG(...)                \
+        do {                        \
+            if (ENABLE_LOGGING) {   \
+                print(__VA_ARGS__); \
+            }                       \
+        } while (0)
+
+void
+test_retaa(bool trigger_fault);
+
+void
+test_retab(bool trigger_fault);
+
+void
+test_braaz(bool trigger_fault);
+
+void
+test_brabz(bool trigger_fault);
+
+void
+test_braa(bool trigger_fault);
+
+void
+test_brab(bool trigger_fault);
+
+void
+test_blraaz(bool trigger_fault);
+
+void
+test_blrabz(bool trigger_fault);
+
+void
+test_blraa(bool trigger_fault);
+
+void
+test_blrab(bool trigger_fault);
+
+uintptr_t
+strip_pac(uintptr_t ptr);
+
+/* Dummy function just used as a branch target for the blr* tests */
+bool
+dummy_func()
+{
+    return true;
+}
+
+static SIGJMP_BUF mark;
+uintptr_t branch_instr_addr;
+uintptr_t branch_target_addr;
+
+enum {
+    TEST_PASS = 1,
+    TEST_PC_MISMATCH = 2,
+};
+
+static void
+handle_signal(int signal, siginfo_t *siginfo, ucontext_t *ucxt)
+{
+    if (signal == SIGSEGV) {
+        LOG("Handled SIGSEGV:\n");
+        /* CPU does not have FEAT_FPACCOMBINE so it branched to a non-canonical address.
+         * We need to strip the PAC from from the fault address to canonicalize it and
+         * compare it to the expected branch target address.
+         */
+        const uintptr_t fault_pc = strip_pac(ucxt->uc_mcontext.pc);
+        LOG("    ucxt->uc_mcontext.pc = " PFX "\n", ucxt->uc_mcontext.pc);
+        LOG("    fault_pc =             " PFX "\n", fault_pc);
+        LOG("    branch_target_addr =   " PFX "\n", branch_target_addr);
+        if (fault_pc == branch_target_addr)
+            SIGLONGJMP(mark, TEST_PASS);
+        else
+            SIGLONGJMP(mark, TEST_PC_MISMATCH);
+
+    } else if (signal == SIGILL) {
+        LOG("Handled SIGILL:\n");
+        /* CPU has FEAT_FPACCOMBINE so the branch instruction generated an authentication
+         * failure exception and the fault PC should match the branch instruction address.
+         */
+        const uintptr_t fault_pc = ucxt->uc_mcontext.pc;
+        LOG("    fault_pc =          " PFX "\n", fault_pc);
+        LOG("    branch_instr_addr = " PFX "\n", branch_instr_addr);
+        if (fault_pc == branch_instr_addr)
+            SIGLONGJMP(mark, TEST_PASS);
+        else
+            SIGLONGJMP(mark, TEST_PC_MISMATCH);
+    } else {
+        print("Unexpected signal!\n");
+    }
+}
+
+int
+main(int argc, const char *argv[])
+{
+#    define FOR_EACH_TEST(M) \
+        do {                 \
+            M(retaa);        \
+            M(retab);        \
+            M(braaz);        \
+            M(brabz);        \
+            M(braa);         \
+            M(brab);         \
+            M(blraaz);       \
+            M(blrabz);       \
+            M(blraa);        \
+            M(blrab);        \
+        } while (0)
+
+#    define NON_FAULT_TEST(instr_name)                \
+        do {                                          \
+            LOG("Non-fault test: " #instr_name "\n"); \
+            test_##instr_name(false);                 \
+        } while (0)
+
+    FOR_EACH_TEST(NON_FAULT_TEST);
+
+    intercept_signal(SIGSEGV, (handler_3_t)&handle_signal, false);
+    intercept_signal(SIGILL, (handler_3_t)&handle_signal, false);
+
+#    define FAULT_TEST(instr_name)                                      \
+        do {                                                            \
+            LOG("Fault test: " #instr_name "\n");                       \
+            switch (SIGSETJMP(mark)) {                                  \
+            default:                                                    \
+                test_##instr_name(true);                                \
+                print(#instr_name " fault test failed: No fault\n");    \
+                break;                                                  \
+            case TEST_PC_MISMATCH:                                      \
+                print(#instr_name " fault test failed: PC mismatch\n"); \
+            case TEST_PASS: break;                                      \
+            }                                                           \
+        } while (0)
+    FOR_EACH_TEST(FAULT_TEST);
+
+    print("Test complete\n");
+
+    return 0;
+}
+
+#else /* asm code *************************************************************/
+/* clang-format off */
+#    include "asm_defines.asm"
+START_FILE
+
+.arch armv8.3-a
+
+DECL_EXTERN(branch_instr_addr)
+DECL_EXTERN(branch_target_addr)
+DECL_EXTERN(dummy_func)
+
+/* Write a value to the global branch_target_addr variable. */
+#define SAVE_BRANCH_TARGET_ADDR(addr_reg, tmp_reg)                    \
+        AARCH64_ADRP_GOT(GLOBAL_REF(branch_target_addr), tmp_reg) @N@ \
+        str     addr_reg, [tmp_reg]
+
+/* Write the address of a label to the global branch_instr_addr variable. */
+#define SAVE_BRANCH_INSTR_ADDR(label, tmp_reg1, tmp_reg2)             \
+        AARCH64_ADRP_GOT(GLOBAL_REF(branch_instr_addr), tmp_reg1) @N@ \
+        adr     tmp_reg2, label                                   @N@ \
+        str     tmp_reg2, [tmp_reg1]
+
+/* Give some names to some of the registers we need in the tests. */
+#define TRIGGER_FAULT_ARG_REG x0
+#define BRANCH_TARGET_REG x1
+#define TMP1_REG x2
+#define TMP2_REG x3
+#define MODIFIER_REG x4
+
+/*
+ * Corrupt the signed address in addr_reg if trigger_fault_reg contains 1.
+ * We corrupt the address by flipping one of the PAC bits so the PAC will no longer be
+ * valid. The bit that we flip can be any bit that is part of the PAC code so we
+ * arbitrarily choose bit 53.
+ *
+ * addr_reg = addr_reg ^ (trigger_fault << 53);
+ */
+#define CORRUPT_ADDR_IF_NEEDED(addr_reg, trigger_fault_reg) \
+        eor     addr_reg, addr_reg, trigger_fault_reg, lsl 53
+
+#define FUNCNAME test_retaa
+        DECLARE_FUNC_SEH(FUNCNAME)
+GLOBAL_LABEL(FUNCNAME:)
+        SAVE_BRANCH_TARGET_ADDR(x30, TMP1_REG)
+        SAVE_BRANCH_INSTR_ADDR(LOCAL_LABEL(branch_instr), TMP1_REG, TMP2_REG)
+        paciasp
+        CORRUPT_ADDR_IF_NEEDED(x30, TRIGGER_FAULT_ARG_REG)
+LOCAL_LABEL(branch_instr):
+        retaa
+        END_FUNC(FUNCNAME)
+#undef FUNCNAME
+
+#define FUNCNAME test_retab
+        DECLARE_FUNC_SEH(FUNCNAME)
+GLOBAL_LABEL(FUNCNAME:)
+        SAVE_BRANCH_TARGET_ADDR(x30, TMP1_REG)
+        SAVE_BRANCH_INSTR_ADDR(LOCAL_LABEL(branch_instr), TMP1_REG, TMP2_REG)
+        pacibsp
+        CORRUPT_ADDR_IF_NEEDED(x30, TRIGGER_FAULT_ARG_REG)
+LOCAL_LABEL(branch_instr):
+        retab
+        END_FUNC(FUNCNAME)
+#undef FUNCNAME
+
+#define FUNCNAME test_braaz
+        DECLARE_FUNC_SEH(FUNCNAME)
+GLOBAL_LABEL(FUNCNAME:)
+        adr     BRANCH_TARGET_REG, LOCAL_LABEL(branch_target)
+        SAVE_BRANCH_TARGET_ADDR(BRANCH_TARGET_REG, TMP1_REG)
+        SAVE_BRANCH_INSTR_ADDR(LOCAL_LABEL(branch_instr), TMP1_REG, TMP2_REG)
+        paciza  BRANCH_TARGET_REG
+        CORRUPT_ADDR_IF_NEEDED(BRANCH_TARGET_REG, TRIGGER_FAULT_ARG_REG)
+LOCAL_LABEL(branch_instr):
+        braaz   BRANCH_TARGET_REG
+LOCAL_LABEL(branch_target):
+        ret
+#undef FUNCNAME
+
+#define FUNCNAME test_brabz
+        DECLARE_FUNC_SEH(FUNCNAME)
+GLOBAL_LABEL(FUNCNAME:)
+        adr     BRANCH_TARGET_REG, LOCAL_LABEL(branch_target)
+        SAVE_BRANCH_TARGET_ADDR(BRANCH_TARGET_REG, TMP1_REG)
+        SAVE_BRANCH_INSTR_ADDR(LOCAL_LABEL(branch_instr), TMP1_REG, TMP2_REG)
+        pacizb  BRANCH_TARGET_REG
+        CORRUPT_ADDR_IF_NEEDED(BRANCH_TARGET_REG, TRIGGER_FAULT_ARG_REG)
+LOCAL_LABEL(branch_instr):
+        brabz   BRANCH_TARGET_REG
+LOCAL_LABEL(branch_target):
+        ret
+#undef FUNCNAME
+
+#define FUNCNAME test_braa
+        DECLARE_FUNC_SEH(FUNCNAME)
+GLOBAL_LABEL(FUNCNAME:)
+        adr     BRANCH_TARGET_REG, LOCAL_LABEL(branch_target)
+        mov     MODIFIER_REG, #42
+        SAVE_BRANCH_TARGET_ADDR(BRANCH_TARGET_REG, TMP1_REG)
+        SAVE_BRANCH_INSTR_ADDR(LOCAL_LABEL(branch_instr), TMP1_REG, TMP2_REG)
+        pacia   BRANCH_TARGET_REG, MODIFIER_REG
+        CORRUPT_ADDR_IF_NEEDED(BRANCH_TARGET_REG, TRIGGER_FAULT_ARG_REG)
+LOCAL_LABEL(branch_instr):
+        braa    BRANCH_TARGET_REG, MODIFIER_REG
+LOCAL_LABEL(branch_target):
+        ret
+#undef FUNCNAME
+
+#define FUNCNAME test_brab
+        DECLARE_FUNC_SEH(FUNCNAME)
+GLOBAL_LABEL(FUNCNAME:)
+#define TRIGGER_FAULT_ARG_REG x0
+        adr     BRANCH_TARGET_REG, LOCAL_LABEL(branch_target)
+        mov     MODIFIER_REG, #42
+        SAVE_BRANCH_TARGET_ADDR(BRANCH_TARGET_REG, TMP1_REG)
+        SAVE_BRANCH_INSTR_ADDR(LOCAL_LABEL(branch_instr), TMP1_REG, TMP2_REG)
+        pacib   BRANCH_TARGET_REG, MODIFIER_REG
+        CORRUPT_ADDR_IF_NEEDED(BRANCH_TARGET_REG, TRIGGER_FAULT_ARG_REG)
+LOCAL_LABEL(branch_instr):
+        brab    BRANCH_TARGET_REG, MODIFIER_REG
+LOCAL_LABEL(branch_target):
+        ret
+#undef FUNCNAME
+
+#define FUNCNAME test_blraaz
+        DECLARE_FUNC_SEH(FUNCNAME)
+GLOBAL_LABEL(FUNCNAME:)
+        AARCH64_ADRP_GOT(GLOBAL_REF(dummy_func), BRANCH_TARGET_REG)
+        SAVE_BRANCH_TARGET_ADDR(BRANCH_TARGET_REG, TMP1_REG)
+        SAVE_BRANCH_INSTR_ADDR(LOCAL_LABEL(branch_instr), TMP1_REG, TMP2_REG)
+        paciza  BRANCH_TARGET_REG
+        CORRUPT_ADDR_IF_NEEDED(BRANCH_TARGET_REG, TRIGGER_FAULT_ARG_REG)
+        str     x30, [sp, #-16]!
+LOCAL_LABEL(branch_instr):
+        blraaz  BRANCH_TARGET_REG
+        ldr     x30, [sp], #16
+        ret
+#undef FUNCNAME
+
+#define FUNCNAME test_blrabz
+        DECLARE_FUNC_SEH(FUNCNAME)
+GLOBAL_LABEL(FUNCNAME:)
+        AARCH64_ADRP_GOT(GLOBAL_REF(dummy_func), BRANCH_TARGET_REG)
+        SAVE_BRANCH_TARGET_ADDR(BRANCH_TARGET_REG, TMP1_REG)
+        SAVE_BRANCH_INSTR_ADDR(LOCAL_LABEL(branch_instr), TMP1_REG, TMP2_REG)
+        pacizb  BRANCH_TARGET_REG
+        CORRUPT_ADDR_IF_NEEDED(BRANCH_TARGET_REG, TRIGGER_FAULT_ARG_REG)
+        str     x30, [sp, #-16]!
+LOCAL_LABEL(branch_instr):
+        blrabz  BRANCH_TARGET_REG
+        ldr     x30, [sp], #16
+        ret
+#undef FUNCNAME
+
+#define FUNCNAME test_blraa
+        DECLARE_FUNC_SEH(FUNCNAME)
+GLOBAL_LABEL(FUNCNAME:)
+        AARCH64_ADRP_GOT(GLOBAL_REF(dummy_func), BRANCH_TARGET_REG)
+        SAVE_BRANCH_TARGET_ADDR(BRANCH_TARGET_REG, TMP1_REG)
+        SAVE_BRANCH_INSTR_ADDR(LOCAL_LABEL(branch_instr), TMP1_REG, TMP2_REG)
+        mov     MODIFIER_REG, #42
+        pacia   BRANCH_TARGET_REG, MODIFIER_REG
+        CORRUPT_ADDR_IF_NEEDED(BRANCH_TARGET_REG, TRIGGER_FAULT_ARG_REG)
+        str     x30, [sp, #-16]!
+LOCAL_LABEL(branch_instr):
+        blraa   BRANCH_TARGET_REG, MODIFIER_REG
+        ldr     x30, [sp], #16
+        ret
+#undef FUNCNAME
+
+#define FUNCNAME test_blrab
+        DECLARE_FUNC_SEH(FUNCNAME)
+GLOBAL_LABEL(FUNCNAME:)
+        AARCH64_ADRP_GOT(GLOBAL_REF(dummy_func), BRANCH_TARGET_REG)
+        SAVE_BRANCH_TARGET_ADDR(BRANCH_TARGET_REG, TMP1_REG)
+        SAVE_BRANCH_INSTR_ADDR(LOCAL_LABEL(branch_instr), TMP1_REG, TMP2_REG)
+        mov     MODIFIER_REG, #42
+        pacib   BRANCH_TARGET_REG, MODIFIER_REG
+        CORRUPT_ADDR_IF_NEEDED(BRANCH_TARGET_REG, TRIGGER_FAULT_ARG_REG)
+        str     x30, [sp, #-16]!
+LOCAL_LABEL(branch_instr):
+        blrab   BRANCH_TARGET_REG, MODIFIER_REG
+        ldr     x30, [sp], #16
+        ret
+#undef FUNCNAME
+
+#define FUNCNAME strip_pac
+        DECLARE_FUNC_SEH(FUNCNAME)
+GLOBAL_LABEL(FUNCNAME:)
+        xpaci   x0
+        ret
+        END_FUNC(FUNCNAME)
+#undef FUNCNAME
+
+END_FILE
+
+#endif
+/* clang-format on */
diff --git a/suite/tests/linux/mangle_pauth.templatex b/suite/tests/linux/mangle_pauth.templatex
new file mode 100644
index 0000000..f75da10
--- /dev/null
+++ b/suite/tests/linux/mangle_pauth.templatex
@@ -0,0 +1 @@
+Test complete