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.
This commit is contained in:
Dean Galvin committed 2026-10-17 14:45:48 +13:00
1 parent fd9e577d98
commit ea33174d7d
2 files changed
+36

No files matched your search

+5
View File
@@ -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)
+31
View File
@@ -0,0 +1,31 @@
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <process.h>
#include <windows.h>
extern "C" int __real_main(int argc, char** argv);
namespace {
template<typename TFunction>
TFunction LibcFunction(const char* name) {
const HMODULE libc = GetModuleHandleW(L"libc.prx");
if (libc == nullptr) return nullptr;
return reinterpret_cast<TFunction>(reinterpret_cast<void*>(GetProcAddress(libc, name)));
}
}
extern "C" int __wrap_main(int argc, char** argv) {
const int code = __real_main(argc, argv);
const auto terminate = LibcFunction<void (*)(int)>("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;
}