diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index ec0962889..4ca3fcde5 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -597,6 +597,16 @@ if(BUILD_TESTING) set_tests_properties(guest_sync_on_address PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_umtx_op_tests ${LIBS_INCLUDE_DIR}/tests/GuestUmtxOp.cpp) + target_include_directories(guest_umtx_op_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_umtx_op_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_umtx_op_tests) + add_test(NAME guest_umtx_op COMMAND guest_umtx_op_tests) + set_tests_properties(guest_umtx_op PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_umtx_op PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_available_cpumask_tests ${LIBS_INCLUDE_DIR}/tests/GuestAvailableCpumask.cpp) target_include_directories(guest_available_cpumask_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_available_cpumask_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index f75b77933..021cc029c 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -1,8 +1,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -13,10 +15,36 @@ #include "prx/libkernel/Time/include/Time.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +extern "C" int* APS5_VABI __error_nid_postfix(); + namespace { constexpr int SYNC_ON_ADDRESS_OK = 0; +constexpr int UMTX_OP_RESERVED1 = 1; +constexpr int UMTX_OP_WAIT = 2; +constexpr int UMTX_OP_WAKE = 3; +constexpr int UMTX_OP_WAIT_UINT = 11; +constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15; +constexpr int UMTX_OP_WAKE_PRIVATE = 16; +constexpr int UMTX_OP_MAX = 27; +constexpr std::uint32_t UMTX_ABSTIME = 1; +constexpr int GUEST_CLOCK_REALTIME = 0; + +constexpr int GUEST_EFAULT = 14; +constexpr int GUEST_EINVAL = 22; +constexpr int GUEST_EOPNOTSUPP = 45; +constexpr int GUEST_ETIMEDOUT = 60; + +constexpr std::int64_t NANOS_PER_SECOND = 1000000000; +constexpr std::uint64_t UNBOUNDED_WAIT_NANOS = 1ULL << 62; + +struct UmtxTime { + KernelTimespec timeout; + std::uint32_t flags; + std::uint32_t clockId; +}; + struct AddressWaiter { TimedWait::Condition condition; bool woken = false; @@ -31,11 +59,11 @@ bool IsAlignedAddress(std::uintptr_t address) { } template -int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeout, const void* caller) { +bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional deadlineNanos, const void* caller) { const auto key = reinterpret_cast(address); std::unique_lock lock(g_waitersLock); if (std::atomic_ref(*address).load() != expected) { - return SYNC_ON_ADDRESS_OK; + return true; } AddressWaiter waiter; @@ -43,10 +71,10 @@ int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeou const auto position = queue.insert(queue.end(), &waiter); const auto isWoken = [&] { return waiter.woken; }; const auto waitStart = std::chrono::steady_clock::now(); - if (timeout == nullptr) { + if (!deadlineNanos) { waiter.condition.Wait(lock, isWoken); } else { - waiter.condition.WaitUntil(lock, TimedWait::DeadlineNanos(*timeout), isWoken); + waiter.condition.WaitUntil(lock, *deadlineNanos, isWoken); } const auto waited = std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart); @@ -59,7 +87,94 @@ int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeou } lock.unlock(); KernelTraceWait_nid_postfix("addr", caller, static_cast(waited.count()), !woken); - return woken ? SYNC_ON_ADDRESS_OK : SCE_KERNEL_ERROR_ETIMEDOUT; + return woken; +} + +template +int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeout, const void* caller) { + const auto deadline = timeout == nullptr ? std::nullopt : std::optional(TimedWait::DeadlineNanos(*timeout)); + return WaitOnAddressUntil(address, expected, deadline, caller) ? SYNC_ON_ADDRESS_OK : SCE_KERNEL_ERROR_ETIMEDOUT; +} + +void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) { + std::lock_guard lock(g_waitersLock); + const auto entry = g_waiters.find(key); + if (entry == g_waiters.end()) { + return; + } + auto& queue = entry->second; + for (std::int32_t woken = 0; !queue.empty() && (woken < count || (wakesAtLeastOne && woken == 0)); ++woken) { + AddressWaiter* waiter = queue.front(); + queue.pop_front(); + waiter->woken = true; + waiter->condition.NotifyOne(); + } + if (queue.empty()) { + g_waiters.erase(entry); + } +} + +std::uint64_t SaturatingNanos(std::int64_t seconds, std::int64_t nanos) { + if (seconds < 0) { + return 0; + } + if (seconds >= static_cast(UNBOUNDED_WAIT_NANOS / NANOS_PER_SECOND)) { + return UNBOUNDED_WAIT_NANOS; + } + const std::int64_t total = seconds * NANOS_PER_SECOND + nanos; + return total <= 0 ? 0 : static_cast(total); +} + +int UmtxDeadline(const void* sizeArgument, const void* timeArgument, std::optional& deadlineNanos) { + deadlineNanos.reset(); + if (timeArgument == nullptr) { + return 0; + } + UmtxTime time{{0, 0}, 0, GUEST_CLOCK_REALTIME}; + if (reinterpret_cast(sizeArgument) <= sizeof(KernelTimespec)) { + time.timeout = *static_cast(timeArgument); + } else { + time = *static_cast(timeArgument); + } + if (time.timeout.tv_sec < 0 || time.timeout.tv_nsec < 0 || time.timeout.tv_nsec >= NANOS_PER_SECOND) { + return GUEST_EINVAL; + } + + std::uint64_t remaining = 0; + if ((time.flags & UMTX_ABSTIME) == 0) { + remaining = SaturatingNanos(time.timeout.tv_sec, time.timeout.tv_nsec); + } else { + KernelTimespec now{}; + clock_gettime_nid_postfix(static_cast(time.clockId), &now); + remaining = SaturatingNanos(time.timeout.tv_sec - now.tv_sec, time.timeout.tv_nsec - now.tv_nsec); + } + if (remaining < UNBOUNDED_WAIT_NANOS) { + deadlineNanos = TimedWait::NowNanos() + remaining; + } + return 0; +} + +template +int UmtxWait(void* object, TValue expected, const void* sizeArgument, const void* timeArgument, const void* caller) { + std::optional deadline; + if (const int error = UmtxDeadline(sizeArgument, timeArgument, deadline); error != 0) { + return error; + } + if (object == nullptr) { + return GUEST_EFAULT; + } + if (!IsAlignedAddress(reinterpret_cast(object))) { + APS5_INVALID_ARG_EX; + } + return WaitOnAddressUntil(static_cast(object), expected, deadline, caller) ? 0 : GUEST_ETIMEDOUT; +} + +int UmtxWake(void* object, std::uint64_t count, bool isPrivate) { + if (object == nullptr) { + return isPrivate ? 0 : GUEST_EFAULT; + } + WakeAddress(reinterpret_cast(object), static_cast(count), true); + return 0; } } // namespace @@ -112,22 +227,40 @@ int APS5_VABI sceKernelSyncOnAddressWake(void* address, std::int32_t count) { APS5_INVALID_ARG_EX; } - std::lock_guard lock(g_waitersLock); - const auto entry = g_waiters.find(key); - if (entry == g_waiters.end()) { - return SYNC_ON_ADDRESS_OK; - } - auto& queue = entry->second; - for (; count > 0 && !queue.empty(); --count) { - AddressWaiter* waiter = queue.front(); - queue.pop_front(); - waiter->woken = true; - waiter->condition.NotifyOne(); - } - if (queue.empty()) { - g_waiters.erase(entry); - } + WakeAddress(key, count, false); return SYNC_ON_ADDRESS_OK; } +int APS5_VABI _umtx_op_nid_postfix(void* object, int operation, std::uint64_t value, void* sizeArgument, void* timeArgument) { + const void* caller = __builtin_return_address(0); + int error = 0; + switch (operation) { + case UMTX_OP_WAIT: + error = UmtxWait(object, value, sizeArgument, timeArgument, caller); + break; + case UMTX_OP_WAIT_UINT: + case UMTX_OP_WAIT_UINT_PRIVATE: + error = UmtxWait(object, static_cast(value), sizeArgument, timeArgument, caller); + break; + case UMTX_OP_WAKE: + case UMTX_OP_WAKE_PRIVATE: + error = UmtxWake(object, value, operation == UMTX_OP_WAKE_PRIVATE); + break; + default: + if (operation < 0 || operation >= UMTX_OP_MAX) { + error = GUEST_EINVAL; + } else if (operation <= UMTX_OP_RESERVED1) { + error = GUEST_EOPNOTSUPP; + } else { + NotImplemented_nid_no_patch(("_umtx_op operation " + std::to_string(operation)).c_str()); + } + break; + } + if (error == 0) { + return 0; + } + *__error_nid_postfix() = error; + return -1; +} + } diff --git a/core/libs/tests/GuestUmtxOp.cpp b/core/libs/tests/GuestUmtxOp.cpp new file mode 100644 index 000000000..9755aba0b --- /dev/null +++ b/core/libs/tests/GuestUmtxOp.cpp @@ -0,0 +1,231 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI _umtx_op_nid_postfix(void* object, int operation, std::uint64_t value, void* sizeArgument, void* timeArgument); +int* APS5_VABI __error_nid_postfix(); +int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp); +} + +static constexpr int UMTX_OP_WAIT = 2; +static constexpr int UMTX_OP_WAKE = 3; +static constexpr int UMTX_OP_MUTEX_LOCK = 5; +static constexpr int UMTX_OP_WAIT_UINT = 11; +static constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15; +static constexpr int UMTX_OP_WAKE_PRIVATE = 16; +static constexpr int UMTX_OP_SEM2_WAIT = 23; +static constexpr int UMTX_OP_MAX = 27; +static constexpr std::uint32_t UMTX_ABSTIME = 1; +static constexpr int CLOCK_MONOTONIC_ID = 4; + +static constexpr int GUEST_EFAULT = 14; +static constexpr int GUEST_EINVAL = 22; +static constexpr int GUEST_EOPNOTSUPP = 45; +static constexpr int GUEST_ETIMEDOUT = 60; + +static constexpr auto SETTLE = std::chrono::milliseconds(100); + +struct UmtxTime { + KernelTimespec timeout; + std::uint32_t flags; + std::uint32_t clockId; +}; + +static void Require(bool value) { if (!value) std::abort(); } + +static KernelTimespec FailsafeTimeout() { return {10, 0}; } + +static int Umtx(void* object, int operation, std::uint64_t value, void* sizeArgument = nullptr, void* timeArgument = nullptr) { + *__error_nid_postfix() = 0; + return _umtx_op_nid_postfix(object, operation, value, sizeArgument, timeArgument); +} + +static bool FailsWith(int result, int error) { + return result == -1 && *__error_nid_postfix() == error; +} + +static void* SizeOf(std::size_t size) { return reinterpret_cast(size); } + +static void AwaitCount(const std::atomic& counter, int value) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (counter.load() != value) { + Require(std::chrono::steady_clock::now() < deadline); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +static void ReturnsWhenTheValueDiffers() { + std::uint64_t value = 2; + Require(Umtx(&value, UMTX_OP_WAIT, 1) == 0); + KernelTimespec none{0, 0}; + Require(Umtx(&value, UMTX_OP_WAIT, 1, nullptr, &none) == 0); + + std::uint32_t word = 2; + Require(Umtx(&word, UMTX_OP_WAIT_UINT, 1) == 0); + Require(Umtx(&word, UMTX_OP_WAIT_UINT_PRIVATE, 1, nullptr, &none) == 0); +} + +static void WaitComparesALongAndWaitUintAnInt() { + std::uint64_t value = 0x100000001ULL; + KernelTimespec none{0, 0}; + Require(Umtx(&value, UMTX_OP_WAIT, 1, nullptr, &none) == 0); + Require(FailsWith(Umtx(&value, UMTX_OP_WAIT, 0x100000001ULL, nullptr, &none), GUEST_ETIMEDOUT)); + + auto* low = reinterpret_cast(&value); + Require(FailsWith(Umtx(low, UMTX_OP_WAIT_UINT, 1, nullptr, &none), GUEST_ETIMEDOUT)); + Require(FailsWith(Umtx(low, UMTX_OP_WAIT_UINT_PRIVATE, 0x700000001ULL, nullptr, &none), GUEST_ETIMEDOUT)); +} + +static void TimesOutWhileTheValueMatches() { + std::uint32_t word = 1; + KernelTimespec none{0, 0}; + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, nullptr, &none), GUEST_ETIMEDOUT)); + + KernelTimespec relative{0, 20000000}; + auto start = std::chrono::steady_clock::now(); + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(relative)), &relative), GUEST_ETIMEDOUT)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + + UmtxTime time{{0, 20000000}, 0, CLOCK_MONOTONIC_ID}; + start = std::chrono::steady_clock::now(); + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(time)), &time), GUEST_ETIMEDOUT)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + + KernelTimespec now{}; + Require(clock_gettime_nid_postfix(CLOCK_MONOTONIC_ID, &now) == 0); + UmtxTime absolute{{now.tv_sec, now.tv_nsec + 20000000}, UMTX_ABSTIME, CLOCK_MONOTONIC_ID}; + if (absolute.timeout.tv_nsec >= 1000000000) { + absolute.timeout.tv_sec += 1; + absolute.timeout.tv_nsec -= 1000000000; + } + start = std::chrono::steady_clock::now(); + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(absolute)), &absolute), GUEST_ETIMEDOUT)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + + UmtxTime past{{now.tv_sec, now.tv_nsec}, UMTX_ABSTIME, CLOCK_MONOTONIC_ID}; + start = std::chrono::steady_clock::now(); + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(past)), &past), GUEST_ETIMEDOUT)); + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(1)); +} + +static void RejectsAnInvalidTimeout() { + std::uint32_t word = 1; + KernelTimespec negative{-1, 0}; + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, nullptr, &negative), GUEST_EINVAL)); + KernelTimespec tooManyNanos{0, 1000000000}; + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, nullptr, &tooManyNanos), GUEST_EINVAL)); + UmtxTime negativeNanos{{0, -1}, 0, CLOCK_MONOTONIC_ID}; + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(negativeNanos)), &negativeNanos), GUEST_EINVAL)); +} + +static void WakeReleasesAWaiterWithoutAValueChange() { + std::uint64_t value = 1; + std::atomic finished{0}; + int result = -2; + std::thread waiter([&] { + KernelTimespec failsafe = FailsafeTimeout(); + result = Umtx(&value, UMTX_OP_WAIT, 1, nullptr, &failsafe); + ++finished; + }); + while (finished.load() == 0) { + Require(Umtx(&value, UMTX_OP_WAKE, 1) == 0); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + waiter.join(); + Require(result == 0); +} + +static void WakeHonoursTheCountAndTheAddress() { + std::uint32_t word = 1; + std::uint32_t other = 1; + std::atomic started{0}; + std::atomic finished{0}; + std::atomic otherFinished{0}; + std::vector results(4, -2); + int otherResult = -2; + + std::vector waiters; + for (int i = 0; i < 4; ++i) { + waiters.emplace_back([&, i] { + KernelTimespec failsafe = FailsafeTimeout(); + ++started; + results[i] = Umtx(&word, UMTX_OP_WAIT_UINT_PRIVATE, 1, nullptr, &failsafe); + ++finished; + }); + } + std::thread otherWaiter([&] { + KernelTimespec failsafe = FailsafeTimeout(); + ++started; + otherResult = Umtx(&other, UMTX_OP_WAIT_UINT, 1, nullptr, &failsafe); + ++otherFinished; + }); + AwaitCount(started, 5); + std::this_thread::sleep_for(SETTLE); + + Require(Umtx(&word, UMTX_OP_WAKE_PRIVATE, 1) == 0); + AwaitCount(finished, 1); + std::this_thread::sleep_for(SETTLE); + Require(finished.load() == 1); + + Require(Umtx(&word, UMTX_OP_WAKE, 0) == 0); + AwaitCount(finished, 2); + std::this_thread::sleep_for(SETTLE); + Require(finished.load() == 2); + + Require(Umtx(&word, UMTX_OP_WAKE, INT_MAX) == 0); + AwaitCount(finished, 4); + std::this_thread::sleep_for(SETTLE); + Require(otherFinished.load() == 0); + + Require(Umtx(&other, UMTX_OP_WAKE_PRIVATE, 1) == 0); + for (auto& waiter : waiters) waiter.join(); + otherWaiter.join(); + for (const int result : results) Require(result == 0); + Require(otherResult == 0); +} + +static void WakeWithoutWaitersSucceeds() { + std::uint32_t word = 0; + Require(Umtx(&word, UMTX_OP_WAKE, 1) == 0); + Require(Umtx(&word, UMTX_OP_WAKE_PRIVATE, INT_MAX) == 0); +} + +static void RejectsBadObjectsAndOperations() { + Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT, 0), GUEST_EFAULT)); + Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT_UINT, 0), GUEST_EFAULT)); + Require(FailsWith(Umtx(nullptr, UMTX_OP_WAKE, 1), GUEST_EFAULT)); + Require(Umtx(nullptr, UMTX_OP_WAKE_PRIVATE, 1) == 0); + + std::uint64_t value = 0; + Require(FailsWith(Umtx(&value, 0, 0), GUEST_EOPNOTSUPP)); + Require(FailsWith(Umtx(&value, 1, 0), GUEST_EOPNOTSUPP)); + Require(FailsWith(Umtx(&value, UMTX_OP_MAX, 0), GUEST_EINVAL)); + Require(FailsWith(Umtx(&value, -1, 0), GUEST_EINVAL)); + + bool threw = false; + try { Umtx(&value, UMTX_OP_MUTEX_LOCK, 0); } + catch (const std::runtime_error&) { threw = true; } + Require(threw); + threw = false; + try { Umtx(&value, UMTX_OP_SEM2_WAIT, 0); } + catch (const std::runtime_error&) { threw = true; } + Require(threw); +} + +int main() { + ReturnsWhenTheValueDiffers(); + WaitComparesALongAndWaitUintAnInt(); + TimesOutWhileTheValueMatches(); + RejectsAnInvalidTimeout(); + WakeReleasesAWaiterWithoutAValueChange(); + WakeHonoursTheCountAndTheAddress(); + WakeWithoutWaitersSucceeds(); + RejectsBadObjectsAndOperations(); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f6f43637d..432cb918a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -166,6 +166,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelGetModuleInfo](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it - [sceKernelReleaseFlexibleMemory](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified - [Memory protection bits 0x100 and 0x200](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits +- [_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` and `UMTX_OP_WAKE_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. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operations 0 and 1 fail with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; 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 - [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