Files
AnyPS5/core/libs/tests/ThreadLocalDestructors.cmake
T
b42ef24e07 fix(libs): keep thread_local objects with destructors out of module teardown on Windows (#4029)
* fix(agc): keep the driver's per-thread scratch out of thread_local teardown

With MinGW's emulated TLS, a thread_local with a destructor in a DLL is
destroyed after its storage is gone: when a std::thread ends, winpthread
runs libgcc's emutls key destructor, which frees the thread's thread_local
storage, and only then does the loader's DLL_THREAD_DETACH run the DLL's
tls_atexit list, so the destructors run on freed memory. The queue worker
touches four of them in libSceAgcDriver (the dispatch-ahead map, the
pending waits, the draw-snapshot compare vectors and the image decode
memo), and Driver::stop's join of the worker then corrupts the heap.
Under cdb's checking heap agc_driver_draw_cache_reuse stopped at the
worker's exit with heap corruption in all 127 runs, with graphics
pipeline libraries, with APS5_NO_PIPELINE_LTO=1 and with APS5_NO_GPL=1.
Without it the damage shows intermittently, as exit 0xc0000374 (2 of 143
runs with a cold NVIDIA shader cache) or as a fault in whatever allocates
next, such as NVIDIA's compiler on the optimized-link thread (#2980).

These objects now live in HostThreadLocal, fiber-local storage that owns
them, as the recorder's buffers do since e424b6b7b.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(libs): keep thread_local objects with destructors out of module teardown

The teardown order behind the driver's heap corruption holds in every
module: with MinGW's emulated TLS a thread's thread_local storage is freed
before the module's DLL_THREAD_DETACH runs its thread_local destructors,
so they run on freed objects. PreciseSleepUs's timer (used, among others,
by the video-out vblank thread) then closes a stale handle; libkernel's
directory path, socket waker and UUID random device, libSceRandom's random
device and libScePosixForWebKit's random device and host lookup free or
close stale objects. They move to HostThreadLocal. The other thread_locals
in the modules are trivially destructible or only built for Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(libs): fail when a module registers a thread_local destructor

prx_thread_local_destructors lists the symbols of every built module and,
where MinGW's thread_local destructor support is linked in, disassembles
the module for calls to __cxa_thread_atexit outside libc's registration of
guest destructors. A module that keeps a thread_local with a destructor
fails it with the function that registers one: with main's libSceRandom
it reports sceRandomGetRandomNumber, and with this branch all 122 modules
pass, libc being the only one that links the support.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agc): keep the image memo behind its pointer

The image decode memo went into HostThreadLocal as the whole array. On
Linux, where HostThreadLocal is a plain thread_local, that put 1024 memo
entries into every thread's TLS block instead of one pointer. It keeps
its unique_ptr as before, now owned by HostThreadLocal, so Linux builds
the same thread_local as main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dean Galvin <dean.galvin@live.com>
2026-10-12 18:25:29 +13:00

39 lines
1.7 KiB
CMake

file(GLOB modules "${MODULES}/*.prx")
if(NOT modules)
message(FATAL_ERROR "no modules in ${MODULES}")
endif()
set(explicit "__cxa_thread_atexit_impl_nid_postfix|LibcInternalExtCxaThreadAtexit_nid_postfix")
set(failures "")
foreach(module IN LISTS modules)
get_filename_component(name "${module}" NAME)
execute_process(COMMAND "${NM}" --defined-only "${module}" OUTPUT_VARIABLE symbols RESULT_VARIABLE result ERROR_QUIET)
if(NOT result EQUAL 0 OR symbols STREQUAL "")
list(APPEND failures "${name}: no symbol table to inspect")
continue()
endif()
if(NOT symbols MATCHES " T __mingw_cxa_thread_atexit\n")
continue()
endif()
execute_process(COMMAND "${OBJDUMP}" -d --no-show-raw-insn "${module}" OUTPUT_VARIABLE code RESULT_VARIABLE result ERROR_QUIET)
if(NOT result EQUAL 0)
list(APPEND failures "${name}: cannot disassemble")
continue()
endif()
string(REGEX MATCHALL "\n[0-9a-f]+ <[^>\n]+>:|(call|jmp)[^\n]*<__cxa_thread_atexit>" lines "${code}")
set(function "")
foreach(line IN LISTS lines)
if(line MATCHES "^\n[0-9a-f]+ <([^>\n]+)>:$")
set(function "${CMAKE_MATCH_1}")
elseif(NOT function MATCHES "^(${explicit})$")
list(APPEND failures "${name}: ${function} registers a thread_local destructor")
endif()
endforeach()
endforeach()
if(failures)
list(REMOVE_DUPLICATES failures)
list(JOIN failures "\n " text)
message(FATAL_ERROR "On Windows a thread_local destructor in a module runs after emulated TLS freed the object; keep such objects in HostThreadLocal:\n ${text}")
endif()
list(LENGTH modules count)
message(STATUS "${count} modules register no thread_local destructors")