Date: Tue, 11 Nov 2025 15:33:42 -0800
> On Nov 11, 2025, at 6:34 AM, Frederick Virchanza Gotham via Std-Proposals <std-proposals_at_[hidden]> wrote:
>
> On Mon, Nov 10, 2025 at 10:08 AM Oliver Hunt wrote:
>>
>> Re: funky - it would actually just be impossible, it is not defined
>> in a context where C++ exists, and it has no support for the C++
>> type system, hence it being invalid to use it over most C++ types.
>
>
> You’re absolutely right about `qsort` using ` void* ` and not having access
> to the C++ type system – I’d simply overlooked that.
>
> I have a question for you Oliver about `std::restart_lifetime` in the presence
> of arm64e pointer authentication.
>
> For the sake of argument, let's suppose that the compiler has a builtin
> function as follows:
>
> void const * __builtin_get_address_vtable(T);
>
> which, for a class type `T`, yields the address of the vtable.
>
> Then we could print the vtable address of 'std::ostream' as follows:
>
> #include <iostream>
> using namespace std;
> int main()
> {
> cout << __builtin_get_address_vtable(ostream) << endl;
> }
This is essentially what copy constructors do - `__builtin_get_address_vtable` is a reference to the vtable symbol.
The problem is that trivial relocation and restart lifetime are/were specified to maintain the dynamic type of the object, so you can’t simply store the vptr for the static type, but rather have to load the vptr from the object being relocated/restarted.
The semantics specified here would mean that even on platforms that don’t have ptrauth relocation/restart would require fixups of all the objects.
I’ve got an example below in terms of an explicitly qualified field that I think makes the issue clearer
>
> I had been thinking of `std::restart_lifetime<T>` as
> “whatever the implementation needs to do so that, after relocation,
> an object of type `T` behaves as if it had always been at the new
> address”. On many current ABIs, this would be effectively a no-op, but
> on arm64e (or on my own new compiler that XOR's the vptr), it clearly
> has real work to do.
>
> In the Kona discussion last Wednesday, it was suggested that an
> implementation might want an interface roughly like:
>
> template<class T>
> T *restart_lifetime(T const *old_p, T *new_p);
>
> where `old_p` is the old address of the object and `new_p` is the new
> address, so the implementation can use both when fixing up vptrs and
> signatures.
>
> What I’m trying to understand though is why the old address has to be
> exposed in the interface at all?
Moving a vptr requires authenticating the vptr, which requires knowing the original address. I’ve realized this is actually easier to explain the problem in terms of an explicit __ptrauth qualified field:
struct MyCoolStruct {
some_cool_type_t field1;
void*(* __ptrauth(1,1,1234) some_fptr)(int);
};
void some_function(MyCoolStruct *dst, const MyCoolStruct *src) {
memmove(dst, src, sizeof(MyCoolStruct));
restart_lifetime(dst);
}
In this case we’re using the `restart_lifetime` definition that does not have an origin address. Because we don’t have the origin address we cannot authenticate `some_fptr`, which means our only option is to strip the signature from the pointer, and then sign that pointer. The effect of this is that we have broken the chain of trust and an attacker can do the following - please note that an attacker does not have control of code execution, these are all “primitives” constructed from chaining other bugs together. The ptrauth threat model assumes that an attacker already has these, and is now trying to leverage them to get code execution:
void *attacker_created_arbitrary_read(address);
void attacker_created_arbitrary_write(address, value);
void attacker_created_mechanism_to_trigger_restart_lifetime();
// Attacker code - not using actual code, but manipulating control flow
// and data using the primitives that they've constructed
desired_function_or_address = attacker_created_arbitrary_read(...);
target_object = attacker_created_arbitrary_read(...);
attacker_created_arbitrary_write(target_object + field_offset, desired_function_or_address);
attacker_created_mechanism_to_trigger_restart_lifetime();
As above, because we did not have the origin address in restart lifetime, we’ve had to just remove the existing signature and apply a new correct one. Because of that an attacker no longer needs to worry about constructing a valid signature on the pointer, we’ll simply provide one for them. The result is that the chain of trust in control flow is broken, and the attacker can now call any code they want to. This is what is called a signing oracle - a method by which an attacker can convert a value they control into a value that is correctly signed and usable in authenticated operations.
> Given the compiler builtin function `__builtin_get_address_vtable(T)`,
> it feels like we ought to be able to implement `restart_lifetime` with
> only the new pointer, by simply discarding the old vptr value and
> reinitializing it, maybe something like:
>
> template<class T>
> T *restart_lifetime(T *const p)
> {
> void const *vptr = __builtin_get_address_vtable(T);
> *reinterpret_cast<void * volatile *>(p) = vptr;
> sign_pointer_at_address(p); // arm64e pointer authentication or similar
> return p;
> }
Right, the problem is that this is copy constructor semantics rather than TR, and as above requires non-memmove behavior even absent ptrauth, and does not work for protected fields that do not have compile/load time constant values
>
> (Here `sign_pointer_at_address` is pseudocode for whatever pointer
> authentication primitive the implementation actually uses to sign the
> vptr based on the address of `*p`.)
>
> So my concrete question is:
>
> Given an implementation with this kind of ability to obtain the
> appropriate vtable address for `T`, why is a two-argument interface:
>
> restart_lifetime(T const *old_p, T *new_p)
>
> still needed on arm64e? What does `old_p` let you do that you could
> not, in principle, do by discarding the old vptr and synthesizing
> a fresh, correctly signed vptr at `new_p`?
>
> I’m trying to see the specific arm64e (or similar) scenario that forces
> the two-argument shape here, rather than a single-argument:
>
> T* restart_lifetime(T *p);
>
> that just reconstructs the vptr at the new address.
>
> And some people also mentioned scenarios, such as serialisation,
> where there might not be an old address.
Hopefully the above explains why the two argument interface is required in detail, but to directly answer this at a high level copy constructors work because a copy constructors and similar are placing the vtable pointer for the static type of the object, which means it is signing a global constant that an attacker cannot change.
Trivial relocation/restart_lifetime copies the existing vtable (i.e. the dynamic type, which may not match the static type) so has to authenticate that pointer and then sign the new one. If you don’t have the initial address of the object, you can’t authenticate, so you either don’t allow it such relocation/restart, or you have to strip the signature and create a correctly signed pointer for whatever was present in the existing object, even if it is attacker controlled.
—Oliver
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>
> On Mon, Nov 10, 2025 at 10:08 AM Oliver Hunt wrote:
>>
>> Re: funky - it would actually just be impossible, it is not defined
>> in a context where C++ exists, and it has no support for the C++
>> type system, hence it being invalid to use it over most C++ types.
>
>
> You’re absolutely right about `qsort` using ` void* ` and not having access
> to the C++ type system – I’d simply overlooked that.
>
> I have a question for you Oliver about `std::restart_lifetime` in the presence
> of arm64e pointer authentication.
>
> For the sake of argument, let's suppose that the compiler has a builtin
> function as follows:
>
> void const * __builtin_get_address_vtable(T);
>
> which, for a class type `T`, yields the address of the vtable.
>
> Then we could print the vtable address of 'std::ostream' as follows:
>
> #include <iostream>
> using namespace std;
> int main()
> {
> cout << __builtin_get_address_vtable(ostream) << endl;
> }
This is essentially what copy constructors do - `__builtin_get_address_vtable` is a reference to the vtable symbol.
The problem is that trivial relocation and restart lifetime are/were specified to maintain the dynamic type of the object, so you can’t simply store the vptr for the static type, but rather have to load the vptr from the object being relocated/restarted.
The semantics specified here would mean that even on platforms that don’t have ptrauth relocation/restart would require fixups of all the objects.
I’ve got an example below in terms of an explicitly qualified field that I think makes the issue clearer
>
> I had been thinking of `std::restart_lifetime<T>` as
> “whatever the implementation needs to do so that, after relocation,
> an object of type `T` behaves as if it had always been at the new
> address”. On many current ABIs, this would be effectively a no-op, but
> on arm64e (or on my own new compiler that XOR's the vptr), it clearly
> has real work to do.
>
> In the Kona discussion last Wednesday, it was suggested that an
> implementation might want an interface roughly like:
>
> template<class T>
> T *restart_lifetime(T const *old_p, T *new_p);
>
> where `old_p` is the old address of the object and `new_p` is the new
> address, so the implementation can use both when fixing up vptrs and
> signatures.
>
> What I’m trying to understand though is why the old address has to be
> exposed in the interface at all?
Moving a vptr requires authenticating the vptr, which requires knowing the original address. I’ve realized this is actually easier to explain the problem in terms of an explicit __ptrauth qualified field:
struct MyCoolStruct {
some_cool_type_t field1;
void*(* __ptrauth(1,1,1234) some_fptr)(int);
};
void some_function(MyCoolStruct *dst, const MyCoolStruct *src) {
memmove(dst, src, sizeof(MyCoolStruct));
restart_lifetime(dst);
}
In this case we’re using the `restart_lifetime` definition that does not have an origin address. Because we don’t have the origin address we cannot authenticate `some_fptr`, which means our only option is to strip the signature from the pointer, and then sign that pointer. The effect of this is that we have broken the chain of trust and an attacker can do the following - please note that an attacker does not have control of code execution, these are all “primitives” constructed from chaining other bugs together. The ptrauth threat model assumes that an attacker already has these, and is now trying to leverage them to get code execution:
void *attacker_created_arbitrary_read(address);
void attacker_created_arbitrary_write(address, value);
void attacker_created_mechanism_to_trigger_restart_lifetime();
// Attacker code - not using actual code, but manipulating control flow
// and data using the primitives that they've constructed
desired_function_or_address = attacker_created_arbitrary_read(...);
target_object = attacker_created_arbitrary_read(...);
attacker_created_arbitrary_write(target_object + field_offset, desired_function_or_address);
attacker_created_mechanism_to_trigger_restart_lifetime();
As above, because we did not have the origin address in restart lifetime, we’ve had to just remove the existing signature and apply a new correct one. Because of that an attacker no longer needs to worry about constructing a valid signature on the pointer, we’ll simply provide one for them. The result is that the chain of trust in control flow is broken, and the attacker can now call any code they want to. This is what is called a signing oracle - a method by which an attacker can convert a value they control into a value that is correctly signed and usable in authenticated operations.
> Given the compiler builtin function `__builtin_get_address_vtable(T)`,
> it feels like we ought to be able to implement `restart_lifetime` with
> only the new pointer, by simply discarding the old vptr value and
> reinitializing it, maybe something like:
>
> template<class T>
> T *restart_lifetime(T *const p)
> {
> void const *vptr = __builtin_get_address_vtable(T);
> *reinterpret_cast<void * volatile *>(p) = vptr;
> sign_pointer_at_address(p); // arm64e pointer authentication or similar
> return p;
> }
Right, the problem is that this is copy constructor semantics rather than TR, and as above requires non-memmove behavior even absent ptrauth, and does not work for protected fields that do not have compile/load time constant values
>
> (Here `sign_pointer_at_address` is pseudocode for whatever pointer
> authentication primitive the implementation actually uses to sign the
> vptr based on the address of `*p`.)
>
> So my concrete question is:
>
> Given an implementation with this kind of ability to obtain the
> appropriate vtable address for `T`, why is a two-argument interface:
>
> restart_lifetime(T const *old_p, T *new_p)
>
> still needed on arm64e? What does `old_p` let you do that you could
> not, in principle, do by discarding the old vptr and synthesizing
> a fresh, correctly signed vptr at `new_p`?
>
> I’m trying to see the specific arm64e (or similar) scenario that forces
> the two-argument shape here, rather than a single-argument:
>
> T* restart_lifetime(T *p);
>
> that just reconstructs the vptr at the new address.
>
> And some people also mentioned scenarios, such as serialisation,
> where there might not be an old address.
Hopefully the above explains why the two argument interface is required in detail, but to directly answer this at a high level copy constructors work because a copy constructors and similar are placing the vtable pointer for the static type of the object, which means it is signing a global constant that an attacker cannot change.
Trivial relocation/restart_lifetime copies the existing vtable (i.e. the dynamic type, which may not match the static type) so has to authenticate that pointer and then sign the new one. If you don’t have the initial address of the object, you can’t authenticate, so you either don’t allow it such relocation/restart, or you have to strip the signature and create a correctly signed pointer for whatever was present in the existing object, even if it is attacker controlled.
—Oliver
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
Received on 2025-11-11 23:33:51
