3

How to type-pun via catching by non-const reference

 6 months ago
source link: https://quuxplusone.github.io/blog/2023/11/17/reinterpret-cast-via-throw/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

How to type-pun via catching by non-const reference

I’ll just leave this here (Godbolt):

int i = 0x4048f5c3;
try {
    try {
        throw (float*)nullptr;
    } catch (void *& pv) {
        pv = &i;
        throw;
    }
} catch (const float *pf) {
    printf("%.2f\n", *pf);
}

On MSVC and Clang, this prints 3.14. On GCC (which I think means “on the libsupc++ runtime”), pv somehow ends up referring to a copy of the original exception object, such that modifications to pv don’t affect the subsequently caught value of pf.

As far as I can tell, none of these three runtimes is doing the right thing. According to [except.handle]/3, if I throw a type E that is convertible to T only via some pointer conversion (e.g. E=float* to T=void*), I’m actually not supposed to be able to hit a catch-handler of type T& at all — I’m supposed to fly right past it! So (unless I’m mistaken) this is a “hole in the type-system” of all three vendors’ C++ runtimes, but it’s not a hole in the paper standard, because the paper standard says you’re not supposed to be able to do this.

UPDATE, 2023-12-12: In GCC bug 23257, it’s stated that this isn’t the fault of the compiler or the runtime library, but a flaw in the Itanium C++ ABI itself — the ABI’s .gcc_except_table section encodes types in terms of typeid, which is identical for T, T&, and const T&. Notice the identical codegen in this Godbolt. Apparently Arm’s C++ ABI can handle references differently from values-and-const-references, although personally I’m fuzzy on how that works and whether any mainstream compiler implements that ABI even on Arm targets. I don’t know whether the MSVC issue is an ABI issue too, but I’d bet that it is.


This seems to have severe implications for P2927 try_cast. Last week I said that it was always fine to take the const T* you got from the cast and const_cast away the constness — the underlying exception object will never be const. That’s true of the underlying exception object, but the pointer you get back might not be pointing to the underlying exception object! — it might be pointing to a temporary T implicitly-converted-by-a-pointer-conversion from the actual exception object (whose type is not T but E). In fact, if try_cast’s result points to a temporary, it’ll be dangling already! I guess we’ll have to do something about this.

auto e = std::make_exception_ptr<int*>(&i);
auto *p = std::try_cast<void*>(e);
  // as-if-by catch (void* const&); a temporary is created
// here p is a dangling pointer

UPDATE, 2023-12-12: My proposed solution is simply to make std::try_cast<T*> ill-formed. Throwing pointers is already sketchy (and, as we’ve seen, buggy in the core language on all mainstream ABIs); std::try_cast can simply refuse to engage.


Previously on this blog:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK