RFC: `__ptrauth` qualifier

Yeah. It’s also problematic for some of the most important use cases like hand-rolled v-tables. V-table entries really want to use both address and constant diversity, but with the intrinsics that means passing a pretty complicated expression as the discriminator argument:

struct vtable my_vtable = {
  ptrauth_sign_constant(&my_function_pointer,
                        ptrauth_key_function_pointer,
                        ptrauth_blend_discriminator(&my_vtable.current_field, 0x1234)),
  ...
};

So, first off, this muddies writing constant v-table initializers a lot, and you have to write it separately and correctly on every single v-table. __ptrauth allows you to just write the qualifier in the struct definition, and then writing initializers just works.

But it also complicates the compiler and implementation quite a bit, because what expressions are you actually going to allow as the constant discriminator? If it can be an arbitrary blend call, then the image format and loader have to support that, which is a lot of extra encoding and runtime complexity. __ptrauth allows you to just support the simple pattern of blending the storage address with a constant. To allow just that with the intrinsics, you’d need to recognize that the pointer passed to ptrauth_blend_discriminator was actually the address of the current field being initialized, which is a pretty weird rule (and probably doesn’t work well in the new age of C++ constant evaluation).

1 Like

Thank you for the information!

It sounds to me like there is a quite small (but important) market for the qualifier today, and that market might grow somewhat but not into a large market (like what we expect for bounds safety, say). It’s tough to call whether that’s “significant” enough or not, but because of the improved security posture the feature brings, I think it meets that criteria (if just barely).

It’s not being proposed to a standards body but there are avenues where it would make sense. A TR like TR 18037 is one possible approach (though the committee has not produced a TR in a long time, I expect this would wind up needing a TS because it’s not really a white paper-like thing, it’s more of a feature-like thing). WG14 N3321 is another possible avenue, but its current design doesn’t seem able to support what you need – it would be worth reaching out to the author of that proposal to mention this qualifier as a good design case for them to consider how to support, but I don’t think this RFC needs to be blocked on that proposal (which hasn’t yet been seen by the committee anyway). That said, I tend to agree that this isn’t really appropriate for standardization as-is because of the lack of hardware support across architectures, so I think this criteria is sufficiently met.

The need to reside upstream is still somewhat questionable, I think. I agree with the point that it would be nice to not restrict this to Apple-provided tools, but there’s not really a sign that this will ever be used outside of Apple’s tools, at least that I can see. I don’t really see basing tools on Apple’s downstream fork is an undesirable splintering of the community by itself as that’s not an uncommon situation for significant downstream extensions. I think it becomes undesirable when the extension gains enough traction in the industry that a lot of users and other vendors want it. Do you know if there’s a large constituency of upstream Clang users who want to target Apple platform SDKs without use of Apple’s Clang?

This sort of feels like a feature we’re going to want, but maybe in a few years once it’s clear the qualifier is not specific to the ARM feature or Apple’s SDKs. That’s not to say that I think we’ve rejected this RFC, but it would help me make a decision if it was more clear why now is the right time for this to be added upstream, or if there was a broader array of hardware support, or if there was a significantly larger community of people wanting to add the qualifier to their own code, etc. (Btw, I’ve started asking around internally at Intel whether this is something we would like to support or already have some support for; if I hear anything back I can speak about in public, I’ll be sure to bring it back here.)

Okay. I’d like to understand the line you’re drawing here. Clang has generally been very accommodating of target-specific features that just come down to adding new things in certain categories, to the point that we typically don’t even require RFCs for them. Target-specific builtin functions are certainly the broadest such category, but there are also:

  • target-specific builtin types (e.g. __ibm128)
  • target-specific calling conventions (almost all of them)
  • target-specific address spaces (e.g. wasm’s __funcref)
  • target-specific vector kinds (e.g. neon_vector_type)

and probably a handful of other things I’m overlooking. Qualifiers are not currently one of those categories, though, except via the general address-space mechanism, and they do require broader logic because of how they combine with other features. So your feeling is that a new qualifier, and the extra checking that that necessarily entails, may be too invasive for a single-target feature and requires this additional justification?

I know there are a few large companies that do this. I can’t speak to their interests, though, and it’s possible that they’ll never be interested in targeting arm64e.

Just a practical concern: the __ptrauth qualifier is already in clang since commit f4efa06743, there is just no syntax/sema for it (this was apparently done to support a use case from lldb)? But can you exercise all functionality from that commit without syntax to write tests? For example, the logic in removeCommonQualifiers.

1 Like

Definitely! And FWIW, I’m mostly trying to understand the shape of things rather than draw any lines in the sand. More below.

We’ve generally never said no to any extension and that’s ended up causing problems, some of which are more obvious than others, some of which we have more control over than others. So I’m trying to add a smidgeon more rigor to deciding whether we should accept an extension or not, hopefully without throwing good work away.

Because we have had a history of saying “yes” to literally everything, it’s really hurting our ability to maintain the compiler as C and (particularly) C++ evolve. C++ adds a lot of incredibly expensive-to-implement-and-support features and each of our extensions have to find some way to fit into those new standard features (as well as all of the existing ones) in a way that our users don’t find frustrating. So I think we need to be more careful about what extensions we add because basic C and C++ support will almost always be more important to the majority of our users than almost any extension we add and the C++ committee in particular has shown no real concern over causing significant problems for existing implementer extensions.

