Date: Tue, 11 Nov 2025 14:34:06 +0000
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;
}
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?
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;
}
(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.
>
> 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;
}
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?
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;
}
(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.
Received on 2025-11-11 14:34:19
