Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

c++

clang

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);
}
like image 686
HolyBlackCat Avatar asked Apr 26 '21 07:04

HolyBlackCat


2 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)
like image 149
HolyBlackCat Avatar answered Oct 20 '22 22:10

HolyBlackCat


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).

like image 25
Paul Sanders Avatar answered Oct 20 '22 22:10

Paul Sanders