PFP is only compatible with LAM57, where only 6 bits of the top byte are ignored. From the latest Intel SDM:
With LAM57 and 4-level paging, bit 63 and bits 56:47 of the original linear address must be identical. With LAM57 and 5-level paging, bit 63 and bit 56 of the original linear address must be identical.
We arrange for the hash difference (in case of a mismatching hash) to appear in bits 55:48 of the pointer in order to trigger a general protection fault (in case of 4-level paging) or an unrecoverable page fault (in case of 5-level paging and in the likely case that most of the address space is unused).
The generic form of PFP only mitigates certain UAF bugs involving a type confusion. For example, if an object of type S1 is deallocated, an object of type S2 is allocated at the same address, and a dangling pointer to the object of type S1 is used to access S1::a (now occupied by S2::a), that will result in a non-canonical pointer being read, leading to a crash when loading or storing data via the pointer.
There is no need to zero the pointer fields on deallocation, which is beneficial for performance. We only rely on the fact that the in-memory pointer representations are different for the different types.
The libc++ developers asked for an attribute for oping a type into PFP to replace the previously implemented hack of adding base classes that force the type to become non-standard layout. I spent some time considering it and decided on this proposal:
The [[clang::pointer_field_protection]] attribute may be added to a struct and will do two things:
Opts a type into PFP, even when PFP is not opted into on the command line. PFP-ness is also inherited by derived classes and classes with a field of the type.
Forces the type to be considered non-standard layout (e.g. std::is_standard_layout returns false).
The justification for 1 is to support users who would prefer a fully source based opt-in approach.
The justification for 2 is that PFP breaks the standard-layout rules (e.g. struct pointer can no longer be cast to/from the first field’s type), so it is reasonable to make queries such as std::is_standard_layout return an appropriate result.
A consequence of the above is that with the previous flag scheme of -fexperimental-pointer-field-protection={none,untagged,tagged} there was no way of specifying that pointers may have address discriminated pointers without also specifying that non-standard-layout types should be auto-opted-into PFP. So I decided to replace -fexperimental-pointer-field-protection={...} with two separate flags with the following semantics:
-fexperimental-pointer-field-protection: Enable pointer field protection on all types that are not considered standard-layout according to the C++ rules for standard layout. Specifying this flag also defines the predefined macro __POINTER_FIELD_PROTECTION__.
-fexperimental-pointer-field-protection-tagged: On architectures that support it (currently only AArch64), for types that are not considered trivially copyable, use the address of the object to compute the pointer encoding. Specifying this flag also defines the predefined macro __POINTER_FIELD_PROTECTION_TAGGED__.