Pointer Authentication — Clang 21.0.0git documentation (original) (raw)

Introduction

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:

Basic Concepts

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 signedand 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:

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:

However, 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>:

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.)

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.

The following details are all implementation-defined:

While the use of the terms “sign” and “signed pointer” suggest the use of a cryptographic signature, other implementations may be possible. SeeAlternative implementations for an exploration of implementation options.

Implementation example: Armv8.3

Readers may find it helpful to know how these terms map to Armv8.3 PAuth:

Discriminators

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 asalt in the signing of a pointer, and the key data acts as apepper. 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.

Language Features

There is currently one main pointer authentication language feature:

Language Extensions

Feature Testing

Whether the current target uses pointer authentication can be tested for with a number of different tests.

__ptrauth Qualifier

__ptrauth(key, address, discriminator) is an extended type qualifier which causes so-qualified objects to hold pointers 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. 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 qualifier’s operands are as follows:

See 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 __ptrauthqualifiers 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:

<ptrauth.h>

This header defines the following types and operations:

ptrauth_key

This enum is the type of abstract signing keys. In addition to defining the set of implementation-specific signing keys (for example, Armv8.3 definesptrauth_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_blend_discriminator

ptrauth_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_discriminator

ptrauth_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_strip

ptrauth_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_constant

ptrauth_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_unauthenticated

ptrauth_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 signing oracles. Programs should use it seldom and carefully.

ptrauth_auth_and_resign

ptrauth_auth_and_resign(pointer, oldKey, oldDiscriminator, newKey, newDiscriminator)

Authenticate that pointer is signed with oldKey andoldDiscriminator 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 aspointer. 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 guaranteed non-attackability if these expressions are safely-derived.

ptrauth_auth_data

ptrauth_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_data

ptrauth_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.

Alternative Implementations

Signature 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:

Hashing vs. Encrypting Pointers

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 authoracles, 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: