From 4b34a6d5a705affde7fef6b47c8539f6316c2e48 Mon Sep 17 00:00:00 2001 From: Leonardo Date: Wed, 14 Oct 2026 16:38:18 +0200 Subject: [PATCH] feat(libkernel): support microsecond timeout in size-0 _umtx_op --- .../SyncOnAddress/src/SyncOnAddress.cpp | 8 +++-- core/libs/tests/GuestUmtxOp.cpp | 33 ++++++++++--------- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp index 178c6459e0..50b6eb5d26 100644 --- a/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp +++ b/core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp @@ -158,6 +158,11 @@ int UmtxDeadline(const void* sizeArgument, const void* timeArgument, std::option if (timeArgument == nullptr) { return 0; } + if (sizeArgument == nullptr) { + const auto usecs = *static_cast(timeArgument); + deadlineNanos = TimedWait::DeadlineNanos(usecs); + return 0; + } UmtxTime time{{0, 0}, 0, GUEST_CLOCK_REALTIME}; if (reinterpret_cast(sizeArgument) <= sizeof(KernelTimespec)) { time.timeout = *static_cast(timeArgument); @@ -165,9 +170,6 @@ int UmtxDeadline(const void* sizeArgument, const void* timeArgument, std::option time = *static_cast(timeArgument); } if (time.timeout.tv_sec < 0 || time.timeout.tv_nsec < 0 || time.timeout.tv_nsec >= NANOS_PER_SECOND) { - if (sizeArgument == nullptr) { - NotImplemented_nid_no_patch("_umtx_op timeout with size argument 0 that is not a valid timespec"); - } return GUEST_EINVAL; } diff --git a/core/libs/tests/GuestUmtxOp.cpp b/core/libs/tests/GuestUmtxOp.cpp index 48ec61d508..56abb976c2 100644 --- a/core/libs/tests/GuestUmtxOp.cpp +++ b/core/libs/tests/GuestUmtxOp.cpp @@ -123,19 +123,20 @@ static void TimesOutWhileTheValueMatches() { static void RejectsAnInvalidTimeout() { std::uint32_t word = 1; KernelTimespec negative{-1, 0}; - Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(negative)), &negative), GUEST_EINVAL)); KernelTimespec tooManyNanos{0, 1000000000}; - Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(tooManyNanos)), &tooManyNanos), GUEST_EINVAL)); for (const int operation : {UMTX_OP_WAIT, UMTX_OP_WAIT_UINT}) { - bool threw = false; - try { Umtx(&word, operation, 1, nullptr, &tooManyNanos); } - catch (const std::runtime_error&) { threw = true; } - Require(threw); - threw = false; - try { Umtx(&word, operation, 1, nullptr, &negative); } - catch (const std::runtime_error&) { threw = true; } - Require(threw); + Require(FailsWith(Umtx(&word, operation, 1, SizeOf(sizeof(negative)), &negative), GUEST_EINVAL)); + Require(FailsWith(Umtx(&word, operation, 1, SizeOf(sizeof(tooManyNanos)), &tooManyNanos), GUEST_EINVAL)); } + struct { + std::uint32_t usecs = 20000; + std::uint32_t zero32 = 0; + std::int64_t zero64 = 0; + } zeroNeighbours; + auto start = std::chrono::steady_clock::now(); + Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, nullptr, &zeroNeighbours), GUEST_ETIMEDOUT)); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15)); + Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(1)); UmtxTime negativeNanos{{0, -1}, 0, CLOCK_MONOTONIC_ID}; Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(negativeNanos)), &negativeNanos), GUEST_EINVAL)); } @@ -146,7 +147,7 @@ static void WakeReleasesAWaiterWithoutAValueChange() { int result = -2; std::thread waiter([&] { KernelTimespec failsafe = FailsafeTimeout(); - result = Umtx(&value, UMTX_OP_WAIT, 1, nullptr, &failsafe); + result = Umtx(&value, UMTX_OP_WAIT, 1, SizeOf(sizeof(failsafe)), &failsafe); ++finished; }); while (finished.load() == 0) { @@ -171,14 +172,14 @@ static void WakeHonoursTheCountAndTheAddress() { waiters.emplace_back([&, i] { KernelTimespec failsafe = FailsafeTimeout(); ++started; - results[i] = Umtx(&word, UMTX_OP_WAIT_UINT_PRIVATE, 1, nullptr, &failsafe); + results[i] = Umtx(&word, UMTX_OP_WAIT_UINT_PRIVATE, 1, SizeOf(sizeof(failsafe)), &failsafe); ++finished; }); } std::thread otherWaiter([&] { KernelTimespec failsafe = FailsafeTimeout(); ++started; - otherResult = Umtx(&other, UMTX_OP_WAIT_UINT, 1, nullptr, &failsafe); + otherResult = Umtx(&other, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(failsafe)), &failsafe); ++otherFinished; }); AwaitCount(started, 5); @@ -227,20 +228,20 @@ static void NwakeWakesEveryWaiterOnEachListedAddress() { waiters.emplace_back([&, i] { KernelTimespec failsafe = FailsafeTimeout(); ++started; - results[i] = Umtx(&first, UMTX_OP_WAIT_UINT_PRIVATE, 1, nullptr, &failsafe); + results[i] = Umtx(&first, UMTX_OP_WAIT_UINT_PRIVATE, 1, SizeOf(sizeof(failsafe)), &failsafe); ++finished; }); } waiters.emplace_back([&] { KernelTimespec failsafe = FailsafeTimeout(); ++started; - results[3] = Umtx(&second, UMTX_OP_WAIT, 1, nullptr, &failsafe); + results[3] = Umtx(&second, UMTX_OP_WAIT, 1, SizeOf(sizeof(failsafe)), &failsafe); ++finished; }); std::thread unlistedWaiter([&] { KernelTimespec failsafe = FailsafeTimeout(); ++started; - unlistedResult = Umtx(&unlisted, UMTX_OP_WAIT_UINT, 1, nullptr, &failsafe); + unlistedResult = Umtx(&unlisted, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(failsafe)), &failsafe); ++unlistedFinished; }); AwaitCount(started, 5); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index efa9156809..031a98bd06 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -47,7 +47,7 @@ - [sceKernelAvailableFlexibleMemorySize](../../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - the segments of the title's executable and of its `.guest.prx` modules count against the configured flexible-memory budget (448 MiB by default) while loaded, for this size and for flexible maps. A title may declare a larger budget through `kernel.flexibleMemorySize` in `param.json`; this does not reduce the 13.5 GiB direct-memory size. Rejection of zero or a value not aligned to 16 KiB has not been measured on a console. This segment-accounting model follows [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L165), which maps every segment of a module outside its system module directory as flexible memory on the PS4. [Counted](../../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) are the segments the title's ELF gives permissions, each rounded to 16 KiB pages; not counted are segments without permissions, the relinker's own blocks (on Linux both are the read/write/execute `PT_LOAD`s, plus the header block at file offset 0; on Windows everything outside the `.elf` sections) and the host system libraries. The Linux image carries no marker for the relinker's blocks, so they are told apart by those flags and that offset only: a title segment that is writable and executable, or starts at file offset 0, is not counted. A module whose segments exceed what is left still loads, and the available size then reads 0; shadPS4 fails the segment map instead ([memory.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/memory.cpp#L519-L524) returns `EINVAL`, [module.cpp](https://github.com/shadps4-emu/shadPS4/blob/3a9f57265d28975658f1ca941daa5c879c7a74bd/src/core/module.cpp#L168-L169) asserts). Which segments the PS5 charges, its rounding, whether system modules use the title's budget and when an unload releases the charge are not measured. PPSA15508 subtracts the reported size and a 10 MiB module allowance from its own 448 MiB budget at start-up and stops when the result exceeds its 146 MiB code and data limit ([#2883](https://github.com/boykopovar/AnyPS5/issues/2883)) - [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 - [Foreign guest module image protection on Windows](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - `sceKernelMprotect` on Windows allows protection mutations on mapped PE images containing a `.elf0` section (as relinked guest modules produce, such as NVIDIA Real-time Denoisers `libNRD.prx` bundled in Dying Light 2: Stay Human PPSA02262), registering their sections into `GuestAllocations`. Host libraries and system DLLs (such as `libkernel.prx` and `kernel32.dll`) without a `.elf0` section continue to be rejected -- [_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 +- [_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` and Unity's `PS5Util.prx` (PPSA18194) pass `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 wait for a running call_once with operation 1. Whether the console reads a microsecond timeout there in all cases, and what operation 1 does on it, is unverified. A call with size argument 0 is read unconditionally as a 4-byte microsecond timeout, matching callers like `libc.prx` and `PS5Util.prx`'s `zlqfTyrQSPk` wrapper which pass size 0 with a 32-bit microsecond count - [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) - 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