From 228532e9a8e02efb634201644c53890072df8dbc Mon Sep 17 00:00:00 2001 From: Romans Pokrovskis Date: Sun, 11 Oct 2026 09:17:24 +0000 Subject: [PATCH] fix(libc): keep unlocked stream status queries lock-free Use unlocked POSIX EOF/error queries after unlocked byte operations so status synchronization cannot reacquire the stream lock. Keep the verified UCRT status calls on Windows, and cover byte, status, error, and held-lock behavior. Co-authored-by: MarcZX100 --- core/libs/prx/libc/include/FileStream.hpp | 14 ++++ core/libs/prx/libc/src/StandardStreams.cpp | 4 +- core/libs/tests/GuestStandardStreams.cpp | 82 ++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 2 + 4 files changed, 100 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/include/FileStream.hpp b/core/libs/prx/libc/include/FileStream.hpp index d97cdf3b3..7a1b467be 100644 --- a/core/libs/prx/libc/include/FileStream.hpp +++ b/core/libs/prx/libc/include/FileStream.hpp @@ -97,6 +97,20 @@ public: (std::feof(GetHandle()) ? 0x20 : 0) | (std::ferror(GetHandle()) || encodingError ? 0x40 : 0)); } + void SyncUnlockedStatus() { + _guest.readRemaining = 0; + _guest.writeRemaining = 0; +#ifdef _WIN32 + const bool endOfFile = std::feof(GetHandle()) != 0; + const bool error = std::ferror(GetHandle()) != 0; +#else + const bool endOfFile = ::feof_unlocked(GetHandle()) != 0; + const bool error = ::ferror_unlocked(GetHandle()) != 0; +#endif + _guest.flags = static_cast((_guest.flags & ~0x60) | + (endOfFile ? 0x20 : 0) | (error || encodingError ? 0x40 : 0)); + } + void SetEncodingError() { encodingError = true; SyncStatus(); diff --git a/core/libs/prx/libc/src/StandardStreams.cpp b/core/libs/prx/libc/src/StandardStreams.cpp index 5fe7d7615..1d51cdb37 100644 --- a/core/libs/prx/libc/src/StandardStreams.cpp +++ b/core/libs/prx/libc/src/StandardStreams.cpp @@ -41,7 +41,7 @@ int APS5_VABI getc_unlocked_nid_postfix(FileStream* stream) { #else const int result = ::getc_unlocked(GetNativeStream(stream)); #endif - stream->SyncStatus(); + stream->SyncUnlockedStatus(); return result; } int APS5_VABI __srget_nid_postfix(FileStream* stream) { return fgetc_nid_postfix(stream); } @@ -58,7 +58,7 @@ int APS5_VABI putc_unlocked_nid_postfix(int value, FileStream* stream) { #else const int result = ::putc_unlocked(value, GetNativeStream(stream)); #endif - stream->SyncStatus(); + stream->SyncUnlockedStatus(); return result; } int APS5_VABI __swbuf_nid_postfix(int value, FileStream* stream) { return fputc_nid_postfix(value, stream); } diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index 168004c82..98ec24e73 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -97,6 +97,85 @@ static bool CheckFileBytes(const std::string& filename, const std::string& expec return false; } +static void CheckUnlockedStreams() { + FileStream stream(std::tmpfile()); + auto& guest = stream.GuestState(); + const auto originalFlags = guest.flags; + for (const int value : {0, 0x7f, 0x80, 0xff, 0x1ab, -2}) { + guest.readRemaining = 13; + guest.writeRemaining = 19; + Require(putc_unlocked_nid_postfix(value, &stream) == static_cast(value)); + Require(guest.readRemaining == 0 && guest.writeRemaining == 0 && guest.flags == originalFlags); + } + std::rewind(stream.GetHandle()); + for (const int value : {0, 0x7f, 0x80, 0xff, 0x1ab, -2}) { + guest.readRemaining = 13; + guest.writeRemaining = 19; + Require(getc_unlocked_nid_postfix(&stream) == static_cast(value)); + Require(guest.readRemaining == 0 && guest.writeRemaining == 0 && guest.flags == originalFlags); + } + Require(getc_unlocked_nid_postfix(&stream) == EOF && (guest.flags & 0x20)); + std::clearerr(stream.GetHandle()); + Require(std::ungetc('Q', stream.GetHandle()) == 'Q'); + Require(getc_unlocked_nid_postfix(&stream) == 'Q' && !(guest.flags & 0x20)); + std::rewind(stream.GetHandle()); + stream.SetEncodingError(); + Require(putc_unlocked_nid_postfix('R', &stream) == 'R' && (guest.flags & 0x40)); + clearerr_nid_postfix(&stream); + Require(!(guest.flags & 0x40)); + stream.Close(); + + const auto filename = "anyps5-unlocked-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + { std::ofstream seed(filename, std::ios::binary); seed.put('A'); Require(seed.good()); } + FileStream readOnly(std::fopen(filename.c_str(), "rb")); + Require(putc_unlocked_nid_postfix('X', &readOnly) == EOF && (readOnly.GuestState().flags & 0x40)); + readOnly.Close(); + Require(CheckFileBytes(filename, "A", "rb", false)); + Require(std::filesystem::remove(filename)); +} + +static void CheckUnlockedStreamLock(bool write) { + FileStream stream(std::tmpfile()); + Require(std::fputc('A', stream.GetHandle()) == 'A'); + std::rewind(stream.GetHandle()); + std::atomic held = false, release = false, entered = false, done = false; + int result = EOF; + std::thread owner([&] { + flockfile_nid_postfix(&stream); + held = true; + while (!release) std::this_thread::yield(); + funlockfile_nid_postfix(&stream); + }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (!held && std::chrono::steady_clock::now() < deadline) std::this_thread::yield(); + if (!held) { + release = true; + owner.join(); + Require(false); + } + std::thread caller([&] { + entered = true; + result = write ? putc_unlocked_nid_postfix('B', &stream) : getc_unlocked_nid_postfix(&stream); + done = true; + }); + const auto enteredDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (!entered && std::chrono::steady_clock::now() < enteredDeadline) std::this_thread::yield(); + if (!entered) { + release = true; + owner.join(); + caller.join(); + Require(false); + } + const auto completionDeadline = std::chrono::steady_clock::now() + std::chrono::seconds(1); + while (!done && std::chrono::steady_clock::now() < completionDeadline) std::this_thread::yield(); + const bool completedWhileHeld = done; + release = true; + owner.join(); + caller.join(); + Require(completedWhileHeld && result == (write ? 'B' : 'A')); + stream.Close(); +} + static void CheckRewind() { FileStream stream(std::tmpfile()); Require(fwrite_nid_postfix("ab", 1, 2, &stream) == 2); @@ -369,5 +448,8 @@ int main() { Require(fgetc_nid_postfix(&locked) == 'A' && fgetc_nid_postfix(&locked) == 'B'); locked.Close(); CheckRewind(); + CheckUnlockedStreams(); + CheckUnlockedStreamLock(false); + CheckUnlockedStreamLock(true); return CheckBinaryModes() ? 0 : 1; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f516d608d..4941e61b0 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -337,6 +337,8 @@ Throughout the project, every function at every stage either **does exactly what ### Functional +- [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. + - [libSceOpusDec](../../core/libs/prx/libSceOpusDec/Export.cpp) uses FFmpeg for 48000 Hz mono/stereo signed-16 decoding. The six signatures, 640-byte opaque state allocation and byte-count return follow [public PS5 decoder research](https://github.com/blackbearreloaded/ps5-hardware-audio-decoding/blob/26be5a12f92b2cdc67f8a9e082fd69ae0e9a35a2/docs/DECODING.md#direct-opus-abi) and [ProsperoRadio](https://github.com/blackbearreloaded/ProsperoRadio/blob/2c4029c3a8f43e9e1dfef98374d94d99b14d745d/src/opus_decoder.cpp). Console PCM equivalence and error codes are unverified. Other sample rates, packet loss concealment, invalid inputs and termination with live decoders throw. Context identifiers and host decoder state are private registries; the firmware's opaque state layout and error recovery are unknown. - [mkstemp and isatty](../../core/libs/prx/libScePosixForWebKit/Filesystem.cpp) (libScePosixForWebKit) follow FreeBSD's [temporary-file API](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/stdio/mktemp.c) and [terminal check](https://github.com/freebsd/freebsd-src/blob/stable/9/lib/libc/gen/isatty.c), without console verification. `mkstemp` replaces all trailing `X` characters, accepts a fixed name, and creates files through the guest path resolver with exclusive read/write access and mode 0600. Windows file permissions follow the existing kernel file implementation. `isatty` tests Windows console handles rather than all character devices; files, pipes and guest sockets return `ENOTTY`, closed descriptors return `EBADF`. - [Dinkumware thread exports](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) follow the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`cthread.c`, `xtime.c`) and its `atomic.c`, [microsoft/STL `cthread.cpp`](https://github.com/microsoft/STL/blob/main/stl/src/cthread.cpp) and C11 Annex K, without console verification. `_Thrd_t` is taken to be the guest `pthread_t`, as the `std::_Pad::_Launch(pthread**)` export suggests: `_Thrd_join` returns `_Thrd_error` (4) when `pthread_join` fails and stores the thread's return value truncated to `int`. `_Thrd_sleep` reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. `_Thrd_join` (32-bit store of the result, 0 or 4) and the spin lock (byte exchange loop, no yield) match that disassembly. The shared_ptr spin lock is one process-wide flag that yields while it spins. `wcstombs_s` is `wcsrtombs_s` with a fresh conversion state, so `*retval` excludes the terminating null as in Annex K, not as in the Microsoft CRT, and `_TRUNCATE` has no meaning