Date: Mon, 02 Jun 2025 18:00:33 -0700
> On Jun 2, 2025, at 4:23 PM, Frederick Virchanza Gotham via Std-Proposals <std-proposals_at_[hidden]> wrote:
>
> On Mon, Jun 2, 2025 at 6:29 PM Oliver Hunt wrote:
>>
>> It could be 6 nines, but if your position is “I don’t like the ABI used for
>> C++ polymorphism on windows and therefore it should be ignored” is
>> DoA, as you’re arguing with reality.
>
>
> What I'm saying is that Microsoft's implementation of polymorphism is
> the worst of all the C++ compilers, and that the C++ standardisation
> process is burdened by this. If you were to take Microsoft out of the
> picture, more would be possible when you have a pointer to a
> type-erased polymorphic object. (I've already pointed out two things
> that would become possible).
I’m really not super familiar with what their layout is, but it sounds like it’s something like
struct Foo {
int i;
};
struct Bar : Foo {
virtual ~Bar();
int j;
};
Turning into
struct Bar {
Foo base;
void** vtable;
int j;
};
Which from my experience is what a lot of developers actually expect, to them it is confusing the size and position of the base class relative to the this pointer changes depending on the existing of a polymorphic subclass. It’s something I have to explain at least a dozen times a year, as it conceptually makes no sense to people.
For example, if you were to manually implement polymorphism by structs, you would get the structure layout above (given COM is meant to support C as well as C++, is it possible that the MS layout exists to support that? I really don’t know, my experience with windows/msvc has mostly been frustration, but not because of vtable layout :D)
>
>
>>> -- any polymorphic
>>> object will always have a pointer to its polymorphic facilitator
>>> located at address [base + 0x00] inside the object. What this means,
>>> is that when we have a "void*", we _can_ actually do the following two
>>> things even though the Standard doesn't let us to:
>>
>> You are correct, the standard does not let you do this. That you want to is irrelevant.
>
>
> But the Standard can very easily allow this.
Any change to the standard that is not implementable on multiple of the most widely deployed platforms that exist, would be a non-starter. A change to the C++ standard that would make at least two major OS families incapable of conforming to the new standard would make the standard meaningless.
>>> The above GodBolt works on every C++ compiler ever made. Except for
>>> one. Microsoft.
>>>
>>
>> Incorrect, this fails on Darwin - the process is terminated the moment you try
>> to use the vtable of a non-Dummy typed object as a Dummy. This is not a recoverable error.
>
>
> What's Darwin? Are you talking about Apple macOS? I don't have an
> Apple computer but I have macOS 15 Sequoia running in a virtual
> machine here on my x86_64 laptop. I took that C++ code, put it in a
> source file, and I did the following at the command line:
>
> healytpk_at_Thomas-MacBook-Pro Desktop % clang++ -o prog main.cpp
> healytpk_at_Thomas-MacBook-Pro Desktop % ./prog
> Address of stringstream: 0x7ff7b92b42e8
> Address of ostream: 0x7ff7b92b42f8
> Address of most derived: 0x7ff7b92b42e8
> Typeinfo:
> NSt3__118basic_stringstreamIcNS_11char_traitsIcEENS_9allocatorIcEEEE
>
> Doesn't crash. Runs fine.
Here’s what happens when you aren’t compiling for x86_64:
Address of stringstream: 0x16efeaf18
Address of ostream: 0x16efeaf28
[1] 74787 bus error ./a.out
Here’s the code for the dynamic cast (noting that your casts remove the actual call to dynamic cast):
ldr x8, [sp, #8]
ldr x16, [x8]
mov x17, x8
movk x17, #61638, lsl #48
autda x16, x17
ldur x0, [x16, #-8]
ldp x29, x30, [sp, #32]
add sp, sp, #48
ret
The autda instruction fails triggering process termination (note I am not saying it causes a signal, or an exception, it’s strictly process termination)
The “61638” number you see is a discriminator derived from the Dummy vtable type, which is not the same as the basic_stringstream’s vtable.
I made a quick example to further expand on this: https://godbolt.org/z/YTxqKfbnz
If you look at the codegen for the function F that you’ll see two
movk x17, #31380, lsl #48
autda x16, x17
For the vtable pointer access (note that 31380 is not the 61638 above)
Then you have the calls to f:
movk x17, #48306, lsl #48
blraa x8, x17
And then g:
movk x17, #56929, lsl #48
blraa x8, x17
And you see the each slot in the vtable has it's own discriminator.
If you look at the function B, you can see that all the discriminators are different again.
e.g. just because you “know” they have the same structure or layout, does not mean they are actually interchangeable.
That’s why the language makes what you’re trying to do UB.
>
>
>>> On Microsoft it will work properly the vast majority of the time, but
>>> sometimes it will crash because of the following fact:
>>> "Where as 99% of compilers have a uniform way of mapping
>>> an object to its polymorphic facilitator, the Microsoft
>>> compiler does not have a uniform way -- it can differ by type."
>>>
>>> To bring that a bit more down to Earth:
>>>
>>> "The Microsoft compiler doesn't always place the
>>> vtable pointer at the very beginning of the object."
>>
>> No, you are assuming the layout of the polymorphic facilitator is such
>> that unrelated types can be used interchangeably, which is UB.
>
>
> Computers aren't magical. I know some people like to get all
> metaphysical and airy fair about UB, talking about "demons out your
> nose" and all that, but really your computer isn't going to grow legs
> and walk away. Have a think about what the compiler will do, and take
> a look at the machine code / assembler it produces. And if you want to
> remove UB from the picture entirely, then just write it in assembler
> instead of C++.
It is undefined behavior, just as integer overflow, unaligned loads, or any number of other things are UB, per the standard. It does not matter whether or not your hardware has a defined outcome.
There have been many bugs in software caused by people saying “I know what the hardware does”, when the language does not say anything of the sort.
e.g. the classic overflow check that gets removed
if (a+b<a) { abort() }
Sure you know the computer operates in two’s complement, but that’s not what the standard says (this is one of the many “UB” cases I disagree with, but the point is that the standard says this is UB, and so it is).
> The following assembler function can get the
> 'type_info' of any type-erased polymorphic object on an x86_64 machine
> using the System V ABI:
>
> GetTypeInfo:
> mov rax, qword ptr [rdi]
> mov rax, qword ptr [rax - 8]
> ret
>
> There's no surprises in the above 3 instructions. No undefined behaviour.
Yes there is, your GetTypeInfo function, the C++ version, made an invalid cast, and so is immediately UB.
The fact that on your hardware, with your selected ABI, allows your above assembly function to load the vtable is nice, but demonstrably not sound because it does not work on darwin or windows.
Compiling your C++ GetTypeInfo function with optimizations to make it closer to what you think would “just work” for everyone:
GetTypeInfo(void*):
cbz x0, .LBB0_2 ; the null check
ldr x16, [x0]
mov x17, x0
movk x17, #61638, lsl #48
autda x16, x17
ldur x0, [x16, #-8]
ret
I’ve made an example where the `typeid()` is performed in a templated function at https://godbolt.org/z/51rj3zvYb so you can compare the codegen for typeid(someDummy) and typeid(someFoo), and see that they are not the same.
>
>
>> Your code is incorrect the moment you static_cast AnythingAtAll* to Dummy*,
>> I’m surprised none of the sanitizers are tripped by this.
>
>
> And even if it did trip something, I'd figure it out with a
> combination of 'volatile' and 'std::launder'. Or I'd just write it in
> assembler.
Does not work for darwin, the vtable is an authenticated pointer, and you need to use the correct authentication schema to perform the load.
>
> By the way 'volatile' is the best thing in the world if you ever need
> to take advantage of UB.
That’s a standard misunderstanding of volatile.
>
>
>> Alas godbolt does not run on hardware that supports pointer auth (or the related , but we can set the appropriate flags:
>>
>> https://godbolt.org/z/4Gb14srK5
>>
>> Then in the body of GetTypeInfo use can see the `autda` that fails and triggers process termination
>
>
> That GodBolt you gave me, it does indeed crash . . . but it also
> crashes on Hello World, check it out:
>
> https://godbolt.org/z/fd4K7T9KT
>
> If you can find me a compiler that doesn't crash on Hello World, but
> does crash on my code, then I'll take a look. And I bet ya I can
> figure it out and get it working.
Of course it crashes, as I already stated godbolt does not run on hardware that supports the required instructions. The crash you’re complaining about as if it’s a compiler bug is no different from telling a compiler to emit avx instructions and then running the resulting code on hardware that does not support avx.
I’m kind of tired of explaining pointer authentication, but as you seem unwilling to accept that what you are trying/expect to able to do I guess I have to again.
ARMv8.3 introduces pointer authentication instructions. These are instructions that can be used to provide hardware supported control flow integrity. There are numerous instructions for this to allow specific optimizations, but the core operations are essentially
sign(value, key, discriminator)->signed_value
auth(signed_value, key, discriminator)->value
The key is constant integer, that in arm 8.3 can be one of 4 different keys, where the keys themselves are in system registers not available to the process that is running. When you sign a value the hardware takes the value and discriminator you provided blends them together, then produces a cryptographic signature of the blended value+discriminator, and returns the original value and attached signature.
Authentication semantically just reverses that.
The darwin pointer authentication schema for vtable pointers constructs a discriminator out of a hash of the type and usage (that it’s a vtable pointer), and the address that the vtable pointer is stored at (e.g `this`). For your purposes the inclusion of the object type is what breaks you, but the include of the storage address also means that you can’t copy a vtable pointer from a compatible type (e.g a different subclass) into a different object either.
The result of this is that loading the vtable pointer requires correct knowledge of the type you are interacting with.
>
>
>>> The above GodBolt shows that the Microsoft compiler places the vtable
>>> pointer _after_ the non-polymorphic base, specifically at [base +
>>> 0x08].
>>
>> So?
>>
>> Your code has broken because you decided to try and treat the layout of two
>> unrelated objects as interchangeable. That’s simply incorrect.
>
>
> It's not a big deal if you have an understanding of what the compiler
> will do.
Which you clearly do not.
> I know that the compiler will only look at the vtableptr in
> that object. It's harmless. I've checked the machine code / assembler
> to be sure. But again, we can write it in less than ten CPU
> instructions in assembler if you really want to eradicate all UB.
On your platform that works. On windows it does not. On darwin it does not.
You are arguing with reality, because you don’t like reality.
>
>
>>> So it's because of the Microsoft compiler -- and _only_ because of the
>>> Microsoft compiler -- that we can't do the two things I talk about
>>> above. But last night I figured out a possible solution to this
>>> conundrum.
>>
>> No, it’s because that is not how the language works, and you are writing code
>> that is wrong. Also again, this fails on darwin, and it will fail on linux once linux
>> adopts pointer authentication. I suspect it would also fail on a hypothetical CHERI
>> system for similar reasons.
>
>
> My code doesn't cause address sanitizer to throw up an alarm. If you
> can beat address sanitizer then you can beat anything. Here's how I
> build my programs on Linux to debug them:
Your code literally results in process termination on darwin, so that’s clearly an incorrect statement.
>
> set(ENV{ASAN_OPTIONS} "detect_invalid_pointer_pairs=2")
> set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -ggdb3
> -pedantic -Wall -Wextra -rdynamic -funwind-tables
> -fno-omit-frame-pointer -fno-common -pthread
> -fsanitize=address,leak,undefined,pointer-compare,pointer-subtract,float-divide-by-zero,float-cast-overflow
> -fsanitize-address-use-after-scope -fcf-protection=full
> -fstack-protector-all -fstack-clash-protection -fvtv-debug
> -fvtv-counts -finstrument-functions -D_GLIBC_DEBUG
> -D_GLIBC_DEBUG_PEDANTIC -D_GLIBCXX_DEBUG -D_GLIBCXX_DEBUG_PEDANTIC")
>
> Not many a bug survives all those options. However my code for
> GetMostDerivedObject and GetTypeInfo compiles and runs just fine.
This seems like a bug in ubsan if anything.
>
>>> Here comes the fun part, the reverse-engineering -- which is necessary because of the
>>> burden Microsoft has placed on the C++ standardisation process.
>>
>> No, MS has not placed a burden here, as you are demanding the standard
>> specify exact behavior of a language implementation detail that is not
>> described anywhere in the standard.
>
>
> Just because it's not described anywhere in the standard, doesn't mean
> that engineers can come up with really backwards solutions that burden
> the rest of the planet. Not putting the vtable pointer at the
> beginning of the object . . . I mean really . . . it's up there with
> naming the 64-Bit version of their kernel library, "kernel32.dll", for
> backward-compatibility reasons. Or referring to x86_32 as "x86", and
> referring to x86_64 as "x64" -- the latter is clear but the former has
> you wondering emmmm do they mean 32-Bit x86 or 64-Bit x86 . . . and
> then you open up Visual Studio and they call it "Win32" instead of
> "x86", even though "_WIN32" is also defined when you build in 64-Bit
> mode. And now they have 64-Bit ARM so you have to remember that x64 is
> really x86_64 and not arm64/aarch64.
Your desire for the ABI to match your preferences, is not relevant here.
>> And again, your constant “this works everywhere else” is wrong.
>>
>> There is a reason why what you are doing is UB, and is precisely
>> because the false assumptions in your code are not sound.
>
>
> It's a proof of concept, that's all. I'm showing that this can already
> be done without an ABI break and without compiler vendors doing much
> work. It just has to be written into the Standard. It can be
> implemented in assembler if you really want to excise the UB.
You’re asking for vendors to support multiple ABIs, and not change the default ABI, simply because there’s one ABI you prefer.
More generally, if the standard were modified to permit you to say “I want this ABI”, why would it not be equally reasonable for someone to be able to request the MS ABI?
Also again, what you’re trying to do is entirely UB, and does not work on multiple OS’s.
>
>
>> What you are currently asking for is:
>>
>> 1. I want to be able to say “use this ABI instead of that ABI for this class”
>> 2. Then I want the standard to make it not defined behavior to cast and use
>> a polymorphic object of one type as an object of an unrelated type. That means
>> type aliasing optimizations cease to become valid,
>
>
> Yeah 1 and 2 aren't really related . . . I started talking about 1 and
> then went off on a tangent talking about 2.
>
>
>> (1) is (in principle) easy: that could be a vendor attribute, just as
>> things like “fastcall”, regparam, etc are today
>>
>> (2) just breaks: the fact the an object of one type cannot be used
>> as an object of an unrelated type is a fairly fundamental feature of C and C++.
>
>
> If you know where the vtable pointer is inside an object, you can have
> a field day with it . . . you can find its type_info . . . you can
> find its most-derived object . . . . all of this stuff is already
> possible, it just needs to be written into the Standard. I thought it
> wasn't possible with Microsoft but it actually is if you use a
> 'polymorph_handle' instead of a ' void * '.
You can already get the type_info - that’s what typeid is for.
>
> If you're really insistent that my proof of concept shouldn't have UB
> then I'm happy to write it in assembler for every architecture . . . I
> mean I wrote assembler for 64-Bit ARM, HPPA, Motorola 68K and also
> SuperH for this paper alone:
> http://www.virjacode.com/papers/paper_nrvo_latest.pdf
It does not matter if you write it in assembly, C++ says what you’re doing is UB, and the fact that you can write it in assembly does not change that. More over what you’re trying to cannot be done in assembly on all platforms.
>
>
>> What you should be asking for is not “I want an ability to override the
>> polymorphism implementation, because then I can use definitionally
>> erroneous code that happens to work on my own platform, to achieve
>> what I want”. You have fixated on your specific implementation, and
>> have correctly identified that your reliance on UB breaks it other platforms.
>
>
> Not sure what you're talking about here. I haven't conceded that my
> code malfunctions anywhere (unless you're talking about the
> quick-and-dirty code I wrote to analyse the machine code at runtime on
> the Microsoft compiler?).
It does: it does not work with msvc objects, it does not work on darwin.
>
>
>> Rather than asking for language changes to support your incorrect code,
>> you should be asking for language features to support what you are actually
>> trying to do. Not all such language features are possible.
>>
>> So what is it you are actually trying to do?
>
>
> Here's what I want, I'll spell it out. If you start off with a
> polymorphic object:
>
> std::stringstream ss;
>
> And then if you type-erase it:
>
> void *pv = &ss;
>
> I want to be able to take that type-erased pointer and do the following:
> (1) Get the address of the most-derived object
> (2) Get the type_info
And that’s simply not possible in C++, as C++ does not have a top type.
>
> These two things are very easily possible on every single C++ compiler
> in the world -- except for Microsoft. On the Microsoft compiler you
> need to get a little creative, which is what I've done with
> "std::polymorph_handle". Using a 'polymorph_handle' on every compiler
> other than Microsoft will be no different that dealing with a simple "
> void * ". So if you've already written code that has a vector<void*>,
> then it won't be an ABI break if you change it to
> vector<polymorph_handle>. And as for the Microsoft compiler, well
> something that was previously impossible has become possible so
> there's no worries about breaking ABI.
>
> I realise that Microsoft software is on 1.45 billion computers
> worldwide. But that figure doesn't somehow, some way, exonerate them
> of bad engineering. If Microsoft are either unwilling or unable to
> change their ABI, then an alternative solution is my proposed
> 'polymorph_handle', which I'm thinking just now I'll shorten to
> 'std::polyhandle'.
The MS ABI as I understand it, does make sense - it is not inherently wrong, the only problem with it is that it does not let you do what you want to do, despite what you wanting to being something that is simply not supported by C++.
>
> So if we deal with 'std::polyhandle' instead of ' void * ', we're
> working with a type-erased pointer to a polymorphic object and the
> following two things become possible:
> (1) Get the address of the most-derived object
> (2) Get the type_info
>
> That's what I want. And I bet that we would have had this
> functionality in the Standard many many years ago if it hadn't been
> for Microsoft with their lack-lustre burdensome ABI.
This is not the fault of the MS ABI, it is quite simply due to the lack of a top type in C++. From a platform security point of view this is actually helpful as it means that you can have more variety in the vtable discriminators - for example on darwin objc does have a top type, and so the type discriminator for the isa pointer is the same across all objects, so use after free bugs are not as strongly protected against type confusion attacks.
—Oliver
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>
> On Mon, Jun 2, 2025 at 6:29 PM Oliver Hunt wrote:
>>
>> It could be 6 nines, but if your position is “I don’t like the ABI used for
>> C++ polymorphism on windows and therefore it should be ignored” is
>> DoA, as you’re arguing with reality.
>
>
> What I'm saying is that Microsoft's implementation of polymorphism is
> the worst of all the C++ compilers, and that the C++ standardisation
> process is burdened by this. If you were to take Microsoft out of the
> picture, more would be possible when you have a pointer to a
> type-erased polymorphic object. (I've already pointed out two things
> that would become possible).
I’m really not super familiar with what their layout is, but it sounds like it’s something like
struct Foo {
int i;
};
struct Bar : Foo {
virtual ~Bar();
int j;
};
Turning into
struct Bar {
Foo base;
void** vtable;
int j;
};
Which from my experience is what a lot of developers actually expect, to them it is confusing the size and position of the base class relative to the this pointer changes depending on the existing of a polymorphic subclass. It’s something I have to explain at least a dozen times a year, as it conceptually makes no sense to people.
For example, if you were to manually implement polymorphism by structs, you would get the structure layout above (given COM is meant to support C as well as C++, is it possible that the MS layout exists to support that? I really don’t know, my experience with windows/msvc has mostly been frustration, but not because of vtable layout :D)
>
>
>>> -- any polymorphic
>>> object will always have a pointer to its polymorphic facilitator
>>> located at address [base + 0x00] inside the object. What this means,
>>> is that when we have a "void*", we _can_ actually do the following two
>>> things even though the Standard doesn't let us to:
>>
>> You are correct, the standard does not let you do this. That you want to is irrelevant.
>
>
> But the Standard can very easily allow this.
Any change to the standard that is not implementable on multiple of the most widely deployed platforms that exist, would be a non-starter. A change to the C++ standard that would make at least two major OS families incapable of conforming to the new standard would make the standard meaningless.
>>> The above GodBolt works on every C++ compiler ever made. Except for
>>> one. Microsoft.
>>>
>>
>> Incorrect, this fails on Darwin - the process is terminated the moment you try
>> to use the vtable of a non-Dummy typed object as a Dummy. This is not a recoverable error.
>
>
> What's Darwin? Are you talking about Apple macOS? I don't have an
> Apple computer but I have macOS 15 Sequoia running in a virtual
> machine here on my x86_64 laptop. I took that C++ code, put it in a
> source file, and I did the following at the command line:
>
> healytpk_at_Thomas-MacBook-Pro Desktop % clang++ -o prog main.cpp
> healytpk_at_Thomas-MacBook-Pro Desktop % ./prog
> Address of stringstream: 0x7ff7b92b42e8
> Address of ostream: 0x7ff7b92b42f8
> Address of most derived: 0x7ff7b92b42e8
> Typeinfo:
> NSt3__118basic_stringstreamIcNS_11char_traitsIcEENS_9allocatorIcEEEE
>
> Doesn't crash. Runs fine.
Here’s what happens when you aren’t compiling for x86_64:
Address of stringstream: 0x16efeaf18
Address of ostream: 0x16efeaf28
[1] 74787 bus error ./a.out
Here’s the code for the dynamic cast (noting that your casts remove the actual call to dynamic cast):
ldr x8, [sp, #8]
ldr x16, [x8]
mov x17, x8
movk x17, #61638, lsl #48
autda x16, x17
ldur x0, [x16, #-8]
ldp x29, x30, [sp, #32]
add sp, sp, #48
ret
The autda instruction fails triggering process termination (note I am not saying it causes a signal, or an exception, it’s strictly process termination)
The “61638” number you see is a discriminator derived from the Dummy vtable type, which is not the same as the basic_stringstream’s vtable.
I made a quick example to further expand on this: https://godbolt.org/z/YTxqKfbnz
If you look at the codegen for the function F that you’ll see two
movk x17, #31380, lsl #48
autda x16, x17
For the vtable pointer access (note that 31380 is not the 61638 above)
Then you have the calls to f:
movk x17, #48306, lsl #48
blraa x8, x17
And then g:
movk x17, #56929, lsl #48
blraa x8, x17
And you see the each slot in the vtable has it's own discriminator.
If you look at the function B, you can see that all the discriminators are different again.
e.g. just because you “know” they have the same structure or layout, does not mean they are actually interchangeable.
That’s why the language makes what you’re trying to do UB.
>
>
>>> On Microsoft it will work properly the vast majority of the time, but
>>> sometimes it will crash because of the following fact:
>>> "Where as 99% of compilers have a uniform way of mapping
>>> an object to its polymorphic facilitator, the Microsoft
>>> compiler does not have a uniform way -- it can differ by type."
>>>
>>> To bring that a bit more down to Earth:
>>>
>>> "The Microsoft compiler doesn't always place the
>>> vtable pointer at the very beginning of the object."
>>
>> No, you are assuming the layout of the polymorphic facilitator is such
>> that unrelated types can be used interchangeably, which is UB.
>
>
> Computers aren't magical. I know some people like to get all
> metaphysical and airy fair about UB, talking about "demons out your
> nose" and all that, but really your computer isn't going to grow legs
> and walk away. Have a think about what the compiler will do, and take
> a look at the machine code / assembler it produces. And if you want to
> remove UB from the picture entirely, then just write it in assembler
> instead of C++.
It is undefined behavior, just as integer overflow, unaligned loads, or any number of other things are UB, per the standard. It does not matter whether or not your hardware has a defined outcome.
There have been many bugs in software caused by people saying “I know what the hardware does”, when the language does not say anything of the sort.
e.g. the classic overflow check that gets removed
if (a+b<a) { abort() }
Sure you know the computer operates in two’s complement, but that’s not what the standard says (this is one of the many “UB” cases I disagree with, but the point is that the standard says this is UB, and so it is).
> The following assembler function can get the
> 'type_info' of any type-erased polymorphic object on an x86_64 machine
> using the System V ABI:
>
> GetTypeInfo:
> mov rax, qword ptr [rdi]
> mov rax, qword ptr [rax - 8]
> ret
>
> There's no surprises in the above 3 instructions. No undefined behaviour.
Yes there is, your GetTypeInfo function, the C++ version, made an invalid cast, and so is immediately UB.
The fact that on your hardware, with your selected ABI, allows your above assembly function to load the vtable is nice, but demonstrably not sound because it does not work on darwin or windows.
Compiling your C++ GetTypeInfo function with optimizations to make it closer to what you think would “just work” for everyone:
GetTypeInfo(void*):
cbz x0, .LBB0_2 ; the null check
ldr x16, [x0]
mov x17, x0
movk x17, #61638, lsl #48
autda x16, x17
ldur x0, [x16, #-8]
ret
I’ve made an example where the `typeid()` is performed in a templated function at https://godbolt.org/z/51rj3zvYb so you can compare the codegen for typeid(someDummy) and typeid(someFoo), and see that they are not the same.
>
>
>> Your code is incorrect the moment you static_cast AnythingAtAll* to Dummy*,
>> I’m surprised none of the sanitizers are tripped by this.
>
>
> And even if it did trip something, I'd figure it out with a
> combination of 'volatile' and 'std::launder'. Or I'd just write it in
> assembler.
Does not work for darwin, the vtable is an authenticated pointer, and you need to use the correct authentication schema to perform the load.
>
> By the way 'volatile' is the best thing in the world if you ever need
> to take advantage of UB.
That’s a standard misunderstanding of volatile.
>
>
>> Alas godbolt does not run on hardware that supports pointer auth (or the related , but we can set the appropriate flags:
>>
>> https://godbolt.org/z/4Gb14srK5
>>
>> Then in the body of GetTypeInfo use can see the `autda` that fails and triggers process termination
>
>
> That GodBolt you gave me, it does indeed crash . . . but it also
> crashes on Hello World, check it out:
>
> https://godbolt.org/z/fd4K7T9KT
>
> If you can find me a compiler that doesn't crash on Hello World, but
> does crash on my code, then I'll take a look. And I bet ya I can
> figure it out and get it working.
Of course it crashes, as I already stated godbolt does not run on hardware that supports the required instructions. The crash you’re complaining about as if it’s a compiler bug is no different from telling a compiler to emit avx instructions and then running the resulting code on hardware that does not support avx.
I’m kind of tired of explaining pointer authentication, but as you seem unwilling to accept that what you are trying/expect to able to do I guess I have to again.
ARMv8.3 introduces pointer authentication instructions. These are instructions that can be used to provide hardware supported control flow integrity. There are numerous instructions for this to allow specific optimizations, but the core operations are essentially
sign(value, key, discriminator)->signed_value
auth(signed_value, key, discriminator)->value
The key is constant integer, that in arm 8.3 can be one of 4 different keys, where the keys themselves are in system registers not available to the process that is running. When you sign a value the hardware takes the value and discriminator you provided blends them together, then produces a cryptographic signature of the blended value+discriminator, and returns the original value and attached signature.
Authentication semantically just reverses that.
The darwin pointer authentication schema for vtable pointers constructs a discriminator out of a hash of the type and usage (that it’s a vtable pointer), and the address that the vtable pointer is stored at (e.g `this`). For your purposes the inclusion of the object type is what breaks you, but the include of the storage address also means that you can’t copy a vtable pointer from a compatible type (e.g a different subclass) into a different object either.
The result of this is that loading the vtable pointer requires correct knowledge of the type you are interacting with.
>
>
>>> The above GodBolt shows that the Microsoft compiler places the vtable
>>> pointer _after_ the non-polymorphic base, specifically at [base +
>>> 0x08].
>>
>> So?
>>
>> Your code has broken because you decided to try and treat the layout of two
>> unrelated objects as interchangeable. That’s simply incorrect.
>
>
> It's not a big deal if you have an understanding of what the compiler
> will do.
Which you clearly do not.
> I know that the compiler will only look at the vtableptr in
> that object. It's harmless. I've checked the machine code / assembler
> to be sure. But again, we can write it in less than ten CPU
> instructions in assembler if you really want to eradicate all UB.
On your platform that works. On windows it does not. On darwin it does not.
You are arguing with reality, because you don’t like reality.
>
>
>>> So it's because of the Microsoft compiler -- and _only_ because of the
>>> Microsoft compiler -- that we can't do the two things I talk about
>>> above. But last night I figured out a possible solution to this
>>> conundrum.
>>
>> No, it’s because that is not how the language works, and you are writing code
>> that is wrong. Also again, this fails on darwin, and it will fail on linux once linux
>> adopts pointer authentication. I suspect it would also fail on a hypothetical CHERI
>> system for similar reasons.
>
>
> My code doesn't cause address sanitizer to throw up an alarm. If you
> can beat address sanitizer then you can beat anything. Here's how I
> build my programs on Linux to debug them:
Your code literally results in process termination on darwin, so that’s clearly an incorrect statement.
>
> set(ENV{ASAN_OPTIONS} "detect_invalid_pointer_pairs=2")
> set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -ggdb3
> -pedantic -Wall -Wextra -rdynamic -funwind-tables
> -fno-omit-frame-pointer -fno-common -pthread
> -fsanitize=address,leak,undefined,pointer-compare,pointer-subtract,float-divide-by-zero,float-cast-overflow
> -fsanitize-address-use-after-scope -fcf-protection=full
> -fstack-protector-all -fstack-clash-protection -fvtv-debug
> -fvtv-counts -finstrument-functions -D_GLIBC_DEBUG
> -D_GLIBC_DEBUG_PEDANTIC -D_GLIBCXX_DEBUG -D_GLIBCXX_DEBUG_PEDANTIC")
>
> Not many a bug survives all those options. However my code for
> GetMostDerivedObject and GetTypeInfo compiles and runs just fine.
This seems like a bug in ubsan if anything.
>
>>> Here comes the fun part, the reverse-engineering -- which is necessary because of the
>>> burden Microsoft has placed on the C++ standardisation process.
>>
>> No, MS has not placed a burden here, as you are demanding the standard
>> specify exact behavior of a language implementation detail that is not
>> described anywhere in the standard.
>
>
> Just because it's not described anywhere in the standard, doesn't mean
> that engineers can come up with really backwards solutions that burden
> the rest of the planet. Not putting the vtable pointer at the
> beginning of the object . . . I mean really . . . it's up there with
> naming the 64-Bit version of their kernel library, "kernel32.dll", for
> backward-compatibility reasons. Or referring to x86_32 as "x86", and
> referring to x86_64 as "x64" -- the latter is clear but the former has
> you wondering emmmm do they mean 32-Bit x86 or 64-Bit x86 . . . and
> then you open up Visual Studio and they call it "Win32" instead of
> "x86", even though "_WIN32" is also defined when you build in 64-Bit
> mode. And now they have 64-Bit ARM so you have to remember that x64 is
> really x86_64 and not arm64/aarch64.
Your desire for the ABI to match your preferences, is not relevant here.
>> And again, your constant “this works everywhere else” is wrong.
>>
>> There is a reason why what you are doing is UB, and is precisely
>> because the false assumptions in your code are not sound.
>
>
> It's a proof of concept, that's all. I'm showing that this can already
> be done without an ABI break and without compiler vendors doing much
> work. It just has to be written into the Standard. It can be
> implemented in assembler if you really want to excise the UB.
You’re asking for vendors to support multiple ABIs, and not change the default ABI, simply because there’s one ABI you prefer.
More generally, if the standard were modified to permit you to say “I want this ABI”, why would it not be equally reasonable for someone to be able to request the MS ABI?
Also again, what you’re trying to do is entirely UB, and does not work on multiple OS’s.
>
>
>> What you are currently asking for is:
>>
>> 1. I want to be able to say “use this ABI instead of that ABI for this class”
>> 2. Then I want the standard to make it not defined behavior to cast and use
>> a polymorphic object of one type as an object of an unrelated type. That means
>> type aliasing optimizations cease to become valid,
>
>
> Yeah 1 and 2 aren't really related . . . I started talking about 1 and
> then went off on a tangent talking about 2.
>
>
>> (1) is (in principle) easy: that could be a vendor attribute, just as
>> things like “fastcall”, regparam, etc are today
>>
>> (2) just breaks: the fact the an object of one type cannot be used
>> as an object of an unrelated type is a fairly fundamental feature of C and C++.
>
>
> If you know where the vtable pointer is inside an object, you can have
> a field day with it . . . you can find its type_info . . . you can
> find its most-derived object . . . . all of this stuff is already
> possible, it just needs to be written into the Standard. I thought it
> wasn't possible with Microsoft but it actually is if you use a
> 'polymorph_handle' instead of a ' void * '.
You can already get the type_info - that’s what typeid is for.
>
> If you're really insistent that my proof of concept shouldn't have UB
> then I'm happy to write it in assembler for every architecture . . . I
> mean I wrote assembler for 64-Bit ARM, HPPA, Motorola 68K and also
> SuperH for this paper alone:
> http://www.virjacode.com/papers/paper_nrvo_latest.pdf
It does not matter if you write it in assembly, C++ says what you’re doing is UB, and the fact that you can write it in assembly does not change that. More over what you’re trying to cannot be done in assembly on all platforms.
>
>
>> What you should be asking for is not “I want an ability to override the
>> polymorphism implementation, because then I can use definitionally
>> erroneous code that happens to work on my own platform, to achieve
>> what I want”. You have fixated on your specific implementation, and
>> have correctly identified that your reliance on UB breaks it other platforms.
>
>
> Not sure what you're talking about here. I haven't conceded that my
> code malfunctions anywhere (unless you're talking about the
> quick-and-dirty code I wrote to analyse the machine code at runtime on
> the Microsoft compiler?).
It does: it does not work with msvc objects, it does not work on darwin.
>
>
>> Rather than asking for language changes to support your incorrect code,
>> you should be asking for language features to support what you are actually
>> trying to do. Not all such language features are possible.
>>
>> So what is it you are actually trying to do?
>
>
> Here's what I want, I'll spell it out. If you start off with a
> polymorphic object:
>
> std::stringstream ss;
>
> And then if you type-erase it:
>
> void *pv = &ss;
>
> I want to be able to take that type-erased pointer and do the following:
> (1) Get the address of the most-derived object
> (2) Get the type_info
And that’s simply not possible in C++, as C++ does not have a top type.
>
> These two things are very easily possible on every single C++ compiler
> in the world -- except for Microsoft. On the Microsoft compiler you
> need to get a little creative, which is what I've done with
> "std::polymorph_handle". Using a 'polymorph_handle' on every compiler
> other than Microsoft will be no different that dealing with a simple "
> void * ". So if you've already written code that has a vector<void*>,
> then it won't be an ABI break if you change it to
> vector<polymorph_handle>. And as for the Microsoft compiler, well
> something that was previously impossible has become possible so
> there's no worries about breaking ABI.
>
> I realise that Microsoft software is on 1.45 billion computers
> worldwide. But that figure doesn't somehow, some way, exonerate them
> of bad engineering. If Microsoft are either unwilling or unable to
> change their ABI, then an alternative solution is my proposed
> 'polymorph_handle', which I'm thinking just now I'll shorten to
> 'std::polyhandle'.
The MS ABI as I understand it, does make sense - it is not inherently wrong, the only problem with it is that it does not let you do what you want to do, despite what you wanting to being something that is simply not supported by C++.
>
> So if we deal with 'std::polyhandle' instead of ' void * ', we're
> working with a type-erased pointer to a polymorphic object and the
> following two things become possible:
> (1) Get the address of the most-derived object
> (2) Get the type_info
>
> That's what I want. And I bet that we would have had this
> functionality in the Standard many many years ago if it hadn't been
> for Microsoft with their lack-lustre burdensome ABI.
This is not the fault of the MS ABI, it is quite simply due to the lack of a top type in C++. From a platform security point of view this is actually helpful as it means that you can have more variety in the vtable discriminators - for example on darwin objc does have a top type, and so the type discriminator for the isa pointer is the same across all objects, so use after free bugs are not as strongly protected against type confusion attacks.
—Oliver
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
Received on 2025-06-03 01:01:01