I have written some arm64 assembly code following the AAPCS64 calling convention.
Now I would like to integrate this code into a C/C++ project built with PAC and BTI enabled. I first noticed trouble when the linker told me that I was missing the GNU_PROPERTY_AARCH64_FEATURE_1_BTI
property:
ld: error: md5block_arm64.o: -z bti-report: file does not have GNU_PROPERTY_AARCH64_FEATURE_1_BTI property
While it would be trivial to just set this property, I am aware that I likely need to change parts of my code to make them PAC/BTI aware. What are the changes needed for this? I am interested in a general answer that can be used as a guide to adapt any code, hence the lack of code to be adapted in this question.
BTI is pretty simple, and doesn't depend on software ABI (beyond BTI being enabled in the first place). In the current version (K.a
) of the manual, the behaviour is documented in section "D8.4.5.3 PSTATE.BTYPE". In essence:
Forward-edge branching instructions fall into two categories:
br
, braa
, braaz
, brab
and brabz
blr
, blraa
, blraaz
, blrab
, blrabz
I will be using br
and blr
as a short-hand for these categories.
There are three new instructions: bti c
, bti j
and bti jc
.
blr
branches must land on a bti c
or bti jc
instruction.
br
branches that use a register other than x16
and x17
for the target address must land on a bti j
or bti jc
instruction.
br
branches that use x16
or x17
for the target address can land on any bti
instruction.
paciasp
and pacibsp
act as either bti c
or bti jc
based on some bits in SCTLR_ELn
for the relevant translation regime - just assume bti c
to be safe.
So in order to be compatible with BTI code, you have to:
paciasp
, pacibsp
or bti c
.br
to call actual functions) use x16
or x17
.bti j
to all possible targets of switch-style br
s.PAC is an entirely different beast. That depends on the exact ABI of everything in your stack - gnu
vs musl
, C vs C++, __attribute__
specifiers in header files, ...
The AAPCS64 specification does not define a pointer authentication ABI at all. PAUTHELF64 has some definitions for how to encode things in the ELF file format, but crucially it doesn't define how those things are to be chosen.
Basically any code pointer synthesis, any indirect branch, and potentially even memory loads might need PAC instructions - entirely implementation-defined.
Now, lucky for you, -mbranch-protection=standard
seems to use PAC just barely at all. From the documentation of the flag:
-mbranch-protection=none|standard|pac-ret[+leaf+b-key]|bti
Select the branch protection features to use.
none
is the default and turns off all types of branch protection.standard
turns on all types of branch protection features. If a feature has additional tuning options, thenstandard
sets it to its standard level.pac-ret[+leaf]
turns on return address signing to its standard level: signing functions that save the return address to memory (non-leaf functions will practically always do this) using the a-key. The optional argumentleaf
can be used to extend the signing to include leaf functions. The optional argumentb-key
can be used to sign the functions with the B-key instead of the A-key.bti
turns on branch target identification mechanism.
Basically this only uses PAC to protect stack frames, which does not affect the ABI between different compilation units at all. If you want to uphold the security properties of the option, then all of your functions that spill x30
will need a paciasp
before the spill and an autiasp
between the reload and the ret
. (If your code does not need to be able to run on hardware without PAC support, you can combine the autiasp
+ret
into a single retaa
).