[PATCH 3/3] aarch64: Support PAC and BTI

Bill Roberts bill.roberts at arm.com
Thu Oct 16 19:01:41 CEST 2025


Enable Pointer Authentication Codes (PAC) and Branch Target
Identification (BTI) support for "ARM64" targets.

PAC works by signing the Link Register (LR) with either the A key
or B key and verifying the return address. Several instructions can
perform this operation, however, the Linux ARM ABI uses hint-compatible
instructions that can safely be NOP'd on older hardware and can still
be assembled and linked with older binutils.

This limits the instruction set to `paciasp`, `pacibsp`, `autiasp`
and `autibsp`. Instructions prefixed with "pac" are for signing
and instructions prefixed with "aut" are for authentication.
Authentication in this context, means verifying the signature
and retrieving the original LR value. Both instructions are
then followed with an "a" or "b" to indicate which signing key
is being used. The keys can be controlled using the following
compiler options: -mbranch-protection=pac-ret for the A key and
-mbranch-protection=pac-ret+b-key for the B key.

BTI works by marking all call and jump targets with bti c and bti j
instructions. If execution control transfers to an instruction other
than a BTI instruction, the execution is killed via SIGILL. Note, that
to remove one instruction, the aforementioned pac instructions also
serve as BTI landing pads for `bti c` usage.

For BTI to function correctly, all object files linked for a unit
of execution, whether an executable or a library, must have the GNU
Notes section of the ELF file marked to indicate BTI support. This
is so loader/linkers can apply the proper permission bits (PROT_BTI)
on the memory region.

PAC can also be annotated in the GNU ELF notes section, but it's not
required for enablement, since interleaved PAC and non-pac code works
as expected since it's the callee that performs all verification. The
linker follows the same rules as BTI for discarding the PAC flag from
the GNU Notes section.

Testing was done under the following CFLAGS and
CXXFLAGS for all combinations:
 1. -mbranch-protection=none
 2. -mbranch-protection=standard
 3. -mbranch-protection=pac-ret
 4. -mbranch-protection=pac-ret+b-key
 5. -mbranch-protection=bti

Add tests that get skipped on non-pac and bti enabled systems, so this
safely limits the tests to aarch64 platforms with support.  One test
dynamically tests that an mpn assembly routine supports BTI when the
binary is enabled AND the system has support by calling that routine
and verifying it's functionality and then by calling it one instruction
past the correct entry point, and thus missing the landing pad.

The other test added, tests that the ELF binary has the proper GNU
Notes section for the set of build flags.

Details on PAC and BTI can be found in the manuals:
 - https://developer.arm.com/documentation/ddi0487/latest
 - https://github.com/ARM-software/abi-aa/blob/main/pauthabielf64/pauthabielf64.rst

However, the TL;DR is the 3 part blog post that explores the relevant
parts for software:
 - https://community.arm.com/arm-community-blogs/b/architectures-and-processors-blog/posts/enabling-pac-and-bti-on-aarch64

Signed-off-by: Bill Roberts <bill.roberts at arm.com>
---
 configure.ac                   |  91 ++++++++++++++++++-
 mpn/arm64/arm64-defs.m4        | 133 ++++++++++++++++++++++++++-
 mpn/arm64/divrem_1.asm         |   8 +-
 tests/mpn/Makefile.am          |  22 ++++-
 tests/mpn/t-arm64_bti.c        |  91 +++++++++++++++++++
 tests/mpn/t-arm64_elf_check.sh | 161 +++++++++++++++++++++++++++++++++
 6 files changed, 496 insertions(+), 10 deletions(-)
 create mode 100644 tests/mpn/t-arm64_bti.c
 create mode 100755 tests/mpn/t-arm64_elf_check.sh

diff --git a/configure.ac b/configure.ac
index 4d1921ee2..928be021d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -85,6 +85,7 @@ AM_MAINTAINER_MODE
 # Check for extra tools needed by configure
 AC_PROG_GREP
 AC_PROG_SED
