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 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.
-mbranch-protection=standard
standard
-mbranch-protection=
pac-ret+bti.
make clean CFLAGS="-mbranch-protection=standard" make
And readelf will indicate both PAC and BTI are supported:
readelf
And the program will execute as expected:
./main Hello from my_jump!
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:
paciasp
pacibsp
bti c
aarch64.h
call_function.S
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:
call_function
objdump -d main <snip/> 0000000000410240 <call_function>: 410240: d503233f paciasp 410244: a9bf7bfd stp x29, x30, [sp, #-16]! <snip/>
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.
hint
Tag: Example-6
#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 32 /* 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:
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.