diff --git a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp index cd8890dcd..3ff9b3088 100644 --- a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp @@ -2,6 +2,7 @@ #include "../include/Rwlock.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +#include #include #include #include @@ -12,10 +13,22 @@ static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; static constexpr int SCE_KERNEL_ERROR_EDEADLK = 0x8002000B; static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = 0x8002003C; +static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; + +static PthreadRwlockPrivate* const DestroyedRwlock = reinterpret_cast(std::uintptr_t{1}); static PthreadRwlockPrivate* RequireRwlock(PthreadRwlock* rwlock, const char* funcName) { - if (!rwlock || !*rwlock) throw std::runtime_error(std::string(funcName) + ": null rwlock"); - return *rwlock; + if (!rwlock) throw std::runtime_error(std::string(funcName) + ": null rwlock"); + std::atomic_ref slot(*rwlock); + PthreadRwlock current = slot.load(std::memory_order_acquire); + if (current == nullptr) { + auto* created = new PthreadRwlockPrivate(); + if (slot.compare_exchange_strong(current, created, std::memory_order_acq_rel, std::memory_order_acquire)) + current = created; + else + delete created; + } + return current == DestroyedRwlock ? nullptr : current; } static bool OwnsWrite(const PthreadRwlockPrivate* lock) { @@ -25,12 +38,16 @@ static bool OwnsWrite(const PthreadRwlockPrivate* lock) { extern "C" { int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock) { - auto* lock = RequireRwlock(rwlock, __func__); - if (lock->_writer.load(std::memory_order_acquire) != std::thread::id{} || - lock->_readers.load(std::memory_order_acquire) != 0) - return SCE_KERNEL_ERROR_EBUSY; - delete lock; - *rwlock = nullptr; + if (!rwlock) throw std::runtime_error("scePthreadRwlockDestroy: null rwlock"); + std::atomic_ref slot(*rwlock); + PthreadRwlock lock = slot.load(std::memory_order_acquire); + do { + if (lock == nullptr) return SCE_OK; + if (lock == DestroyedRwlock) return SCE_KERNEL_ERROR_EINVAL; + } while (!slot.compare_exchange_weak(lock, DestroyedRwlock, std::memory_order_acq_rel, std::memory_order_acquire)); + if (lock->_writer.load(std::memory_order_acquire) == std::thread::id{} && + lock->_readers.load(std::memory_order_acquire) == 0) + delete lock; return SCE_OK; } @@ -46,6 +63,7 @@ int APS5_VABI scePthreadRwlockInit(PthreadRwlock* rwlock, const PthreadRwlockatt int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; #ifdef _WIN32 while (!lock->_lock.try_lock_shared()) TimedWait::SleepNanos(1000000); @@ -58,6 +76,7 @@ int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EBUSY; if (!lock->_lock.try_lock_shared()) return SCE_KERNEL_ERROR_EBUSY; lock->_readers.fetch_add(1, std::memory_order_release); @@ -66,6 +85,7 @@ int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTimedrdlock(PthreadRwlock* rwlock, KernelUseconds usec) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; const bool locked = TimedWait::AcquireUntil(TimedWait::DeadlineNanos(usec), [&] { return lock->_lock.try_lock_shared(); }, [&](std::uint64_t micros) { return lock->_lock.try_lock_shared_for(std::chrono::microseconds(micros)); }); if (!locked) return SCE_KERNEL_ERROR_ETIMEDOUT; @@ -75,6 +95,7 @@ int APS5_VABI scePthreadRwlockTimedrdlock(PthreadRwlock* rwlock, KernelUseconds int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; #ifdef _WIN32 while (!lock->_lock.try_lock()) TimedWait::SleepNanos(1000000); @@ -87,6 +108,7 @@ int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTrywrlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock) || !lock->_lock.try_lock()) return SCE_KERNEL_ERROR_EBUSY; lock->_writer.store(std::this_thread::get_id(), std::memory_order_release); return SCE_OK; @@ -94,6 +116,7 @@ int APS5_VABI scePthreadRwlockTrywrlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTimedwrlock(PthreadRwlock* rwlock, KernelUseconds usec) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; const bool locked = TimedWait::AcquireUntil(TimedWait::DeadlineNanos(usec), [&] { return lock->_lock.try_lock(); }, [&](std::uint64_t micros) { return lock->_lock.try_lock_for(std::chrono::microseconds(micros)); }); if (!locked) return SCE_KERNEL_ERROR_ETIMEDOUT; @@ -103,6 +126,7 @@ int APS5_VABI scePthreadRwlockTimedwrlock(PthreadRwlock* rwlock, KernelUseconds int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock) { auto* lock = RequireRwlock(rwlock, __func__); + if (!lock) return SCE_KERNEL_ERROR_EINVAL; if (OwnsWrite(lock)) { lock->_writer.store(std::thread::id{}, std::memory_order_release); lock->_lock.unlock(); diff --git a/core/libs/tests/GuestPthreadRwlock.cpp b/core/libs/tests/GuestPthreadRwlock.cpp index 8a2258575..503e2dc69 100644 --- a/core/libs/tests/GuestPthreadRwlock.cpp +++ b/core/libs/tests/GuestPthreadRwlock.cpp @@ -23,12 +23,16 @@ int APS5_VABI pthread_rwlock_timedrdlock_nid_postfix(PthreadRwlock* rwlock, cons int APS5_VABI pthread_rwlock_timedwrlock_nid_postfix(PthreadRwlock* rwlock, const KernelTimespec* abstime); int APS5_VABI scePthreadRwlockInit(PthreadRwlock* rwlock, const PthreadRwlockattr* attr, const char* name); int APS5_VABI scePthreadRwlockDestroy(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockRdlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock); +int APS5_VABI scePthreadRwlockUnlock(PthreadRwlock* rwlock); } using TimedLock = int (APS5_VABI *)(PthreadRwlock*, const KernelTimespec*); static constexpr int SCE_OK = 0; -static constexpr int SCE_KERNEL_ERROR_EBUSY = 0x80020010; +static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int GUEST_EPERM = 1; static constexpr int GUEST_EDEADLK = 11; static constexpr int GUEST_EBUSY = 16; @@ -39,6 +43,8 @@ static constexpr std::int64_t NANOS_PER_SECOND = 1000000000; static void Require(bool value) { if (!value) std::abort(); } +static const PthreadRwlock DestroyedRwlock = reinterpret_cast(std::uintptr_t{1}); + static KernelTimespec After(std::int64_t millis) { KernelTimespec now{}; Require(clock_gettime_nid_postfix(GUEST_REALTIME_CLOCK, &now) == 0); @@ -151,19 +157,44 @@ static void CheckWriterHandoff() { Require(scePthreadCreate(&state.firstThread, nullptr, HandoffFirst, &state, nullptr) == SCE_OK); Require(scePthreadCreate(&state.secondThread, nullptr, HandoffSecond, &state, nullptr) == SCE_OK); while (!state.firstHeld.load()) std::this_thread::yield(); - Require(pthread_rwlock_destroy_nid_postfix(&state.rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&state.rwlock) == SCE_KERNEL_ERROR_EBUSY); Require(state.rwlock != nullptr); state.firstRelease.store(true); Require(scePthreadJoin(state.firstThread, nullptr) == SCE_OK); while (!state.secondAcquired.load()) std::this_thread::yield(); - Require(pthread_rwlock_destroy_nid_postfix(&state.rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&state.rwlock) == SCE_KERNEL_ERROR_EBUSY); Require(state.rwlock != nullptr); state.secondRelease.store(true); Require(scePthreadJoin(state.secondThread, nullptr) == SCE_OK); Require(pthread_rwlock_destroy_nid_postfix(&state.rwlock) == 0); - Require(state.rwlock == nullptr); + Require(state.rwlock == DestroyedRwlock); +} + +static void CheckDestroyLifecycle() { + PthreadRwlock held = nullptr; + Require(scePthreadRwlockInit(&held, nullptr, nullptr) == SCE_OK); + Require(scePthreadRwlockWrlock(&held) == SCE_OK); + Require(scePthreadRwlockDestroy(&held) == SCE_OK); + Require(held == DestroyedRwlock); + Require(scePthreadRwlockUnlock(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadRwlockRdlock(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadRwlockTryrdlock(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadRwlockDestroy(&held) == SCE_KERNEL_ERROR_EINVAL); + Require(pthread_rwlock_rdlock_nid_postfix(&held) == GUEST_EINVAL); + Require(held == DestroyedRwlock); + Require(scePthreadRwlockInit(&held, nullptr, nullptr) == SCE_OK); + Require(held != nullptr && held != DestroyedRwlock); + Require(scePthreadRwlockRdlock(&held) == SCE_OK); + Require(scePthreadRwlockRdlock(&held) == SCE_OK); + Require(scePthreadRwlockDestroy(&held) == SCE_OK); + Require(held == DestroyedRwlock); + + PthreadRwlock zero = nullptr; + Require(scePthreadRwlockDestroy(&zero) == SCE_OK); + Require(zero == nullptr); + Require(scePthreadRwlockRdlock(&zero) == SCE_OK); + Require(zero != nullptr && zero != DestroyedRwlock); + Require(scePthreadRwlockUnlock(&zero) == SCE_OK); + Require(scePthreadRwlockDestroy(&zero) == SCE_OK); + Require(zero == DestroyedRwlock); } static void CheckFailedLockAttempts() { @@ -172,23 +203,20 @@ static void CheckFailedLockAttempts() { const KernelTimespec timeout = After(10); Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &timeout) == GUEST_EDEADLK); Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&rwlock) == SCE_KERNEL_ERROR_EBUSY); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); - Require(rwlock == nullptr); + Require(rwlock == DestroyedRwlock); + rwlock = nullptr; Holder reader{&rwlock, false}; Start(reader); const KernelTimespec past = After(-1000); Require(pthread_rwlock_timedwrlock_nid_postfix(&rwlock, &past) == GUEST_ETIMEDOUT); Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&rwlock) == SCE_KERNEL_ERROR_EBUSY); reader.release.store(true); Require(scePthreadJoin(reader.thread, nullptr) == SCE_OK); Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); - Require(rwlock == nullptr); + Require(rwlock == DestroyedRwlock); } static void CheckDeadlinesBeyondTheClockRange() { @@ -253,8 +281,6 @@ int main() { Require(pthread_rwlock_timedrdlock_nid_postfix(&rwlock, &invalid) == 0); Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == 0); Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&rwlock) == SCE_KERNEL_ERROR_EBUSY); ExpectTimeout(pthread_rwlock_timedwrlock_nid_postfix, &rwlock); deadline = After(5000); reader.release.store(true); @@ -267,8 +293,6 @@ int main() { Require(pthread_rwlock_unlock_nid_postfix(&rwlock) == GUEST_EPERM); Require(pthread_rwlock_tryrdlock_nid_postfix(&rwlock) == GUEST_EBUSY); Require(pthread_rwlock_trywrlock_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&rwlock) == SCE_KERNEL_ERROR_EBUSY); ExpectTimeout(pthread_rwlock_timedrdlock_nid_postfix, &rwlock); ExpectTimeout(pthread_rwlock_timedwrlock_nid_postfix, &rwlock); deadline = After(5000); @@ -283,58 +307,51 @@ int main() { catch (const std::runtime_error&) { rejected = true; } Require(rejected); Require(pthread_rwlock_destroy_nid_postfix(&rwlock) == 0); - Require(rwlock == nullptr); + Require(rwlock == DestroyedRwlock); PthreadRwlock unheld = nullptr; Require(scePthreadRwlockInit(&unheld, nullptr, nullptr) == SCE_OK); Require(unheld != nullptr); Require(pthread_rwlock_destroy_nid_postfix(&unheld) == 0); - Require(unheld == nullptr); + Require(unheld == DestroyedRwlock); PthreadRwlock rd = nullptr; Require(pthread_rwlock_rdlock_nid_postfix(&rd) == 0); - Require(pthread_rwlock_destroy_nid_postfix(&rd) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&rd) == SCE_KERNEL_ERROR_EBUSY); - Require(rd != nullptr); - Require(pthread_rwlock_unlock_nid_postfix(&rd) == 0); - Require(pthread_rwlock_tryrdlock_nid_postfix(&rd) == 0); - Require(pthread_rwlock_unlock_nid_postfix(&rd) == 0); Require(pthread_rwlock_destroy_nid_postfix(&rd) == 0); - Require(rd == nullptr); + Require(rd == DestroyedRwlock); + Require(pthread_rwlock_unlock_nid_postfix(&rd) == GUEST_EINVAL); + Require(pthread_rwlock_tryrdlock_nid_postfix(&rd) == GUEST_EINVAL); + Require(pthread_rwlock_destroy_nid_postfix(&rd) == GUEST_EINVAL); + Require(rd == DestroyedRwlock); PthreadRwlock wr = nullptr; Require(pthread_rwlock_wrlock_nid_postfix(&wr) == 0); - Require(pthread_rwlock_destroy_nid_postfix(&wr) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&wr) == SCE_KERNEL_ERROR_EBUSY); - Require(wr != nullptr); - Require(pthread_rwlock_unlock_nid_postfix(&wr) == 0); - Require(pthread_rwlock_trywrlock_nid_postfix(&wr) == 0); - Require(pthread_rwlock_unlock_nid_postfix(&wr) == 0); Require(pthread_rwlock_destroy_nid_postfix(&wr) == 0); - Require(wr == nullptr); + Require(wr == DestroyedRwlock); + Require(pthread_rwlock_unlock_nid_postfix(&wr) == GUEST_EINVAL); + Require(pthread_rwlock_trywrlock_nid_postfix(&wr) == GUEST_EINVAL); + Require(pthread_rwlock_destroy_nid_postfix(&wr) == GUEST_EINVAL); + Require(wr == DestroyedRwlock); PthreadRwlock remoteRd = nullptr; Holder bgReader{&remoteRd, false}; Start(bgReader); - Require(pthread_rwlock_destroy_nid_postfix(&remoteRd) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&remoteRd) == SCE_KERNEL_ERROR_EBUSY); Require(remoteRd != nullptr); bgReader.release.store(true); Require(scePthreadJoin(bgReader.thread, nullptr) == SCE_OK); Require(pthread_rwlock_destroy_nid_postfix(&remoteRd) == 0); - Require(remoteRd == nullptr); + Require(remoteRd == DestroyedRwlock); PthreadRwlock remoteWr = nullptr; Holder bgWriter{&remoteWr, true}; Start(bgWriter); - Require(pthread_rwlock_destroy_nid_postfix(&remoteWr) == GUEST_EBUSY); - Require(scePthreadRwlockDestroy(&remoteWr) == SCE_KERNEL_ERROR_EBUSY); Require(remoteWr != nullptr); bgWriter.release.store(true); Require(scePthreadJoin(bgWriter.thread, nullptr) == SCE_OK); Require(pthread_rwlock_destroy_nid_postfix(&remoteWr) == 0); - Require(remoteWr == nullptr); + Require(remoteWr == DestroyedRwlock); CheckWriterHandoff(); CheckFailedLockAttempts(); + CheckDestroyLifecycle(); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 71c07945e..432adce85 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -209,7 +209,7 @@ Throughout the project, every function at every stage either **does exactly what - [_umtx_op](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE`, `UMTX_OP_WAKE_PRIVATE` and `UMTX_OP_NWAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. `UMTX_OP_NWAKE_PRIVATE` reads the value as an `int` count of pointers at the object and wakes every waiter on each one; a count of 0 or fewer reads nothing, and a null list fails with `EFAULT`, where FreeBSD's `copyin` would also fail for any other unreadable list. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operation 0 fails with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; `UMTX_OP_WAIT_UINT` with a value that does not fit in 32 bits returns 0 at once, as FreeBSD's zero-extended compare does. Operation 1 throws, since PPSA12544's call_once uses it in a way FreeBSD rejects; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` passes `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and waits for a running call_once with operation 1; its `PS5Util.prx` passes a `timespec`. Whether the console reads a microsecond timeout there, and what operation 1 does on it, is unverified. A libc call cannot be told apart from a `timespec` call by the size argument alone, so a timeout with size argument 0 is read as a `timespec` and only throws when it is not a valid one (negative seconds, nanoseconds outside [0, 1e9)), which is where FreeBSD would return `EINVAL`; a microsecond value that happens to read as a valid `timespec` is waited on as one - [sceKernelSyncOnAddressWait](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed -- [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 +- [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - measured on a PS5 (firmware 13.60): destroying a rwlock returns 0 whether it is free or held for reading or writing, and leaves 1 in the handle; every call on that handle then returns `EINVAL` until it is initialized again, and a zero handle is initialized on first use by the `scePthreadRwlock` calls as by the POSIX ones. A rwlock destroyed while held is not freed, so its memory leaks. 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