`std::call_once` always segfaults on Clang 12 on Windows (when using libstdc++)

Viewed 857

I'm looking for a workaround, which will probably involve patching libstdc++ headers. Preserving binary compatibility is preferred but not obligatory, since I'm not using any precompiled C++ code except libstdc++.

I want to keep the std::call_once interface, since I'm trying to compile third-party code that uses is, which I don't want to change.


Here's my code:

#include <iostream>
#include <mutex>

int main()
{
    std::once_flag flag;
    std::call_once(flag, []{std::cout << "Once!\n";});
}

Running it causes a segmentation fault.

I cross-compile it to Windows from Ubuntu using Clang 12, using the standard library from MSYS2 GCC 10.2. Then I test the result with Wine (a quick test showed that it crashes on a VM too). But you should be able to reproduce the results by compiling on Windows natively (using the official Clang binaries + MSYS2 GCC, since MSYS2 doesn't have Clang 12 yet).

I compile it like this:

clang++-12 1.cpp --target=x86_64-w64-mingw32 --sysroot=/mingw64 -pthread -femulated-tls

If I add -g, GDB shows following:

Program received signal SIGSEGV, Segmentation fault.
0x00000001e014dc4a in ?? () from Z:\home\holyblackcat\Sandbox\2\libgcc_s_seh-1.dll
(gdb) bt
#0  0x00000001e014dc4a in ?? () from Z:\home\holyblackcat\Sandbox\2\libgcc_s_seh-1.dll
#1  0x00000000004015f3 in std::call_once<main::$_0> (__once=..., __f=...) at /mingw64/include/c++/10.2.0/mutex:721
#2  0x00000000004015b5 in main () at 1.cpp:8
(gdb) f 1
#1  0x00000000004015f3 in std::call_once<main::$_0> (__once=..., __f=...) at /mingw64/include/c++/10.2.0/mutex:721
721           __once_callable = std::__addressof(__callable);
(gdb) list
716           auto __callable = [&] {
717               std::__invoke(std::forward<_Callable>(__f),
718                             std::forward<_Args>(__args)...);
719           };
720     #ifdef _GLIBCXX_HAVE_TLS
721           __once_callable = std::__addressof(__callable);
722           __once_call = []{ (*(decltype(__callable)*)__once_callable)(); };
723     #else
724           unique_lock<mutex> __functor_lock(__get_once_mutex());
725           __once_functor = __callable;

Clang version is:

# clang++-12 --version --target=x86_64-w64-mingw32 --sysroot=/mingw64 -pthread -femulated-tls
Ubuntu clang version 12.0.1-++20210423082613+072c90a863aa-1~exp1~20210423063319.76
Target: x86_64-w64-windows-gnu
Thread model: posix

GCC version (which provides libstdc++) is:

# g++ --version
g++.exe (Rev10, Built by MSYS2 project) 10.2.0

Compiling the code with this GCC (which is native, not cross-compiling), produces a working code.

What's going on here? Are there any workarounds, or do I have to downgrade to Clang 11?


I reported a Clang bug.

This bug looks related.


Here is the current segfaulting implementation of call_once, after preprocessing:

struct once_flag
{
  private:
    typedef __gthread_once_t __native_type;
    __native_type _M_once = 0;

  public:
    constexpr once_flag() noexcept = default;
    once_flag(const once_flag &) = delete;
    once_flag &operator=(const once_flag &) = delete;
    template <typename _Callable, typename... _Args>
    friend void call_once(once_flag &__once, _Callable &&__f, _Args &&...__args);
};

extern __thread void *__once_callable;
extern __thread void (*__once_call)();
extern "C" void __once_proxy(void);

template <typename _Callable, typename... _Args>
void call_once(once_flag &__once, _Callable &&__f, _Args &&...__args)
{
    auto __callable = [&]
    {
        std::__invoke(std::forward<_Callable>(__f), std::forward<_Args>(__args)...);
    };
    __once_callable = std::__addressof(__callable);
    __once_call = []{(*(decltype(__callable) *)__once_callable)();};
    int __e = __gthread_once(&__once._M_once, &__once_proxy);
    if (__e)
        __throw_system_error(__e);
}
3 Answers

This was fixed in Clang 13, in commit 0e4cf80. (Thanks @mstorsjo.)


If you're stuck with Clang 12, you can patch libstdc++ headers as a workaround. The patch below replaces global thread-local variables with static function-local thread-local variables, which are not affected by the bug.

To apply, save following to patch.txt, then do patch /mingw64/include/c++/10.3.0/mutex patch.txt.

@@ -691,8 +691,8 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION

   /// @cond undocumented
 #ifdef _GLIBCXX_HAVE_TLS
-  extern __thread void* __once_callable;
-  extern __thread void (*__once_call)();
+  inline void *&__once_callable_get() {static __thread void *__ret; return __ret;}
+  inline void (*&__once_call_get())() {static __thread void (*__ret)(); return __ret;}
 #else
   extern function<void()> __once_functor;

@@ -703,7 +703,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
   __get_once_mutex();
 #endif

-  extern "C" void __once_proxy(void);
+  extern "C" inline void __once_proxy_inline(void) {__once_call_get()();}
   /// @endcond

   /// Invoke a callable and synchronize with other calls using the same flag
@@ -718,15 +718,15 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
            std::forward<_Args>(__args)...);
       };
 #ifdef _GLIBCXX_HAVE_TLS
