Skip to content
9 changes: 9 additions & 0 deletions core/libs/prx/libc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ target_sources(libc PRIVATE

target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/BinaryWordIo.cpp)



target_include_directories(libc PRIVATE
${LIBS_INCLUDE_DIR}
)
Expand Down Expand Up @@ -815,11 +817,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:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(log_macros_tests ${LIBS_INCLUDE_DIR}/tests/LogMacros.cpp)
target_include_directories(log_macros_tests PRIVATE ${LIBS_INCLUDE_DIR})
Expand Down
63 changes: 61 additions & 2 deletions core/libs/prx/libc/src/Backtrace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,35 @@

#include "prx/libc/include/General.hpp"

#ifdef __linux__
#if defined(__linux__)
#include <dlfcn.h>
#include <execinfo.h>
#elif defined(_WIN32)
#include <windows.h>
#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<DWORD>(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

#if defined(__linux__) || defined(_WIN32)

namespace {

Expand All @@ -31,15 +57,48 @@ static_assert(sizeof(FrameRecord) == NameOffset);

[[gnu::noinline]] std::vector<Frame> 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<TraceState*>(arg);
if (s->count < s->max) {
s->addrs[s->count++] = reinterpret_cast<void*>(_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<int>(RtlCaptureStackBackTrace(0, MaxFrames, addresses, nullptr));
}
#endif
std::vector<Frame> frames;
for (int index = 2; index < captured && (limit < 0 || static_cast<int>(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<std::uintptr_t>(addresses[index]) - reinterpret_cast<std::uintptr_t>(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<LPCSTR>(addresses[index]), &handle) || handle == nullptr) {
continue;
}
const auto offset = reinterpret_cast<std::uintptr_t>(addresses[index]) - reinterpret_cast<std::uintptr_t>(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<std::uint32_t>(offset), std::move(module)});
Expand Down
26 changes: 26 additions & 0 deletions core/libs/tests/GuestBacktrace.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,20 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#if defined(_WIN32)
#include <windows.h>
#else
#include <dlfcn.h>
#endif
#include <string>
#include <vector>

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 {
Expand Down Expand Up @@ -66,10 +73,21 @@ std::size_t Used(const std::vector<Record>& 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<LPCSTR>(&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<void*>(&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;
Expand All @@ -85,9 +103,17 @@ std::size_t Used(const std::vector<Record>& 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<std::uintptr_t>(handle) + records.front().offset;
Require(GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCSTR>(address), &callerHandle) != 0 && callerHandle == handle,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows assertion currently proves only that the first frame resolves to the same executable module. The Linux branch also checks the exact caller symbol (caller.dli_saddr == self.dli_saddr), so it would catch an off-by-one in the skipped frames that this Windows branch could miss. Since handle came from &CheckBacktrace, could this assert that reinterpret_cast<std::uintptr_t>(handle) + records.front().offset == reinterpret_cast<std::uintptr_t>(&CheckBacktrace) as well?

@hamb3r hamb3r Oct 20, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 28d0b16: since records.front().offset is the call-site return RVA inside CheckBacktrace rather than the function entry &CheckBacktrace, the Windows branch now brackets sceLibcBacktraceSelf_nid_postfix with CurrentIp() (address > before && address < after) and checks that records[1].offset matches __builtin_return_address(0).

"first frame is not the caller");
#else
Dl_info caller{};
const auto address = reinterpret_cast<std::uintptr_t>(self.dli_fbase) + records.front().offset;
Require(dladdr(reinterpret_cast<void*>(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");
Expand Down
2 changes: 1 addition & 1 deletion docs/dev/debt/libc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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; 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.
Expand Down
Loading