RFC: libc++ and pointer field protection

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.

Uploaded #152414 with the proposed CI changes.

Thanks for the RFC. I think most of the aspects discussed here make sense, but I have some comments:

  • Trivial relocation: I think it’s OK to temporarily disable libc++'s emulation of TR until the C++26 feature is stable and landed, at which point the compiler builtin should handle PFP fields properly.
  • Forcing non-standard layout on some types: I don’t think that’s the right way to handle that. Forcing types to be non standard-layout in order to achieve PFP protection on their fields is a roundabout way of doing things. Instead, here are some ideas:
    • Could we change the compiler’s rule for PFPing fields such that that standard-layout types with only private fields still get PFP protection? The libc++ types we want to enable PFP for likely all fall in that category (of having only private members). However, that would make changing a field from private to public be ABI breaking when PFP is enabled, which would be an unusual and surprising ABI particularity of that mode.
    • We could mark those fields with an attribute that makes them PFP protected even if the type is standard layout. However it’s not 100% clear on what fields we should put that attribute, and in theory we would end up annotating almost all pointer fields of all types with that attribute, which doesn’t scale. I still think that would be the best solution here: we can document the macro that applies the attribute with a guideline about when it’s needed, and it looks like such an attribute is already part of your plans (based on skimming the original RFC).
  • For RTTI fields: Do we lose anything significant by leaving those fields be PFP protected? Unless there is a fundamental reason to avoid that, I would just use the default rules and not try to opt-out these fields.

Generally speaking: We already have a bit of custom code to control arm64e pointer authentication related things, for example _LIBCPP_TYPE_INFO_VTABLE_POINTER_AUTH in <typeinfo>. I would like to better understand the relationship between PFP and pointer authentication, especially as it relates to its interaction with the library. Can we handle both in the same way, have attributes that pertain to both, etc? I think it’s important for our sanity to avoid a proliferation of similar-but-different security-related extensions that all require different annotations and have different characteristics. When I say “our sanity”, I mean primarily libc++'s but I really think that extends to arbitrary end users who might try to use these extensions in a similar way: if we can’t find a simple way to handle them uniformly for libc++, I have concerns that end users would be just as much if not more confused than ourselves.

Ack. I spent some time last week working on the builtin support, so it should be ready for when TR support in C++26 is added to libc++.

Right. Another concern is that, unless I’m misunderstanding the rules, standard-layout structs which are identical except for their access controls are required to be layout compatible (permitting casts between them), which would cause problems if we only apply PFP to one of them.

I think this might work. The attribute could live on the struct to minimize the amount of annotation (the existing field attribute could be used to opt out individual fields), and we could make the attribute infectious in the same way as non-standard-layout. Let me try this.

The goal was to avoid unnecessary complexity in the compiler. Without the opt-out, the compiler would need to be taught to sign the pointers in the compiler-generated initializers even though the security benefits are marginal.

At least in terms of the implementation, PAuth ABI and PFP share several implementation details. In terms of the user interface, PFP tries to be largely an implicit mechanism with explicit opt-outs, which differentiates it from things like the opt-in __ptrauth qualifier, and is intended to be transparent as long as you follow the language rules (so normal end users shouldn’t need to think about it). The explicit opt-in for certain libc++ types is expected to be a special case, it should be unusual for user-written types to require annotations because the goal of the PFP design is to maximize the statistical likelihood that a particular field is protected, and individual annotations are less effective at that. It may be worth doing for user-written vocabulary types, though (imagine something like ABSL).

It seems difficult to have the two approaches share a user interface because of the fundamental design differences, but given the explicit opt-out nature I think the additional burden of PFP for users is fairly minimal.

In other words: PFP would automatically opt into this RFC.

Okay, this seems to work well. I’ve updated the libc++ and Clang PRs and posted an update on the main RFC: RFC: Structure protection, a family of UAF mitigation techniques - #22 by pcc