+AC_PROG_EGREP
 
 AC_ARG_ENABLE(assert,
 AS_HELP_STRING([--enable-assert],[enable ASSERT checking [default=no]]),
@@ -3420,14 +3421,14 @@ include][($mpn_relative_top_srcdir/mpn/$tmp_dir/$tmp_base.asm)
             CPUVEC_SETUP="$CPUVEC_SETUP    decided_cpuvec.$tmp_fbase = __gmpn_${tmp_fbase}_${tmp_suffix}; \\
 "
             # Ditto for any preinv variant (preinv_divrem_1, preinv_mod_1).
-            if $GREP "^PROLOGUE(mpn_preinv_$tmp_fn)" $tmp_file >/dev/null; then
+            if ${EGREP} "^(PROLOGUE|PROLOGUE_NONLEAF)(mpn_preinv_$tmp_fn)" $tmp_file >/dev/null; then
               echo "DECL_preinv_$tmp_fbase (__gmpn_preinv_${tmp_fbase}_$tmp_suffix);" >>fat.h
               CPUVEC_SETUP="$CPUVEC_SETUP    decided_cpuvec.preinv_$tmp_fbase = __gmpn_preinv_${tmp_fbase}_${tmp_suffix}; \\
 "
             fi
 
             # Ditto for any mod_1...cps variant
-            if $GREP "^PROLOGUE(mpn_${tmp_fbase}_cps)" $tmp_file >/dev/null; then
+            if ${EGREP} "^(PROLOGUE|PROLOGUE_NONLEAF)(mpn_${tmp_fbase}_cps)" $tmp_file >/dev/null; then
               echo "DECL_${tmp_fbase}_cps (__gmpn_${tmp_fbase}_cps_$tmp_suffix);" >>fat.h
               CPUVEC_SETUP="$CPUVEC_SETUP    decided_cpuvec.${tmp_fbase}_cps = __gmpn_${tmp_fbase}_cps_${tmp_suffix}; \\
 "
@@ -3559,7 +3560,8 @@ for tmp_fn in $gmp_mpn_functions; do
           # The PROLOGUE pattern excludes the optional second parameter.
           gmp_ep=[`
             $SED -n 's/^[ 	]*MULFUNC_PROLOGUE(\(.*\))/\1/p' $tmp_file ;
-            $SED -n 's/^[ 	]*PROLOGUE(\([^,]*\).*)/\1/p' $tmp_file
+            $SED -n 's/^[ 	]*PROLOGUE(\([^,]*\).*)/\1/p' $tmp_file ;
+            $SED -n 's/^[ 	]*NONLEAF_PROLOGUE(\([^,]*\).*)/\1/p' $tmp_file
           `]
           for gmp_tmp in $gmp_ep; do
             AC_DEFINE_UNQUOTED(HAVE_NATIVE_$gmp_tmp)
@@ -3766,7 +3768,88 @@ if test "$gmp_asm_syntax_testing" != no; then
 	    *-*-darwin*)
 	      GMP_INCLUDE_MPN(arm64/darwin.m4) ;;
 	    *)
