On Sun, 9 Nov 2025, 14:51 Frederick Virchanza Gotham via Gcc, <
[email protected]> wrote:

> Hi guys
>
> I'm kind of new to editing compilers but I'm enthusiastic to learn.
> I'm currently working on the following patch to the GNU g++ compiler:
>
>
> https://github.com/gcc-mirror/gcc/compare/trunk...healytpk:gcc-vptr-xor:trunk
>
> For polymorphic objects in C++, I want to XOR the vptr's value with
> its own address, meaning that if you relocate a polymorphic object,
> you need to re-encode the vptr, otherwise you'll get a segfault when
> you go to invoke a virtual method. I am doing this in order to
> simulate what happens on Apple Silicon computers with the arm64e
> architecture with pointer authentication, i.e. to prove the need for a
> 'std::restart_lifetime' function in the C++ standard library.
>
> I was hoping some of you guys could help me with my patch. For
> instance, I don't know how to XOR the vptr when the variable is either
> constinit or constexpr, and this is why I do the following inside the
> file "gcc/cp/class.cc":
>
>     if (classtype && TYPE_HAS_CONSTEXPR_CTOR (classtype))
>         return vtbl;  /* For classes with constexpr ctors, never
> encode the vptr. */
>
> Do you think might it be possible to also XOR the vptr's of
> consinit/constexpr objects?
>
> Also my implementation of "__restart_lifetime" crashes . . . I'm not
> sure which enum I'm supposed to edit, "builtin_function" or
> "cp_builtin_function". Perhaps could someone do a quick scroll through
> my patch above and give me some pointers?
>
> Below is the email I sent earlier today to the C++ standard proposals
> mailing list:
>      - - - - -   - - - - -
> At the Kona talk on Wednesday evening just gone, we talked about
> relocation.
>
> We went into the complications of the 'arm64e' architecture which has
> pointer authentication. Most of us on this mailing list have an x86_64
> computer, and so I wanted to put together a test suite that doesn't
> require a new Apple Silicon computer.
>
> I have edited the GNU g++ compiler to obfuscate the vtable pointer
> inside a polymorphic object. I have it tested and working on x86_64.
> From the looks of things, I think it will work on every CPU and
> operating system that g++ can run on.
>
> I've changed g++ so that when it writes a vptr to a polymorphic
> object, it XOR's the address of the vtable with the address of the
> vptr. Here's my compiler patch:
>
>
> https://github.com/gcc-mirror/gcc/compare/trunk...healytpk:gcc-vptr-xor:trunk
>
> Just one thing: If the polymorphic class has one or more
> constexpr/consteval constructors, I don't obfuscate the vptr -- this
> is because I couldn't figure out how to do it without getting an ICE
> error. So when testing, make sure the class hasn't got a
> constexpr/consteval constructor. When writing the vtpr to the
> polymorphic object, I set the least significant bit to 1 as a flag to
> indicate that it has been XOR'ed (which is fine because this bit will
> always be zero -- even after the XOR).
>
> I built this compiler and then got it to compile the following source file:
>
>     #include <cstdio>     // puts
>
>     struct Monkey {
>         int n;
>         Monkey(int const arg) : n(arg) {}
>         virtual void Func(void);
>     };
>
>     void Monkey::Func(void)
>     {
>         std::puts("Hello World");
>     }
>
>     void Invoke(Monkey &m)
>     {
>         m.Func();
>     }
>
> Before I show you how the new compiler assembled the function
> 'Invoke', let's take a look at how a normal g++ compiler does it:
>
>     Invoke:
>         mov (%rdi), %rax
>         jmp *(%rax)
>
> The first instruction dereferences the object pointer to get the vptr.
> The second instruction dereferences the vptr to get the address of the
> first virtual function, and then jumps to the first virtual function's
> machine code.
>
> So with the new compiler, we're expecting to see the vptr XOR'ed with
> its own address before it's dereferenced . . . okay so let's see the
> output from 'objdump -d source.o' for the new compiler:
>
> Invoke:
>     mov    (%rdi), %rax          ; load vptr (possibly encoded)
>     test   $0x1, %al             ; test low bit of vptr (tag bit: 1 =
> encoded, 0 = plain)
>     je     .Lplain               ; if low bit is 0 -> not encoded,
> jump to plain case
>
>     and    $0xfffffffffffffffe, %rax ; clear low tag bit (RAX &= ~1)
>     xor    %rdi, %rax            ; decode vptr: RAX = RAX ^ this (this
> is in RDI)
>     mov    (%rax), %rax          ; load function pointer from decoded
> vtable[0] into RAX
>     cmp    $0x0, %rax            ; compare function pointer to null
> (sanity check)
>     jne    .Lcall                ; if non-null, go call it
>     ret                          ; if null, just return
>
>     nopw   0x0(%rax,%rax,1)      ; 6-byte NOP (padding/alignment)
>
> .Lplain:
>     mov    (%rax), %rax          ; plain vptr case: original vptr is a
> real vtable ptr; load vtable[0]
>     cmp    $0x0, %rax            ; compare function pointer to null
>     je     .Lret                 ; if null, return
>
> .Lcall:
>     jmp    *%rax                 ; tail-call via the function pointer
> (virtual Monkey::Func)
>
> .Lret:
>     ret
>
> This is mildly cool. We've got some sort of basic pointer validation
> going on now, and we can test it natively on x86_64 computers running
> Linux or MS-Windows or macOS or FreeBSD. I think it should work on
> every computer for which you can build the GNU compiler.
>
> The following program works fine when compiled with the normal g++
> compiler, but it crashes with the new compiler:
>
>     #include <cstring>    // memcpy
>
>     struct Monkey {
>         int n;
>         Monkey(int const arg) : n(arg) {}
>         virtual void Func(void);
>     };
>
>     extern void Invoke(Monkey&);
>
>     int main(int const argc, char **const argv)
>     {
>         Monkey m(argc);
>         alignas(Monkey) char unsigned buf[ sizeof(Monkey) ];
>         std::memcpy( &buf, &m, sizeof buf );
>         Monkey &m2 = *static_cast<Monkey*>( static_cast<void*>( &buf ) );
>         Invoke(m2);
>     }
>
> It crashes because the vptr is now corrupt after relocation. The
> solution is to use "std::restart_lifetime". In the new compiler, I
> have added a new built-in function called '__builtin_restart_lifetime'
> but I haven't got it working quite perfectly yet, and so for the time
> being I have edited the standard library header file <memory> and
> given it a rudimentary naive implementation of 'std::restart_lifetime'
> as follows:
>
>     template <typename _Tp>
>       _Tp*
>       restart_lifetime(const _Tp* const __old_p, _Tp* const __new_p)
>       {
>         // I have only used C++11 features in this implementation.
>
>         // This is a naive implementation that will only XOR the vptr
>         // of the most-derived object -- it won't XOR the vptr inside
>         // sub-objects, and so will only work properly for simple classes.
>         // This implementation is only intended for testing until the
>         // compiler gets a built-in function to do this properly, e.g.
>         //                 __restart_lifetime
>
>         static_assert( false == is_const<_Tp>::value, "T must not be
> const" );
>         if ( false == is_polymorphic<_Tp>::value ) return __new_p;
>         uintptr_t volatile &n = *static_cast<uintptr_t volatile*>(
> static_cast<void volatile*>( __new_p ) );
>         if ( 0u == (n & 1u) ) return __new_p;
>         n ^= reinterpret_cast<uintptr_t>( __old_p );
>         n ^= reinterpret_cast<uintptr_t>( __new_p );
>         return __new_p;
>       }
>
> So let's go back to our program that was crashing, and let's try add
> one line of code to get it to work:
>
>     int main(int const argc, char **const argv)
>     {
>         Monkey m(argc);
>         alignas(Monkey) char unsigned buf[ sizeof(Monkey) ];
>         std::memcpy( &buf, &m, sizeof buf );
>         Monkey &m2 = *static_cast<Monkey*>( static_cast<void*>( &buf ) );
> ++++    std::restart_lifetime(&m,&m2);
>         Invoke(m2);
>     }
>
> With this new line invoking restart_lifetime, the program no longer
> crashes :-)
>
> So now we have a working compiler for x86_64 that we can use for
> testing relocation of polymorphic objects that have an obfuscated
> vptr. I wonder if Matt will put this up on GodBolt. . . . I'll email
> him now.
>

You need to send a pull request to add it yourself, not just ask Matt or
the other CE admins to do it for you. They're all busy people.




> I do realise the compiler needs more polishing and that I have to fix
> __builtin_restart_lifetime. Plus if anyone reading this has
> compiler-writing experience and knows how to make this work with
> consinit/constexpr variables, I am of course all ears.
>

Reply via email to