@@ -947,12 +947,7 @@ void ListenConnections(EventLoop& loop, SocketId fd, InitImpl& init, std::option
947947 });
948948}
949949
950- extern thread_local ThreadContext g_thread_context; // NOLINT(bitcoin-nontrivial-threadlocal)
951- // Silence nonstandard bitcoin tidy error "Variable with non-trivial destructor
952- // cannot be thread_local" which should not be a problem on modern platforms, and
953- // could lead to a small memory leak at worst on older ones.
954-
955- // ! Return the current thread's ThreadContext.
950+ // ! Return the current thread's ThreadContext, creating it on first use.
956951// !
957952// ! Why per-thread state is needed at all: libmultiprocess has no control over
958953// ! which threads the C++ application uses to call ProxyClient methods after
@@ -969,7 +964,60 @@ extern thread_local ThreadContext g_thread_context; // NOLINT(bitcoin-nontrivial
969964// ! are freed. A thread_local object is the C++ mechanism that provides both
970965// ! of these: per-thread storage plus a destructor that runs at thread exit
971966// ! (the C equivalent would be a pthread key destructor). This is why
972- // ! ThreadContext is thread_local and why its destructor is nontrivial.
967+ // ! ThreadContext is thread_local and why its destructor is nontrivial —
968+ // ! which is what makes the MinGW bug below bite here.
969+ // !
970+ // ! It is normally an ordinary function-local thread_local object destroyed at
971+ // ! thread exit, except on MinGW, where it is a lazily-created heap object
972+ // ! that is deliberately leaked (held by a trivially-destructible thread_local
973+ // ! pointer, so no destructor is registered at thread exit at all). Note that
974+ // ! on MinGW this skips more than freeing memory: releasing the remote Thread
975+ // ! capabilities at client-thread exit is what lets the server destroy that
976+ // ! thread's dedicated server threads, so with the workaround, server threads
977+ // ! belonging to exited client threads are only freed when the connection
978+ // ! closes (via the disconnect cleanup callbacks registered in SetThread /
979+ // ! ProxyClient<Thread>::m_disconnect_cb).
980+ // !
981+ // ! The MinGW workaround exists because MinGW-w64's emutls implementation can
982+ // ! free the heap storage backing thread_local variables before the C++
983+ // ! destructors registered via __cxa_thread_atexit run at thread exit. Both
984+ // ! cleanups are implemented as winpthreads pthread key destructors (run by
985+ // ! _pthread_cleanup_dest in winpthreads src/thread.c), and pthread key
986+ // ! destructor order is unspecified, so gcc's emutls key destructor
987+ // ! (emutls_destroy in libgcc/emutls.c, which frees each thread_local
988+ // ! object's storage and the per-thread array) can run before the key
989+ // ! destructor that invokes the C++ thread_local destructors. When that
990+ // ! happens, any thread_local object with a nontrivial destructor is
991+ // ! destroyed after its own memory was already freed. For this ThreadContext
992+ // ! struct that meant ~ThreadContext walking the request_threads /
993+ // ! callback_threads std::map trees through freed memory and double-freeing
994+ // ! their nodes, corrupting the heap.
995+ // !
996+ // ! This was observed in Bitcoin Core Windows CI as intermittent
997+ // ! STATUS_HEAP_CORRUPTION (0xC0000374) exit code 3221226356 crashes of both
998+ // ! bitcoin-cli (its EventLoop::loop thread exiting at IPC teardown) and
999+ // ! bitcoin-node (per-request server threads created by
1000+ // ! ProxyServer<ThreadMap>::makeThread() exiting after client disconnects) in
1001+ // ! x86_64-w64-mingw32 (msvcrt) cross builds, but not in
1002+ // ! x86_64-w64-mingw32ucrt builds — presumably pthread-key creation-order
1003+ // ! luck, since the underlying bug is CRT-independent and unsafe on all
1004+ // ! mingw-posix toolchains. Under gdb the crash appears as SIGSEGV in
1005+ // ! ~ThreadContext inside _pthread_cleanup_dest with the object memory
1006+ // ! containing the 0xfeeefeee "freed heap" debug fill pattern, matching the
1007+ // ! signature described in the upstream reports below. (Without a debugger
1008+ // ! there is no fill pattern, so the destructor walks stale-but-intact
1009+ // ! pointers and double-frees them, which is why the corruption was
1010+ // ! intermittent and often detected later, e.g. in kj::AsyncIoContext
1011+ // ! teardown, rather than at the faulting site.)
1012+ // !
1013+ // ! Known upstream reports of this mingw-w64 bug:
1014+ // ! - https://sourceforge.net/p/mingw-w64/bugs/527/ "thread_local stl object's destructor causing crash"
1015+ // ! - https://sourceforge.net/p/mingw-w64/bugs/727/ "Crash during destruction of thread_local objects"
1016+ // ! - https://sourceforge.net/p/mingw-w64/bugs/445/ "thread_local destructors broken with posix threading"
1017+ // ! - https://sourceforge.net/p/mingw-w64/bugs/859/ "thread_local destructors not called at thread exit"
1018+ // ! - https://github.com/msys2/MINGW-packages/issues/2519 "gcc thread_local destructor cause use after free"
1019+ // ! - https://www.mail-archive.com/mingw-w64-public@lists.sourceforge.net/msg17975.html
1020+ // ! (proposed fix discussion: "fix __cxa_thread_atexit destructors on GCC")
9731021ThreadContext& CurrentThread ();
9741022
9751023} // namespace mp
0 commit comments