Also, the original RFC you linked was not really commented on by the community at all (it was up for two years without any comment, then when it was mentioned that patches were coming there was a question asked and answered, but basically nobody expressed much interest in the feature. So that’s a sign the community may not want the maintenance burden. It may also be that Discourse is very hard to spot RFCs on (I know I’ve missed many).

All that said, I (personally) want to see Clang continue to improve security posture in the ecosystem and this feature absolutely hits that goal. I want to find ways to say “yes” to it, but I also want to make sure we’re not causing ourselves more long-term pain by doing so. (I also think we need to start saying “no” to more extensions – we’ve become very top-heavy as a compiler and our bug database shows it; we accrue technical debt far, far faster than we’re able to pay down currently.)

Hopefully that makes sense, but if you want to hop into a call to discuss this more, I’d be more than happy to do so.

Right, so I think we want a more complete model of how this qualifier would interact with template argument deduction as we can definitively end up with cases where we would deduce a __ptrauth in a function parameter.

I mean, even before talking about deduction, the simplest case would be

int * __ptrauth() a;`
void f(decltype(a));

Is that well formed? If it is we do need overload resolution rules for this qualifier vs const/volatile/restrict/etc
And if all of that works, why the restriction that it cannot be syntactically applied to a parameter?

Before even talking about C++, adding qualifiers also impacts _Generic in C, typeof_unqual, the various builtins to decay/strip qualifiers, [[clang::overloadable]], etc.

So in all of theses case we’d need to understand:

  • When we do strip or not the qualifiers
  • Deduction rule
  • Ordering rules

These are all really good concerns Corentin, to go further, what is the library implications here: we already have issues with __restrict__ not supported properly in libc++, I wonder what expectation THIS is going to cause, particularly in the C libraries.

One concern I have vs a lot of the other target specific extensions is that qualifiers are more ‘expensive’, even when not used. The builtins for example only cost very little (the cost of searching by them during name lookup) if you don’t use them.

Calling conventions WILL SOON start costing a lot (since we’ve only got room for ~8 more before we have to increase the size of a function decl), which I’ve very much had problems with.

Qualifiers/Qualtype/etc are used everywhere, so extending it is going to have fairly sizable implications on the speed/size of every single compilation, so I’d want to ensure any extension in this space mitigates that.

Just to make things a bit more visible :slight_smile: I am aware of some downstream ELF-based platforms that are interested in using pointer authentication in their SDKs by default. Also, there is some indication that other ELF-related platforms might want to optionally enable pointer authentication for some components.

I hope that the code available as a part of LLVM 19 release will allow users to finally start trying to use pointer authentication on their platforms, so this definitely would expand beyond Apple SDKs.

The mentioned qualifier from the RCF is an extremely useful addition especially for runtime / library writers as it makes pointer authentication explicit as a part of type system and would prevent accidental mistakes. Even more, it could be considered as a useful addition to improve the security: in the recent pauth sync we discussed potential weakness when function pointer is casted to void* as a part of some type-erasure scheme, stored somewhere and then loaded casted to normal function pointer back. Having this void* to be _ptrauth-qualified would significantly reduce the overall attack surface while keeping the required source code changes minimal.

You’re right that the rules above don’t technically cover this. They also don’t technically cover using type aliases. People often use “typedef” as a gloss for every indirect way that you might get a type without writing it out in that exact location, and that was the intent here; but we can make that clearer in the specification.

The intent is that parameters are never qualified at the top level — the code is either ill-formed or the qualifier is silently removed. This rule is specifically in place to avoid the possibility of such qualifiers becoming part of the function type (and affecting the argument-passing ABI), which is otherwise unprecedented in the base language standards. If we did support that, then yes, we would need to add rules on how they affected overload resolution, template argument deduction, and so on. But we don’t, so there are no special deduction or ordering rules needed.

Clang supports several extended qualifiers today, and we already have rules that seem to work quite well for everything you’ve mentioned. Are you asking me to tell you those rules? I guess I can, but this is not new to __ptrauth.

As a general rule, all of these features either only consider a specific qualifier or apply consistently to all top-level qualifiers. For example, _Generic is specified (in the base standard, in fact) as applying l-value conversions to the controlling expression before determining the type, and those conversions are understood to remove all top-level qualifiers, which would include __ptrauth. std::remove_const, on the other hand, just affects the const qualifier and leaves everything else alone, including volatile, address spaces, all of the ObjC qualifiers, and (under this proposal) __ptrauth.

Under a general understanding of qualifiers, it turns out that the rule in which qualifiers can be added in indirect positions, affecting overload resolution[1], is a special behavior of the const and volatile qualifiers. Other qualifiers generally cannot be added or removed in pointee position because they affect the representation in memory at that level; as a result, no ordering rule is required to handle this because, if candidates were mismatched in this way, at most one of them would be viable in the first place.


  1. This is the rule that makes e.g. void foo(T const * const *); a worse match than void foo(T **);. ↩︎

Sorry for this RFC falling off my radar, thank you to @akorobeynikov for reminding me. :slight_smile:

I spoke with folks internally at Intel about a similar technology they’re working on and whether the ptrauth design would meet their needs and they came back with positive support for the work.

I believe this RFC has consensus, so if anyone disagrees, please speak up ASAP with details.

2 Likes