feat(libkernel): implement _umtx_op wait and wake operations

Titles that ship their own libc.prx or Unity's PS5Util.prx import
_umtx_op (04AjkP0jO9U) for atomic waits, call_once and futex-style
waits. Implement UMTX_OP_WAIT, UMTX_OP_WAKE, UMTX_OP_WAIT_UINT,
UMTX_OP_WAIT_UINT_PRIVATE and UMTX_OP_WAKE_PRIVATE as FreeBSD 11's
kern_umtx.c does, on the sceKernelSyncOnAddressWait waiter queue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HeC9bA3n74TzJRYv9AgBE8
This commit is contained in:
clem-gb 2026-10-08 09:17:23 +02:00 committed by Adrià Franch
parent 90c152e840
commit 79f90f3d9b
4 changed files with 395 additions and 20 deletions

View File

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

View File

@ -1,8 +1,10 @@
#include <atomic>
#include <chrono>
#include <cstdint>
#include <limits>
#include <list>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <string>
#include <unordered_map>
@ -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 <class TValue>
int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeout, const void* caller) {
bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional<std::uint64_t> deadlineNanos, const void* caller) {
const auto key = reinterpret_cast<std::uintptr_t>(address);
std::unique_lock<std::mutex> lock(g_waitersLock);
if (std::atomic_ref<TValue>(*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::nanoseconds>(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<std::uint64_t>(waited.count()), !woken);
return woken ? SYNC_ON_ADDRESS_OK : SCE_KERNEL_ERROR_ETIMEDOUT;
return woken;
}
template <class TValue>
int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeout, const void* caller) {
const auto deadline = timeout == nullptr ? std::nullopt : std::optional<std::uint64_t>(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<std::mutex> 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<std::int64_t>(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<std::uint64_t>(total);
}
int UmtxDeadline(const void* sizeArgument, const void* timeArgument, std::optional<std::uint64_t>& deadlineNanos) {
deadlineNanos.reset();
if (timeArgument == nullptr) {
return 0;
}
UmtxTime time{{0, 0}, 0, GUEST_CLOCK_REALTIME};
if (reinterpret_cast<std::uintptr_t>(sizeArgument) <= sizeof(KernelTimespec)) {
time.timeout = *static_cast<const KernelTimespec*>(timeArgument);
} else {
time = *static_cast<const UmtxTime*>(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<int>(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 <class TValue>
int UmtxWait(void* object, TValue expected, const void* sizeArgument, const void* timeArgument, const void* caller) {
std::optional<std::uint64_t> deadline;
if (const int error = UmtxDeadline(sizeArgument, timeArgument, deadline); error != 0) {
return error;
}
if (object == nullptr) {
return GUEST_EFAULT;
}
if (!IsAlignedAddress<TValue>(reinterpret_cast<std::uintptr_t>(object))) {
APS5_INVALID_ARG_EX;
}
return WaitOnAddressUntil(static_cast<TValue*>(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<std::uintptr_t>(object), static_cast<std::int32_t>(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<std::mutex> 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<std::uint64_t>(object, value, sizeArgument, timeArgument, caller);
break;
case UMTX_OP_WAIT_UINT:
case UMTX_OP_WAIT_UINT_PRIVATE:
error = UmtxWait<std::uint32_t>(object, static_cast<std::uint32_t>(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;
}
}

View File

@ -0,0 +1,231 @@
#include "SceTypes.hpp"
#include <atomic>
#include <chrono>
#include <climits>
#include <cstdint>
#include <cstdlib>
#include <stdexcept>
#include <thread>
#include <vector>
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<void*>(size); }
static void AwaitCount(const std::atomic<int>& 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<std::uint32_t*>(&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<int> 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<int> started{0};
std::atomic<int> finished{0};
std::atomic<int> otherFinished{0};
std::vector<int> results(4, -2);
int otherResult = -2;
std::vector<std::thread> 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();
}

View File

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