diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index f599cdb20..f01f14114 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -369,6 +369,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_start PROPERTIES TIMEOUT 60 ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_pthread_attr_errors_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadAttrErrors.cpp) + target_include_directories(guest_pthread_attr_errors_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_pthread_attr_errors_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_pthread_attr_errors_tests) + add_test(NAME guest_pthread_attr_errors COMMAND guest_pthread_attr_errors_tests) + set_tests_properties(guest_pthread_attr_errors PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_pthread_attr_errors PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_pthread_stack_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadStack.cpp) target_include_directories(guest_pthread_stack_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_pthread_stack_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index 4a6216d00..9d2f03274 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -33,29 +33,31 @@ int APS5_VABI scePthreadAttrInit(PthreadAttr* attr) { } int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; delete *attr; *attr = nullptr; return SCE_OK; } int APS5_VABI scePthreadAttrSetdetachstate(PthreadAttr* attr, int detachstate) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (detachstate != DETACH_JOINABLE && detachstate != DETACH_DETACHED) - throw std::runtime_error(std::string(__func__) + ": invalid state"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + if (detachstate != DETACH_JOINABLE && detachstate != DETACH_DETACHED) return SCE_KERNEL_ERROR_EINVAL; (*attr)->_detachstate = detachstate; return SCE_OK; } int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param) { - if (!attr || !*attr || !param) - throw std::runtime_error(std::string(__func__) + ": null arg"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + constexpr int HighestPriority = 256; + constexpr int LowestPriority = 767; + if (!param || param->sched_priority < HighestPriority || param->sched_priority > LowestPriority) + return SCE_KERNEL_ERROR_EOPNOTSUPP; (*attr)->_schedpriority = param->sched_priority; return SCE_OK; } int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stacksize) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; if (stacksize < MIN_STACK_SIZE) return SCE_KERNEL_ERROR_EINVAL; #ifdef _WIN32 if (stacksize > std::numeric_limits::max() - 0xffffu) @@ -89,14 +91,14 @@ int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr) { } int APS5_VABI scePthreadAttrGetaffinity(const PthreadAttr* attr, KernelCpumask* mask) { - if (!attr || !*attr || !mask) throw std::runtime_error("scePthreadAttrGetaffinity: null argument"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + if (!mask) throw std::runtime_error("scePthreadAttrGetaffinity: null argument"); *mask = (*attr)->_affinity; return SCE_OK; } int APS5_VABI scePthreadAttrGetdetachstate(const PthreadAttr* attr, int* state) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!state) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !state) return SCE_KERNEL_ERROR_EINVAL; *state = (*attr)->_detachstate; return SCE_OK; } @@ -109,8 +111,7 @@ int APS5_VABI scePthreadAttrGetguardsize(const PthreadAttr* attr, size_t* guard_ } int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedParam* param) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!param) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !param) return SCE_KERNEL_ERROR_EINVAL; param->sched_priority = (*attr)->_schedpriority; return SCE_OK; } @@ -130,14 +131,13 @@ int APS5_VABI scePthreadAttrGetstackaddr(const PthreadAttr* attr, void** stack_a } int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, size_t* stack_size) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); - if (!stack_size) throw std::runtime_error(std::string(__func__) + ": null output"); + if (!attr || !*attr || !stack_size) return SCE_KERNEL_ERROR_EINVAL; *stack_size = (*attr)->_stacksize; return SCE_OK; } int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask) { - if (!attr || !*attr) throw std::runtime_error("scePthreadAttrSetaffinity: null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; (*attr)->_affinity = mask; (*attr)->_affinityExplicit = true; return SCE_OK; @@ -150,13 +150,18 @@ int APS5_VABI scePthreadAttrSetguardsize(PthreadAttr* attr, size_t guard_size) { } int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inherit_sched) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + constexpr int ExplicitSched = 0; + constexpr int InheritSched = 4; + if (inherit_sched != ExplicitSched && inherit_sched != InheritSched) return SCE_KERNEL_ERROR_EOPNOTSUPP; (*attr)->_inheritsched = inherit_sched; return SCE_OK; } int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy) { - if (!attr || !*attr) throw std::runtime_error(std::string(__func__) + ": null attr"); + if (!attr || !*attr) return SCE_KERNEL_ERROR_EINVAL; + constexpr int SchedRr = 3; + if (policy != SCHED_FIFO_PS5 && policy != SchedRr) return SCE_KERNEL_ERROR_EOPNOTSUPP; (*attr)->_schedpolicy = policy; return SCE_OK; } diff --git a/core/libs/tests/GuestPthreadAttrErrors.cpp b/core/libs/tests/GuestPthreadAttrErrors.cpp new file mode 100644 index 000000000..259ee7789 --- /dev/null +++ b/core/libs/tests/GuestPthreadAttrErrors.cpp @@ -0,0 +1,85 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrSetstacksize(PthreadAttr* attr, std::size_t stackSize); +int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, std::size_t* stackSize); +int APS5_VABI scePthreadAttrSetdetachstate(PthreadAttr* attr, int state); +int APS5_VABI scePthreadAttrGetdetachstate(const PthreadAttr* attr, int* state); +int APS5_VABI scePthreadAttrSetschedpolicy(PthreadAttr* attr, int policy); +int APS5_VABI scePthreadAttrSetinheritsched(PthreadAttr* attr, int inheritSched); +int APS5_VABI scePthreadAttrSetschedparam(PthreadAttr* attr, const KernelSchedParam* param); +int APS5_VABI scePthreadAttrGetschedparam(const PthreadAttr* attr, KernelSchedParam* param); +int APS5_VABI scePthreadAttrSetaffinity(PthreadAttr* attr, KernelCpumask mask); +int APS5_VABI scePthreadAttrGetaffinity(const PthreadAttr* attr, KernelCpumask* mask); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016u); +static constexpr int SCE_KERNEL_ERROR_EOPNOTSUPP = static_cast(0x8002002Du); + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK); + + for (std::size_t size : {std::size_t{0}, std::size_t{1}, std::size_t{16383}}) + Require(scePthreadAttrSetstacksize(&attr, size) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetstacksize(&attr, 16384) == SCE_OK); + + for (int state : {-1, 2, 3, 100}) Require(scePthreadAttrSetdetachstate(&attr, state) == SCE_KERNEL_ERROR_EINVAL); + for (int state : {0, 1}) Require(scePthreadAttrSetdetachstate(&attr, state) == SCE_OK); + + for (int policy : {-1, 0, 2, 4}) Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_KERNEL_ERROR_EOPNOTSUPP); + for (int policy : {1, 3}) Require(scePthreadAttrSetschedpolicy(&attr, policy) == SCE_OK); + + for (int inherit : {-1, 1, 2, 8}) Require(scePthreadAttrSetinheritsched(&attr, inherit) == SCE_KERNEL_ERROR_EOPNOTSUPP); + for (int inherit : {0, 4}) Require(scePthreadAttrSetinheritsched(&attr, inherit) == SCE_OK); + + KernelSchedParam param{}; + for (int priority : {-1, 0, 255, 768, 900, 960}) { + param.sched_priority = priority; + Require(scePthreadAttrSetschedparam(&attr, ¶m) == SCE_KERNEL_ERROR_EOPNOTSUPP); + } + Require(scePthreadAttrSetschedparam(&attr, nullptr) == SCE_KERNEL_ERROR_EOPNOTSUPP); + for (int priority : {256, 767}) { + param.sched_priority = priority; + Require(scePthreadAttrSetschedparam(&attr, ¶m) == SCE_OK); + KernelSchedParam read{}; + Require(scePthreadAttrGetschedparam(&attr, &read) == SCE_OK && read.sched_priority == priority); + } + for (KernelCpumask mask : {KernelCpumask{0}, KernelCpumask{1}, ~KernelCpumask{0}}) + Require(scePthreadAttrSetaffinity(&attr, mask) == SCE_OK); + + std::size_t size = 0; + int state = 0; + KernelCpumask mask = 0; + Require(scePthreadAttrGetstacksize(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetdetachstate(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetschedparam(&attr, nullptr) == SCE_KERNEL_ERROR_EINVAL); + + for (PthreadAttr* missing : {static_cast(nullptr), static_cast(&attr)}) { + PthreadAttr unset = nullptr; + PthreadAttr* target = missing ? &unset : nullptr; + Require(scePthreadAttrSetstacksize(target, 16384) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetdetachstate(target, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetschedpolicy(target, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetinheritsched(target, 0) == SCE_KERNEL_ERROR_EINVAL); + param.sched_priority = 700; + Require(scePthreadAttrSetschedparam(target, ¶m) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrSetaffinity(target, 1) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetstacksize(target, &size) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetdetachstate(target, &state) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetschedparam(target, ¶m) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrGetaffinity(target, &mask) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadAttrDestroy(target) == SCE_KERNEL_ERROR_EINVAL); + } + + Require(scePthreadAttrDestroy(&attr) == SCE_OK); + Require(scePthreadAttrDestroy(&attr) == SCE_KERNEL_ERROR_EINVAL); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 213bd8988..485ef3a57 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -202,6 +202,7 @@ Throughout the project, every function at every stage either **does exactly what - [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - a rwlock held for reading or writing returns `EBUSY` and keeps its handle, as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does; FreeBSD's [libthr](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_rwlock.c) and shadPS4 free it without checking, and the console's answer is unverified. Destroying a rwlock while another thread is acquiring it is not guarded - [scePthreadCondWait / scePthreadCondTimedwait](../../core/libs/prx/libkernel/Pthread/src/Cond.cpp) (libkernel) - before waiting, FreeBSD libthr's [`cond_wait_common`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_cond.c#L1992-L2029) checks ownership through [`_mutex_owned`](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_mutex.c#L1166-L1178): an unowned mutex returns `EPERM`, while its destroyed marker returns `EINVAL`. An untouched adaptive static initializer (slot value 1) therefore returns `EPERM`; the test covers this, an uninitialized slot, a mutex owned by another thread, and the destroyed marker for both timed and untimed sce and POSIX waits. A recursive mutex locked several times is released completely for the wait and relocked to the same depth afterwards, as FreeBSD libthr does (`_mutex_cv_unlock`, `_mutex_cv_lock`). These FreeBSD-derived behaviors remain unverified on console hardware - [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioPollRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed, poll copies every state without waiting); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written +- [scePthreadAttr setters and getters](../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) (libkernel) - measured on a PS5 (firmware 13.60): a null or uninitialized attribute returns `EINVAL`, as does a null output of the stack size, detach state and scheduling parameter getters; a policy other than 1 and 3, an inheritance other than 0 and 4, or a priority outside 256 to 767 (or a null parameter) returns `ENOTSUP` (`0x8002002D`); a detach state other than 0 and 1 and a stack size below 16384 return `EINVAL`; any affinity mask is accepted. A null attribute pointer to `scePthreadAttrInit` and a null output of `scePthreadAttrGetaffinity` crashed the payload, so they still throw. A new attribute holds a 64 KiB stack size on the console and 1 MiB here, which the host code that runs on guest threads needs - [sceKernelGetAvailableCpumask](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelGetCurrentCpu](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - guest affinities are not applied to host threads, so the host CPU index is mapped into the calling thread's affinity clipped to `sceKernelGetAvailableCpumask` (the available mask when they do not overlap): host CPU n reports the (n modulo the CPU count)-th CPU of that mask, so a thread pinned to one CPU reports that CPU, as a FreeBSD `cpuset` guarantees on the console. Which CPU the console reports for a thread allowed on several is not known; shadPS4 returns 0 - [pthread_attr_setsolosched_np](../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) takes `(attr, int)` and stores the value through `scePthreadAttrSetsolosched`, returning `EINVAL` (22) for a null or uninitialized attribute like the other POSIX attribute setters. Only the name is public (aerolib `2+pVfgiEd7A`); the signature and behaviour follow the SCE twin, which [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/947627d10d4ef4b68ba3c0189aab6c368e70a8d2/src/kernel/pthread.cpp#L2188-L2199) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/a2983692930564815a77f81e74c151268f5b935f/src/SharpEmu.Libs/Kernel/KernelPthreadExtendedCompatExports.cs#L891-L913) store as an `int` without a range check, and [PS5PCEM](https://github.com/iStark/PS5PCEM/blob/71de7681f06c5db59a1120a3bb554c86780c3f5f/src/hle/libs/kernel_threading.zig#L1396-L1398) implements this alias over it. On a PS5 (firmware 13.60) only 0 and 16 are accepted; every other value returns `ENOTSUP` (45, `0x8002002D` from the SCE call) and keeps the stored one, and the SCE calls return `EINVAL` for a null or uninitialized attribute. PPSA15878 passes 16. Threads created from the attribute are not scheduled differently; what solo scheduling does on the console is unknown. Returnal (PPSA01285 v03.000.005) stopped on it with `what(): pthread_attr_setsolosched_np_nid_postfix not implemented` (#2872, #1155); LEGO Horizon Adventures (PPSA14632, #734) and Palworld (PPSA23126, #2598) import it