-	      GMP_INCLUDE_MPN(arm64/arm64-defs.m4) ;;
+	      GMP_INCLUDE_MPN(arm64/arm64-defs.m4)
+
+	      AC_CACHE_CHECK([if Armv8.5-A BTI is enabled],
+	        [gmp_cv_asm_arm64_bti],
+	        [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
+	      #if !(defined __ARM_FEATURE_BTI_DEFAULT)
+	        #error Armv8.5-A BTI is not enabled
+	      #endif
+	        ]], [[]])],
+	        [gmp_cv_asm_arm64_bti=yes],
+	        [gmp_cv_asm_arm64_bti=no])])
+	      # Convert yes no values to BTI value, yes no is nice for the output message on the screen
+	      # Example: checking if Armv8.5-A BTI is enabled... yes
+	      AS_IF([test "$gmp_cv_asm_arm64_bti" = yes],
+	        [ARM64_FEATURE_BTI_DEFAULT=1], [ARM64_FEATURE_BTI_DEFAULT=0])
+
+	      GMP_DEFINE_RAW(["define(<ARM64_FEATURE_BTI_DEFAULT>,$ARM64_FEATURE_BTI_DEFAULT)"])
+	      AC_SUBST([ARM64_FEATURE_BTI_DEFAULT])
+
+	      AC_CACHE_CHECK([if Armv8.3-A PAC is enabled],
+	        [gmp_cv_asm_arm64_pac],
+	        [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
+	      #ifndef __ARM_FEATURE_PAC_DEFAULT
+	      #error Armv8.3-A PAC is not enabled
+	      #endif
+	        ]], [[]])],
+	        [gmp_cv_asm_arm64_pac=yes],
+	        [gmp_cv_asm_arm64_pac=no])])
+
+	      AS_IF([test "$gmp_cv_asm_arm64_pac" = yes], [
+	        AC_CACHE_CHECK([if PAC is using A or B key],
+	          [gmp_cv_asm_arm64_pac_key],
+	          [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([[
+	        #if __ARM_FEATURE_PAC_DEFAULT & 1
+	        // Armv8.3-A PAC A Key enabled else B Key enabled
+	        #endif
+	          ]], [[]])],
+	          [gmp_cv_asm_arm64_pac_key=A],
+	          [gmp_cv_asm_arm64_pac_key=B])])
+	      ])
+	      # Same as the reason for BTI, convert to the actual value from a nice
+	      # name for checking messages.
+	      AS_CASE([$gmp_cv_asm_arm64_pac_key],
+	        [A], [ARM64_FEATURE_PAC_DEFAULT=1],
+	        [B], [ARM64_FEATURE_PAC_DEFAULT=2],
+	        [*], [ARM64_FEATURE_PAC_DEFAULT=0])
+
+	      GMP_DEFINE_RAW(["define(<ARM64_FEATURE_PAC_DEFAULT>,$ARM64_FEATURE_PAC_DEFAULT)"])
+	      AC_SUBST([ARM64_FEATURE_PAC_DEFAULT])
+
+	      # If BTI is enabled, we must have bash and readelf for tests to execute
+	      AC_CHECK_PROG([have_readelf], [readelf], [yes], [no])
+	      AS_IF([test "$have_readelf" != "yes" && test "$gmp_cv_asm_arm64_bti" == "yes"],
+	        [AC_MSG_ERROR([readelf not found; some tests will be disabled])])
+	      AM_CONDITIONAL([HAVE_READELF], [test "x$have_readelf" = xyes])
+
+	      AC_CHECK_PROG([have_bash], [bash], [yes], [no])
+	      AS_IF([test "$have_bash" != "yes" && test "$gmp_cv_asm_arm64_bti" == "yes"]],
+	        [AC_MSG_ERROR([bash not found; some tests will be disabled])])
+	      AM_CONDITIONAL([HAVE_BASH], [test "x$have_bash" = xyes])
+
+	      AC_CACHE_CHECK([whether __ELF__ is defined],
+	        [gmp_cv_asm_arm64_have_elf],
+	        [AC_PREPROC_IFELSE(
+	           [AC_LANG_SOURCE([[
+	      #ifdef __ELF__
+	      int main(void) { return 0; }
+	      #else
+	      #error "__ELF__ not defined"
+	      #endif
+	      ]])],
+	        [gmp_cv_asm_arm64_have_elf=yes],
+	        [gmp_cv_asm_arm64_have_elf=no])])
+
+	      if test "$gmp_cv_asm_arm64_have_elf" = yes; then
+	        gmp_cv_asm_arm64_have_elf_value=1
+	      else
+	        gmp_cv_asm_arm64_have_elf_value=0
+	      fi
+
+	      GMP_DEFINE_RAW(["define(<ARM64_ELF>,$gmp_cv_asm_arm64_have_elf_value)"])
+	      ;;
           esac
 	  ;;
       esac
diff --git a/mpn/arm64/arm64-defs.m4 b/mpn/arm64/arm64-defs.m4
index 46149f7bf..590d57edc 100644
--- a/mpn/arm64/arm64-defs.m4
+++ b/mpn/arm64/arm64-defs.m4
@@ -2,8 +2,8 @@ divert(-1)
 
 dnl  m4 macros for ARM64 ELF assembler.
 
-dnl  Copyright 2020 Free Software Foundation, Inc.
-
+dnl  Copyright 2025 Free Software Foundation, Inc.
+dnl
 dnl  This file is part of the GNU MP Library.
 dnl
 dnl  The GNU MP Library is free software; you can redistribute it and/or modify
@@ -36,6 +36,129 @@ dnl  don't want to disable macro expansions in or after them.
 
 changecom
 