-      __once_callable = std::__addressof(__callable); // NOLINT: PR 82481
-      __once_call = []{ (*(decltype(__callable)*)__once_callable)(); };
+      __once_callable_get() = std::__addressof(__callable); // NOLINT: PR 82481
+      __once_call_get() = []{ (*(decltype(__callable)*)__once_callable_get())(); };
 #else
       unique_lock<mutex> __functor_lock(__get_once_mutex());
       __once_functor = __callable;
       __set_once_functor_lock_ptr(&__functor_lock);
 #endif

-      int __e = __gthread_once(&__once._M_once, &__once_proxy);
+      int __e = __gthread_once(&__once._M_once, &__once_proxy_inline);

 #ifndef _GLIBCXX_HAVE_TLS
       if (__functor_lock)

And here is an equivalent patch for GCC 10.2 (above is for 10.3):

@@ -691,8 +691,8 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
 
   /// @cond undocumented
 #ifdef _GLIBCXX_HAVE_TLS
-  extern __thread void* __once_callable;
-  extern __thread void (*__once_call)();
+  inline void *&__once_callable_get() {static __thread void *__ret; return __ret;}
+  inline void (*&__once_call_get())() {static __thread void (*__ret)(); return __ret;}
 #else
   extern function<void()> __once_functor;
 
@@ -703,7 +703,7 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
   __get_once_mutex();
 #endif
 
-  extern "C" void __once_proxy(void);
+  extern "C" inline void __once_proxy_inline(void) {__once_call_get()();}
   /// @endcond
 
   /// Invoke a callable and synchronize with other calls using the same flag
@@ -718,15 +718,15 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
            std::forward<_Args>(__args)...);
       };
 #ifdef _GLIBCXX_HAVE_TLS
-      __once_callable = std::__addressof(__callable);
-      __once_call = []{ (*(decltype(__callable)*)__once_callable)(); };
+      __once_callable_get() = std::__addressof(__callable);
+      __once_call_get() = []{ (*(decltype(__callable)*)__once_callable_get())(); };
 #else
       unique_lock<mutex> __functor_lock(__get_once_mutex());
       __once_functor = __callable;
       __set_once_functor_lock_ptr(&__functor_lock);
 #endif
 
-      int __e = __gthread_once(&__once._M_once, &__once_proxy);
+      int __e = __gthread_once(&__once._M_once, &__once_proxy_inline);
 
 #ifndef _GLIBCXX_HAVE_TLS
       if (__functor_lock)
@@ -735,8 +735,8 @@ _GLIBCXX_BEGIN_NAMESPACE_VERSION
 
 #ifdef __clang_analyzer__
       // PR libstdc++/82481
-      __once_callable = nullptr;
-      __once_call = nullptr;
+      __once_callable_get() = nullptr;
+      __once_call_get() = nullptr;
 #endif
 
       if (__e)

One way you might work around this is to take advantage of the fact that static variables are, since C++11, initialised in a threadsafe way. Example:

#include <iostream>

void test ()
{
    std::cout << "test\n";
    static bool once = [] { std::cout << "Once!\n"; return true; } ();
    (void) once;
}

int main()
{
    test ();
    test ();
}

Output:

test
Once!
test

As you can see, the lambda is called the first time the static variable comes into scope (and will only be called once).

Wild guess: It is crashing due to the absence of InitializeCriticalSectionEx(), which means you either need to upgrade your Windows VM past XP, or specify to the compiler/linker that you are targeting Windows subsystem v5.02. No patch needed. ;-)

Edit #1:

Shot in the dark: Try adding -D_WIN32_WINNT=0x0502 to your compile. It might resolve some link errors. Although mixing and matching libstd++ versions may never work here, and the exact issues will different from one platform/compiler to the next.

As another commenter noted, the initialization of static variables local to a function are now thread-safe in C++, and this seems to have forced some changes to the way TLS works on Windows, right down to the "subsystem" level. In our own build, we pass -Zc:threadSafeInit- and either /subsystem:windows,5.02 (x64) or /subsystem:windows,5.01 (x86) to VC++, simply so that it works on XP. But it is with the understanding that these newfangled thread-safe static locals won't happen. I can only imagine what the issues might be on Windows 10, where the NT loader is multi-threaded.

Edit #2:

Last attempt to steal bounty, now from the OP himself: I was going down this path. My current theory is that there is a bug in this new clang 11 compiler that causes it to fetch the "known" header <iostream> from the wrong place, despite --sysroot. Perhaps only when cross-compiling.

lstrand@styx:~/scratch/emulated-tls$ strace -o strace.out clang++-12 -E 1.exe 1.cpp --target=x86_64-w64-mingw32 --sysroot=/home/lstrand/scratch/emulated-tls/root > pp
clang: warning: 1.exe: 'linker' input unused [-Wunused-command-line-argument]
1.cpp:1:10: fatal error: 'iostream' file not found
#include <iostream>
         ^~~~~~~~~~
1 error generated.
lstrand@styx:~/scratch/emulated-tls$ grep iostream strace.out 
pread64(3, "#include <iostream>\n#include <mu"..., 134, 0) = 134
openat(AT_FDCWD, "/usr/lib/llvm-12/lib/clang/12.0.1/include/iostream", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "./iostream", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
write(2, "'iostream' file not found", 25) = 25
write(2, "#include <iostream>", 19)     = 19
lstrand@styx:~/scratch/emulated-tls$ 
Related