From bc5bfd0ed631e4bd22b1b2eb0ec94c53f22ff6fc Mon Sep 17 00:00:00 2001 From: Mouad-Scriptz-0 Date: Sun, 11 Oct 2026 18:01:22 +0000 Subject: [PATCH] feat(libkernel): export setuid, setreuid, setgroups and getgroups `setuid`, `setreuid` and `setgroups` are absent imports of XPSemu (PPSA97358) and PS5MC (PPSA99420) on `main`. The guest process is modelled as root with ids 0, as `seteuid` and `setegid` already assume: `setuid` accepts 0 and `setreuid` 0 or -1 per id and change nothing, any other id throws. `setgroups` keeps the supplementary group list as FreeBSD's `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 throws) and the new `getgroups` reports it. `guest_credentials` covers the accepted ids, the default list, a set list, an empty list and the EINVAL/EFAULT cases. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit d12b062074dcf64bff694800d77f7261adf6f59e) --- core/libs/prx/libkernel/CMakeLists.txt | 10 +++ .../libs/prx/libkernel/System/src/Process.cpp | 56 +++++++++++++++ core/libs/tests/GuestCredentials.cpp | 68 +++++++++++++------ docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 114 insertions(+), 22 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 41cb24584..7e1d1d809 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -206,6 +206,16 @@ if(BUILD_TESTING) set_tests_properties(guest_filesystem PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_credentials_tests ${LIBS_INCLUDE_DIR}/tests/GuestCredentials.cpp) + target_include_directories(guest_credentials_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_credentials_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_credentials_tests) + add_test(NAME guest_credentials COMMAND guest_credentials_tests) + set_tests_properties(guest_credentials PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_credentials PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_truncate_tests ${LIBS_INCLUDE_DIR}/tests/GuestTruncate.cpp) target_include_directories(guest_truncate_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_truncate_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 45528cd7a..0b3fb9674 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -18,6 +18,7 @@ #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Pthread/include/Pthread.hpp" +#include #include #include #include @@ -44,7 +45,11 @@ namespace { constexpr int sceInvalidArgument = static_cast(0x80020016u); constexpr int errnoNoChild = 10; +constexpr int errnoBadAddress = 14; constexpr int errnoInvalidArgument = 22; +constexpr int freebsdGroupsMax = 1023; +std::mutex supplementaryGroupsMutex; +std::vector supplementaryGroups{0}; constexpr unsigned freebsdWaitOptions = 0x8000003Fu; std::atomic gpoBits{0}; @@ -473,4 +478,55 @@ int APS5_VABI setegid_nid_postfix(std::uint32_t egid) { return 0; } +int APS5_VABI setuid_nid_postfix(std::uint32_t uid) { + if (uid != 0) + throw std::runtime_error("setuid: unsupported user id " + std::to_string(uid)); + return 0; +} + +int APS5_VABI setreuid_nid_postfix(std::uint32_t ruid, std::uint32_t euid) { + constexpr std::uint32_t unchanged = 0xffffffffu; + if (ruid != 0 && ruid != unchanged) + throw std::runtime_error("setreuid: unsupported real user id " + std::to_string(ruid)); + if (euid != 0 && euid != unchanged) + throw std::runtime_error("setreuid: unsupported effective user id " + std::to_string(euid)); + return 0; +} + +int APS5_VABI setgroups_nid_postfix(int count, const std::uint32_t* groups) { + if (count < 0 || count > freebsdGroupsMax) { + *__error_nid_postfix() = errnoInvalidArgument; + return -1; + } + if (count > 0 && groups == nullptr) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } + if (count > 0 && groups[0] != 0) + throw std::runtime_error("setgroups: unsupported effective group id " + std::to_string(groups[0])); + std::lock_guard lock(supplementaryGroupsMutex); + if (count == 0) { + supplementaryGroups.resize(1); + return 0; + } + supplementaryGroups.assign(groups, groups + count); + return 0; +} + +int APS5_VABI getgroups_nid_postfix(int capacity, std::uint32_t* groups) { + std::lock_guard lock(supplementaryGroupsMutex); + const int count = static_cast(supplementaryGroups.size()); + if (capacity == 0) return count; + if (capacity < count) { + *__error_nid_postfix() = errnoInvalidArgument; + return -1; + } + if (groups == nullptr) { + *__error_nid_postfix() = errnoBadAddress; + return -1; + } + std::copy(supplementaryGroups.begin(), supplementaryGroups.end(), groups); + return count; +} + } diff --git a/core/libs/tests/GuestCredentials.cpp b/core/libs/tests/GuestCredentials.cpp index aa2c8544f..b401588d6 100644 --- a/core/libs/tests/GuestCredentials.cpp +++ b/core/libs/tests/GuestCredentials.cpp @@ -1,31 +1,57 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include #include #include -#include + extern "C" { -int APS5_VABI seteuid_nid_postfix(std::uint32_t); -int APS5_VABI setegid_nid_postfix(std::uint32_t); +int APS5_VABI setuid_nid_postfix(std::uint32_t); +int APS5_VABI setreuid_nid_postfix(std::uint32_t, std::uint32_t); +int APS5_VABI setgroups_nid_postfix(int, const std::uint32_t*); +int APS5_VABI getgroups_nid_postfix(int, std::uint32_t*); +int APS5_VABI getuid_nid_postfix(void); +int APS5_VABI geteuid_nid_postfix(void); +int APS5_VABI getegid_nid_postfix(void); int* APS5_VABI __error_nid_postfix(); } -using SetId = int (APS5_VABI *)(std::uint32_t); + static void Require(bool value) { if (!value) std::abort(); } -static bool Throws(SetId function, std::uint32_t id) { - try { - function(id); - } catch (const std::runtime_error&) { - return true; - } - return false; + +constexpr int GuestEfault = 14; +constexpr int GuestEinval = 22; +constexpr std::uint32_t Unchanged = 0xffffffffu; + +static void RequireFailure(int result, int error) { + Require(result == -1); + Require(*__error_nid_postfix() == error); } + int main() { - *__error_nid_postfix() = 13; - Require(seteuid_nid_postfix(0) == 0); - Require(setegid_nid_postfix(0) == 0); - Require(*__error_nid_postfix() == 13); - Require(Throws(seteuid_nid_postfix, 1000)); - Require(Throws(setegid_nid_postfix, 1000)); - Require(Throws(seteuid_nid_postfix, 0xFFFFFFFFu)); - Require(Throws(setegid_nid_postfix, 0xFFFFFFFFu)); - Require(seteuid_nid_postfix(0) == 0); - Require(setegid_nid_postfix(0) == 0); + Require(setuid_nid_postfix(0) == 0); + Require(getuid_nid_postfix() == 0 && geteuid_nid_postfix() == 0); + Require(setreuid_nid_postfix(Unchanged, Unchanged) == 0); + Require(setreuid_nid_postfix(0, Unchanged) == 0); + Require(setreuid_nid_postfix(Unchanged, 0) == 0); + Require(setreuid_nid_postfix(0, 0) == 0); + + std::array groups{}; + Require(getgroups_nid_postfix(0, nullptr) == 1); + Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 1 && groups[0] == 0); + Require(groups[0] == static_cast(getegid_nid_postfix())); + + const std::array wanted{0, 5, 7}; + Require(setgroups_nid_postfix(static_cast(wanted.size()), wanted.data()) == 0); + Require(getgroups_nid_postfix(0, nullptr) == 3); + RequireFailure(getgroups_nid_postfix(2, groups.data()), GuestEinval); + RequireFailure(getgroups_nid_postfix(3, nullptr), GuestEfault); + Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 3); + Require(groups[0] == 0 && groups[1] == 5 && groups[2] == 7); + + Require(setgroups_nid_postfix(0, nullptr) == 0); + Require(getgroups_nid_postfix(static_cast(groups.size()), groups.data()) == 1 && groups[0] == 0); + + RequireFailure(setgroups_nid_postfix(-1, wanted.data()), GuestEinval); + RequireFailure(setgroups_nid_postfix(1024, wanted.data()), GuestEinval); + RequireFailure(setgroups_nid_postfix(1, nullptr), GuestEfault); + Require(getgroups_nid_postfix(0, nullptr) == 1); + return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..c6e0af03d 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -197,7 +197,7 @@ Throughout the project, every function at every stage either **does exactly what - [select](../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) - descriptor 0 is never reported readable and descriptors 1 and 2 are always writable, as for a console title with no input; reads from descriptor 0 still go to the host stdin. `/dev/random` and `/dev/urandom` are always readable and writable, as FreeBSD reports a seeded random device. Other descriptors that are not libSceNet sockets throw. Dungeons (PPSA16064) selects on descriptor 0 - [__progname](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export - [environ](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - data export pointing to a fixed empty environment so titles that import it load; it is not the environment libc's `getenv` reads, so it lists none of those variables and `setenv`/`putenv` changes do not appear in it -- [seteuid, setegid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid` and `sys_setegid`, id 0 is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change the effective id, which is not modelled, and throws. The ids and privileges a title has on a console are unverified +- [seteuid, setegid, setuid, setreuid, setgroups, getgroups](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the guest process is assumed to run as root, with real, effective and saved user and group ids 0. As in FreeBSD's `sys_seteuid`, `sys_setegid`, `sys_setuid` and `sys_setreuid`, id 0 (or -1 for an unchanged `setreuid` id) is then accepted without a privilege check and changes no credential, so 0 is returned; any other id would change an id, which is not modelled, and throws. `setgroups` keeps the supplementary group list as `kern_setgroups` does (`NGROUPS_MAX` 1023, an empty list leaves the effective group as the only entry, a first entry other than 0 would change the effective group and throws) and `getgroups` reports it. The ids and privileges a title has on a console are unverified - [pthread_barrierattr_setpshared](../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - PTHREAD_PROCESS_SHARED is accepted on the assumption that the console follows FreeBSD 11.0 libthr, which accepts it (9.0 rejects it with EINVAL); unverified on hardware. Guest code runs in one process, so a shared barrier behaves as a private one - [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 - [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature