From 89351601607f8ca85f8288875e27f20a50fae85b Mon Sep 17 00:00:00 2001 From: MarcZX100 Date: Sun, 11 Oct 2026 15:23:14 +0000 Subject: [PATCH] fix(libkernel): handle available sem_timedwait counts first Co-authored-by: milescoviello --- core/libs/prx/libkernel/Pthread/Posix/Sem.cpp | 8 ++++ core/libs/tests/GuestPosixSem.cpp | 41 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 3 ++ 3 files changed, 52 insertions(+) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp index e71072e2a..d11bda07a 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp @@ -84,6 +84,14 @@ int APS5_VABI sem_trywait_nid_postfix(void* sem) { int APS5_VABI sem_timedwait_nid_postfix(void* sem, const KernelTimespec* abstime) { auto* semaphore = Get(sem); if (!semaphore) return Fail(PosixThread::GUEST_EINVAL); + { + std::lock_guard lock(semaphore->lock); + if (semaphore->count > 0) { + --semaphore->count; + return 0; + } + } + if (!abstime) return WaitFor(semaphore, nullptr); KernelUseconds usec = 0; if (!PosixThread::RelativeMicroseconds(GUEST_REALTIME_CLOCK, abstime, &usec)) return Fail(PosixThread::GUEST_EINVAL); return WaitFor(semaphore, &usec); diff --git a/core/libs/tests/GuestPosixSem.cpp b/core/libs/tests/GuestPosixSem.cpp index aae90c1ae..f6a296312 100644 --- a/core/libs/tests/GuestPosixSem.cpp +++ b/core/libs/tests/GuestPosixSem.cpp @@ -1,16 +1,20 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include +#include #include #include #include #include #include +#include extern "C" { int APS5_VABI sem_init_nid_postfix(void*, int, unsigned int); int APS5_VABI sem_destroy_nid_postfix(void*); int APS5_VABI sem_post_nid_postfix(void*); int APS5_VABI sem_trywait_nid_postfix(void*); +int APS5_VABI sem_timedwait_nid_postfix(void*, const KernelTimespec*); int APS5_VABI sem_getvalue_nid_postfix(void*, int*); int* APS5_VABI __error_nid_postfix(); } @@ -58,4 +62,41 @@ int main(int argc, char** argv) { Require(sem_trywait_nid_postfix(&sem) == 0, "normal wait must succeed"); Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "normal wait must decrement"); Require(sem_destroy_nid_postfix(&sem) == 0, "normal destroy must succeed"); + + KernelTimespec invalidTimeout{0, 1000000000}; + Require(sem_init_nid_postfix(&sem, 0, 1) == 0, "available semaphore must initialize"); + Require(sem_timedwait_nid_postfix(&sem, nullptr) == 0, "available semaphore must ignore null timeout"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "null-timeout wait must consume the available count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "null-timeout semaphore destroy must succeed"); + + Require(sem_init_nid_postfix(&sem, 0, 1) == 0, "invalid-timeout semaphore must initialize"); + Require(sem_timedwait_nid_postfix(&sem, &invalidTimeout) == 0, "available semaphore must ignore invalid timeout"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "invalid-timeout wait must consume the available count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "invalid-timeout semaphore destroy must succeed"); + + Require(sem_init_nid_postfix(&sem, 0, 0) == 0, "blocked semaphore must initialize"); + std::atomic entered = false; + std::atomic completed = false; + int waitResult = -2; + std::thread waiter([&] { + entered.store(true, std::memory_order_release); + waitResult = sem_timedwait_nid_postfix(&sem, nullptr); + completed.store(true, std::memory_order_release); + }); + while (!entered.load(std::memory_order_acquire)) std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + Require(!completed.load(std::memory_order_acquire), "blocked null-timeout wait must remain blocked"); + Require(sem_post_nid_postfix(&sem) == 0, "post must release a null-timeout waiter"); + waiter.join(); + Require(waitResult == 0, "null-timeout waiter must succeed after a post"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "null-timeout waiter must consume the posted count"); + + Require(sem_init_nid_postfix(&sem, 0, 0) == 0, "invalid-timeout blocked semaphore must initialize"); + *__error_nid_postfix() = 0; + Require(sem_timedwait_nid_postfix(&sem, &invalidTimeout) == -1 && *__error_nid_postfix() == 22, "blocked invalid-timeout wait must report EINVAL"); + KernelTimespec expiredTimeout{0, 0}; + *__error_nid_postfix() = 0; + Require(sem_timedwait_nid_postfix(&sem, &expiredTimeout) == -1 && *__error_nid_postfix() == 60, "blocked expired-timeout wait must report ETIMEDOUT"); + Require(sem_getvalue_nid_postfix(&sem, &value) == 0 && value == 0, "failed timed waits must preserve the count"); + Require(sem_destroy_nid_postfix(&sem) == 0, "blocked semaphore destroy must succeed"); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index c699ad506..fccc07514 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -357,6 +357,9 @@ Throughout the project, every function at every stage either **does exactly what ### Functional - [sceAudioInHqOpen, sceAudioInAsyncOpen](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) open the same port as `sceAudioInOpen`, so `sceAudioInInput`, `sceAudioInGetSilentState` and `sceAudioInClose` accept their handles. The signatures agree across [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/fc5d2cc219c43c8c8f354153e7b3d0c04a5d506e/src/core/libraries/audio/audioin.cpp), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/60977b29103452b9681ae10c7a9011d44965bf16/src/SharpProspero/Interop/Audio/AudioIn.cs) and [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/c86c8b4d9333a766cc8c4495f89f893bf35e6199/src/libs/audio.cpp). HqOpen is limited to 128 samples at 48 kHz; AsyncOpen rejects sizes above 384 and throws for currently unverified inputs. Host capture pacing and console behavior remain unverified. + +- [sem_timedwait](../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. + - [getc_unlocked, putc_unlocked](../../core/libs/prx/libc/src/StandardStreams.cpp) (libc) - byte I/O uses the host unlocked operations; guest EOF/error flags and macro counters follow `FileStream`. Linux status queries use `feof_unlocked`/`ferror_unlocked` because [glibc `feof`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/feof.c) and [`ferror`](https://github.com/bminor/glibc/blob/d2097651cc57834dbfcaa102ddfacae0d86cfb66/libio/ferror.c) can acquire the stream lock. Windows uses [the CRT no-lock byte operations](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fgetc-nolock-fgetwc-nolock) and `feof`/`ferror`, verified under a held stream lock on UCRT. Console behavior and other host CRTs remain unverified. - [ftrylockfile](../../core/libs/prx/libc/src/RuntimeSupport.cpp) (libc) - tries the host CRT stream lock that `flockfile`, `funlockfile` and each locking host stdio call on the stream take, so it returns -1 while another thread holds the stream through `flockfile` or is inside such a call, and 0 when it acquires the lock or already owns it (recursively, released by as many `funlockfile` calls), as FreeBSD 11 [`_ftrylockfile`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdio/_flock_stub.c#L89-L104) does. A libc export holds the lock only for each host call it makes, not across several host calls or while it updates the guest `FILE` prefix. Linux uses glibc `ftrylockfile`. UCRT has no try variant of `_lock_file`, so Windows calls `TryEnterCriticalSection` on the `CRITICAL_SECTION` that `_lock_file` enters, found through the private UCRT stream layout (`__crt_stdio_stream_data` in `ucrt/inc/corecrt_internal_stdio.h`, unchanged from Windows SDK 10.0.19041 to 10.0.26100); another CRT or a change of that layout breaks it. A null or closed stream throws where FreeBSD dereferences it