-
Notifications
You must be signed in to change notification settings - Fork 15.9k
Description
We ran into an interesting crash when upgrading from C++17 to C++20. We had an inline function being compiled into some object files built with C++17 and some built with C++20. This inline function has a static const std::string variable, which becomes constexpr in C++20. A simplified version is as follows (the [[clang::nodestroy]] generates shorter code but the issue also exists without it):
inline const std::string &get() {
[[clang::no_destroy]] static const std::string empty;
return empty;
}
The generated code for C++17 and C++20 can be seen in https://godbolt.org/z/evo95T1dj. In C++17, the string object is zero-initialized at runtime, so the object is stored in writable memory. (It's stored in .bss, so I don't know why the runtime zero-initialization is necessary, but I digress.) In C++20, the string object is stored as a bunch of zero bytes in the read-only section .rodata and not initialized at runtime.
The crash arose because the linker ended up picking get from a C++17 object but get::empty from a C++20 object. This meant that get's runtime initialization was trying to write to read-only memory, which of course segfaulted. I haven't dug into how this particular linker selection ended up occurring (it was in an LTO build, which might have complicated things), but as far as I understand, since Clang is emitting get and get::empty in different COMDAT section groups, it's valid for the linker to mix and match them like this. On the other hand, if Clang had emitted get::empty in the same section group as get, the linker would have had to pick or discard both of them as a unit, so the bad mixing couldn't have occurred. Is this understanding correct, and if so, what's the reason for Clang using two different section groups here?