| Pointer Authentication |
| ====================== |
| |
| .. contents:: |
| :local: |
| |
| 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: |
| |
| - **Pointer authentication** is a target-independent language technology. |
| |
| - **PAuth** (sometimes referred to as **PAC**, for Pointer Authentication |
| Codes) is an AArch64 architecture extension that provides hardware support |
| for pointer authentication. Additional extensions either modify some of the |
| PAuth instruction behavior (notably FPAC), or provide new instruction |
| variants (PAuth_LR). |
| |
| - **Armv8.3** is an AArch64 architecture revision that makes PAuth mandatory. |
| |
| - **arm64e** is a specific ABI (not yet fully stable) for implementing pointer |
| authentication using PAuth on certain Apple operating systems. |
| |
| This document serves four purposes: |
| |
| - It describes the basic ideas of pointer authentication. |
| |
| - It documents several language extensions that are useful on targets using |
| pointer authentication. |
| |
| - It will eventually present a theory of operation for the security mitigation, |
| describing the basic requirements for correctness, various weaknesses in the |
| mechanism, and ways in which programmers can strengthen its protections |
| (including recommendations for language implementors). |
| |
| - It will eventually document the language ABIs currently used for C, C++, |
| Objective-C, and Swift on arm64e, although these are not yet stable on any |
| target. |
| |
| 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 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: |
| |
| - immediately, on implementations that enforce ``auth`` success (e.g., when |
| using compiler-generated ``auth`` failure checks, or Armv8.3 with the FPAC |
| extension), or |
| |
| - when the resulting pointer value is used, on implementations that don't. |
| |
| 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>``: |
| |
| - ``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. |
| |
| The following details are all implementation-defined: |
| |
| - the nature of a signed pointer |
| - the size of a discriminator |
| - the number and nature of the signing keys |
| - the implementation of the ``sign``, ``auth``, ``strip``, and ``sign_generic`` |
| operations |
| |
| While 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: |
| |
| - A signed pointer is a pointer with a signature stored in the |
| otherwise-unused high bits. The kernel configures the address width based |
| on the system's addressing needs, and enables TBI for I or D keys as |
| needed. The bits above the address bits and below the TBI bits (if |
| enabled) are unused. The signature width then depends on this addressing |
| configuration. |
| |
| - A discriminator is a 64-bit integer. Constant discriminators are 16-bit |
| integers. Blending a constant discriminator into an address consists of |
| replacing the top 16 bits of the pointer containing the address with the |
| constant. Pointers used for blending purposes should only have address |
| bits, since higher bits will be at least partially overwritten with the |
| constant discriminator. |
| |
| - There are five 128-bit signing-key registers, each of which can only be |
| directly read or set by privileged code. Of these, four are used for |
| signing pointers, and the fifth is used only for ``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. While ``aut*`` instructions do not themselves trap on |
| failure in Armv8.3 PAuth, they do with the later optional FPAC extension. |
| An implementation can also choose to emulate this trapping behavior by |
| emitting additional instructions around ``aut*``. |
| |
| - ``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. |
| |
| 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 a |
| `salt <https://en.wikipedia.org/wiki/Salt_(cryptography)>`_ in the signing of a |
| pointer, and the key data acts as a |
| `pepper <https://en.wikipedia.org/wiki/Pepper_(cryptography)>`_. 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 address at which the pointer is stored in memory. A pointer signed with |
| a discriminator which incorporates its storage address is said to have |
| **address diversity**. In general, using address diversity means that |
| a pointer cannot be reliably copied by an attacker to or from a different |
| memory location. However, an attacker may still be able to attack a larger |
| call sequence if they can alter the address through which the pointer is |
| accessed. Furthermore, some situations cannot use address diversity because |
| of language or other restrictions. |
| |
| - A constant integer, called a **constant discriminator**. A pointer signed |
| with a non-zero constant discriminator is said to have **constant |
| diversity**. If the discriminator is specific to a single declaration, it is |
| said to have **declaration diversity**; if the discriminator is specific to |
| a type of value, it is said to have **type diversity**. For example, C++ |
| v-tables on arm64e sign their component functions using a hash of their |
| method names and signatures, which provides declaration diversity; similarly, |
| C++ member function pointers sign their invocation functions using a hash of |
| the member pointer type, which provides type diversity. |
| |
| 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: |
| |
| Signing Schemas |
| ~~~~~~~~~~~~~~~ |
| |
| Correct use of pointer authentication requires the signing code and the |
| authenticating code to agree about the **signing schema** for the pointer: |
| |
| - the abstract signing key with which the pointer should be signed and |
| - an algorithm for computing the discriminator. |
| |
| 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: |
| |
| - the abstract signing key, |
| - a constant discriminator, and |
| - whether to use address diversity. |
| |
| 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: |
| |
| - The language provides the ``<ptrauth.h>`` intrinsic interface for manually |
| signing and authenticating pointers in code. These can be used in |
| circumstances where very specific behavior is required. |
| |
| |
| Language Extensions |
| ~~~~~~~~~~~~~~~~~~~ |
| |
| Feature Testing |
| ^^^^^^^^^^^^^^^ |
| |
| Whether the current target uses pointer authentication can be tested for with |
| a number of different tests. |
| |
| - ``__has_feature(ptrauth_intrinsics)`` is true if ``<ptrauth.h>`` provides its |
| normal interface. This may be true even on targets where pointer |
| authentication is not enabled by default. |
| |
| ``<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 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_t`` |
| ^^^^^^^^^^^^^^^^^^^^^^^^ |
| |
| This 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_discriminator`` |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| |
| .. code-block:: c |
| |
| 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`` |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| |
| .. code-block:: c |
| |
| 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`` |
| ^^^^^^^^^^^^^^^^^ |
| |
| .. code-block:: c |
| |
| 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`` |
| ^^^^^^^^^^^^^^^^^^^^^^^^^ |
| |
| .. code-block:: c |
| |
| 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`` |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| |
| .. code-block:: c |
| |
| 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`` |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| |
| .. code-block:: c |
| |
| ptrauth_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 guaranteed non-attackability if these expressions are safely-derived. |
| |
| ``ptrauth_auth_data`` |
| ^^^^^^^^^^^^^^^^^^^^^ |
| |
| .. code-block:: c |
| |
| 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`` |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| |
| .. code-block:: c |
| |
| 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: |
| |
| - Disadvantage: there are substantially fewer bits available for the signature, |
| weakening the mitigation by making it much easier for an attacker to simply |
| guess the correct signature. |
| |
| - Disadvantage: future growth of the address space will necessarily further |
| weaken the mitigation. |
| |
| - Advantage: memory layouts don't change, so it's possible for |
| pointer-authentication-enabled code (for example, in a system library) to |
| efficiently interoperate with existing code, as long as pointer |
| authentication can be disabled dynamically. |
| |
| - Advantage: the size of a signed pointer doesn't grow, which might |
| significantly increase memory requirements, code size, and register pressure. |
| |
| - Advantage: the size of a signed pointer is the same as a raw pointer, so |
| generic APIs which work in types like `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. |
| |
| 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 ``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: |
| |
| - It's unclear whether this kind of encryption is even possible without |
| increasing the storage size of a signed pointer. If the storage size can be |
| increased, brute-force atacks can be equally well mitigated by simply storing |
| a larger signature. |
| |
| - It would likely be impossible to implement a ``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. |
| |
| - Implementations can benefit from being able to extract the raw pointer |
| immediately from a signed pointer. An Armv8.3 processor executing an |
| ``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. |