From 65920e6fdc69f88b9d07c12e1568ceac08a6ead1 Mon Sep 17 00:00:00 2001 From: Kraken <10617703+UnhealthyKraken@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:52:51 +0100 Subject: [PATCH] test(libc): cover CRT exit and TLS cleanup lifecycle --- core/libs/tests/GuestLibcThreads.cpp | 80 ++++++++++++++++++++++++++++ docs/dev/debt/libc.md | 4 ++ 2 files changed, 84 insertions(+) diff --git a/core/libs/tests/GuestLibcThreads.cpp b/core/libs/tests/GuestLibcThreads.cpp index 001558be39..5e296a0e5d 100644 --- a/core/libs/tests/GuestLibcThreads.cpp +++ b/core/libs/tests/GuestLibcThreads.cpp @@ -1,13 +1,20 @@ +#include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include +#include #include +#include #include #include extern "C" { +int APS5_VABI scePthreadCreate(Pthread*, const PthreadAttr*, PthreadEntry, void*, const char*); +int APS5_VABI scePthreadJoin(Pthread, void**); +void APS5_VABI _Thrd_exit_nid_postfix(int); int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); int APS5_VABI _Mtx_lock_nid_postfix(void** handle); @@ -263,6 +270,78 @@ static unsigned APS5_VABI TestPadGo(GuestPad* base) { return 42; } +static void RequireExit(bool value, const char* message) { + if (!value) { + std::fprintf(stderr, "CRT exit lifecycle: %s\n", message); + std::abort(); + } +} + +struct CrtExitState { + int code = 0; + int key = -1; + std::barrier<>* ready = nullptr; + void* parent = nullptr; + void* self = nullptr; + std::atomic destructors{0}; + std::atomic returned{false}; +}; + +static void APS5_VABI CrtExitDestructor(void* value) { + auto& state = *static_cast(value); + RequireExit(_Thrd_current_nid_postfix() == state.self, "identity during destructor"); + RequireExit(_Tss_get_nid_postfix(state.key) == nullptr, "value cleared before destructor"); + if (state.destructors.fetch_add(1) == 0) + RequireExit(_Tss_set_nid_postfix(state.key, &state) == 0, "reinstall value for another cleanup pass"); +} + +static void* APS5_VABI CrtExitEntry(void* value) { + auto& state = *static_cast(value); + state.self = _Thrd_current_nid_postfix(); + RequireExit(state.self && _Thrd_equal_nid_postfix(state.self, state.self) == 1, "current identity"); + RequireExit(_Thrd_equal_nid_postfix(state.self, state.parent) == 0, "different parent identity"); + RequireExit(_Tss_get_nid_postfix(state.key) == nullptr, "child starts with empty value"); + RequireExit(_Tss_set_nid_postfix(state.key, &state) == 0, "set child value"); + state.ready->arrive_and_wait(); + RequireExit(_Tss_get_nid_postfix(state.key) == &state, "child value isolation"); + _Thrd_exit_nid_postfix(state.code); + state.returned = true; + return nullptr; +} + +static void ThreadExitLifecycle() { + constexpr std::array codes{0, 42, -1, std::numeric_limits::min(), std::numeric_limits::max()}; + constexpr std::array expected{0, 42, 0xffffffffu, 0x80000000u, 0x7fffffffu}; + std::barrier ready(6); + std::array states; + std::array threads{}; + int key = -1, parentValue = 31; + void* parent = _Thrd_current_nid_postfix(); + RequireExit(_Tss_create_nid_postfix(&key, CrtExitDestructor) == 0, "create destructor key"); + RequireExit(_Tss_set_nid_postfix(key, &parentValue) == 0, "set parent value"); + for (unsigned i = 0; i < states.size(); ++i) { + states[i].code = codes[i]; + states[i].key = key; + states[i].ready = &ready; + states[i].parent = parent; + RequireExit(scePthreadCreate(&threads[i], nullptr, CrtExitEntry, &states[i], "CRT exit audit") == 0, "create child"); + } + RequireExit(_Tss_get_nid_postfix(key) == &parentValue, "parent isolated before release"); + ready.arrive_and_wait(); + for (unsigned i = 0; i < states.size(); ++i) { + void* result = nullptr; + RequireExit(scePthreadJoin(threads[i], &result) == 0, "join exiting child"); + RequireExit(reinterpret_cast(result) == expected[i], "full zero-extended exit result"); + RequireExit(!states[i].returned, "exit must not return"); + RequireExit(states[i].destructors == 2, "reinstalled destructor runs twice before join returns"); + RequireExit(states[i].self == threads[i], "created and current identity match"); + } + RequireExit(_Tss_get_nid_postfix(key) == &parentValue, "parent isolated after children exit"); + RequireExit(_Tss_delete_nid_postfix(key) == 0, "delete key"); + RequireExit(_Tss_get_nid_postfix(key) == nullptr, "deleted key value unavailable"); + std::puts("PASS CRT lifecycle: five concurrent exit codes, full return width, identity, parent/child isolation, two destructor passes, key deletion"); +} + static void PadLaunchAndJoin() { unsigned (APS5_VABI* vtable[1])(GuestPad*) = {TestPadGo}; TestPad unregPad{}; @@ -375,6 +454,7 @@ static void ForwardedCalls() { } int main() { + ThreadExitLifecycle(); KernelKeys(); ForwardedCalls(); MutexBasics(); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 581f589299..dd5684d3b9 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -1,5 +1,9 @@ # libc technical debt +### Silent stubs + +- [_Mtx_current_owns](../../../core/libs/prx/libc/src/Threads.cpp) (libSceLibcInternal, implemented in libc, NID `VYQwFs4CC4Y`) returns 1 and ignores its argument. This follows the PS5 12.00 body reading reported in [PR8859](https://github.com/boykopovar/AnyPS5/pull/8859), rather than inspecting host mutex ownership. The firmware observation is attributed to that contributor; other firmware versions and the argument's console type remain unverified. + ### Unknown function info - [strtonum](../../../core/libs/prx/libc/src/StringExtensions.cpp) (libc) is unverified on the console. Its signature, decimal parsing, inclusive bounds, error strings, error precedence and errno preservation follow [FreeBSD 11.4 strtonum.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/strtonum.c). Parsing uses the host `strtoll` with base 10.