From 7785933807d70e12b337d1df0bd2d3e2c8d257b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0l=C3=A9gl?= Date: Thu, 8 Oct 2026 21:31:30 +0200 Subject: [PATCH] feat(libkernel): implement sceKernelGetFsSandboxRandomWord Returns the name of the directory under which a title's sandbox exposes system files, which homebrew SDKs splice into paths such as //common/cert/CA_LIST.cer and //common/lib/libc.sprx. The name is fixed here; recorded in TechnicalDebt together with the fact that nothing is mounted under it. Registers guest_sandbox_random_word. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libkernel/CMakeLists.txt | 11 ++++++ .../libs/prx/libkernel/System/src/Sandbox.cpp | 6 ++++ core/libs/tests/GuestSandboxRandomWord.cpp | 34 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 52 insertions(+) create mode 100644 core/libs/prx/libkernel/System/src/Sandbox.cpp create mode 100644 core/libs/tests/GuestSandboxRandomWord.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 2520ab477..e21e0d6f0 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -45,6 +45,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Sanitizer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Rtld.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Process.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Sandbox.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SystemConfiguration.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Signals.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Notification.cpp @@ -471,4 +472,14 @@ if(BUILD_TESTING) set_tests_properties(guest_file_vector PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sandbox_random_word_tests ${LIBS_INCLUDE_DIR}/tests/GuestSandboxRandomWord.cpp) + target_include_directories(guest_sandbox_random_word_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sandbox_random_word_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sandbox_random_word_tests) + add_test(NAME guest_sandbox_random_word COMMAND guest_sandbox_random_word_tests) + set_tests_properties(guest_sandbox_random_word PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_sandbox_random_word PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libkernel/System/src/Sandbox.cpp b/core/libs/prx/libkernel/System/src/Sandbox.cpp new file mode 100644 index 000000000..dd3918c68 --- /dev/null +++ b/core/libs/prx/libkernel/System/src/Sandbox.cpp @@ -0,0 +1,6 @@ +#include "prx/libc/include/general/VabiMacros.hpp" + +extern "C" const char* APS5_VABI sceKernelGetFsSandboxRandomWord() { + static constexpr char word[] = "gkbHtXc3Wq"; + return word; +} diff --git a/core/libs/tests/GuestSandboxRandomWord.cpp b/core/libs/tests/GuestSandboxRandomWord.cpp new file mode 100644 index 000000000..15eb6eb24 --- /dev/null +++ b/core/libs/tests/GuestSandboxRandomWord.cpp @@ -0,0 +1,34 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +extern "C" { +const char* APS5_VABI sceKernelGetFsSandboxRandomWord(); +int APS5_VABI open_nid_postfix(const char*, int, int); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + const char* word = sceKernelGetFsSandboxRandomWord(); + Require(word != nullptr); + const std::size_t length = std::strlen(word); + Require(length >= 1); + for (std::size_t index = 0; index < length; ++index) + Require(std::isalnum(static_cast(word[index]))); + + Require(sceKernelGetFsSandboxRandomWord() == word); + const char* fromThread = nullptr; + std::thread([&] { fromThread = sceKernelGetFsSandboxRandomWord(); }).join(); + Require(fromThread == word); + + const std::string path = std::string("/") + word + "/common/lib/libc.sprx"; + *__error_nid_postfix() = 0; + Require(open_nid_postfix(path.c_str(), 0, 0) == -1); + Require(*__error_nid_postfix() == 2); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 371c4318b..3269b0e3b 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -244,6 +244,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done +- [sceKernelGetFsSandboxRandomWord](../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist - [sceKernelSync](../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync` - [sceKernelSync](../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way - [sceKernelInstallExceptionHandler](../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection