Part 2: Enabling PAC and BTI on AArch64 for Linux
Utilizing Pointer Authentication Codes (PAC) and Branch Target Instructions (BTI) together and optimizations in instruction counts.
By Bill Roberts

| This is Part 2 of a 3-part blog series. See Part 1 and Part 3 released November 20. |
In Part 1, we looked at instrumenting assembly code to support both PAC and BTI. Now we will look at utilizing them both together and taking advantage of certain architectural optimizations in instruction counts when both features are enabled.
PAC and BTI Together
PAC and BTI will function independently of each other, but like chocolate and peanut butter, they go better together. With -mbranch-protection=standard we can enable them both. Currently, the standard argument to -mbranch-protection= option is analogous to pac-ret+bti.
make clean
CFLAGS="-mbranch-protection=standard" make
And readelf will indicate both PAC and BTI are supported:
readelf -n main
Displaying notes found in: .note.gnu.property
Owner Data size Description
GNU 0x00000010 NT_GNU_PROPERTY_TYPE_0
Properties: AArch64 feature: BTI, PAC
And the program will execute as expected:
./main
Hello from my_jump!
Optimizing
When both PAC and BTI are enabled, function prologs, which is the common boiler plate at the beginning of a function, will have 2 extra instructions, this is less than ideal. However, certain PAC instructions can also act as BTI landing pads, specifically in this example, the paciasp and B-Key variant pacibsp can be used to replace a bti c instruction. So, let's modify the aarch64.h and call_function.S files to take advantage of this:
Tag: Example-5
aarch64.h:
#ifndef _AARCH_64_H_
#define _AARCH_64_H_
/*
* References:
* - https://developer.arm.com/documentation/101028/0012/5--Feature-test-macros
* - https://github.com/ARM-software/abi-aa/blob/main/aaelf64/aaelf64.rst
*/
#if defined(__ARM_FEATURE_BTI_DEFAULT) && __ARM_FEATURE_BTI_DEFAULT == 1
#define BTI_J bti j /* for jumps, IE br instructions */
#define BTI_C bti c /* for calls, IE bl instructions */
#define GNU_PROPERTY_AARCH64_BTI 1 /* bit 0 GNU Notes is for BTI support */
#else
#define BTI_J
#define BTI_C
#define GNU_PROPERTY_AARCH64_BTI 0
#endif
#if defined(__ARM_FEATURE_PAC_DEFAULT)
#if __ARM_FEATURE_PAC_DEFAULT & 1
#define SIGN_LR paciasp /* sign with the A key */
#define VERIFY_LR autiasp /* verify with the A key */
#elif __ARM_FEATURE_PAC_DEFAULT & 2
#define SIGN_LR pacibsp /* sign with the b key */
#define VERIFY_LR autibsp /* verify with the b key */
#endif
#define GNU_PROPERTY_AARCH64_POINTER_AUTH 2 /* bit 1 GNU Notes is for PAC support */
#else
#define SIGN_LR BTI_C
#define VERIFY_LR
#define GNU_PROPERTY_AARCH64_POINTER_AUTH 0
#endif
/* Add the BTI support to GNU Notes section */
#if GNU_PROPERTY_AARCH64_BTI != 0 || GNU_PROPERTY_AARCH64_POINTER_AUTH != 0
.pushsection .note.gnu.property, "a"; /* Start a new allocatable section */
.balign 8; /* align it on a byte boundry */
.long 4; /* size of "GNU\0" */
.long 0x10; /* size of descriptor */
.long 0x5; /* NT_GNU_PROPERTY_TYPE_0 */
.asciz "GNU";
.long 0xc0000000; /* GNU_PROPERTY_AARCH64_FEATURE_1_AND */
.long 4; /* Four bytes of data */
.long (GNU_PROPERTY_AARCH64_BTI|GNU_PROPERTY_AARCH64_POINTER_AUTH); /* BTI or PAC is enabled */
.long 0; /* padding for 8 byte alignment */
.popsection; /* end the section */
#endif
#endif
call_function.s:
#include "aarch64.h"
.section .rodata
.align 3
.Lstring:
.string "Hello From My Jump!"
.section .text
.global my_jump
.global call_function
my_jump:
BTI_J
stp x29, x30, [sp, #-16]!
// Print "Hello From My Jump!" using puts.
// puts can modify registers, so push the return address in x1
// to the stack
adrp x0, .Lstring // Get the page the string is within
add x0, x0, :lo12:.Lstring // Get the page offset (handles relocations ADD_ABS_LO12_NC)
bl puts // puts prints the string in x0
ldp x29, x30, [sp], #16
ret
// Function prototype
// void call_function(void (*func)())
call_function:
SIGN_LR
// Save link register and frame pointer, allocating enough space for
// saving the return location.
stp x29, x30, [sp, #-16]!
mov x29, sp
// x0 is the caller's first argument, so jump
// to the "function" pointed by x0 and save
// the return address to the stack
adr lr, return_loc
br x0 //Later has arrived, it's to highlight use of bti j.
return_loc:
// Restore link register and frame pointer
ldp x29, x30, [sp], #16
// Return from the function
VERIFY_LR
ret
Then build and run the example:
make clean
CFLAGS="-mbranch-protection=standard" make
./main
Hello From My Jump!
Examining the prolog to call_function shows a single paciasp instruction as the valid BTI landing pad:
objdump -d main
<snip/>
0000000000410240 <call_function>:
410240: d503233f paciasp
410244: a9bf7bfd stp x29, x30, [sp, #-16]!
<snip/>
Backwards Compatibility
During this whole tutorial, we have been using the PAC and BTI instruction mnemonics directly. This poses a problem if using older toolchains that cannot support those instructions. Fortunately, the engineers foresaw this problem and utilized the hint space within the ARM architecture. The hint space, is a space for encoding instructions where they will NOP on architectures that do not support them, and work as intended on architectures that do. Also, existing toolchains are aware of hint instructions, so older toolchains will happily interact with new uses of hint instructions. Note that the encoding between the PAC or BTI instruction is the same as the hint space instruction, so this is merely for toolchains and the hardware sees no difference. So armed with this knowledge, let us modify the header file use hint instructions so older toolchains can compile our code.
Tag: Example-6
aarch64.h:
#ifndef _AARCH_64_H_
#define _AARCH_64_H_
/*
* References:
* - https://developer.arm.com/documentation/101028/0012/5--Feature-test-macros
* - https://github.com/ARM-software/abi-aa/blob/main/aaelf64/aaelf64.rst
*/
#if defined(__ARM_FEATURE_BTI_DEFAULT) && __ARM_FEATURE_BTI_DEFAULT == 1
#define BTI_J hint 36 /* bti j: for jumps, IE br instructions */
#define BTI_C hint 34 /* bti c: for calls, IE bl instructions */
#define GNU_PROPERTY_AARCH64_BTI 1 /* bit 0 GNU Notes is for BTI support */
#else
#define BTI_J
#define BTI_C
#define GNU_PROPERTY_AARCH64_BTI 0
#endif
#if defined(__ARM_FEATURE_PAC_DEFAULT)
#if __ARM_FEATURE_PAC_DEFAULT & 1
#define SIGN_LR hint 25 /* paciasp: sign with the A key */
#define VERIFY_LR hint 29 /* autiasp: verify with the A key */
#elif __ARM_FEATURE_PAC_DEFAULT & 2
#define SIGN_LR hint 27 /* pacibsp: sign with the b key */
#define VERIFY_LR hint 31 /* autibsp: verify with the b key */
#endif
#define GNU_PROPERTY_AARCH64_POINTER_AUTH 2 /* bit 1 GNU Notes is for PAC support */
#else
#define SIGN_LR BTI_C
#define VERIFY_LR
#define GNU_PROPERTY_AARCH64_POINTER_AUTH 0
#endif
/* Add the BTI support to GNU Notes section */
#if GNU_PROPERTY_AARCH64_BTI != 0 || GNU_PROPERTY_AARCH64_POINTER_AUTH != 0
.pushsection .note.gnu.property, "a"; /* Start a new allocatable section */
.balign 8; /* align it on a byte boundry */
.long 4; /* size of "GNU\0" */
.long 0x10; /* size of descriptor */
.long 0x5; /* NT_GNU_PROPERTY_TYPE_0 */
.asciz "GNU";
.long 0xc0000000; /* GNU_PROPERTY_AARCH64_FEATURE_1_AND */
.long 4; /* Four bytes of data */
.long (GNU_PROPERTY_AARCH64_BTI|GNU_PROPERTY_AARCH64_POINTER_AUTH); /* BTI or PAC is enabled */
.long 0; /* padding for 8 byte alignment */
.popsection; /* end the section */
#endif
#endif
As always, clean and run the example:
make clean
CFLAGS="-mbranch-protection=standard" make
./main
Hello From My Jump!
Conclusion
In part 2, we explored how PAC instructions can also be valid BTI landing pads providing a saving of instructions when both PAC and BTI are enabled together as well as how those instructions interact with the hint space for providing backwards compatibility not only in hardware, but toolchains as well. In part 3, we will explore how PAC interacts with the C++ exception handling mechanisms and DWARF.
By Bill Roberts
Re-use is only permitted for informational and non-commercial or personal use only.
