Merge pull request #722 from alfalfa-star/feat/libkernel-rtld-thread-atexit

feat(libkernel): implement the rtld thread atexit reference count
This commit is contained in:
Dean 2026-10-07 15:27:44 +13:00 committed by GitHub
commit 1d773dad2e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 98 additions and 16 deletions

View File

@ -2,16 +2,78 @@
#include <cstddef>
#include <array>
#include <cstring>
#include <map>
#include <mutex>
#include <stdexcept>
#include <string>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp"
#ifdef _WIN32
#define NOMINMAX
#include <windows.h>
#else
#include <dlfcn.h>
#include <link.h>
#endif
namespace {
std::mutex rtldHeapMutex;
std::array<void*, 10> rtldHeapApi{};
std::mutex threadAtexitMutex;
std::multimap<const void*, void*> threadAtexitReferences;
const void* imageContaining(const void* address, bool reference, void*& handle, const char* caller) {
handle = nullptr;
#ifdef _WIN32
const DWORD flags = GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | (reference ? 0 : GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT);
HMODULE module = nullptr;
if (!GetModuleHandleExW(flags, reinterpret_cast<LPCWSTR>(address), &module))
throw std::runtime_error(std::string(caller) + ": address outside every loaded image");
if (reference) handle = module;
return module;
#else
Dl_info info{};
link_map* image = nullptr;
if (!dladdr1(address, &info, reinterpret_cast<void**>(&image), RTLD_DL_LINKMAP) || !image)
throw std::runtime_error(std::string(caller) + ": address outside every loaded image");
if (reference && image->l_name && *image->l_name) {
handle = dlopen(image->l_name, RTLD_LAZY | RTLD_NOLOAD);
if (!handle) throw std::runtime_error(std::string(caller) + ": cannot reference " + image->l_name);
}
return image;
#endif
}
int referenceThreadAtexitImage(const void* address, const char* caller) {
void* handle = nullptr;
const void* image = imageContaining(address, true, handle, caller);
std::lock_guard lock(threadAtexitMutex);
threadAtexitReferences.emplace(image, handle);
return 0;
}
int releaseThreadAtexitImage(const void* address, const char* caller) {
void* handle = nullptr;
const void* image = imageContaining(address, false, handle, caller);
{
std::lock_guard lock(threadAtexitMutex);
const auto found = threadAtexitReferences.find(image);
if (found == threadAtexitReferences.end())
throw std::runtime_error(std::string(caller) + ": image holds no thread atexit reference");
handle = found->second;
threadAtexitReferences.erase(found);
}
if (!handle) return 0;
#ifdef _WIN32
if (!FreeLibrary(static_cast<HMODULE>(handle)))
#else
if (dlclose(handle) != 0)
#endif
throw std::runtime_error(std::string(caller) + ": cannot release image");
return 0;
}
void registerApplicationHeapApi(void* const* api) {
if (api == nullptr)
@ -34,16 +96,12 @@ void APS5_VABI sceKernelRtldSetApplicationHeapAPI(void* api[]) {
registerApplicationHeapApi(api);
}
int APS5_VABI sceKernelRtldThreadAtexitDecrement(uint64_t* c) {
(void)c;
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceKernelRtldThreadAtexitDecrement(const void* dsoSymbol) {
return releaseThreadAtexitImage(dsoSymbol, __func__);
}
int APS5_VABI sceKernelRtldThreadAtexitIncrement(uint64_t* c) {
(void)c;
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI sceKernelRtldThreadAtexitIncrement(const void* dsoSymbol) {
return referenceThreadAtexitImage(dsoSymbol, __func__);
}
void APS5_VABI sceKernelSetThreadAtexitCount(get_thread_atexit_count_func_t func) {
@ -66,16 +124,12 @@ void APS5_VABI _sceKernelRtldSetApplicationHeapAPI_nid_postfix(void* api[]) {
registerApplicationHeapApi(api);
}
int APS5_VABI _sceKernelRtldThreadAtexitDecrement_nid_postfix(std::uint64_t* counter) {
(void)counter;
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI _sceKernelRtldThreadAtexitDecrement_nid_postfix(const void* dsoSymbol) {
return releaseThreadAtexitImage(dsoSymbol, "_sceKernelRtldThreadAtexitDecrement");
}
int APS5_VABI _sceKernelRtldThreadAtexitIncrement_nid_postfix(std::uint64_t* counter) {
(void)counter;
NotImplemented_nid_no_patch(__func__);
return 0;
int APS5_VABI _sceKernelRtldThreadAtexitIncrement_nid_postfix(const void* dsoSymbol) {
return referenceThreadAtexitImage(dsoSymbol, "_sceKernelRtldThreadAtexitIncrement");
}
void APS5_VABI _sceKernelSetThreadAtexitCount_nid_postfix(get_thread_atexit_count_func_t callback) {

View File

@ -2,14 +2,26 @@
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <thread>
extern "C" {
void* APS5_VABI dlopen_nid_postfix(const char*, int);
void* APS5_VABI dlsym_nid_postfix(void*, const char*);
int APS5_VABI dlclose_nid_postfix(void*);
char* APS5_VABI dlerror_nid_postfix();
int APS5_VABI _sceKernelRtldThreadAtexitIncrement_nid_postfix(const void*);
int APS5_VABI _sceKernelRtldThreadAtexitDecrement_nid_postfix(const void*);
}
static void Require(bool value) { if (!value) std::abort(); }
template<typename TFunction>
static bool ThrowsRuntimeError(TFunction function) {
try {
function();
} catch (const std::runtime_error&) {
return true;
}
return false;
}
int main(int argc, char** argv) {
Require(argc == 2);
Require(dlerror_nid_postfix() == nullptr);
@ -61,4 +73,19 @@ int main(int argc, char** argv) {
Require(dlclose_nid_postfix(redirected) == 0);
std::filesystem::remove(guest);
std::filesystem::remove(relinked);
void* pinned = dlopen_nid_postfix(argv[1], 2);
add = reinterpret_cast<Add>(dlsym_nid_postfix(pinned, "GuestModuleAdd"));
Require(add && _sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast<const void*>(add)) == 0);
Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast<const void*>(add)) == 0);
Require(dlclose_nid_postfix(pinned) == 0);
Require(add(20, 22) == 42);
Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast<const void*>(add)) == 0);
Require(add(40, 2) == 42);
Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast<const void*>(add)) == 0);
Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast<const void*>(&ThrowsRuntimeError<void (*)()>)) == 0);
Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast<const void*>(&ThrowsRuntimeError<void (*)()>)) == 0);
Require(ThrowsRuntimeError([] { _sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast<const void*>(&Require)); }));
int local = 0;
Require(ThrowsRuntimeError([&] { _sceKernelRtldThreadAtexitIncrement_nid_postfix(&local); }));
}

