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 <noreply@anthropic.com>
(cherry picked from commit d12b062074dcf64bff694800d77f7261adf6f59e)
This commit is contained in:
Mouad-Scriptz-0 2026-10-11 18:01:22 +00:00 committed by MarcZX100
parent a11c8f7f70
commit bc5bfd0ed6
4 changed files with 114 additions and 22 deletions

View File

@ -206,6 +206,16 @@ if(BUILD_TESTING)
set_tests_properties(guest_filesystem PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
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:$<TARGET_FILE_DIR:libkernel>")
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)

View File

@ -18,6 +18,7 @@
#include "prx/libkernel/DirectMemory/DirectMemory.hpp"
#include "prx/libkernel/KernelErrors.hpp"
#include "prx/libkernel/Pthread/include/Pthread.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <cerrno>
@ -44,7 +45,11 @@ namespace {
constexpr int sceInvalidArgument = static_cast<int>(0x80020016u);
constexpr int errnoNoChild = 10;
constexpr int errnoBadAddress = 14;
constexpr int errnoInvalidArgument = 22;
constexpr int freebsdGroupsMax = 1023;
std::mutex supplementaryGroupsMutex;
std::vector<std::uint32_t> supplementaryGroups{0};
constexpr unsigned freebsdWaitOptions = 0x8000003Fu;
std::atomic<std::uint32_t> 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<int>(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;
}
}

View File

@ -1,31 +1,57 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <cstdint>
#include <cstdlib>
#include <stdexcept>
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<std::uint32_t, 4> groups{};
Require(getgroups_nid_postfix(0, nullptr) == 1);
Require(getgroups_nid_postfix(static_cast<int>(groups.size()), groups.data()) == 1 && groups[0] == 0);
Require(groups[0] == static_cast<std::uint32_t>(getegid_nid_postfix()));
const std::array<std::uint32_t, 3> wanted{0, 5, 7};
Require(setgroups_nid_postfix(static_cast<int>(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<int>(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<int>(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;
}

View File

@ -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