Skip to content
7 changes: 7 additions & 0 deletions core/libs/prx/libc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -839,11 +839,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
60 changes: 58 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,45 @@ 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);
const int captured = state.count;
#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
35 changes: 34 additions & 1 deletion 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 @@ -63,13 +70,29 @@ std::size_t Used(const std::vector<Record>& records) {
return total;
}

[[gnu::noinline]] std::uintptr_t CurrentIp() {
asm volatile("" ::: "memory");
return reinterpret_cast<std::uintptr_t>(__builtin_return_address(0));
}

}

[[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 @@ -78,16 +101,26 @@ std::size_t Used(const std::vector<Record>& records) {

std::vector<unsigned char> 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");
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)
const auto address = reinterpret_cast<std::uintptr_t>(handle) + records.front().offset;
const auto callerReturn = reinterpret_cast<std::uintptr_t>(handle) + records[1].offset;
Require(address > before && address < after, "first frame is not the caller");
Require(callerReturn == reinterpret_cast<std::uintptr_t>(__builtin_return_address(0)), "second frame is not the caller of CheckBacktrace");
#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. 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 the title's replacement table has an allocator entry and the exports belong to one native module. With an empty table it keeps the default heap, since a bundled `libc.prx` may export the allocator NIDs while forwarding them to HLE. 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