From c8379be9226271befd37624611390822de276d6c Mon Sep 17 00:00:00 2001 From: screamm Date: Mon, 12 Oct 2026 01:11:01 +0200 Subject: [PATCH] feat(libkernel): implement the coredump user-data calls sceCoredumpWriteUserData (Dbbkj6YHWdo) and sceCoredumpDebugTextOut (dei8oUx6DbU) get the signatures of Orbital's db_types.json (cc408367) and SharpProspero's KernelSystem.cs (5dfdb4b0): ssize_t (const void*, size_t) and void (const char*, int). WriteUserData moves out of Unimplemented.cpp. User data belongs to the core dump handler: the Vita's sceCoredumpWriteUserData returns 0x800A0003 ("No user-data capture is active") outside it (vita-headers e66ebe90), and the RED engine's PS4 crash handler makes every coredump call from its handler (Vangaurd 4f86d2b0). The handler is never invoked here, so WriteUserData returns SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER (0x81180003, shadPS4 coredump_error.h e0a46675) sign-extended and keeps nothing; a null, empty or wrapping buffer throws because the order of the console's checks is unknown. DebugTextOut prints its text to stderr with the [coredump] prefix; a negative length or a null text with a nonzero length throws. Evidence: Broken Sword Reforged (PPSA21480) reaches the family through its sibling, #3704 comment 6113687542: "`ERROR agc_graphics.cpp:266 Sony call failed with code 8a6c9018` (the resource registration error, followed by the error dialog), then `sceCoredumpWriteUserString not implemented`." WriteUserString has no declaration in any source and keeps throwing; its TechnicalDebt entry records the call. 007 First Light (PPSA11386) imports WriteUserData (#2523) without calling it before its stop. Test: guest_coredump_user_data. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ .../prx/libkernel/System/src/Coredump.cpp | 20 ++++- core/libs/prx/libkernel/Unimplemented.cpp | 6 -- core/libs/tests/GuestCoredumpUserData.cpp | 74 +++++++++++++++++++ docs/dev/debt/libkernel.md | 1 + 5 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 core/libs/tests/GuestCoredumpUserData.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 606159fa6..33f26ae0a 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1225,4 +1225,14 @@ if(BUILD_TESTING) set_tests_properties(guest_coredump_attach PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_coredump_user_data_tests ${LIBS_INCLUDE_DIR}/tests/GuestCoredumpUserData.cpp) + target_include_directories(guest_coredump_user_data_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_coredump_user_data_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_coredump_user_data_tests) + add_test(NAME guest_coredump_user_data COMMAND guest_coredump_user_data_tests) + set_tests_properties(guest_coredump_user_data PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_coredump_user_data PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 3ddac3c4c..7af27f2b3 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -13,9 +13,13 @@ static std::atomic g_coredumpContext{0}; static constexpr int SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER = static_cast(0x81180003u); -static int attachOutsideCoredumpHandler(const char* function, const void* mem, size_t size) { +static void requireRegionOutsideHandler(const char* function, const void* mem, size_t size) { if (mem == nullptr || size == 0 || size > UINTPTR_MAX - reinterpret_cast(mem)) throw std::runtime_error(std::string(function) + ": null, empty or wrapping region outside the core dump handler"); +} + +static int attachOutsideCoredumpHandler(const char* function, const void* mem, size_t size) { + requireRegionOutsideHandler(function, mem, size); return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } @@ -61,9 +65,17 @@ int APS5_VABI sceCoredumpSetUserDataType(void) { return 0; } -int APS5_VABI sceCoredumpDebugTextOut(void) { - NotImplemented_nid_no_patch("dei8oUx6DbU"); - return 0; +void APS5_VABI sceCoredumpDebugTextOut(const char* str, int len) { + if (len < 0 || (str == nullptr && len != 0)) + throw std::runtime_error(std::string(__func__) + ": null text or negative length"); + if (len == 0) return; + const int shown = str[len - 1] == '\n' ? len - 1 : len; + std::fprintf(stderr, "[coredump] %.*s\n", shown, str); +} + +int64_t APS5_VABI sceCoredumpWriteUserData(const void* data, size_t size) { + requireRegionOutsideHandler(__func__, data, size); + return SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER; } int APS5_VABI sceCoredumpGetStopInfoCpu(void) { diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index eb776741a..e186c7e9c 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -5,16 +5,10 @@ extern "C" { -int APS5_VABI sceCoredumpWriteUserData() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceKernelGetCpuTemperature(int *out_celcius) { NotImplemented_nid_no_patch(__func__); return 0; } - int APS5_VABI __tls_get_addr_nid_postfix(void) { NotImplemented_nid_no_patch("vNe1w4diLCs"); return 0; diff --git a/core/libs/tests/GuestCoredumpUserData.cpp b/core/libs/tests/GuestCoredumpUserData.cpp new file mode 100644 index 000000000..289cbf81d --- /dev/null +++ b/core/libs/tests/GuestCoredumpUserData.cpp @@ -0,0 +1,74 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceCoredumpRegisterCoredumpHandler(uint64_t handler, size_t stackSize, uint64_t context); +int APS5_VABI sceCoredumpUnregisterCoredumpHandler(void); +int64_t APS5_VABI sceCoredumpWriteUserData(const void* data, size_t size); +void APS5_VABI sceCoredumpDebugTextOut(const char* str, int len); +} + +namespace { +constexpr int64_t NotInCoredumpHandler = static_cast(0x81180003u); +bool handlerRan = false; +int handlerContext = 0; + +int APS5_VABI Handler(void*) { + handlerRan = true; + return 0; +} + +void Require(bool value) { if (!value) std::abort(); } + +template +bool Throws(TCall call, const char* function) { + try { call(); } + catch (const std::runtime_error& error) { return std::strstr(error.what(), function) != nullptr; } + return false; +} + +void RequireWritesRefused(const std::array& region) { + Require(sceCoredumpWriteUserData(region.data(), region.size()) == NotInCoredumpHandler); + Require(sceCoredumpWriteUserData(region.data(), 1) == NotInCoredumpHandler); + int64_t fromThread = 0; + std::thread([&] { fromThread = sceCoredumpWriteUserData(region.data(), region.size()); }).join(); + Require(fromThread == NotInCoredumpHandler); +} +} + +int main() { + std::array region{}; + for (size_t index = 0; index < region.size(); ++index) region[index] = static_cast(index * 5 + 1); + const auto original = region; + const char text[] = "coredump text\n"; + + Require(NotInCoredumpHandler < 0); + RequireWritesRefused(region); + sceCoredumpDebugTextOut(text, static_cast(sizeof(text) - 1)); + sceCoredumpDebugTextOut(text, 8); + sceCoredumpDebugTextOut(text, 0); + sceCoredumpDebugTextOut(nullptr, 0); + + Require(sceCoredumpRegisterCoredumpHandler(reinterpret_cast(&Handler), 16 * 1024, reinterpret_cast(&handlerContext)) == 0); + RequireWritesRefused(region); + sceCoredumpDebugTextOut(text, static_cast(sizeof(text) - 1)); + Require(!handlerRan); + + Require(Throws([&] { sceCoredumpWriteUserData(nullptr, region.size()); }, "sceCoredumpWriteUserData")); + Require(Throws([&] { sceCoredumpWriteUserData(region.data(), 0); }, "sceCoredumpWriteUserData")); + Require(Throws([&] { sceCoredumpWriteUserData(region.data(), SIZE_MAX); }, "sceCoredumpWriteUserData")); + Require(Throws([&] { sceCoredumpDebugTextOut(nullptr, 4); }, "sceCoredumpDebugTextOut")); + Require(Throws([&] { sceCoredumpDebugTextOut(text, -1); }, "sceCoredumpDebugTextOut")); + + Require(sceCoredumpUnregisterCoredumpHandler() == 0); + RequireWritesRefused(region); + Require(region == original); + Require(!handlerRan); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 2e5574be7..059d896d3 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -66,6 +66,7 @@ - [pthread_getname_np](../../../core/libs/prx/libkernel/Pthread/Posix/Thread.cpp) (libkernel) - a thread that has already been joined returns `ESRCH` (3) on a PS5 (firmware 13.60), but its object is freed after the join here, so the pointer cannot be told apart from a live thread and its name is read as for a live one - [sceKernelGetOperationMode](../../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call and ignores the result. A homebrew payload on a PS5 (firmware 13.60) reads mode 2 and submode 0, nothing else is written, and the result register holds no status, so the function is taken to return nothing. Whether a title process reads the same values is unverified. Null outputs throw - [sceCoredumpAttachMemoryRegion, sceCoredumpAttachUserMemoryFile](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - both take `(uint32_t userValue, const void* mem, size_t size)`: for the first, the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings agree; the second has only SharpProspero (the PS4 5.05 firmware does not export it, so Orbital has no entry) and is described there as attaching the region as an unnamed user file. Not confirmed on a title; the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/main/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) calls the sibling `sceCoredumpAttachMemoryRegionAsUserFile(999, buffer, bytes, name)` from inside its handler. Attaching belongs inside the core dump handler, which the system runs on its own thread after the process stops (SharpProspero's remark, the RED engine's usage and `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER`, "function was not called from registered core dump handler", in the Vita's SceCoredump error table, [RivetDB](https://github.com/CreepNT/RivetDB/blob/master/rivetdb.json)). The handler is never invoked here, so every call is outside it and returns that error; nothing is recorded, as there is no dump to attach to. Its value `0x81180003` comes from shadPS4 (PS4), not confirmed on a PS5: [fpPS4](https://github.com/red-prig/fpPS4/blob/master/kernel/ps4_libkernel.pas) has `NOT_REGISTERED` 0x81180001 and `ALREADY_REGISTERED` 0x81180002, shadPS4's `coredump_error.h` has `NOT_IN_COREDUMP_HANDLER` = 0x81180003, and the Vita table numbers `PARAM` 0, `NOT_REGISTERED` 1, `ALREADY_REGISTERED` 2 and `NOT_IN_COREDUMP_HANDLER` 3. Whether the console checks the region before the calling context is unknown, so a null, empty or wrapping region throws. `userValue` is not interpreted; SharpProspero names its bits 31 and 30 as truncation flags for the user file variants +- [sceCoredumpWriteUserData, sceCoredumpDebugTextOut](../../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - the signatures `ssize_t (const void* data, size_t size)` and `void (const char* str, int len)` follow the [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json) IDA type database and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelSystem.cs) bindings ("Repeated calls append", "The bytes written, or a negative error code"). User data is written from inside the core dump handler: the Vita's [`sceCoredumpWriteUserData`](https://github.com/vitasdk/vita-headers/blob/e66ebe90b73d1fa4cce005a5b2072cec27322544/include/psp2/coredump_nounlink.h) returns `0x800A0003` ("No user-data capture is active") outside it, and the RED engine's PS4 crash handler ([Vangaurd](https://github.com/vurimad/Vangaurd/blob/4f86d2b0e1a6c0d50526152c517a0292b1a68af4/source/imported/common/redSystem/src/errorHandlerImplErrorHooksOrbis.cpp)) makes all its coredump calls, `sceCoredumpDebugTextOut` included, from its handler. The handler is never invoked here, so `sceCoredumpWriteUserData` returns `SCE_COREDUMP_ERROR_NOT_IN_COREDUMP_HANDLER` (0x81180003 in shadPS4's [`coredump_error.h`](https://github.com/shadps4-emu/shadPS4/blob/e0a466753a1aacbc8db8253be685bf7ab4d9daae/src/core/libraries/kernel/coredump/coredump_error.h), PS4 numbering) sign-extended to 64 bits and keeps nothing; whether the console checks the buffer before the calling context is unknown, so a null, empty or wrapping buffer throws. `sceCoredumpDebugTextOut` returns nothing; its text, which goes into the report on the console, is printed to stderr with the `[coredump]` prefix of `sceKernelDebugWriteCppExceptionInfo`, and a negative length or a null text with a nonzero length throws. No title is seen calling either: 007 First Light (PPSA11386) imports `sceCoredumpWriteUserData` and does not call it before its stop (#2523) - [sceKernelAddTimerEvent](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - `usec` is taken as the period in microseconds; re-adding an existing timer keeps its pending expiration count and restarts the period from the time of the call, where FreeBSD 12+ `filt_timertouch` clears pending expirations on `EV_ADD`; `usec == 0` fires on every wait. None of this is confirmed on a PS5 title - [sceKernelWaitEqueue](../../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - a level-triggered user event (`sceKernelAddUserEvent`) is reported at most once per call, as in FreeBSD `kqueue_scan` (`sys/kern/kern_event.c`), which re-queues a reported knote without `EV_CLEAR` or `EV_DISPATCH` behind the scan marker, and kevent(2), where several triggers do not produce several kevents. Not measured on a console. Events are scanned in list order and the list is not rotated, so when `num` is smaller than the number of ready level events the later ones can starve, where FreeBSD moves a reported level knote to the tail. Several completions of one AMPR or VideoOut event within one call are returned as separate entries, where FreeBSD would aggregate them; the Orbis behaviour is unconfirmed - [AC1FtjqMCL0](../../../core/libs/prx/libkernel/Unimplemented.cpp) (libkernel) - unknown name, signature