Skip to content

Commit 1b9254d

Browse files
committed
Merge libmultiprocess Windows support and MinGW cleanup fix
Rebase bitcoin-core/libmultiprocess#231 onto upstream master 2dba130478 and include the MinGW ThreadContext workaround from bitcoin/bitcoin#32387. Preserve current upstream race fixes and CI. The rebased upstream history is retained locally on codex/libmultiprocess-windows.
2 parents f8e9033 + 86c02b9 commit 1b9254d

18 files changed

Lines changed: 871 additions & 158 deletions

File tree

‎src/ipc/libmultiprocess/.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ jobs:
148148
strategy:
149149
fail-fast: false
150150
matrix:
151-
config: [default, llvm, gnu32, sanitize, olddeps, newdeps]
151+
config: [default, llvm, gnu32, sanitize, olddeps, newdeps, windows]
152152

153153
name: build • ${{ matrix.config }}
154154

‎src/ipc/libmultiprocess/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ target_link_libraries(multiprocess PUBLIC CapnProto::capnp)
193193
target_link_libraries(multiprocess PUBLIC CapnProto::capnp-rpc)
194194
target_link_libraries(multiprocess PUBLIC CapnProto::kj)
195195
target_link_libraries(multiprocess PUBLIC CapnProto::kj-async)
196+
target_link_libraries(multiprocess PUBLIC Threads::Threads)
196197
set_target_properties(multiprocess PROPERTIES
197198
PUBLIC_HEADER "${MP_PUBLIC_HEADERS}")
198199
install(TARGETS multiprocess EXPORT LibTargets

‎src/ipc/libmultiprocess/ci/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ CI_CONFIG=ci/configs/gnu32.bash ci/scripts/run.sh
2222
CI_CONFIG=ci/configs/sanitize.bash ci/scripts/run.sh
2323
CI_CONFIG=ci/configs/olddeps.bash ci/scripts/run.sh
2424
CI_CONFIG=ci/configs/newdeps.bash ci/scripts/run.sh
25+
CI_CONFIG=ci/configs/windows.bash ci/scripts/run.sh
2526
```
2627

2728
By default CI jobs will reuse their build directories. `CI_CLEAN=1` can be specified to delete them before running instead.
@@ -50,4 +51,3 @@ macOS or Linux:
5051
--matrix config:sanitize
5152
```
5253

53-
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
CI_DESC="CI job cross-compiling to Windows with MinGW, tested with Wine"
2+
CI_DIR=build-windows
3+
CI_CACHE_NIX_STORE=true
4+
NIX_ARGS=(
5+
--arg windows true
6+
--arg minimal true
7+
)
8+
CMAKE_ARGS=(
9+
-G Ninja
10+
-DCMAKE_SYSTEM_NAME=Windows
11+
-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER
12+
-DCMAKE_CROSSCOMPILING_EMULATOR=wine
13+
# -Wa,-mbig-obj: template-heavy C++ generates >32K COFF sections per .obj;
14+
# BigCOFF format raises the limit. Must be passed to the assembler via -Wa.
15+
"-DCMAKE_CXX_FLAGS=-Wa,-mbig-obj"
16+
)
17+
# CXX is set by the nix cross shell to the mingw g++ wrapper; cmake picks
18+
# it up from the environment, so no CMAKE_CXX_COMPILER override needed.
19+
BUILD_ARGS=(-k 0)
20+
# Build native mpgen first (as a code generator for cross-compiled test/example targets).
21+
MPGEN_PRE_BUILD=1

‎src/ipc/libmultiprocess/ci/scripts/ci.sh‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,49 @@ if [ -n "${CAPNP_CHECKOUT-}" ]; then
3838
fi
3939

4040
src_dir=$PWD
41+
42+
# If cross-compiling, build native mpgen first so it can be used as a code
43+
# generator when cmake invokes it from add_custom_command (which does not go
44+
# through CMAKE_CROSSCOMPILING_EMULATOR, unlike add_test executables).
45+
if [ -n "${MPGEN_PRE_BUILD-}" ]; then
46+
native_dir="${src_dir}/${CI_DIR}-native"
47+
[ -n "${CI_CLEAN-}" ] && rm -rf "$native_dir"
48+
mkdir -p "$native_dir"
49+
# Unset cross-compilation env vars so cmake uses the native compiler and
50+
# does a native (not cross) build. Key vars to clear:
51+
# CXX/CC/AR/RANLIB/LD - set to cross-compiler by the cross shell
52+
# cmakeFlags - nix cross shell injects -DCMAKE_SYSTEM_NAME=Windows etc.
53+
# Pass NATIVE_CAPNPROTO_PREFIX as CMAKE_PREFIX_PATH so cmake finds the
54+
# native Cap'n Proto rather than the cross-compiled one.
55+
native_cmake_args=()
56+
if [ -n "${NATIVE_CAPNPROTO_PREFIX-}" ]; then
57+
# Build a cmake prefix path with native capnproto and its dependencies
58+
# (openssl, zlib) so find_package and find_dependency succeed.
59+
native_prefix="${NATIVE_CAPNPROTO_PREFIX}"
60+
[ -n "${NATIVE_OPENSSL_DEV-}" ] && native_prefix="${native_prefix};${NATIVE_OPENSSL_DEV}"
61+
[ -n "${NATIVE_OPENSSL_LIB-}" ] && native_prefix="${native_prefix};${NATIVE_OPENSSL_LIB}"
62+
[ -n "${NATIVE_ZLIB_DEV-}" ] && native_prefix="${native_prefix};${NATIVE_ZLIB_DEV}"
63+
[ -n "${NATIVE_ZLIB_LIB-}" ] && native_prefix="${native_prefix};${NATIVE_ZLIB_LIB}"
64+
native_cmake_args+=(
65+
"-DCMAKE_PREFIX_PATH=${native_prefix}"
66+
"-DCapnProto_DIR=${NATIVE_CAPNPROTO_PREFIX}/lib/cmake/CapnProto"
67+
)
68+
fi
69+
# -static-libstdc++ / -static-libgcc: the native mpgen must run inside the
70+
# cross nix shell where the native libstdc++.so may not be in LD_LIBRARY_PATH.
71+
native_cmake_args+=("-DCMAKE_EXE_LINKER_FLAGS=-static-libstdc++ -static-libgcc")
72+
(cd "$native_dir" && env -u CXX -u CC -u AR -u RANLIB -u LD -u cmakeFlags cmake "$src_dir" "${native_cmake_args[@]+${native_cmake_args[@]}}" && cmake --build . -t mpgen)
73+
CMAKE_ARGS+=("-DMPGEN_EXECUTABLE=${native_dir}/mpgen")
74+
75+
# Override capnp tool executables: the cross capnproto cmake config sets
76+
# CAPNP_EXECUTABLE to capnp.exe (Windows binary), which can't run on Linux.
77+
# Use the native capnp/capnpc-c++ binaries from pkgs.capnproto in nativeBuildInputs.
78+
_capnp=$(command -v capnp 2>/dev/null || true)
79+
_capnpc=$(command -v capnpc-c++ 2>/dev/null || true)
80+
[ -n "$_capnp" ] && CMAKE_ARGS+=("-DCAPNP_EXECUTABLE=$_capnp")
81+
[ -n "$_capnpc" ] && CMAKE_ARGS+=("-DCAPNPC_CXX_EXECUTABLE=$_capnpc")
82+
fi
83+
4184
mkdir -p "$CI_DIR"
4285
cd "$CI_DIR"
4386
export CMAKE_BUILD_PARALLEL_LEVEL="$(nproc)"
@@ -67,4 +110,13 @@ else
67110
cmake --build . --target "$t" -- "${BUILD_ARGS[@]+"${BUILD_ARGS[@]}"}"
68111
done
69112
fi
113+
# When cross-compiling for Windows, copy GCC and MCF thread runtime DLLs
114+
# alongside the test executables so wine can find them (wine DLL loading
115+
# checks the executable's directory first, before any search-path logic).
116+
if [ -n "${WIN_RUNTIME_DLLS-}" ]; then
117+
IFS=: read -ra _dll_dirs <<< "$WIN_RUNTIME_DLLS"
118+
for _dir in "${_dll_dirs[@]}"; do
119+
find "$_dir" -maxdepth 1 -name "*.dll" -exec cp -n {} test/ \;
120+
done
121+
fi
70122
ctest --output-on-failure

‎src/ipc/libmultiprocess/example/example.cpp‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <cstring> // IWYU pragma: keep
99
#include <filesystem>
1010
#include <fstream>
11+
#include <functional>
1112
#include <future>
1213
#include <iostream>
1314
#include <kj/common.h>
@@ -26,11 +27,11 @@ namespace fs = std::filesystem;
2627

2728
static auto Spawn(mp::EventLoop& loop, const std::string& process_argv0, const std::string& new_exe_name)
2829
{
29-
const auto [pid, socket] = mp::SpawnProcess([&](mp::SpawnConnectInfo info) -> std::vector<std::string> {
30+
const auto [pid, socket] = mp::SpawnProcess([&](std::string connect_info) -> std::vector<std::string> {
3031
fs::path path = process_argv0;
3132
path.remove_filename();
3233
path.append(new_exe_name);
33-
return {path.string(), std::move(info)};
34+
return {path.string(), std::move(connect_info)};
3435
});
3536
return std::make_tuple(mp::ConnectStream<InitInterface>(loop, mp::MakeStream(loop, socket)), pid);
3637
}

‎src/ipc/libmultiprocess/include/mp/proxy-io.h‎

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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")
9731021
ThreadContext& CurrentThread();
9741022

9751023
} // namespace mp

‎src/ipc/libmultiprocess/include/mp/util.h‎

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@
2929
#include <memory>
3030
#endif
3131

32+
#ifdef WIN32
33+
// WIN32_LEAN_AND_MEAN excludes commdlg.h which defines `#define INTERFACE
34+
// IPrintDialogServices` — this conflicts with capnp::Kind::INTERFACE used
35+
// in CAPNP_DECLARE_INTERFACE_HEADER. Must be defined before winsock2.h.
36+
#ifndef WIN32_LEAN_AND_MEAN
37+
#define WIN32_LEAN_AND_MEAN
38+
#endif
39+
#include <windows.h>
40+
#include <winsock2.h>
41+
#endif
42+
3243
namespace mp {
3344

3445
//! Generic utility functions used by capnp code.
@@ -289,33 +300,53 @@ std::string ThreadName(const char* exe_name);
289300
//! errors in python unit tests.
290301
std::string LogEscape(const kj::StringTree& string, size_t max_size);
291302

303+
//! Convert an argument vector into a single command line string suitable for
304+
//! CreateProcess, following the quoting rules of CommandLineToArgvW, which
305+
//! executables use to split the command line back into arguments. Declared
306+
//! unconditionally (not just on windows) so it can be unit tested on any
307+
//! platform.
308+
std::string CommandLineFromArgv(const std::vector<std::string>& argv);
309+
292310
using Stream = kj::Own<kj::AsyncIoStream>;
293311

312+
#ifdef WIN32
313+
// On Windows, ProcessId is defined to be the local process HANDLE rather than
314+
// global process ID, because handles are more useful for controlling and
315+
// waiting for processes. It it possible to obtain the actual process ID from
316+
// handles by calling the GetProcessId API.
317+
using ProcessId = HANDLE;
318+
using SocketId = SOCKET;
319+
constexpr SocketId SocketError{INVALID_SOCKET};
320+
#else
294321
using ProcessId = int;
295322
using SocketId = int;
296323
constexpr SocketId SocketError{-1};
297-
298-
//! Information about parent process passed to child process as a command-line
299-
//! argument. On unix this is the child socket fd number formatted as a string.
300-
using SpawnConnectInfo = std::string;
301-
302-
//! Callback type used by SpawnProcess below.
303-
using SpawnConnectInfoToArgsFn = std::function<std::vector<std::string>(const SpawnConnectInfo&)>;
324+
#endif
304325

305326
//! Spawn a new process that communicates with the current process over a socket
306-
//! pair. Calls connect_info_to_args callback with a connection string that
307-
//! needs to be passed to the child process, and executes the argv command line
308-
//! it returns. Returns child process id and socket id.
309-
std::tuple<ProcessId, SocketId> SpawnProcess(SpawnConnectInfoToArgsFn&& connect_info_to_args);
310-
311-
//! Initialize spawned child process using the SpawnConnectInfo string passed to it,
312-
//! returning a socket id for communicating with the parent process.
313-
SocketId StartSpawned(const SpawnConnectInfo& connect_info);
327+
//! pair. Calls spawn_argv callback with a connection string that needs to be
328+
//! passed to the child process, and executes the argv command line it returns.
329+
//! Returns child process id and socket id.
330+
//!
331+
//! The connection string is just a file descriptor number on unix. On windows,
332+
//! it is a path to a named pipe the parent process will write
333+
//! WSADuplicateSocket info to. In both cases, the child process can call
334+
//! StartSpawned to get a socket handle from the connection string.
335+
std::tuple<ProcessId, SocketId> SpawnProcess(const std::function<std::vector<std::string>(std::string)>& spawn_argv);
336+
337+
//! Initialize spawned child process. The connect_info argument is the
338+
//! connection string SpawnProcess generated in the parent process and passed
339+
//! to the child on its command line. Returns socket id for communicating with
340+
//! the parent process.
341+
SocketId StartSpawned(const std::string& connect_info);
314342

315343
//! Create a socket pair that can be used to communicate within a process or
316344
//! between parent and child processes.
317345
std::array<SocketId, 2> SocketPair();
318346

347+
//! Close a socket, throwing a KJ exception on failure.
348+
void CloseSocket(SocketId fd);
349+
319350
//! Start a process and return its process id. Caller should call WaitProcess
320351
//! on the returned id.
321352
ProcessId StartProcess(const std::vector<std::string>& args);

0 commit comments

Comments
 (0)