Introduction
In this RFC, we propose adding support for the __ptrauth qualifier
. This qualifier causes so-qualified objects to hold pointers signed using the specified pointer authentication schema rather than the default schema for the type. The default schema is constrained by the rules of the base language standard in ways that generally reduce the effectiveness of pointer authentication as a security mitigation; __ptrauth
allows programmers to conveniently opt in to stronger mitigation.
This RFC assumes a basic understanding of what pointer authentication is and how it works. If you’d like to learn more about pointer authentication, you can visit the following links:
clang documentation
RFC: Pointer authentication for arm64e
The __ptrauth qualifier
The __ptrauth
qualifier has the following grammar:
type-qualifier:
__ptrauth ( argument-expression-list[opt] )
At least three argument expressions are required:
- The first argument, the key, must be an integer constant expression evaluating to one of the integral key values declared in
ptrauth.h
. - The second argument, the address diversity flag, is contextually converted to
bool
and must then be a constant expression of boolean type. If the value of the expression istrue
, the type is said to be qualified with address diversity, which means the storage address of the object will be used as part of computing the discriminator for values stored in the object. - The third argument, the constant discriminator, must be an integer constant expression. This value is used to provide constant diversity to values stored in the object.
The ideas of address diversity and constant diversity are explained here:
https://clang.llvm.org/docs/PointerAuthentication.html#discriminators
As a brief summary, pointer authentication provides a more effective mitigation when pointers are signed with both address diversity and a constant discriminator that is different from other places that store signed pointers. For example, signing function pointers in a virtual table with both address diversity and method-specific discriminators makes it difficult to either forge a virtual table from scratch or replace an object’s virtual table with a different valid table.
Additional arguments are allowed to provide room for further extension. The rules and restrictions on these arguments will be documented here as extensions are implemented.
Example
typedef int (*FuncPtr)(int);
extern int foo(int);
int i;
// Sign &foo with key=1, address discrimination disabled, and extra discriminator 123.
FuncPtr __ptrauth(1, 0, 123) fp1 = &foo;
// Sign &foo with key=0, address discrimination enabled, and extra discriminator 234.
FuncPtr __ptrauth(0, 1, 234) fp2 = &foo;
// Sign &i with key=2, address discrimination enabled, and extra discriminator 345.
int * __ptrauth(2, 1, 345) p = &i;
// Sign &foo with key=0, address discrimination enabled, and extra discriminator 234. Initialize s.fp with the signed pointer.
struct S {
FuncPtr __ptrauth(0, 1, 234) fp;
} s = { &foo };
Restrictions
- The qualified type must be a C/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.
__ptrauth
cannot currently be combined with the_Atomic
qualifier.- A program is ill-formed if it uses a
__ptrauth
qualifier directly on a function parameter or return type. The qualifier is silently removed in these positions if added by atypedef
or by template substitution. - The operands currently must always be constant expressions, even within templates.
Several of these restrictions are implementation limitations and may be lifted in the future.
Type identity and compatibility
The type __ptrauth(...) T
is the same type as __ptrauth(...) U
if the qualifiers are the same and T
and U
are the same. Two __ptrauth
qualifiers are the same if the constant values of the operands are the same; they need not use similar expressions to compute those values.
The type __ptrauth(...) T
is compatible with __ptrauth(...) U
if T
and U
are compatible and the __ptrauth
qualifiers are the same. Note that this implies that it is generally invalid to access objects of __ptrauth
-qualified type through l-values of a different type. In effect, the mechanics of pointer authentication enforce a weak form of strict aliasing around signed pointers even when strict aliasing is otherwise disabled in the compiler. Only the pointer authentication schema is relevant to this enforcement. In particular, when strict aliasing is disabled, accessing an object of type __ptrauth(...) T
through an l-value of type __ptrauth(...) U
is effectively well-defined as long as the __ptrauth
qualifiers are the same, even if the underlying types T
and U
are not compatible types. When strict aliasing is enabled, of course, the standard restrictions on incompatibly-typed accesses still apply and the underlying types must also be compatible; however, this is not enforced by pointer authentication.
Non-triviality from address diversity
Address diversity imposes additional restrictions in order to allow the implementation to correctly copy values.
C++ has standard concepts of non-triviality for copying and destroying values, and address diversity naturally affects these. In C++, the builtin operations to copy-initialize, move-initialize, copy-assign, or move-assign an object of a type qualified with address diversity are considered to be non-trivial operations, exactly as if the qualified type were a class with a user-provided corresponding special member. Default initialization and destruction remain trivial. Note that adding a field whose type is qualified with address diversity to a class that is otherwise trivially copyable is likely to be an ABI-breaking change even if the size and alignment of the class do not change.
C does not have standard concepts of non-triviality, and so we must describe the basic rules here, with the intention of imitating the emergent rules of C++:
- A type may be non-trivial to copy.
- A type may also be illegal to copy. Types that are illegal to copy are always non-trivial to copy.
- A type may also be address-sensitive.
- A type qualified with address diversity is non-trivial to copy and address-sensitive.
- An array type is illegal to copy, non-trivial to copy, or address-sensitive if its element type is illegal to copy, non-trivial to copy, or address-sensitive, respectively.
- A struct type is illegal to copy, non-trivial to copy, or address-sensitive if it has a field whose type is illegal to copy, non-trivial to copy, or address-sensitive, respectively.
- A union type is both illegal and non-trivial to copy if it has a field whose type is either non-trivial to copy or illegal to copy.
- A union type is address-sensitive if it has a field whose type is address-sensitive.
- A program is ill-formed if it uses a type that is illegal to copy as a function parameter, argument, or return type.
- A program is ill-formed if an expression requires a type to be copied that is illegal to copy.
- Otherwise, copying a value of a type that is non-trivial to copy must correctly copy its subobjects as if they were individually loaded and then assigned into the new location.
- Types that are address-sensitive must always be passed and returned indirectly. Thus, changing the address-sensitivity of a type may be ABI-breaking even if its size and alignment do not change.
Properly-signed values
An object of a type with the __ptrauth
qualifier is said to have a properly-signed value if:
- it is assigned a value by an initializer;
- it is assigned a value by an assignment operator, such as
=
or+=
; - the object representation of a null pointer of the underlying type is written into the object (as if by
bzero
, on platforms where the null pointer has the zero bit-pattern); or else - the object representation of an object of the same type and with a properly-signed value is written into the object, as if by
memcpy
. Furthermore, if the type is qualified with address diversity, the object representation must originate from an object at the same storage address (e.g. it may have beenmemcpy
’d away and then back again) or otherwise be properly signed for that address (e.g. using theptrauth
intrinsics).
Reading the value of an object whose type has the __ptrauth
qualifier that does not have a properly-signed value may (but cannot be guaranteed to) immediately halt the program. Situations causing such a read include applying an lvalue-to-rvalue conversion on an lvalue that resolves to the object, using an lvalue that resolves to the object as the left operand of a compound assignment operator, and copying or moving an aggregate object containing the object. An object need not contain a properly-signed value prior to a new value being assigned into it or when the object is destroyed.
Clang consensus called in this message.