View File

@ -93,6 +93,7 @@ Throughout the project, every function at every stage either **does exactly what
- [sceLibcInternalHeapErrorReportForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature
- [std::_Fiopen](../../core/libs/prx/libc/src/CxxAbiSupport.cpp) (libc) - `_ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi` (NID `vYWK2Pz8vGE`; the name comes from the public NID list): the values of the `_Openmode` argument and the meaning of the protection argument are unknown, so it throws when called. It is exported so that a title that imports it (PPSA21564 and PPSA21567 do) loads against the host libc when its own `libc.prx` is left out of the guest conversion.
- [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw
- [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match
- [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - unknown return type and behaviour; a title's `libc.prx` calls it from `__cxa_finalize` with the module handle of a non-null dso
- [std_execute_once](../../core/libs/prx/libc/Export.cpp) (libc) - assumed to be the plain-named twin of `std::_Execute_once` (`_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_`, the callee of Dinkumware `call_once`): it delegates there and gets the [thread.once.callonce] semantics already verified for that symbol (one invocation per flag, concurrent callers block until it completes, and a failing or throwing invocation leaves the flag unready so the next caller runs it). The name equivalence is not confirmed on a title, and the callback convention (called with (null, arg, null), non-zero return meaning success) is the twin's, unverified for this symbol
- [__progname](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export