Merge pull request #9269 from UnhealthyKraken/test/libc-crt-exit-lifecycle

test(libc): cover CRT exit and TLS cleanup lifecycle
This commit is contained in:
Dean authored and GitHub committed 2026-10-26 17:38:22 +13:00
commit a401865a1a
2 files changed
+82 -3

No files matched your search

+78 -3
View File
@@ -5,15 +5,15 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <atomic>
#include <barrier>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <limits>
#include <stdexcept>
#include <thread>
extern "C" {
}
static void Require(bool value) { if (!value) std::abort(); }
@@ -275,6 +275,80 @@ 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<unsigned> destructors{0};
std::atomic<bool> returned{false};
};
static void APS5_VABI CrtExitDestructor(void* value) {
auto& state = *static_cast<CrtExitState*>(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 auto volatile crtExit = &_Thrd_exit_nid_postfix;
static void* APS5_VABI CrtExitEntry(void* value) {
auto& state = *static_cast<CrtExitState*>(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");
crtExit(state.code);
state.returned = true;
return nullptr;
}
static void ThreadExitLifecycle() {
constexpr std::array<int, 5> codes{0, 42, -1, std::numeric_limits<int>::min(), std::numeric_limits<int>::max()};
constexpr std::array<std::uintptr_t, 5> expected{0, 42, 0xffffffffu, 0x80000000u, 0x7fffffffu};
std::barrier ready(6);
std::array<CrtExitState, 5> states;
std::array<Pthread, 5> 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<std::uintptr_t>(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{};
@@ -508,6 +582,7 @@ static void ThreadHelpers() {
}
int main() {
ThreadExitLifecycle();
KernelKeys();
VersionedSync();
ForwardedCalls();
+4
View File
@@ -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. It is not exported by `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), so a payload process cannot call it there