This RFC proposes changes to libc++ to support structure protection’s pointer field protection (PFP).
Inhibit non-standard trivial relocation with PFP enabled
An interim non-standard implementation of trivial relocation exists in libc++ and was introduced in #76657. This implementation performs UB because it memcpys non-trivially-copyable objects. This UB is benign with existing compilers, but results in crashes with PFP because pointer fields of non-trivially-copyable types contain a hash derived from the object address (i.e. they are address discriminated) leading to an authentication failure when the pointer is later read.
The initial proposal is to make std::__libcpp_is_trivially_relocatable be equivalent to std::is_trivially_copyable when PFP is enabled. This eliminates the UB, but it effectively disables the interim optimization when PFP is enabled. This change is implemented in #151652.
With C++26, we will generally be able to restore the trivial relocation optimization with PFP. C++26 requires trivial relocations to be done using std::trivially_relocate instead of memcpy, and once libc++ switches to using std::trivially_relocate for trivial relocation, PFP-specific exceptions to trivial relocation in libc++ will no longer be necessary because a PFP-compatible implementation of std::trivially_relocate is possible by extending the compiler intrinsic __builtin_trivially_relocate to re-sign pointer fields and use deactivation symbols to globally disable PFP for pointer fields that are members of union types contained within the type that is being trivially relocated.
Force non-standard layout on certain standard types
As noted, PFP may not be applied to fields of structs that are standard-layout for compatibility reasons. Because the current implementations of various standard library types, including std::unique_ptr, are standard layout, their pointers would not be protected by PFP under this rule. For this reason, we also propose to make various common standard library types non-standard layout when PFP is enabled. Although this is a C++ ABI change, it should be acceptable because PFP already changes the C++ ABI. Because non-standard-layout is an infectious property (it propagates to derived classes and classes with a field of the type) this should also result in many user-defined types becoming non-standard-layout and subject to PFP.
This could be done by, for example, declaring a class with two identical base classes and having the standard library types inherit from it, taking advantage of the C++ rule in [class.prop] that a standard-layout class “has at most one base class subobject of any given type”, such as the following:
class __force_nonstandard_layout_base1 {}; class __force_nonstandard_layout_base2 : __force_nonstandard_layout_base1 {}; class __force_nonstandard_layout : __force_nonstandard_layout_base1, __force_nonstandard_layout_base2 {};
We propose to add the above base class to certain commonly used standard types, starting with std::unique_ptr, std::shared_ptr, std::vector, std::__tree (used by std::set and std::map) and std::function, conditional on PFP being enabled. This change is implemented as part of #151651. This list may expand over time while PFP is still an experimental feature.
The base class is conditional on PFP being enabled in order to avoid changing the result of std::is_standard_layout which could in theory result in ABI breaks for existing programs that do not use PFP.
Opt out PFP for RTTI fields
Pointer fields in RTTI types are unsigned. Signing these fields is unnecessary because PFP is a mechanism for protecting the heap, and the RTTI objects typically live in global variables. Therefore, we should mark the fields with the no_field_protection to inhibit PFP for these fields. This change is implemented as part of #151651.
Consider further ABI breaks for PFP
It is proposed to take advantage of PFP’s ABI break to make further ABI changes that make PFP work better, conditional on PFP being enabled. These changes may be made as long as PFP is an experimental feature. An example of a further change that we may consider is to replace std::vector’s __end_ field with a size field; this will reduce the number of pointer authentication operations that are required when reading and writing the vector’s fields.
Add continuous integration for PFP
Running the libc++ tests built with PFP provides some degree of assurance that libc++’s code remains PFP compatible, and will also act as an additional in-tree integration test of all PFP changes throughout LLVM, on top of the integration tests being added in #151655.
An LLVM build is necessary in order to pick up the latest changes for PFP, which is an in-development compiler feature. The configuration would be similar to the bootstrapping-build configuration in libcxx/utils/ci/run-buildbot. I could not find where bootstrapping-build is being invoked from (it is unreferenced from buildkite-pipeline.yml and zorg), but it seems to be maintained (e.g. #119028) so I guess it is being run some other way. Hopefully that other way would accommodate an additional AArch64 Linux builder. Note that it would not be a requirement for the builder to support pointer authentication; emulated PAC will allow the tests to run regardless of whether pointer authentication is supported, with the same ability to detect PFP-breaking bugs.