+dnl use the hint instructions so they NOP on older machines.
+dnl Add comments so the assembly is notated with the instruction
+
+define(`PACIASP', `hint #25  /* paciasp */')
+define(`AUTIASP', `hint #29  /* autiasp */')
+define(`PACIBSP', `hint #27  /* pacibsp */')
+define(`AUTIBSP', `hint #31  /* autibsp */')
+
+
+dnl if BTI is enabled we want the SIGN_LR to be a valid
+dnl landing pad, we don't need VERIFY_LR and we need to
+dnl indicate the valid BTI support for gnu notes.
+
+ifelse(ARM64_FEATURE_BTI_DEFAULT, `1',
+  `define(`BTI_C',   `hint #34  /* bti c */')
+   define(`SIGN_LR', `BTI_C')
+   define(`GNU_PROPERTY_AARCH64_BTI', `1')
+   define(`PAC_OR_BTI')', `
+   define(`BTI_C', `')
+   define(`GNU_PROPERTY_AARCH64_BTI', `0')'
+')
+
+
+dnl define instructions for PAC, which can use the A
+dnl or the B key. PAC instructions are also valid BTI
+dnl landing pads, so we re-define SIGN_LR if BTI is
+dnl enabled.
+
+ifelse(ARM64_FEATURE_PAC_DEFAULT, `1',
+    `define(`SIGN_LR', `PACIASP')
+     define(`VERIFY_LR', `AUTIASP')
+     define(`GNU_PROPERTY_AARCH64_POINTER_AUTH', `2')
+     define(`PAC_OR_BTI')',
+   ARM64_FEATURE_PAC_DEFAULT, `2',
+    `define(`SIGN_LR', `PACIBSP')
+     define(`VERIFY_LR', `AUTIBSP')
+     define(`GNU_PROPERTY_AARCH64_POINTER_AUTH', `2')
+     define(`PAC_OR_BTI')',
+    `ifdef(`SIGN_LR', , `define(`SIGN_LR', `')')
+     define(`VERIFY_LR', `')
+     define(`GNU_PROPERTY_AARCH64_POINTER_AUTH', `0')'
+')
+
+
+dnl NOTE OVERRIDES asm-defs.m4 definition for arch specific functionality
+dnl
+dnl Usage: PROLOGUE_cpu(GSYM_PREFIX`'foo[,param])
+dnl         EPILOGUE_cpu(GSYM_PREFIX`'foo)
+dnl
+dnl  These macros hold the CPU-specific parts of PROLOGUE and is called
+dnl  with the function name, with GSYM_PREFIX already prepended.
+dnl
+dnl  The CPU specific overide markes functions with a BTI instruction,
+dnl  if BTI is enabled, or signes the LR is PAC is enabled and the function
+dnl  pushes the LR to the stack. A second, boolean argument, may be provided,
+dnl  and that argument determines if the function pushes the LR to the stack.
+dnl
+dnl  This macro is not intended for general use, and users should use EPILOGUE
+dnl  for functions that do not push the LR to stack or use EPILOGUE_NONLEAF for
+dnl  for functions that do make use of the stack. We override PROLOGUE_cpu to take
+dnl  the extra argument, becuase the consuming EPILOG macro, and it's associated
+dnl  house keeping, we don't want to copy and paste.
+dnl
+dnl  Details on PAC and BTI can be found in the manuals:
+dnl    - https://developer.arm.com/documentation/ddi0487/latest
+dnl    - https://github.com/ARM-software/abi-aa/blob/main/pauthabielf64/pauthabielf64.rst
+dnl
+dnl  However, the TL;DR is the 3 part blog post that explores the relevant parts for software:
+dnl    - https://community.arm.com/arm-community-blogs/b/architectures-and-processors-blog/posts/enabling-pac-and-bti-on-aarch64
+
+define(`PROLOGUE_cpu',
+m4_assert_numargs_range(1,2)
+`	TEXT
+	ALIGN(8)
+	GLOBL	`$1' GLOBL_ATTR
+	TYPE(`$1',`function')
+`$1'LABEL_SUFFIX
+	ifelse(`$2',`true',
+		`SIGN_LR',
+		`BTI_C')
+')
+
+
+dnl  Generate a prologue for an arm64 function that pushes the LR to the stack
+dnl
+dnl  Usage: PROLOGUE_NONLEAF(foo[,param])
+dnl         EPILOGUE(foo)
+dnl
+dnl  When PAC is enabled, functions that push the LR to the stack need to be signed using
+dnl  a PAC instruction. This way, corruption of the LR on the stack can be detected by
+dnl  validating the signature before returning. It's important to note that some PAC
+dnl  instructions can also be valid BTI landing pads. Becuase of this, we can optimize the
+dnl  assembly when PAC and BTI are enabled by omitting the BTI instruction and just making
+dnl  the first instruction in function a PAC instruction.
+
+#define(`NONLEAF_PROLOGUE', `indir(`PROLOGUE', $1, true)')
+define(`PROLOGUE_NONLEAF', `PROLOGUE($1, true)')
+
+
+dnl ADD_GNU_NOTES_IF_NEEDED
+dnl
+dnl Conditionally add into ELF assembly files the GNU notes indicating if
+dnl BTI or PAC is support. BTI is required by the linkers and loaders, however
+dnl PAC is a nice to have for auditing. Use readelf -n to display.
+
+define(`ADD_GNU_NOTES_IF_NEEDED', `
+  ifdef(`ARM64_ELF', `
+    ifdef(`PAC_OR_BTI', `
+      .pushsection .note.gnu.property, "a";
+      .balign 8;
+      .long 4;
+      .long 0x10;
+      .long 0x5;
+      .asciz "GNU";
+      .long 0xc0000000; /* GNU_PROPERTY_AARCH64_FEATURE_1_AND */
+      .long 4;
+      .long eval(indir(`GNU_PROPERTY_AARCH64_POINTER_AUTH') + indir(`GNU_PROPERTY_AARCH64_BTI'));
+      .long 0;
+      .popsection;
+    ')
+  ')
+')
+
 
 dnl  LEA_HI(reg,gmp_symbol), LEA_LO(reg,gmp_symbol)
 dnl
@@ -50,4 +173,10 @@ define(`LEA_HI', `adrp	$1, $2')dnl
 define(`LEA_LO', `add	$1, $1, :lo12:$2')dnl
 ')dnl
 
+
+dnl divert output to the following m4 file to shove the GNU Notes section into subsequent
+dnl files implicitly.
+divert(1)
+ADD_GNU_NOTES_IF_NEEDED
+
 divert`'dnl
diff --git a/mpn/arm64/divrem_1.asm b/mpn/arm64/divrem_1.asm
index 9d5bb5959..d02e7c272 100644
--- a/mpn/arm64/divrem_1.asm
+++ b/mpn/arm64/divrem_1.asm
@@ -65,7 +65,7 @@ dnl                      mp_limb_t d_unnorm, mp_limb_t dinv, int cnt)
 
 ASM_START()
 
-PROLOGUE(mpn_preinv_divrem_1)
+NONLEAF_PROLOGUE(mpn_preinv_divrem_1)
 	cbz	n_arg, L(fz)
 	stp	x29, x30, [sp, #-80]!
 	mov	x29, sp
@@ -85,7 +85,7 @@ PROLOGUE(mpn_preinv_divrem_1)
 	b	L(uentry)
 EPILOGUE()
 
-PROLOGUE(mpn_divrem_1)
+NONLEAF_PROLOGUE(mpn_divrem_1)
 	cbz	n_arg, L(fz)
 	stp	x29, x30, [sp, #-80]!
 	mov	x29, sp
@@ -154,6 +154,7 @@ L(uend):add	x2, x11, #1
 	ldp	x21, x22, [sp, #32]
 	ldp	x23, x24, [sp, #48]
 	ldp	x29, x30, [sp], #80
+	VERIFY_LR
 	ret
 
 L(ufx):	add	x2, x2, #1
@@ -194,6 +195,7 @@ L(nend):cbnz	fn, L(frac)
 	ldp	x21, x22, [sp, #32]
 	ldp	x23, x24, [sp, #48]
 	ldp	x29, x30, [sp], #80
+	VERIFY_LR
 	ret
 
 L(nfx):	add	x2, x2, #1
@@ -219,6 +221,7 @@ L(ftop):add	x2, x11, #1
 	ldp	x21, x22, [sp, #32]
 	ldp	x23, x24, [sp, #48]
 	ldp	x29, x30, [sp], #80
+	VERIFY_LR
 	ret
 
 C Block zero. We need this for the degenerated case of n = 0, fn != 0.
@@ -227,5 +230,6 @@ L(ztop):str	xzr, [qp_arg], #8
 	sub	fn_arg, fn_arg, #1
 	cbnz	fn_arg, L(ztop)
 L(zend):mov	x0, #0
+	VERIFY_LR
 	ret
 EPILOGUE()
diff --git a/tests/mpn/Makefile.am b/tests/mpn/Makefile.am
index 0e979a3ad..cbf47820d 100644
--- a/tests/mpn/Makefile.am
+++ b/tests/mpn/Makefile.am
@@ -17,6 +17,11 @@
 # You should have received a copy of the GNU General Public License along with
 # the GNU MP Library test suite.  If not, see https://www.gnu.org/licenses/.
 
+# Arguments for the t-arm64_elf_check.sh test script.
+AM_TESTS_ENVIRONMENT = T_ARM64_ELF_CHECK_SH_ARGS="--enable-pac=@ARM64_FEATURE_PAC_DEFAULT@ \
+  --enable-bti=@ARM64_FEATURE_BTI_DEFAULT@ $(top_builddir)/.libs/libgmp.so"
+
+TEST_EXTENSIONS = .sh
 
 AM_CPPFLAGS = -I$(top_srcdir) -I$(top_srcdir)/tests
 AM_LDFLAGS = -no-install
@@ -30,11 +35,24 @@ check_PROGRAMS = t-asmtype t-aors_1 t-divrem_1 t-mod_1 t-fat t-get_d	\
   t-div t-mul t-mullo t-sqrlo t-mulmod_bnm1 t-sqrmod_bnm1 t-mulmid	\
   t-mulmod_bknp1 t-sqrmod_bknp1						\
   t-addaddmul t-hgcd t-hgcd_appr t-matrix22 t-invert t-bdiv t-fib2m	\
-  t-broot t-brootinv t-minvert t-sizeinbase t-gcd_11 t-gcd_22 t-gcdext_1
+  t-broot t-brootinv t-minvert t-sizeinbase t-gcd_11 t-gcd_22 t-gcdext_1	\
+  t-arm64_bti
 
 EXTRA_DIST = toom-shared.h toom-sqr-shared.h
 
-TESTS = $(check_PROGRAMS)
+test_scripts =
+if HAVE_BASH
+if HAVE_READELF
+  test_scripts += t-arm64_elf_check.sh
+endif
+endif
+check_SCRIPTS = $(test_scripts)
+
+EXTRA_DIST = toom-shared.h toom-sqr-shared.h t-arm64_elf_check.sh
+
+TESTS = $(check_PROGRAMS) $(check_SCRIPTS)
+
+XFAIL_TESTS = t-arm64_bti
 
 $(top_builddir)/tests/libtests.la:
 	cd $(top_builddir)/tests; $(MAKE) $(AM_MAKEFLAGS) libtests.la
diff --git a/tests/mpn/t-arm64_bti.c b/tests/mpn/t-arm64_bti.c
new file mode 100644
index 000000000..befa12c9d
--- /dev/null
+++ b/tests/mpn/t-arm64_bti.c
@@ -0,0 +1,91 @@
+/*
+Copyright 2025 Free Software Foundation, Inc.
+
+This file is part of the GNU MP Library test suite.
+
+The GNU MP Library test suite is free software; you can redistribute it
+and/or modify it under the terms of the GNU General Public License as
+published by the Free Software Foundation; either version 3 of the License,
+or (at your option) any later version.
+
+The GNU MP Library test suite is distributed in the hope that it will be
+useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
+Public License for more details.
+
+You should have received a copy of the GNU General Public License along with
+the GNU MP Library test suite.  If not, see https://www.gnu.org/licenses/.  */
+
+/*
+ * Test if if BTI is working within the GMP assembly stubs for AArch64 aka arm64
+ * within GMP. This test gets a function pointer to mpn_lshift avoiding the PLT
+ * using dlsym and calls the function and checks for a valid return. It then
+ * advances the function pointer by 2, which points us to the next instruction,
+ * and calls. The following scenarios are possible:
+ * | Binary BTI Enabled | Hardware BTI Enabled | Executable Outcome  | Test Outcome |
+ * | 0                  | 0                    | Works returning 77  | SKIP          |
+ * | 0                  | 1                    | Works returning 77  | SKIP          |
+ * | 1                  | 0                    | Works returning 77  | SKIP          |
+ * | 1                  | 1                    | BTI Exception       | PASS          |
+ * Note: 77 is the magic value for autotools to indicate to skip a test.
+ * Note: You MUST run this test when enabled on a BTI enabled hardware setup.
+ * Note: That for non-aarch64 platforms, this also just skips.
+ */
+
+#define SKIP 77
+
+/* AArch64 BTI Binary enabled code ONLY */
+#ifdef __ARM_FEATURE_BTI_DEFAULT
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <stdio.h>
+
+#include <dlfcn.h>
+#include <sys/auxv.h>
+#include <asm/hwcap.h>
+
+#include "gmp-impl.h"
+#include "tests.h"
+
+typedef mp_limb_t (*fn_mpn_lshift)(mp_ptr, mp_srcptr, mp_size_t, unsigned int);
+
+int
+main (int argc, char **argv)
+{
+	unsigned long hwcap2 = getauxval(AT_HWCAP2);
+	if (!(hwcap2 & HWCAP2_BTI)) {
+		fprintf(stderr, "Hardware does not support BTI\n");
+		return SKIP;
+	}
+
+	/*
+	 * 0x1001 is the value we will left shift by one, we expect 0x2002 as
+	 * the valid result.
+	 */
+	mp_limb_t xp = 0x1001, wp;
+
+	fn_mpn_lshift fn = dlsym(RTLD_DEFAULT, "__gmpn_lshift");
+	if (!fn) {
+	  fprintf(stderr, "Could not find the symbol __gmpn_lshift\n");
+	  return 0;
+	}
+
+	/* should work as this will land on a BTI landing pad as expected */
+	fn (&wp, &xp, (mp_size_t) 1, 1);
+	/* And provide the proper result 0x1001 << 1 = 0x2002 */
+	ASSERT_ALWAYS (wp == 0x2002);
+
+	/* this should fail as it's off 1 instruction */
+	fn = (fn_mpn_lshift)((uintptr_t)fn + 4);
+	fn(&wp, &xp, (mp_size_t) 1, 1);
+	fprintf(stderr, "This should cause an exception, does your system support BTI?\n");
+	return 0;
+}
+#else
+/* No binary support for BTI or another arch, just skips */
+int
+main (int argc, char **argv) {
+	return SKIP;
+}
+#endif
diff --git a/tests/mpn/t-arm64_elf_check.sh b/tests/mpn/t-arm64_elf_check.sh
new file mode 100755
index 000000000..ccb0bc02f
--- /dev/null
+++ b/tests/mpn/t-arm64_elf_check.sh
@@ -0,0 +1,161 @@
+#!/usr/bin/env bash
+#
+# Copyright 2025 Free Software Foundation, Inc.
+#
+# This file is part of the GNU MP Library test suite.
+#
+# The GNU MP Library test suite is free software; you can redistribute it
+# and/or modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; either version 3 of the License,
+# or (at your option) any later version.
+#
+# The GNU MP Library test suite is distributed in the hope that it will be
+# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
+# Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# the GNU MP Library test suite.  If not, see https://www.gnu.org/licenses/.  */
+
+
+# *Script Details*
+# This script verifies that a given ELF shared library includes the GNU Notes
+# annotations for PAC and BTI, based on the command-line options provided.
+# It is designed for use within the GMP test suite but can also be run
+# independently by developers. The script can accept options and arguments
+# from the environment variable "T_ARM64_ELF_CHECK_SH_ARGS" or accept them
+# directly via the command line.
+
+set -e -o pipefail
+
+usage() {
+  cat <<EOF
+Usage: $0 [OPTIONS] <gmp-shared-library>
+
+Description:
+  This script checks whether the specified ARM64 shared library
+  has Branch Target Identification (BTI) and/or Pointer Authentication (PAC)
+  enabled, based on the provided options.
+
+  It can also take command-line arguments from the environment variable:
+    T_ARM64_ELF_CHECK_SH_ARGS
+
+  This allows for embedding within a persistent configuration or scripted invocations.
+
+Options:
+  --enable-bti=0|1     Enable or disable Branch Target Identification check (default: 0)
+  --enable-pac=0|1     Enable or disable Pointer Authentication check (default: 0)
+  --help               Display this help message and exit
+
+Arguments:
+  <gmp-shared-library> Path to the GMP shared library (e.g., libgmp.so)
+
+Examples:
+  $0 --enable-bti=1 --enable-pac=0 /usr/lib/libgmp.so
+  T_ARM64_ELF_CHECK_SH_ARGS="--enable-bti=1" $0 /usr/lib/libgmp.so
+EOF
+}
+
+check_val() {
+
+  local grep_flags="-qi"
+  local not_msg=""
+  # invert the grep match if it SHOULDN'T be found in the flags.
+  # ie BTI 0 means BTI should not be in the notes.
+  if [ "${2}" -eq 0 ]; then
+    grep_flags+="v"
+    not_msg="Not "
+  fi
+
+  printf 'Checking for %s in "%s". Expecting "%sPresent", ' "${1}" "${ELF_BINARY}" "${not_msg}"
+
+  set +e
+  readelf -n "${ELF_BINARY}" | grep $grep_flags -- "${1}"
+  local r="${?}"
+  set -e
+  # Possible states we care about, which grep will fail under:
+  #   - State 1: Not expecting and Found
+  #   - State 2: Expecting and not Found
+  if [[ "${r}" -ne 0 ]]; then
+    # Flip the not message
+    if [ -z "${not_msg}" ]; then
+      not_msg="Not "
+    else
+      not_msg=""
+    fi
+  fi
+
+  # print found or not found
+  printf 'got "%sPresent."\n' "${not_msg}"
+
+  # The grep result means we return the rc through the named variable
+  # this way consumers can just add all the values to determine if it
+  # is a failure.
+  declare -g "${1}=${r}"
+}
+
+# Pull in the "command line" options from this env variable if set.
+if [ -n "${T_ARM64_ELF_CHECK_SH_ARGS}" ]; then
+  printf "Using command line arguments from env variable \"T_ARM64_ELF_CHECK_SH_ARGS\"\n"
+  printf "Got: ${T_ARM64_ELF_CHECK_SH_ARGS}\n"
+  set -- $T_ARM64_ELF_CHECK_SH_ARGS
+fi
+
+# Initialize variables
+BTI="0"
+PAC="0"
+ELF_BINARY=""
+
+# Loop through the arguments
+while [[ "${#}" -gt 0 ]]; do
+  case "${1}" in
+  --enable-bti=*)
+    BTI="${1#*=}"
+    shift
+    ;;
+  --enable-pac=*)
+    PAC="${1#*=}"
+    shift
+    ;;
+  --enable-bti | --enable-pac)
+    # If the argument is in the form --enable-bti value (without =)
+    printf 'Error: Option %s requires a value, like --%s=value. For help, use --help\n' "${1}" "${1}"
+    exit 1
+    ;;
+  --help)
+    usage
+    exit 0
+    ;;
+  *)
+    # Handle the non-option argument
+    if [[ -z "${ELF_BINARY}" ]]; then
+      ELF_BINARY="${1}"
+    else
+      printf 'Error: More than one non-option argument provided, got: "%s". For help, use --help\n' "${1}"
+      exit 1
+    fi
+    shift
+    ;;
+  esac
+done
+
+if [ -z "${ELF_BINARY}" ]; then
+  printf "Must specify the ELF binary as the ONLY script argument. For help, use --help\n"
+  exit 1
+fi
+
+# Skip if nothing is enabled, 77 is automake magic for SKIP this test.
+# For non-supporting architectures and ABIs both of these will be 0
+# and thus skip.
+if [[ "${BTI}" -eq 0 && "${PAC}" -eq 0 ]]; then
+  printf "PAC and BTI disabled...skipping\n"
+  exit 77
+fi
+
+check_val "BTI" "${BTI}"
+check_val "PAC" "${PAC}"
+
+# don't use expr as it returns non-zero when the addition result is non-zero
+# and causes the set -e script to fail.
+rc=$((BTI + PAC))
+exit ${rc}
-- 
2.51.0



More information about the gmp-devel mailing list