From ba1a85ebb39c471daf4818c43bb6af34b3809160 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:28:17 +0300 Subject: [PATCH 1/6] feat(libc): add Windows module path helper for backtrace resolution --- core/libs/prx/libc/src/Backtrace.cpp | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp index 29c7dae1113a..59113988abbf 100644 --- a/core/libs/prx/libc/src/Backtrace.cpp +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -7,10 +7,33 @@ #include "prx/libc/include/General.hpp" -#ifdef __linux__ +#if defined(__linux__) #include #include - +#elif defined(_WIN32) +#include +#include "prx/libc/include/exceptions/Unwind.hpp" +extern "C" { +_Unwind_Reason_Code APS5_VABI _Unwind_Backtrace_nid_postfix(_Unwind_Trace_Fn, void*); +_Unwind_Ptr APS5_VABI _Unwind_GetIP_nid_postfix(_Unwind_Context*); +std::string BacktraceModulePath_nid_no_patch(HMODULE handle, std::size_t initialCapacity) { + std::string module; + std::size_t capacity = initialCapacity == 0 ? 1 : initialCapacity; + while (true) { + module.resize(capacity); + SetLastError(ERROR_SUCCESS); + const DWORD length = GetModuleFileNameA(handle, module.data(), static_cast(module.size())); + if (length == 0) return {}; + if (length < capacity && GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + module.resize(length); + return module; + } + if (capacity >= 32768) return {}; + capacity *= 2; + } +} +} +#endif namespace { constexpr int MaxFrames = 256; From 6fb1b56526e0326fb0432cbcdc4afc5132de92ae Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:28:24 +0300 Subject: [PATCH 2/6] feat(libc): implement sceLibcBacktraceSelf and GetBufferSize on Windows --- core/libs/prx/libc/src/Backtrace.cpp | 38 +++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp index 59113988abbf..53662a77d0a4 100644 --- a/core/libs/prx/libc/src/Backtrace.cpp +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -34,6 +34,9 @@ std::string BacktraceModulePath_nid_no_patch(HMODULE handle, std::size_t initial } } #endif + +#if defined(__linux__) || defined(_WIN32) + namespace { constexpr int MaxFrames = 256; @@ -54,15 +57,48 @@ static_assert(sizeof(FrameRecord) == NameOffset); [[gnu::noinline]] std::vector CaptureFrames(int limit) { void* addresses[MaxFrames]; +#if defined(__linux__) const int captured = backtrace(addresses, MaxFrames); +#elif defined(_WIN32) + struct TraceState { + void** addrs; + int max; + int count; + }; + static const auto traceCallback = [](_Unwind_Context* ctx, void* arg) APS5_VABI -> _Unwind_Reason_Code { + auto* s = static_cast(arg); + if (s->count < s->max) { + s->addrs[s->count++] = reinterpret_cast(_Unwind_GetIP_nid_postfix(ctx)); + } + return (s->count >= s->max) ? _URC_NORMAL_STOP : _URC_NO_REASON; + }; + TraceState state{addresses, MaxFrames, 0}; + _Unwind_Backtrace_nid_postfix(traceCallback, &state); + int captured = state.count; + if (captured == 0) { + captured = static_cast(RtlCaptureStackBackTrace(0, MaxFrames, addresses, nullptr)); + } +#endif std::vector frames; for (int index = 2; index < captured && (limit < 0 || static_cast(frames.size()) < limit); ++index) { +#if defined(__linux__) Dl_info info{}; if (dladdr(addresses[index], &info) == 0 || info.dli_fbase == nullptr || info.dli_fname == nullptr) continue; const auto offset = reinterpret_cast(addresses[index]) - reinterpret_cast(info.dli_fbase); if (offset > UINT32_MAX) continue; std::string module = info.dli_fname; - if (const auto slash = module.find_last_of('/'); slash != std::string::npos) module.erase(0, slash + 1); +#elif defined(_WIN32) + HMODULE handle = nullptr; + if (!GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + static_cast(addresses[index]), &handle) || handle == nullptr) { + continue; + } + const auto offset = reinterpret_cast(addresses[index]) - reinterpret_cast(handle); + if (offset > UINT32_MAX) continue; + std::string module = BacktraceModulePath_nid_no_patch(handle, MAX_PATH); + if (module.empty()) continue; +#endif + if (const auto slash = module.find_last_of("/\\"); slash != std::string::npos) module.erase(0, slash + 1); if (module.size() > sizeof(GuestSuffix) - 1 && module.compare(module.size() - (sizeof(GuestSuffix) - 1), std::string::npos, GuestSuffix) == 0) module.resize(module.size() - (sizeof(GuestSuffix) - 1)); frames.push_back({static_cast(offset), std::move(module)}); From 1819d3965e68a2d81259ff9d9552d3f9e03a78a2 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:28:30 +0300 Subject: [PATCH 3/6] test(libc): enable guest_backtrace on Windows and cover path buffer growth --- core/libs/prx/libc/CMakeLists.txt | 9 +++++++++ core/libs/tests/GuestBacktrace.cpp | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index db26a5c92568..756e9c4f5ea9 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -88,6 +88,8 @@ target_sources(libc PRIVATE target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/BinaryWordIo.cpp) + + target_include_directories(libc PRIVATE ${LIBS_INCLUDE_DIR} ) @@ -805,11 +807,18 @@ if(BUILD_TESTING) add_test_executable(cpu_topology_tests ${LIBS_INCLUDE_DIR}/tests/CpuTopology.cpp) target_include_directories(cpu_topology_tests PRIVATE ${LIBS_INCLUDE_DIR}) add_test(NAME cpu_topology COMMAND cpu_topology_tests) + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Linux" OR WIN32) add_test_executable(guest_backtrace_tests ${LIBS_INCLUDE_DIR}/tests/GuestBacktrace.cpp) target_include_directories(guest_backtrace_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_backtrace_tests PRIVATE libc) + configure_windows_unwind(guest_backtrace_tests) set_target_properties(guest_backtrace_tests PROPERTIES ENABLE_EXPORTS ON) add_test(NAME guest_backtrace COMMAND guest_backtrace_tests) + if(WIN32) + set_tests_properties(guest_backtrace PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() add_test_executable(log_macros_tests ${LIBS_INCLUDE_DIR}/tests/LogMacros.cpp) target_include_directories(log_macros_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/tests/GuestBacktrace.cpp b/core/libs/tests/GuestBacktrace.cpp index 25e958cdb96c..e2a29db11753 100644 --- a/core/libs/tests/GuestBacktrace.cpp +++ b/core/libs/tests/GuestBacktrace.cpp @@ -4,13 +4,20 @@ #include #include #include +#if defined(_WIN32) +#include +#else #include +#endif #include #include extern "C" { int APS5_VABI sceLibcBacktraceGetBufferSize_nid_postfix(int, int*, std::size_t*); int APS5_VABI sceLibcBacktraceSelf_nid_postfix(int, void*, std::size_t, int*); +#if defined(_WIN32) +std::string BacktraceModulePath_nid_no_patch(HMODULE, std::size_t); +#endif } namespace { @@ -66,10 +73,21 @@ std::size_t Used(const std::vector& records) { } [[gnu::noinline]] void CheckBacktrace() { +#if defined(_WIN32) + HMODULE handle = nullptr; + Require(GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&CheckBacktrace), &handle) != 0 && handle != nullptr, + "GetModuleHandleExA of the test failed"); + std::string executable = BacktraceModulePath_nid_no_patch(handle, 1); + const auto slash = executable.find_last_of("/\\"); + Require(!executable.empty() && slash != std::string::npos, "BacktraceModulePath of the test failed"); + executable.erase(0, slash + 1); +#else Dl_info self{}; Require(dladdr(reinterpret_cast(&CheckBacktrace), &self) != 0 && self.dli_fname != nullptr && self.dli_saddr != nullptr, "dladdr of the test failed"); std::string executable = self.dli_fname; executable.erase(0, executable.find_last_of('/') + 1); +#endif int count = -1; std::size_t size = 0; @@ -85,9 +103,17 @@ std::size_t Used(const std::vector& records) { for (std::size_t index = size; index < buffer.size(); ++index) Require(buffer[index] == Fill, "Self wrote past the buffer size"); for (const auto& record : records) Require(record.reserved == 0 && !record.module.empty(), "record has a nonzero reserved word or no module"); Require(records.front().module == executable, "first frame is not in the calling module"); +#if defined(_WIN32) + HMODULE callerHandle = nullptr; + const auto address = reinterpret_cast(handle) + records.front().offset; + Require(GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(address), &callerHandle) != 0 && callerHandle == handle, + "first frame is not the caller"); +#else Dl_info caller{}; const auto address = reinterpret_cast(self.dli_fbase) + records.front().offset; Require(dladdr(reinterpret_cast(address), &caller) != 0 && caller.dli_saddr == self.dli_saddr, "first frame is not the caller"); +#endif for (const int limit : {0, 1, 2}) { Require(sceLibcBacktraceGetBufferSize_nid_postfix(limit, &count, &size) == 0 && count == limit, "GetBufferSize ignored the frame limit"); From 47591f2e92c3b1803b0e961b1be4ba638f11a954 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:28:37 +0300 Subject: [PATCH 4/6] docs(debt): update sceLibcBacktrace technical debt entry for Windows --- docs/dev/debt/libc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ebf631971afb..060e29020909 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -2,7 +2,7 @@ ### Unknown function info -- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Implemented only on Linux; other platforms throw +- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Implemented only on Linux and Windows; other platforms throw - [ApplicationGuestHeapApi](../../../core/libs/prx/libc/src/ApplicationProcessParameters.cpp) (libc) - on Linux, HLE application allocation uses a loaded guest SDK's ten allocator exports when they belong to one native module. Its CRT owns application allocator initialization and finalization; HLE retains the module until process exit. Partial or mixed providers throw. This avoids reinitializing an allocator already used by SDK startup; console behavior and other host platforms remain unverified. - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. - [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use libc's own guest heap, not the title's registered allocator. A title that ships its own `libc.prx` (Metal Gear Solid Master Collection, PPSA18194) has already initialized that allocator, and reaching it from here would run its initializer a second time. That title's IL2CPP runtime looks up only `scriptingGetMem`, by name from `Il2cppUserAssemblies.prx`, and asks it for 256 KiB blocks aligned to 4 KiB. Which console library exports the hooks is unknown, and there is no console measurement. Allocation failure and sizes that overflow return null. From 28d0b1664bafa4b3a18ec43fd87306b5ad620f33 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:49:54 +0300 Subject: [PATCH 5/6] fix(libc): address Windows backtrace review feedback --- core/libs/prx/libc/CMakeLists.txt | 2 -- core/libs/prx/libc/src/Backtrace.cpp | 2 +- core/libs/tests/GuestBacktrace.cpp | 17 ++++++++++++----- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index cb8591199cff..ea9fae155b42 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -89,8 +89,6 @@ target_sources(libc PRIVATE target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/BinaryWordIo.cpp) - - target_include_directories(libc PRIVATE ${LIBS_INCLUDE_DIR} ) diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp index 53662a77d0a4..b38d8dfdcbf1 100644 --- a/core/libs/prx/libc/src/Backtrace.cpp +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -75,7 +75,7 @@ static_assert(sizeof(FrameRecord) == NameOffset); TraceState state{addresses, MaxFrames, 0}; _Unwind_Backtrace_nid_postfix(traceCallback, &state); int captured = state.count; - if (captured == 0) { + if (captured <= 2) { captured = static_cast(RtlCaptureStackBackTrace(0, MaxFrames, addresses, nullptr)); } #endif diff --git a/core/libs/tests/GuestBacktrace.cpp b/core/libs/tests/GuestBacktrace.cpp index e2a29db11753..7ae55383e3b5 100644 --- a/core/libs/tests/GuestBacktrace.cpp +++ b/core/libs/tests/GuestBacktrace.cpp @@ -70,6 +70,11 @@ std::size_t Used(const std::vector& records) { return total; } +[[gnu::noinline]] std::uintptr_t CurrentIp() { + asm volatile("" ::: "memory"); + return reinterpret_cast(__builtin_return_address(0)); +} + } [[gnu::noinline]] void CheckBacktrace() { @@ -96,7 +101,10 @@ std::size_t Used(const std::vector& records) { std::vector buffer(size + 64, Fill); int written = -1; - Require(sceLibcBacktraceSelf_nid_postfix(-1, buffer.data(), size, &written) == 0, "Self failed"); + const auto before = CurrentIp(); + const int selfStatus = sceLibcBacktraceSelf_nid_postfix(-1, buffer.data(), size, &written); + const auto after = CurrentIp(); + Require(selfStatus == 0, "Self failed"); Require(written == count, "Self wrote a different count than GetBufferSize reported"); const auto records = Parse(buffer, size, written); Require(Used(records) == size, "Self used a different size than GetBufferSize reported"); @@ -104,11 +112,10 @@ std::size_t Used(const std::vector& records) { for (const auto& record : records) Require(record.reserved == 0 && !record.module.empty(), "record has a nonzero reserved word or no module"); Require(records.front().module == executable, "first frame is not in the calling module"); #if defined(_WIN32) - HMODULE callerHandle = nullptr; const auto address = reinterpret_cast(handle) + records.front().offset; - Require(GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, - reinterpret_cast(address), &callerHandle) != 0 && callerHandle == handle, - "first frame is not the caller"); + const auto callerReturn = reinterpret_cast(handle) + records[1].offset; + Require(address > before && address < after, "first frame is not the caller"); + Require(callerReturn == reinterpret_cast(__builtin_return_address(0)), "second frame is not the caller of CheckBacktrace"); #else Dl_info caller{}; const auto address = reinterpret_cast(self.dli_fbase) + records.front().offset; From 2693c445693571a94cba3592155c5786656abf43 Mon Sep 17 00:00:00 2001 From: hamb3r <48532909+hamb3r@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:22:50 +0300 Subject: [PATCH 6/6] docs(libc): document Windows backtrace stop at PE unwind handlers --- core/libs/prx/libc/src/Backtrace.cpp | 5 +---- docs/dev/debt/libc.md | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libc/src/Backtrace.cpp b/core/libs/prx/libc/src/Backtrace.cpp index b38d8dfdcbf1..a07a476b4ac7 100644 --- a/core/libs/prx/libc/src/Backtrace.cpp +++ b/core/libs/prx/libc/src/Backtrace.cpp @@ -74,10 +74,7 @@ static_assert(sizeof(FrameRecord) == NameOffset); }; TraceState state{addresses, MaxFrames, 0}; _Unwind_Backtrace_nid_postfix(traceCallback, &state); - int captured = state.count; - if (captured <= 2) { - captured = static_cast(RtlCaptureStackBackTrace(0, MaxFrames, addresses, nullptr)); - } + const int captured = state.count; #endif std::vector frames; for (int index = 2; index < captured && (limit < 0 || static_cast(frames.size()) < limit); ++index) { diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index df77324692db..e51a57c8761a 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -4,7 +4,7 @@ - [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. -- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. Implemented only on Linux and Windows; other platforms throw +- [sceLibcBacktraceGetBufferSize, sceLibcBacktraceSelf](../../../core/libs/prx/libc/src/Backtrace.cpp) (libc) - signatures and frame record layout inferred from a title (next pointer, u32 module offset, module name at +0x10); the u32 at +0xC is unknown and written as 0. Module names are host file names without the `.guest.prx` suffix, so their case may differ from the PS5 module name. On Windows, unwinding uses `_Unwind_Backtrace` and stops at a frame without `.ehmeta` or `.ehfram` tables whose PE unwind data has an exception or termination handler, leaving the trace empty or cut short while still returning 0. Implemented only on Linux and Windows; other platforms throw - [ApplicationGuestHeapApi](../../../core/libs/prx/libc/src/ApplicationProcessParameters.cpp) (libc) - on Linux, HLE application allocation uses a loaded guest SDK's ten allocator exports when they belong to one native module. Its CRT owns application allocator initialization and finalization; HLE retains the module until process exit. Partial or mixed providers throw. This avoids reinitializing an allocator already used by SDK startup; console behavior and other host platforms remain unverified. - [fdopen](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) validates descriptor access modes as FreeBSD 11.4 [fdopen.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/fdopen.c) does: a requested mode incompatible with a descriptor not opened for both reading and writing returns null with `EINVAL`, leaving the descriptor open. Windows reads the handle's granted access through the same host helper as `fcntl(F_GETFL)`; a closed descriptor returns null with `EBADF`. Console behavior is unverified. - [scriptingGetMem, scriptingFreeMem](../../../core/libs/prx/libc/src/ApplicationHeap.cpp) (libc) - runtime allocator hooks for Unity titles. The allocation signature `(alignment, size)`, minimum alignment 16 and null result for a non-power-of-two alignment follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/df5b9b92be7dfd463b47756b707d6d356a9f0550/src/libs/libKernel.cpp); the free hook's one-pointer signature follows [Raeen's recorded contract](https://github.com/Whoisraeen/Raeen/blob/2db56923204b89719916b95e29c2f3c4e1d9f90b/docs/unity-dlsym-blocker.md). Both use libc's own guest heap, not the title's registered allocator. A title that ships its own `libc.prx` (Metal Gear Solid Master Collection, PPSA18194) has already initialized that allocator, and reaching it from here would run its initializer a second time. That title's IL2CPP runtime looks up only `scriptingGetMem`, by name from `Il2cppUserAssemblies.prx`, and asks it for 256 KiB blocks aligned to 4 KiB. Which console library exports the hooks is unknown, and there is no console measurement. Allocation failure and sizes that overflow return null.