:local:
Pointer authentication is a technology which offers strong probabilistic protection against exploiting a broad class of memory bugs to take control of program execution. When adopted consistently in a language ABI, it provides a form of relatively fine-grained control flow integrity (CFI) check that resists both return-oriented programming (ROP) and jump-oriented programming (JOP) attacks.
While pointer authentication can be implemented purely in software, direct hardware support (e.g. as provided by Armv8.3 PAuth) can dramatically improve performance and code size. Similarly, while pointer authentication can be implemented on any architecture, taking advantage of the (typically) excess addressing range of a target with 64-bit pointers minimizes the impact on memory performance and can allow interoperation with existing code (by disabling pointer authentication dynamically). This document will generally attempt to present the pointer authentication feature independent of any hardware implementation or ABI. Considerations that are implementation-specific are clearly identified throughout.
Note that there are several different terms in use:
This document serves four purposes:
The simple address of an object or function is a raw pointer. A raw pointer can be signed to produce a signed pointer. A signed pointer can be then authenticated in order to verify that it was validly signed and extract the original raw pointer. These terms reflect the most likely implementation technique: computing and storing a cryptographic signature along with the pointer.
An abstract signing key is a name which refers to a secret key which is used to sign and authenticate pointers. The concrete key value for a particular name is consistent throughout a process.
A discriminator is an arbitrary value used to diversify signed pointers so that one validly-signed pointer cannot simply be copied over another. A discriminator is simply opaque data of some implementation-defined size that is included in the signature as a salt (see Discriminators for details.)
Nearly all aspects of pointer authentication use just these two primary operations:
sign(raw_pointer, key, discriminator) produces a signed pointer given a raw pointer, an abstract signing key, and a discriminator.auth(signed_pointer, key, discriminator) produces a raw pointer given a signed pointer, an abstract signing key, and a discriminator.auth(sign(raw_pointer, key, discriminator), key, discriminator) must succeed and produce raw_pointer. auth applied to a value that was ultimately produced in any other way is expected to fail, which halts the program either:
auth success (e.g., when using compiler-generated auth failure checks, or Armv8.3 with the FPAC extension), orHowever, regardless of the implementation's handling of auth failures, it is permitted for auth to fail to detect that a signed pointer was not produced in this way, in which case it may return anything; this is what makes pointer authentication a probabilistic mitigation rather than a perfect one.
There are two secondary operations which are required only to implement certain intrinsics in <ptrauth.h>:
strip(signed_pointer, key) produces a raw pointer given a signed pointer and a key without verifying its validity, unlike auth. This is useful for certain kinds of tooling, such as crash backtraces; it should generally not be used in the basic language ABI except in very careful ways.sign_generic(value) produces a cryptographic signature for arbitrary data, not necessarily a pointer. This is useful for efficiently verifying that non-pointer data has not been tampered with.Whenever any of these operations is called for, the key value must be known statically. This is because the layout of a signed pointer may vary according to the signing key. (For example, in Armv8.3, the layout of a signed pointer depends on whether Top Byte Ignore (TBI) is enabled, which can be set independently for I and D keys.)
:::{admonition} Note for API designers and language implementors These are the primitive operations of pointer authentication, provided for clarity of description. They are not suitable either as high-level interfaces or as primitives in a compiler IR because they expose raw pointers. Raw pointers require special attention in the language implementation to avoid the accidental creation of exploitable code sequences; see the section on Attackable code sequences. :::
The following details are all implementation-defined:
sign, auth, strip, and sign_generic operationsWhile the use of the terms “sign” and “signed pointer” suggest the use of a cryptographic signature, other implementations may be possible. See Alternative implementations for an exploration of implementation options.
:::{admonition} Implementation example: Armv8.3 Readers may find it helpful to know how these terms map to Armv8.3 PAuth:
sign_generic. The key data is simply a pepper added to the hash, not an encryption key, and so can be initialized using random data.sign computes a cryptographic hash of the pointer, discriminator, and signing key, and stores it in the high bits as the signature. auth removes the signature, computes the same hash, and compares the result with the stored signature. strip removes the signature without authenticating it. The aut instructions in the baseline Armv8.3 PAuth feature do not guarantee to trap on authentication failure; instead, they simply corrupt the pointer so that later uses will likely trap. Unless the “later use” follows immediately and cannot be recovered from (e.g. with a signal handler), this does not provide adequate protection against {ref}authentication oracles <authentication-oracles>, so implementations must emit additional instructions to force an immediate trap. This is unnecessary if the processor provides the optional FPAC extension, which guarantees an immediate trap.sign_generic corresponds to the pacga instruction, which takes two 64-bit values and produces a 64-bit cryptographic hash. Implementations of this instruction are not required to produce meaningful data in all bits of the result. :::A discriminator is arbitrary extra data which alters the signature calculated for a pointer. When two pointers are signed differently --- either with different keys or with different discriminators --- an attacker cannot simply replace one pointer with the other.
To use standard cryptographic terminology, a discriminator acts as a salt in the signing of a pointer, and the key data acts as a pepper. That is, both the discriminator and key data are ultimately just added as inputs to the signing algorithm along with the pointer, but they serve significantly different roles. The key data is a common secret added to every signature, whereas the discriminator is a value that can be derived from the context in which a specific pointer is signed. However, unlike a password salt, it's important that discriminators be independently derived from the circumstances of the signing; they should never simply be stored alongside a pointer. Discriminators are then re-derived in authentication operations.
The intrinsic interface in <ptrauth.h> allows an arbitrary discriminator value to be provided, but can only be used when running normal code. The discriminators used by language ABIs must be restricted to make it feasible for the loader to sign pointers stored in global memory without needing excessive amounts of metadata. Under these restrictions, a discriminator may consist of either or both of the following:
The implementation may need to restrict constant discriminators to be significantly smaller than the full size of a discriminator. For example, on arm64e, constant discriminators are only 16-bit values. This is believed to not significantly weaken the mitigation, since collisions remain uncommon.
The algorithm for blending a constant discriminator with a storage address is implementation-defined.
(signing-schemas)=
Correct use of pointer authentication requires the signing code and the authenticating code to agree about the signing schema for the pointer:
As described in the section above on Discriminators, in most situations, the discriminator is produced by taking a constant discriminator and optionally blending it with the storage address of the pointer. In these situations, the signing schema breaks down even more simply:
It is important that the signing schema be independently derived at all signing and authentication sites. Preferably, the schema should be hard-coded everywhere it is needed, but at the very least, it must not be derived by inspecting information stored along with the pointer. See the section on Attacks on pointer authentication for more information.
There are three levels of the pointer authentication language feature:
__ptrauth type qualifier can be used to change how pointers or pointer sized integers are signed when they are stored in a particular variable or field; this provides much stronger protection than is guaranteed by the default rules for C function and data pointers.<ptrauth.h> intrinsic interface for manually signing and authenticating pointers in code. These can be used in circumstances where very specific behavior is required.For the most part, pointer authentication is an unobserved detail of the implementation of the programming language. Any element of the language implementation that would perform an indirect branch to a pointer is implicitly altered so that the pointer is signed when first constructed and authenticated when the branch is performed. This includes:
For more information about this, see the Language ABI section.
However, some aspects of the implementation are observable by the programmer or otherwise require special notice.
The current implementation in Clang does not sign pointers to ordinary data by default. For a partial explanation of the reasoning behind this, see the Theory of operation section.
A specific data pointer which is more security-sensitive than most can be signed using the __ptrauth qualifier or using the <ptrauth.h> intrinsics.
The C standard imposes restrictions on the representation and semantics of function pointer types which make it difficult to achieve satisfactory signature diversity in the default language rules. See Attacks on pointer authentication for more information about signature diversity. Programmers should strongly consider using the __ptrauth qualifier to improve the protections for important function pointers, such as the components of of a hand-rolled “v-table”; see the section on the __ptrauth qualifier for details.
The value of a pointer to a C function includes a signature, even when the value is cast to a non-function-pointer type like void* or intptr_t. On implementations that use high bits to store the signature, this means that relational comparisons and hashes will vary according to the exact signature value, which is likely to change between executions of a program. In some implementations, it may also vary based on the exact function pointer type.
In principle, an implementation could derive the signed null pointer value simply by applying the standard signing algorithm to the raw null pointer value. However, for likely signing algorithms, this would mean that the signed null pointer value would no longer be statically known, which would have many negative consequences. For one, it would become substantially more expensive to emit null pointer values or to perform null-pointer checks. For another, the pervasive (even if technically unportable) assumption that null pointers are bitwise zero would be invalidated, making it substantially more difficult to adopt pointer authentication, as well as weakening common optimizations for zero-initialized memory such as the use of .bzz sections. Therefore it is beneficial to treat null pointers specially by giving them their usual representation. On AArch64, this requires additional code when working with possibly-null pointers, such as when copying a pointer field that has been signed with address diversity.
While this representation of nulls is the safest option for the general case, there are some situations in which a null pointer may have important semantic or security impact. For that purpose Clang has the concept of a pointer authentication schema that signs and authenticates null values.
The current implementation in Clang implicitly signs the return addresses in function calls. While the value of the return address is technically an implementation detail of a function, there are some important libraries and development tools which rely on manually walking the chain of stack frames. These tools must be updated to correctly account for pointer authentication, either by stripping signatures (if security is not important for the tool, e.g. if it is capturing a stack trace during a crash) or properly authenticating them. More information about how these values are signed is available in the Language ABI section.
The current implementation in Clang signs virtual function pointers with a discriminator derived from the full signature of the overridden method, including the method name and parameter types. It is possible to write C++ code that relies on v-table layout remaining constant despite changes to a method signature; for example, a parameter might be a typedef that resolves to a different type based on a build setting. Such code violates C++'s One Definition Rule (ODR), but that violation is not normally detected; however, pointer authentication will detect it.
Whether the current target uses pointer authentication can be tested for with a number of different tests.
__PTRAUTH__ macro is defined if <ptrauth.h> provides its normal interface. This implies support for the pointer authentication intrinsics and the __ptrauth qualifier.__has_feature(ptrauth_returns) is true if the target uses pointer authentication to protect return addresses.__has_feature(ptrauth_calls) is true if the target uses pointer authentication to protect indirect branches. On arm64e this implies __has_feature(ptrauth_returns), __has_feature(ptrauth_intrinsics), and the __PTRAUTH__ macro.__has_feature(ptrauth_intrinsics) and __has_feature(ptrauth_qualifier) are available on arm64e targets. These features are synonymous with each other, and are equivalent to testing for the __PTRAUTH__ macro definition. Use of these features should be restricted to cases where backwards compatibility is required, and should be paired with defined(__PTRAUTH__).Clang provides several other tests only for historical purposes; for current purposes they are all equivalent to ptrauth_calls.
__ptrauth qualifier__ptrauth(key, address, discriminator) is an extended type qualifier which causes so-qualified objects to hold pointers or pointer sized integers signed using the specified schema rather than the default schema for such types.
In the current implementation in Clang, the qualified type must be a C pointer type, either to a function or to an object, or a pointer sized integer. It currently cannot be an Objective-C pointer type, a C++ reference type, or a block pointer type; these restrictions may be lifted in the future.
The current implementation in Clang is known to not provide adequate safety guarantees against the creation of {ref}signing oracles <signing-oracles> when assigning data pointers to __ptrauth-qualified gl-values. See the section on safe derivation for more information.
The qualifier's operands are as follows:
key - an expression evaluating to a key value from <ptrauth.h>; must be a constant expressionaddress - whether to use address diversity (1) or not (0); must be a constant expression with one of these two valuesdiscriminator - a constant discriminator; must be a constant expressionSee Discriminators for more information about discriminators.
Currently the operands must be constant-evaluable even within templates. In the future this restriction may be lifted to allow value-dependent expressions as long as they instantiate to a constant expression.
Consistent with the ordinary C/C++ rule for parameters, top-level __ptrauth qualifiers on a parameter (after parameter type adjustment) are ignored when deriving the type of the function. The parameter will be passed using the default ABI for the unqualified pointer type.
If x is an object of type __ptrauth(key, address, discriminator) T, then the signing schema of the value stored in x is a key of key and a discriminator determined as follows:
address is 0, then the discriminator is discriminator;address is 1 and discriminator is 0, then the discriminator is &x; otherwiseaddress is 1 and discriminator is non-zero, then the discriminator is ptrauth_blend_discriminator(&x, discriminator); see ptrauth_blend_discriminator.Address diversity must impose additional restrictions in order to allow the implementation to correctly copy values. In C++, a type qualified with address diversity is treated like a class type with non-trivial copy/move constructors and assignment operators, with the usual effect on containing classes and unions. C does not have a standard concept of non-triviality, and so we must describe the basic rules here, with the intention of imitating the emergent rules of C++:
ptrauth qualifier or implicit authentication schema that requires address diversity is non-trivial to copy and address-sensitive.<ptrauth.h>This header defines the following types and operations:
ptrauth_keyThis enum is the type of abstract signing keys. In addition to defining the set of implementation-specific signing keys (for example, Armv8.3 defines ptrauth_key_asia), it also defines some portable aliases for those keys. For example, ptrauth_key_function_pointer is the key generally used for C function pointers, which will generally be suitable for other function-signing schemas.
In all the operation descriptions below, key values must be constant values corresponding to one of the implementation-specific abstract signing keys from this enum.
ptrauth_extra_data_tThis is a typedef of a standard integer type of the correct size to hold a discriminator value.
In the signing and authentication operation descriptions below, discriminator values must have either pointer type or integer type. If the discriminator is an integer, it will be coerced to ptrauth_extra_data_t.
ptrauth_blend_discriminatorptrauth_blend_discriminator(pointer, integer)
Produce a discriminator value which blends information from the given pointer and the given integer.
Implementations may ignore some bits from each value, which is to say, the blending algorithm may be chosen for speed and convenience over theoretical strength as a hash-combining algorithm. For example, arm64e simply overwrites the high 16 bits of the pointer with the low 16 bits of the integer, which can be done in a single instruction with an immediate integer.
pointer must have pointer type, and integer must have integer type. The result has type ptrauth_extra_data_t.
ptrauth_string_discriminatorptrauth_string_discriminator(string)
Compute a constant discriminator from the given string.
string must be a string literal of char character type. The result has type ptrauth_extra_data_t.
The result value is never zero and always within range for both the __ptrauth qualifier and ptrauth_blend_discriminator.
This can be used in constant expressions.
ptrauth_type_discriminatorptrauth_type_discriminator(type)
Compute the constant discriminator derived from the given type, as is computed for automatically type diversified schemas.
type must be a type. The result has the type ptrauth_extra_data_t.
This can be used in constant expressions.
ptrauth_function_pointer_type_discriminatorptrauth_function_pointer_type_discriminator(function_type)
Compute the constant discriminator derived from the provided function type, for use in contexts where the default function authentication schema. If function pointer type diversity is enabled, this is equivalent to ptrauth_type_discriminator(function_type), if it is not enabled this is 0.
function_type must be a function type. The result has the type ptrauth_extra_data_t.
This can be used in constant expressions.
ptrauth_stripptrauth_strip(signedPointer, key)
Given that signedPointer matches the layout for signed pointers signed with the given key, extract the raw pointer from it. This operation does not trap and cannot fail, even if the pointer is not validly signed.
ptrauth_sign_constantptrauth_sign_constant(pointer, key, discriminator)
Return a signed pointer for a constant address in a manner which guarantees a non-attackable sequence.
pointer must be a constant expression of pointer type which evaluates to a non-null pointer. key must be a constant expression of type ptrauth_key. discriminator must be a constant expression of pointer or integer type; if an integer, it will be coerced to ptrauth_extra_data_t. The result will have the same type as pointer.
This can be used in constant expressions.
ptrauth_sign_unauthenticatedptrauth_sign_unauthenticated(pointer, key, discriminator)
Produce a signed pointer for the given raw pointer without applying any authentication or extra treatment. This operation is not required to have the same behavior on a null pointer that the language implementation would.
This is a treacherous operation that can easily result in {ref}signing oracles <signing-oracles>. Programs should use it seldom and carefully.
(ptrauth-auth-and-resign)=
ptrauth_auth_and_resignptrauth_auth_and_resign(pointer, oldKey, oldDiscriminator, newKey, newDiscriminator)
Authenticate that pointer is signed with oldKey and oldDiscriminator and then resign the raw-pointer result of that authentication with newKey and newDiscriminator.
pointer must have pointer type. The result will have the same type as pointer. This operation is not required to have the same behavior on a null pointer that the language implementation would.
The code sequence produced for this operation must not be directly attackable. However, if the discriminator values are not constant integers, their computations may still be attackable. In the future, Clang should be enhanced to guarantee non-attackability if these expressions are {ref}safely-derived <safe-derivation>.
ptrauth_auth_with_pc_and_resignptrauth_auth_with_pc_and_resign(pointer, oldKey, oldDiscriminator, oldPC, newKey, newDiscriminator)
Similar to {ref}ptrauth_auth_and_resign <ptrauth-auth-and-resign>, but additionally requires that the original signing schema includes the address of the signing instruction (i.e. uses paciasppc / pacibsppc instead of paciasp / pacibsp). This authenticates pointer signed with oldKey and oldDiscriminator at oldPC, then resigns the raw-pointer result with newKey and newDiscriminator.
Note: oldKey must be ptrauth_key_asia (IA) or ptrauth_key_asib (IB), as only these keys support PC-based authentication instructions. Data keys (ptrauth_key_asda / ptrauth_key_asdb) are not supported.
ptrauth_auth_functionptrauth_auth_function(pointer, key, discriminator)
Authenticate that pointer is signed with key and discriminator and re-sign it to the standard schema for a function pointer of its type.
pointer must have function pointer type. The result will have the same type as pointer. This operation is not required to have the same behavior on a null pointer that the language implementation would.
This operation makes the same attackability guarantees as ptrauth_auth_and_resign.
If this operation appears syntactically as the function operand of a call, Clang guarantees that the call will directly authenticate the function value using the given schema rather than re-signing to the standard schema.
ptrauth_auth_dataptrauth_auth_data(pointer, key, discriminator)
Authenticate that pointer is signed with key and discriminator and remove the signature.
pointer must have object pointer type. The result will have the same type as pointer. This operation is not required to have the same behavior on a null pointer that the language implementation would.
In the future when Clang makes safe derivation guarantees, the result of this operation should be considered safely-derived.
ptrauth_sign_generic_dataptrauth_sign_generic_data(value1, value2)
Computes a signature for the given pair of values, incorporating a secret signing key.
This operation can be used to verify that arbitrary data has not been tampered with by computing a signature for the data, storing that signature, and then repeating this process and verifying that it yields the same result. This can be reasonably done in any number of ways; for example, a library could compute an ordinary checksum of the data and just sign the result in order to get the tamper-resistance advantages of the secret signing key (since otherwise an attacker could reliably overwrite both the data and the checksum).
value1 and value2 must be either pointers or integers. If the integers are larger than uintptr_t then data not representable in uintptr_t may be discarded.
The result will have type ptrauth_generic_signature_t, which is an integer type. Implementations are not required to make all bits of the result equally significant; in particular, some implementations are known to not leave meaningful data in the low bits.
__ptrauth qualifiers<ptrauth.h> additionally provides several macros which expand to __ptrauth qualifiers for common ABI situations.
For convenience, these macros expand to nothing when pointer authentication is disabled.
These macros can be found in the header; some details of these macros may be unstable or implementation-specific.
The threat model of pointer authentication is as follows:
In both of the above paragraphs, it is merely assumed that the attacker‘s current capabilities are restricted; that is, their current exploit does not directly give them the power to do these things. The attacker’s immediate goal may well be to leverage their exploit to gain these capabilities, e.g. to load a malicious dynamic library into the process, even though the process does not directly contain code to do so.
Note that any bug that fits the above threat model can be immediately exploited as a denial-of-service attack by simply performing an illegal access and crashing the program. Pointer authentication cannot protect against this. While denial-of-service attacks are unfortunate, they are also unquestionably the best possible result of a bug this severe. Therefore, pointer authentication enthusiastically embraces the idea of halting the program on a pointer authentication failure rather than continuing in a possibly-compromised state.
Pointer authentication is a form of control-flow integrity (CFI) enforcement. The basic security hypothesis behind CFI enforcement is that many bugs can only be usefully exploited (other than as a denial-of-service) by leveraging them to subvert the control flow of the program. If this is true, then by inhibiting or limiting that subversion, it may be possible to largely mitigate the security consequences of those bugs by rendering them impractical (or, ideally, impossible) to exploit.
Every indirect branch in a program has a purpose. Using human intelligence, a programmer can describe where a particular branch should go according to this purpose: a return in printf should return to the call site, a particular call in qsort should call the comparator that was passed in as an argument, and so on. But for CFI to enforce that every branch in a program goes where it should in this sense would require CFI to perfectly enforce every semantic rule of the program‘s abstract machine; that is, it would require making the programming environment perfectly sound. That is out of scope. Instead, the goal of CFI is merely to catch attempts to make a branch go somewhere that its obviously *shouldn’t* for its purpose: for example, to stop a call from branching into the middle of a function rather than its beginning. As the information available to CFI gets better about the purpose of the branch, CFI can enforce tighter and tighter restrictions on where the branch is permitted to go. Still, ultimately CFI cannot make the program sound. This may help explain why pointer authentication makes some of the choices it does: for example, to sign and authenticate mostly code pointers rather than every pointer in the program. Preventing attackers from redirecting branches is both particularly important and particularly approachable as a goal. Detecting corruption more broadly is infeasible with these techniques, and the attempt would have far higher cost.
Pointer authentication works as follows. Every indirect branch in a program has a purpose. For every purpose, the implementation chooses a {ref}signing schema <signing-schemas>. At some place where a pointer is known to be correct for its purpose, it is signed according to the purpose‘s schema. At every place where the pointer is needed for its purpose, it is authenticated according to the purpose’s schema. If that authentication fails, the program is halted.
There are a variety of ways to attack this.
These attacks arise from weaknesses in the default protections offered by pointer authentication. They can be addressed by using attributes or intrinsics to opt in to stronger protection.
An attacker can simply overwrite a pointer intended for one purpose with a pointer intended for another purpose if both purposes use the same signing schema and that schema does not use address diversity.
The most common source of this weakness is when code relies on using the default language rules for C function pointers. The current implementation uses the exact same signing schema for all C function pointers, even for functions of substantially different type. While efforts are ongoing to improve constant diversity for C function pointers of different type, there are necessary limits to this. The C standard requires function pointers to be copyable with memcpy, which means that function pointers can never use address diversity. Furthermore, even if a function pointer can only be replaced with another function of the exact same type, that can still be useful to an attacker, as in the following example of a hand-rolled “v-table”:
struct ObjectOperations { void (*retain)(Object *); void (*release)(Object *); void (*deallocate)(Object *); void (*logStatus)(Object *); };
The weakness in this design is that by lacking any context specific discriminator, this means an attacker can substitute any of these fields with any other function pointer signed with the default schema. Similarly the lack of address diversity allows an attacker to replace the functions in one type‘s “v-table” with those of another. This can be mitigated by overriding the default authentication schema with a more specific signing schema for each purpose. For instance, in this example, the __ptrauth qualifier can be used with a different constant discriminator for each field. Since there’s no particular reason it's important for this v-table to be copyable with memcpy, the functions can also be signed with address diversity:
#if defined(__PTRAUTH__) #define objectOperation(discriminator) \ __ptrauth(ptrauth_key_function_pointer, 1, discriminator) #else #define objectOperation(discriminator) #endif struct ObjectOperations { void (*objectOperation(0xf017) retain)(Object *); void (*objectOperation(0x2639) release)(Object *); void (*objectOperation(0x8bb0) deallocate)(Object *); void (*objectOperation(0xc5d4) logStatus)(Object *); };
This weakness can also sometimes be mitigated by simply keeping the signed pointer in constant memory, but this is less effective than using better signing diversity.
(access-path-attacks)=
If a signed pointer is often accessed indirectly (that is, by first loading the address of the object where the signed pointer is stored), an attacker can affect uses of it by overwriting the intermediate pointer in the access path.
The most common scenario exhibiting this weakness is an object with a pointer to a “v-table” (a structure holding many function pointers). An attacker does not need to replace a signed function pointer in the v-table if they can instead simply replace the v-table pointer in the object with their own pointer --- perhaps to memory where they‘ve constructed their own v-table, or to existing memory that coincidentally happens to contain a signed pointer at the right offset that’s been signed with the right signing schema.
This attack arises because data pointers are not signed by default. It works even if the signed pointer uses address diversity: address diversity merely means that each pointer is signed with its own storage address, which (by design) is invariant to changes in the accessing pointer.
Using sufficiently diverse signing schemas within the v-table can provide reasonably strong mitigation against this weakness. Always use address and type diversity in v-tables to prevent attackers from assembling their own v-table. Avoid re-using constant discriminators to prevent attackers from replacing a v-table pointer with a pointer to totally unrelated memory that just happens to contain an similarly-signed pointer, or reused memory containing a different type.
Further mitigation can be attained by signing pointers to v-tables. Any signature at all should prevent attackers from forging v-table pointers; they will need to somehow harvest an existing signed pointer from elsewhere in memory. Using a meaningful constant discriminator will force this to be harvested from an object with similar structure (e.g. a different implementation of the same interface). Using address diversity will prevent such harvesting entirely. However, care must be taken when sourcing the v-table pointer originally; do not blindly sign a pointer that is not {ref}safely derived <safe-derivation>.
(signing-oracles)=
A signing oracle is a bit of code which can be exploited by an attacker to sign an arbitrary pointer in a way that can later be recovered. Such oracles can be used by attackers to forge signatures matching the oracle‘s signing schema, which is likely to cause a total compromise of pointer authentication’s effectiveness.
This attack only affects ordinary programmers if they are using certain treacherous patterns of code. Currently this includes:
__ptrauth_sign_unauthenticated intrinsic and__ptrauth-qualified l-values.Care must be taken in these situations to ensure that the pointer being signed has been {ref}safely derived <safe-derivation> or is otherwise not possible to attack. (In some cases, this may be challenging without compiler support.)
A diagnostic will be added in the future for implicitly dangerous patterns of code, such as assigning a non-safely-derived values to a __ptrauth-qualified l-value.
(authentication-oracles)=
An authentication oracle is a bit of code which can be exploited by an attacker to leak whether a signed pointer is validly signed without halting the program if it isn‘t. Such oracles can be used to forge signatures matching the oracle’s signing schema if the attacker can repeatedly invoke the oracle for different candidate signed pointers. This is likely to cause a total compromise of pointer authentication's effectiveness.
There should be no way for an ordinary programmer to create an authentication oracle using the current set of operations. However, implementation flaws in the past have occasionally given rise to authentication oracles due to a failure to immediately trap on authentication failure.
The likelihood of creating an authentication oracle is why there is currently no intrinsic which queries whether a signed pointer is validly signed.
These attacks are not inherent to the model; they arise from mistakes in either implementing or using the sign and auth operations. Avoiding these mistakes requires careful work throughout the system.
Any failure to halt the program on an authentication failure is likely to be exploitable by attackers to create an {ref}authentication oracle <authentication-oracles>.
There are several different ways to introduce this problem:
The implementation might try to halt the program in some way that can be intercepted.
For example, the Armv8.3 aut instructions do not directly trap on authentication failure on processors that lack the FPAC extension. Instead, they corrupt their results to be invalid pointers, with the idea that subsequent uses of those pointers will trigger traps as bad memory accesses. However, most kernels do not immediately halt programs that trap due to bad memory accesses; instead, they notify the process to give it an opportunity to recover. If this happens with an auth failure, the attacker may be able to exploit the recovery path in a way that creates an oracle. Kernels must provide a way for a process to trap unrecoverably, and this should cover all FPAC traps. Compilers must ensure that auth failures trigger an unrecoverable trap, ideally by taking advantage of FPAC, but if necessary by emitting extra instructions.
A compiler might use an intermediate representation (IR) for sign and auth operations that cannot make adequate correctness guarantees.
For example, suppose that an IR uses ARMv8.3-like semantics for auth: the operation merely corrupts its result on failure instead of promising to trap. A frontend might emit patterns of IR that always follow an auth with a memory access, thinking that this ensures correctness. But if the IR can be transformed to insert code between the auth and the access, or if the auth can be speculated, then this potentially creates an oracle. It is better for auth to semantically guarantee to trap, potentially requiring an explicit check in the generated code. An ARMv8.3-like target can avoid this explicit check in the common case by recognizing the pattern of an auth followed immediately by an access.
If code that is part of a pointer authentication operation is interleaved with code that may itself be vulnerable to attacks, an attacker may be able to use this to create a {ref}signing <signing-oracles> or {ref}authentication <authentication-oracles> oracle.
For example, suppose that the compiler is generating a call to a function and passing two arguments: a signed constant pointer and a value derived from a call. In ARMv8.3, this code might look like so:
adr x19, _callback. ; compute &_callback paciza x19 ; sign it with a constant discriminator of 0 blr _argGenerator ; call _argGenerator() (returns in x0) mov x1, x0 ; move call result to second arg register mov x0, x19 ; move signed &_callback to first arg register blr _function ; call _function
This code is correct, as would be a sequencing that does both the adr and the paciza after the call to _argGenerator. But a sequence that computes the address of _callback but leaves it as a raw pointer in a register during the call to _argGenerator would be vulnerable:
adr x19, _callback. ; compute &_callback blr _argGenerator ; call _argGenerator() (returns in x0) mov x1, x0 ; move call result to second arg register paciza x19 ; sign &_callback mov x0, x19 ; move signed &_callback to first arg register blr _function ; call _function
If _argGenerator spills x19 (a callee-save register), and if the attacker can perform a write during this call, then the attacker can overwrite the spill slot with an arbitrary pointer that will eventually be unconditionally signed after the function returns. This would be a signing oracle.
The implementation can avoid this by obeying two basic rules:
The compiler's intermediate representations (IR) should not provide operations that expose intermediate raw pointers. This may require providing extra operations that perform useful combinations of operations.
For example, there should be an “atomic” auth-and-resign operation that should be used instead of emitting an auth operation whose result is fed into a sign.
Similarly, if a pointer should be authenticated as part of doing a memory access or a call, then the access or call should be decorated with enough information to perform the authentication; there should not be a separate auth whose result is used as the pointer operand for the access or call. (In LLVM IR, we do this for calls, but not yet for loads or stores.)
“Operations” includes things like materializing a signed value to a known function or global variable. The compiler must be able to recognize and emit this as a unified operation, rather than potentially splitting it up as in the example above.
The compiler backend should not be too aggressive about scheduling instructions that are part of a pointer authentication operation. This may require custom code-generation of these operations in some cases.
As a refinement of the section on Attackable code sequences, if the attacker has the ability to modify arbitrary register state at arbitrary points in the program, then special care must be taken.
For example, ARMv8.3 might materialize a signed function pointer like so:
adr x0, _callback. ; compute &_callback paciza x0 ; sign it with a constant discriminator of 0
If an attacker has the ability to overwrite x0 between these two instructions, this code sequence is vulnerable to becoming a signing oracle.
For the most part, this sort of attack is not possible: it is a basic element of the design of modern computation that register state is private and inviolable. However, in systems that support asynchronous interrupts, this property requires the cooperation of the interrupt-handling code. If that code saves register state to memory, and that memory can be overwritten by an attacker, then essentially the attack can overwrite arbitrary register state at an arbitrary point. This could be a concern if the threat model includes attacks on the kernel or if the program uses user-space preemptive multitasking.
(Readers might object that an attacker cannot rely on asynchronous interrupts triggering at an exact instruction boundary. In fact, researchers have had some success in doing exactly that. Even ignoring that, though, we should aim to protect against lucky attackers just as much as good ones.)
To protect against this, saved register state must be at least partially signed (using something like ptrauth_sign_generic_data). This is required for correctness anyway because saved thread states include security-critical registers such as SP, FP, PC, and LR (where applicable). Ideally, this signature would cover all the registers, but since saving and restoring registers can be very performance-sensitive, that may not be acceptable. It is sufficient to set aside a small number of scratch registers that will be guaranteed to be preserved correctly; the compiler can then be careful to only store critical values like intermediate raw pointers in those registers.
setjmp and longjmp should sign and authenticate the core registers (SP, FP, PC, and LR), but they do not need to worry about intermediate values because setjmp can only be called synchronously, and the compiler should never schedule pointer-authentication operations interleaved with arbitrary calls.
(relative-addresses)=
Relative addressing is a technique used to compress and reduce the load-time cost of infrequently-used global data. The pointer authentication system is unlikely to support signing or authenticating a relative address, and in most cases it would defeat the point to do so: it would take additional storage space, and applying the signature would take extra work at load time.
Relative addressing is not precluded by the use of pointer authentication, but it does take extra considerations to make it secure:
ptrauth_sign_generic_data.access path attack to replace the pointer through which the relative address is accessed, they can easily cause the relative address to point wherever they want.If an attacker can exactly reproduce the behavior of the signing algorithm, and they know all the correct inputs to it, then they can perfectly forge a signature on an arbitrary pointer.
There are three components to avoiding this mistake:
If an attacker can change the memory protections on certain pages of the program's memory, that can substantially weaken the protections afforded by pointer authentication.
signing oracle <signing-oracles>. The same is true if they can write to the instruction stream.relative addresses <relative-addresses> in global data becomes insecure.Remapping memory in this way often requires the attacker to have already substantively subverted the control flow of the process. Nonetheless, if the operating system has a mechanism for mapping pages in a way that cannot be remapped, this should be used wherever possible.
(safe-derivation)=
Whether a data pointer is stored, even briefly, as a raw pointer can affect the security-correctness of a program. (Function pointers are never implicitly stored as raw pointers; raw pointers to functions can only be produced with the <ptrauth.h> intrinsics.) Repeated re-signing can also impact performance. Clang makes a modest set of guarantees in this area:
An expression of pointer type is said to be safely derived if:
__ptrauth-qualified type, ordata const section of a binary or library.If a value that is safely derived is assigned to a __ptrauth-qualified object, including by initialization, then the value will be directly signed as appropriate for the target qualifier and will not be stored as a raw pointer.
If the function expression of a call is a gl-value of __ptrauth-qualified type, then the call will be authenticated directly according to the source qualifier and will not be resigned to the default rule for a function pointer of its type.
These guarantees are known to be inadequate for data pointer security. In particular, Clang should be enhanced to make the following guarantees:
A pointer should additionally be considered safely derived if it is:
A gl-value should be considered safely derived if it is:
An access to a safely derived gl-value should be guaranteed to not allow replacement of any of the safely-derived component values at any point in the access. “Access” should include loading a function pointer.
Assignments should include pointer-arithmetic operators like +=.
Making these guarantees will require further work, including significant new support in LLVM IR.
Furthermore, Clang should implement a warning when assigning a data pointer that is not safely derived to a __ptrauth-qualified gl-value.
This section describes the pointer-authentication ABI currently implemented in Clang for the Apple arm64e target. As other targets adopt pointer authentication, this section should be generalized to express their ABIs as well.
ARMv8.3 provides four abstract signing keys: IA, IB, DA, and DB. The architecture designates IA and IB for signing code pointers and DA and DB for signing data pointers; this is reinforced by two properties:
I keys and D keys, respectively.arm64e then further subdivides the keys as follows:
A keys are used for primarily “global” purposes like signing v-tables and function pointers. These keys are sometimes called process-independent or cross-process because on existing OSes they are not changed when changing processes, although this is not a platform guarantee.B keys are used for primarily “local” purposes like signing return addresses. These keys are sometimes called process-specific because they are typically different between processes. However, they are in fact shared across processes in one situation: systems which provide fork cannot change these keys in the child process; they can only be changed during exec.The cryptographic hash algorithm used to compute signatures in ARMv8.3 is a private detail of the hardware implementation.
arm64e restricts constant discriminators (used in __ptrauth and ptrauth_blend_discriminator) to the range from 0 to 65535, inclusive. A 0 discriminator generally signifies that no blending is required; see the documentation for ptrauth_blend_discriminator. This range is somewhat narrow but has two advantages:
The AArch64 ISA allows an arbitrary 16-bit immediate to be written over the top 16 bits of a register in a single instruction:
movk xN, #0x4849, LSL 48
This is ideal for the discriminator blending operation because it adds minimal code-size overhead and avoids overwriting any interesting bits from the pointer. Blending in a wider constant discriminator would either clobber interesting bits (e.g. if it was loaded with movk xN, #0x4c4f, LSL 32) or require significantly more code (e.g. if the discriminator was loaded with a mov+bfi sequence).
It is possible to pack a 16-bit discriminator into loader metadata with minimal compromises, whereas a wider discriminator would require extra metadata storage and therefore significantly impact load times.
The string hash used by ptrauth_string_discriminator is a 64-bit SipHash-2-4 using the constant seed b5d4c9eb79104a796fec8b1b428781d4 (big-endian), with the result reduced by modulo to the range of non-zero discriminators (i.e. (rawHash % 65535) + 1).
The kernel must ensure that attackers cannot replace LR due to an asynchronous exception; see Register clobbering. If this is done by generally protecting LR, then functions which don't spill LR to the stack can avoid signing it entirely. Otherwise, the return address must be signed; on arm64e it is signed with the IB key using the stack pointer on entry as the discriminator.
Protecting return addresses is of such particular importance that the IB key is almost entirely reserved for this purpose.
The global offset table (GOT) is not part of the language ABI, but it is a common implementation technique for dynamic linking which deserves special discussion here.
Whenever possible, signed pointers should be materialized directly in code rather than via the GOT, e.g. using an adrp+add+pac sequence on ARMv8.3. This decreases the amount of work necessary at load time to initialize the GOT, but more importantly, it defines away the potential for several attacks:
access path attacks <access-path-attacks> to replace a pointer to a signed pointer with a pointer to the GOT if the signing schema used within the GOT happens to be the same as the original pointer. This kind of collision becomes much less likely to be useful the fewer pointers are in the GOT in the first place.If this can be done for a symbol, then the compiler need only ensure that it materializes the signed pointer using registers that are safe against register clobbering.
However, many symbols can only be accessed via the GOT, e.g. because they resolve to definitions outside of the current image. In this case, care must be taken to ensure that using the GOT does not introduce weaknesses.
access path attack <access-path-attacks>.In either case, the compiler must ensure that materializing the address of a GOT entry as part of producing a signed pointer constant is not vulnerable to register clobbering. If the linker also generates code for this, e.g. for call stubs, this generated code must take the same precautions.
On platforms that support dynamically loading or resolving symbols it is necessary for them to define the pointer authentication semantics of the APIs provided to perform such lookups. While the platform may choose to reply unsigned pointers from such function and rely on the caller performing the initial signing, doing so creates the opportunity for caller side errors that create {ref}signing oracles <signing-oracles>.
On arm64e the dlsym function is used to resolve a symbol at runtime. If the resolved symbol is a function or other code pointer the returned pointer is signed using the default function signing schema described in {ref}C function pointers <c-function-abi>. If the resolved symbol is not a code pointer it is returned as an unsigned pointer.
(c-function-abi)=
On arm64e, C function pointers are currently signed with the IA key without address diversity and with a constant discriminator of 0.
The C and C++ standards do not permit C function pointers to be signed with address diversity by default: in C++ terms, function pointer types are required to be trivially copyable, which means they must be copyable with memcpy.
The use of a uniform constant discriminator greatly simplifies the adoption of arm64e, but it is a significant weakness in the mitigation because it allows any C function pointer to be replaced with another. Clang supports -fptrauth-function-pointer-type-discrimination, which enables a variant ABI that uses type discrimination for function pointers. When generating the type based discriminator for a function type all primitive integer types are considered equivalent due to the prevalence of mismatching integer parameter types in real world code. Type discrimination of function pointers is ABI-incompatible with the standard arm64e ABI, but it can be used in constrained contexts such as embedded systems or in code that does not require function pointer interoperation with the standard ABI (e.g. because it does not pass function pointers back and forth, or only does so through __ptrauth-qualified l-values).
By default the pointer to a C++ virtual table is currently signed with the DA key, address diversity, and a constant discriminator equal to the string hash (see ptrauth_string_discriminator) of the mangled v-table identifier of the primary base class for the v-table. To support existing code or ABI constraints it is possible to use the ptrauth_vtable_pointer attribute to override the schema used for the v-table pointer of the base type of polymorphic class hierarchy. This attribute permits the configuration of the key, address diversity mode, and any extra constant discriminator to be used.
Virtual functions in a C++ virtual table are signed with the IA key, address diversity, and a constant discriminator equal to the string hash (see ptrauth_string_discriminator) of the mangled name of the function which originally gave rise to the v-table slot.
C++'s dynamic_cast presents a difficulty relative to other polymorphic languages that have a top type <https://en.wikipedia.org/wiki/Any_type> as the use of declaration diversity for v-table pointers results in distinct signing schemas for each isolated type hierarchy. As a result it is not possible for the Itanium ABI defined __dynamic_cast entry point to directly authenticate the v-table pointer of the provided object.
The current implementation uses a forced authentication of the subject object‘s v-table prior to invoking __dynamic_cast to partially verify that the object’s vtable is valid. The __dynamic_cast implementation currently relies on this caller side check to limit the substitutability of the v-table pointer with an incorrect or invalid v-table. The subsequent implementation of the dynamic cast algorithm is built on pointer auth protected type_info objects.
In future a richer solution may be developed to support vending the correct authentication schema directly to the dynamic_cast implementation.
The v-table pointer of the std::type_info type is signed with the DA key and no additional diversity.
A member function pointer is signed with the IA key, no address diversity, and a constant discriminator equal to the string hash (see ptrauth_string_discriminator) of the member pointer type. Address diversity is not permitted by C++ for member function pointers because they must be trivially-copyable types.
The Itanium C++ ABI specifies that member function pointers to virtual functions simply store an offset to the correct v-table slot. This ABI cannot be used securely with pointer authentication because there is no safe place to store the constant discriminator for the target v-table slot: if it's stored with the offset, an attacker can simply overwrite it with the right discriminator for the offset. Even if the programmer never uses pointers to virtual functions, the existence of this code path makes all member function pointer dereferences insecure.
arm64e changes this ABI so that virtual function pointers are stored using dispatch thunks with vague linkage. Because arm64e supports interoperation with arm64 code when pointer authentication is disabled, an arm64e member function pointer dereference still recognizes the virtual-function representation but uses an bogus discriminator on that path that should always trap if pointer authentication is enabled dynamically.
The use of dispatch thunks means that == on member function pointers is no longer reliable for virtual functions, but this is acceptable because the standard makes no guarantees about it in the first place.
The use of dispatch thunks also is required to support declaration specific authentication schemas for v-table pointers.
When the __ptrauth qualifier appears in a C++ mangled name, it is mangled as a vendor qualifier with the signature U9__ptrauthILj<key>ELb<addressDiscriminated>ELj<extraDiscriminator>EE.
e.g. int * __ptrauth(1, 0, 1234) will be mangled as U9__ptrauthILj1ELb0ELj1234EE.
If the vtable pointer authentication scheme of a polymorphic class is overridden we mangle the override information with the vendor qualifier __vtptrauth(int key, bool addressDiscriminated, unsigned extraDiscriminator), where the extra discriminator is the explicit value the specified discrimination mode evalutes to.
Block pointers are data pointers which must interoperate with the ObjC id type and therefore cannot be signed themselves. As blocks conform to the ObjC id type, they contain an isa pointer signed as described {ref}below <objc-isa-and-super>.
The invocation pointer in a block is signed with the IA key using address diversity and a constant discriminator of 0. Using a uniform discriminator is seen as a weakness to be potentially improved, but this is tricky due to the subtype polymorphism directly permitted for blocks.
Block descriptors and __block variables can contain pointers to functions that can be used to copy or destroy the object. These functions are signed with the IA key, address diversity, and a constant discriminator of 0. The structure of block descriptors is under consideration for improvement.
In addition to the compile time ABI design, the Objective-C runtime provides additional protection to methods and other metadata that have been loaded into the Objective-C method cache; this protection is private to the runtime.
Objective-C method lists sign methods with the IA key using address diversity and a constant discriminator of 0. Using a uniform constant discriminator is believed to be acceptable because these tables are only accessed internally to the Objective-C runtime.
The method list pointer in Objective-C classes are signed with the DA key using address diversity, and a constant discriminator of 0xC310.
The read-only data pointer in Objective-C classes are signed with the DA key using address diversity, and a constant discriminator of 0x61F8.
(objc-isa-and-super)=
isa and super pointersAn Objective-C object's isa and super pointers are both signed with the DA key using address diversity and constant discriminators of 0x6AE1 and 0x25DA respectively.
SEL pointersBy default, the type of an Objective-C instance variable of type SEL, when the qualifiers do not include an explicit __ptrauth qualifier, is adjusted to be qualified with __ptrauth(ptrauth_key_asdb, 1, 0x57C2).
This provides a measure of implicit at-rest protection to Objective-C classes that store selectors, as in the common target-action design pattern. This prevents attackers from overriding the selector to invoke an arbitrary different method, which is a major attack vector in Objective-C. Since SEL values are not normally passed around as signed pointers, there is a {ref}signing oracle <signing-oracles> associated with the initialization of the ivar, but the use of address and constant diversity limit the risks.
The implicit qualifier means that the type of the ivar does not match its declaration, which can cause type errors if the address of the ivar is taken:
@interface A : NSObject { SEL _s; } @end void f(SEL *); @implementation A -(void)g { f(&_s); } @end
To fix such a mismatch the schema macro from <ptrauth.h>:
#include <ptrauth.h> void f(SEL __ptrauth_objc_sel*);
or less safely, and introducing the possibility of an {ref}signing or authentication oracle <signing-oracles>, an unauthenticated temporary may be used as intermediate storage.
It is not critical for the security of pointer authentication that the signature be stored “together” with the pointer, as it is in Armv8.3. An implementation could just as well store the signature in a separate word, so that the sizeof a signed pointer would be larger than the sizeof a raw pointer.
Storing the signature in the high bits, as Armv8.3 does, has several trade-offs:
void * (such as dlsym) can still return signed pointers. This means that clients of these APIs will not require insecure code in order to correctly receive a function pointer.Armv8.3 implements sign by computing a cryptographic hash and storing that in the spare bits of the pointer. This means that there are relatively few possible values for the valid signed pointer, since the bits corresponding to the raw pointer are known. Together with an auth oracle, this can make it computationally feasible to discover the correct signature with brute force. (The implementation should of course endeavor not to introduce auth oracles, but this can be difficult, and attackers can be devious.)
If the implementation can instead encrypt the pointer during sign and decrypt it during auth, this brute-force attack becomes far less feasible, even with an auth oracle. However, there are several problems with this idea:
strip operation, which might make debuggers and other out-of-process tools far more difficult to write, as well as generally making primitive debugging more challenging.auth-and-load instruction can perform the load and auth in parallel; a processor which instead encrypted the pointer would be forced to perform these operations serially.