From ea33174d7dc18041dbce83e9093d332110cbaa8b Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Sat, 17 Oct 2026 14:38:28 +1300 Subject: [PATCH] fix(tests): end Windows test processes through libc before DLL detach On Windows a test that returns from main ends through exit and ExitProcess, which kills every other thread before the prx static destructors run at DLL_PROCESS_DETACH. Detached host threads (the driver's async log thread, libkernel's file watch thread, ...) die holding winpthreads and queue locks, and a destructor that then locks a mutex, waits for the dead logger or destroys a condition variable never returns: the test prints its last line and hangs until the CTest timeout (#6791). Every test executable now wraps main on MinGW. After main returns the wrapper runs the executable's own exit handlers and static destructors (_cexit), as exit does today, flushes the streams and ends the process with LibcTerminate_nid_no_patch, the path the guest exit already takes. That flushes the driver log while its thread is alive and calls TerminateProcess, so no DLL_PROCESS_DETACH work runs after the threads are gone. The libc shutdown callbacks are not run, as on a plain return from main on Linux. A test that does not load libc.prx returns as before. --- CMakeLists.txt | 5 +++++ cmake/WindowsTestExit.cpp | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 cmake/WindowsTestExit.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 85d556c961..2f79844c69 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,10 +6,15 @@ endif() project(Relinker CXX) set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") +set(ANYPS5_WINDOWS_TEST_EXIT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/WindowsTestExit.cpp") function(add_test_executable target) add_executable(${target} ${ARGN}) set_target_properties(${target} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>") + if(MINGW) + target_sources(${target} PRIVATE "${ANYPS5_WINDOWS_TEST_EXIT}") + target_link_options(${target} PRIVATE LINKER:--wrap=main) + endif() endfunction() option(BUILD_TESTING "Build and register relinker tests" OFF) diff --git a/cmake/WindowsTestExit.cpp b/cmake/WindowsTestExit.cpp new file mode 100644 index 0000000000..abe70fd2c7 --- /dev/null +++ b/cmake/WindowsTestExit.cpp @@ -0,0 +1,31 @@ +#include +#include +#include +#include +#include + +extern "C" int __real_main(int argc, char** argv); + +namespace { + +template +TFunction LibcFunction(const char* name) { + const HMODULE libc = GetModuleHandleW(L"libc.prx"); + if (libc == nullptr) return nullptr; + return reinterpret_cast(reinterpret_cast(GetProcAddress(libc, name))); +} + +} + +extern "C" int __wrap_main(int argc, char** argv) { + const int code = __real_main(argc, argv); + const auto terminate = LibcFunction("LibcTerminate_nid_no_patch"); + if (terminate == nullptr) return code; + _cexit(); + std::cout.flush(); + std::clog.flush(); + std::cerr.flush(); + std::fflush(nullptr); + terminate(code); + return code; +}