diff --git a/core/libs/prx/libc/include/ScanWideField.hpp b/core/libs/prx/libc/include/ScanWideField.hpp index f210ec5d5ebf..27f230e266a6 100644 --- a/core/libs/prx/libc/include/ScanWideField.hpp +++ b/core/libs/prx/libc/include/ScanWideField.hpp @@ -6,6 +6,7 @@ namespace LibcDetail { +std::size_t WideScanCharacterLength(unsigned char lead); std::size_t ReadWideScanCharacter(const char* text, char32_t* point); bool InWideScanset(const std::string& specifier, char32_t point); void RequireNoPositionalWideScan(const char* format); diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 087f3b15b733..a153cabf75f1 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -84,10 +84,6 @@ bool ScansWideInProcess(const char* format) { return true; } -void RejectGuestWideScan(const char* format) { - if (HasGuestWideScan(format)) NotImplemented_nid_no_patch("fscanf 16-bit wide conversion on Linux"); -} - bool IsWideScan(const std::string& length, char conversion) { return conversion == 'S' || conversion == 'C' || (length == "l" && (conversion == 's' || conversion == 'c' || conversion == '[')); } @@ -211,6 +207,12 @@ class StringSource { std::ptrdiff_t Consumed() const { return position - start; } const char* Remaining() const { return position; } +#ifndef _WIN32 + std::size_t PeekWide(char32_t* point) const { + return *position == '\0' ? 0 : LibcDetail::ReadWideScanCharacter(position, point); + } +#endif + int Scan(const char* directive, bool suppress, void* target, int* consumed) const { return suppress ? HostSscanf(position, directive, consumed) : HostSscanf(position, directive, target, consumed); } @@ -239,6 +241,26 @@ class StreamSource { void Advance(std::ptrdiff_t count) { consumed += count; } std::ptrdiff_t Consumed() const { return consumed; } +#ifndef _WIN32 + std::size_t PeekWide(char32_t* point) const { + char bytes[5] = {}; + int next = std::fgetc(file); + if (next == EOF) return 0; + const std::size_t length = std::max(LibcDetail::WideScanCharacterLength(static_cast(next)), 1); + std::size_t size = 0; + while (true) { + bytes[size++] = static_cast(next); + if (size == length || (next = std::fgetc(file)) == EOF) break; + if ((next & 0xC0) != 0x80) { + std::ungetc(next, file); + break; + } + } + for (std::size_t index = size; index != 0; --index) std::ungetc(static_cast(bytes[index - 1]), file); + return LibcDetail::ReadWideScanCharacter(bytes, point); + } +#endif + int Scan(const char* directive, bool suppress, void* target, int* count) const { return suppress ? HostFscanf(file, directive, count) : HostFscanf(file, directive, target, count); } @@ -301,14 +323,12 @@ int ScanSource(TSource source, const char* format, bool secure, NextPointer next continue; } #ifndef _WIN32 - if constexpr (TSource::Contiguous) if (IsWideScan(length, conversion)) { - const char* const input = source.Remaining(); + if (IsWideScan(length, conversion)) { const char kind = conversion == 'S' ? 's' : conversion == 'C' ? 'c' : conversion; - const char* field = input; if (kind == 's') { - while (std::isspace(static_cast(*field))) ++field; + while (std::isspace(source.Peek())) source.Skip(); } - if (*field == '\0') return finish(converted ? assigned : EOF); + if (source.Peek() == EOF) return finish(converted ? assigned : EOF); auto* target = suppress ? nullptr : static_cast(nextPointer()); unsigned long limit = ULONG_MAX; if (secure && !suppress) { @@ -327,18 +347,17 @@ int ScanSource(TSource source, const char* format, bool secure, NextPointer next if (kind == '[') return LibcDetail::InWideScanset(specifier, point); return true; }; - while (text.size() < taken && *field != '\0') { + while (text.size() < taken) { char32_t point = 0; - const std::size_t size = LibcDetail::ReadWideScanCharacter(field, &point); - if (!accepts(point)) break; + const std::size_t size = source.PeekWide(&point); + if (size == 0 || !accepts(point)) break; text.push_back(static_cast(point)); - field += size; + for (std::size_t index = 0; index < size; ++index) source.Skip(); } if (text.empty()) return finish(assigned); - if (target != nullptr && kind != 'c' && wanted > limit && text.size() == limit && *field != '\0') { + if (target != nullptr && kind != 'c' && wanted > limit && text.size() == limit) { char32_t point = 0; - LibcDetail::ReadWideScanCharacter(field, &point); - if (accepts(point)) { + if (source.PeekWide(&point) != 0 && accepts(point)) { target[0] = u'\0'; return finish(assigned); } @@ -348,7 +367,6 @@ int ScanSource(TSource source, const char* format, bool secure, NextPointer next if (kind != 'c') target[text.size()] = u'\0'; ++assigned; } - source.Advance(field - input); converted = true; continue; } @@ -446,7 +464,7 @@ int ScanGuestStream(FileStream* stream, const char* format, NextPointer nextPoin auto* native = GetNativeStream(stream); if (!stream->SwitchToReading()) return EOF; #ifndef _WIN32 - RejectGuestWideScan(format); + if (HasGuestWideScan(format)) LibcDetail::RequireNoPositionalWideScan(format); #endif const int result = ScanSource(StreamSource(native), format, true, nextPointer, nextCapacity); stream->SyncStatus(); @@ -545,10 +563,10 @@ int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); __builtin_sysv_va_end(args); #else - RejectGuestWideScan(format); std::va_list args; va_start(args, format); - const int result = HostVfscanf(native, format, args); + const int result = ScansWideInProcess(format) ? ScanSource(StreamSource(native), format, false, [&] { return va_arg(args, void*); }, [] { return 0u; }) + : HostVfscanf(native, format, args); va_end(args); #endif stream->SyncStatus(); @@ -874,11 +892,16 @@ int APS5_VABI vfscanf_nid_postfix(FileStream* stream, const char* format, VaList #ifdef _WIN32 const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); #else - RejectGuestWideScan(format); - std::va_list copy; - va_copy(copy, *reinterpret_cast(args)); - const int result = HostVfscanf(native, format, copy); - va_end(copy); + int result = 0; + if (ScansWideInProcess(format)) { + LibcDetail::FormatArguments arguments(args); + result = ScanSource(StreamSource(native), format, false, [&] { return arguments.Next(); }, [] { return 0u; }); + } else { + std::va_list copy; + va_copy(copy, *reinterpret_cast(args)); + result = HostVfscanf(native, format, copy); + va_end(copy); + } #endif stream->SyncStatus(); stream->KeepDirectoryReadError(result == -1); diff --git a/core/libs/prx/libc/src/ScanWideField.cpp b/core/libs/prx/libc/src/ScanWideField.cpp index 5d5a35559de0..616af9af1988 100644 --- a/core/libs/prx/libc/src/ScanWideField.cpp +++ b/core/libs/prx/libc/src/ScanWideField.cpp @@ -11,12 +11,9 @@ std::size_t DecodeUtf8(const char* text, char32_t* point) { *point = lead; return 1; } - std::size_t size = 0; - char32_t value = 0; - if ((lead & 0xE0) == 0xC0) { size = 2; value = lead & 0x1F; } - else if ((lead & 0xF0) == 0xE0) { size = 3; value = lead & 0x0F; } - else if ((lead & 0xF8) == 0xF0) { size = 4; value = lead & 0x07; } - else return 0; + const std::size_t size = LibcDetail::WideScanCharacterLength(lead); + if (size == 0) return 0; + char32_t value = lead & (0x7F >> size); for (std::size_t index = 1; index < size; ++index) { const auto next = static_cast(text[index]); if ((next & 0xC0) != 0x80) return 0; @@ -32,6 +29,14 @@ std::size_t DecodeUtf8(const char* text, char32_t* point) { namespace LibcDetail { +std::size_t WideScanCharacterLength(unsigned char lead) { + if (lead < 0x80) return 1; + if ((lead & 0xE0) == 0xC0) return 2; + if ((lead & 0xF0) == 0xE0) return 3; + if ((lead & 0xF8) == 0xF0) return 4; + return 0; +} + std::size_t ReadWideScanCharacter(const char* text, char32_t* point) { const std::size_t size = DecodeUtf8(text, point); if (size == 0) NotImplemented_nid_no_patch("scanf 16-bit wide conversion of input that is not UTF-8"); diff --git a/core/libs/tests/GuestWideStreamScan.cpp b/core/libs/tests/GuestWideStreamScan.cpp index 1f4f1971d09c..73bcaabfb4c3 100644 --- a/core/libs/tests/GuestWideStreamScan.cpp +++ b/core/libs/tests/GuestWideStreamScan.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include extern "C" { @@ -55,6 +56,29 @@ static int APS5_VABI ScanInputSized(const char16_t* format, ...) { return result; } +#ifndef _WIN32 +static int APS5_VABI ScanNarrow(FileStream* stream, const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfscanf_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanNarrowSized(FileStream* stream, const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vfscanf_s_nid_postfix(stream, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + +static int APS5_VABI ScanNarrowInput(const char* format, ...) { + APS5_VA_BEGIN(format); + const int result = vscanf_nid_postfix(format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} +#endif + static FileStream* Open(const char* text) { auto* stream = new FileStream(TemporaryFile::Open()); Require(std::fputs(text, stream->GetHandle()) >= 0, "seed the stream"); @@ -67,6 +91,61 @@ static void Close(FileStream* stream) { delete stream; } +#ifndef _WIN32 +static void CheckNarrowFormatWideFields() { + std::int32_t value = 0; + std::int64_t count = -1; + char16_t word[8]{}; + char16_t letter = u'!'; + char narrow[8]{}; + + FileStream* stream = Open(" \xc5\xbelu\xc5\xa5 k\xc5\xaf\xc5\x88 7"); + Require(fscanf_nid_postfix(stream, "%ls %lc", word, &letter) == 2 && std::u16string(word) == u"\u017elu\u0165" && letter == u'k', "fscanf %ls and %lc decode UTF-8"); + Require(ScanNarrow(stream, "%S%ln %d", word, &count, &value) == 2 && std::u16string(word) == u"\u016f\u0148" && count == 4 && value == 7, "vfscanf %S with %ln counting bytes"); + Require(fgetc_nid_postfix(stream) == EOF, "the whole input is consumed"); + Close(stream); + + stream = Open("12 ab cd"); + Require(fscanf_nid_postfix(stream, "%d %ls %7s", &value, word, narrow) == 3 && value == 12 && std::u16string(word) == u"ab" && std::strcmp(narrow, "cd") == 0, "wide and narrow fields share the stream"); + Close(stream); + + stream = Open("ab\xc3\xa9t rest"); + Require(fscanf_nid_postfix(stream, "%l[a-c]", word) == 1 && std::u16string(word) == u"ab", "a scanset stops at a non-ASCII character"); + Require(fscanf_nid_postfix(stream, "%l[^ ] %7s", word, narrow) == 2 && std::u16string(word) == u"\u00e9t" && std::strcmp(narrow, "rest") == 0, "the non-ASCII character stays in the stream"); + Close(stream); + + char16_t three[5] = {u'!', u'!', u'!', u'!', u'!'}; + stream = Open("xyzw"); + Require(fscanf_nid_postfix(stream, "%3lc", three) == 1 && three[0] == u'x' && three[1] == u'y' && three[2] == u'z' && three[3] == u'!', "%3lc stores three units without a terminator"); + Require(fgetc_nid_postfix(stream) == 'w', "the character after %3lc stays in the stream"); + Close(stream); + + stream = Open("xyz"); + Require(fscanf_nid_postfix(stream, "%l[a-c]", word) == 0, "a scanset matching nothing"); + Require(fgetc_nid_postfix(stream) == 'x', "the rejected character stays in the stream"); + Close(stream); + + stream = Open(" "); + Require(fscanf_nid_postfix(stream, "%ls", word) == EOF, "only white space before %ls"); + Close(stream); + + stream = Open("hello world"); + char16_t fits[6]{}; + char16_t small[4] = {u'!', u'!', u'!', u'!'}; + Require(fscanf_s_nid_postfix(stream, "%ls", fits, 6u) == 1 && std::u16string(fits) == u"hello", "fscanf_s %ls that fits"); + Require(ScanNarrowSized(stream, "%ls", small, 4u) == 0 && small[0] == 0, "vfscanf_s rejects a too small buffer"); + Close(stream); + + FileStream* const standardInput = __stdinp_nid_postfix; + stream = Open("ab c"); + __stdinp_nid_postfix = stream; + Require(scanf_nid_postfix("%ls", word) == 1 && std::u16string(word) == u"ab", "scanf %ls reads the guest standard input"); + Require(ScanNarrowInput(" %C", &letter) == 1 && letter == u'c', "vscanf %C reads the guest standard input"); + __stdinp_nid_postfix = standardInput; + Close(stream); +} +#endif + static void CheckConsumption() { std::int32_t value = -1; std::int64_t count = -1; @@ -166,5 +245,8 @@ int main() { CheckConsumption(); CheckWideText(); CheckStandardInput(); +#ifndef _WIN32 + CheckNarrowFormatWideFields(); +#endif return 0; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 2138f7822314..8c21d266ea98 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -215,7 +215,7 @@ - [std::__throw_bad_variant_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) throws `std::bad_variant_access` with the `what()` string `bad_variant_access`, the one [LLVM libc++](https://github.com/llvm/llvm-project/blob/main/libcxx/src/variant.cpp) returns, since the export carries libc++'s `__throw_bad_variant_access` name; the console's string is unverified. Onimusha: Way of the Sword (PPSA27836 01.000.001) imports it from `libc`, and its bundled `libc.prx` does not export it. - [std::bad_optional_access and std::_Throw_bad_optional_access](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) provide the exception type, constructor, destructor, and `what()` exports alongside `_Throw_bad_optional_access`, returning the `what()` string `bad_optional_access` matching Dinkumware and libc++ conventions; the console libc.prx string is unverified. PPSA26578 (issue #7624) and PPSA34015 import `_ZSt26_Throw_bad_optional_accessv` (`KwzEip7m6TM`) from `libc`. - [std::_Throw(const std::bad_array_new_length&)](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc, NID `1cKlQv7Kvmg`) is a 52-byte function at `0xcb2d0` in the `libc.prx` bundled with PPSA02530: it ignores its argument, allocates the 8-byte exception object with the static `_ZTVSt20bad_array_new_length`, and throws it. PPSA28416 imports it from `libc`. -- [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, decodes the input of the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) on Linux as UTF-8, mirroring the narrow printf family, where FreeBSD uses the locale's `mbrtowc` (one byte per character in the C locale); the console's locale encoding is unverified. Input that is not UTF-8, a character outside the BMP, a scanset member outside ASCII, positional arguments next to a wide conversion and wide conversions in `fscanf` throw. Each character is stored as one 16-bit unit, widths and `_s` sizes count characters, and a `%lc` field cut short by the end of the input keeps what it read, as FreeBSD's `convert_wchar` does. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits +- [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc), the `_s` forms included, decodes the input of the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) on Linux as UTF-8, mirroring the narrow printf family, where FreeBSD uses the locale's `mbrtowc` (one byte per character in the C locale); the console's locale encoding is unverified. Input that is not UTF-8, a character outside the BMP, a scanset member outside ASCII and positional arguments next to a wide conversion throw. On a stream a field stops before the first character it does not take, which stays in the stream. Each character is stored as one 16-bit unit, widths and `_s` sizes count characters, and a `%lc` field cut short by the end of the input keeps what it read, as FreeBSD's `convert_wchar` does. Windows hands `%ls`, `%lc` and `%l[` to the host, whose `wchar_t` is 16 bits - [The scanf family](../../../core/libs/prx/libc/include/WindowsScanning.hpp) (libc) on Windows returns 0, as C17 and FreeBSD do, when input fails after only suppressed conversions have completed (`sscanf("12", "%*d%d", &value)`). Linux hands the format to glibc, which returns `EOF` there. On Windows the host stops a `%x`, `%X`, `%i` or `%p` after the `x` of a `0x` that no hex digit follows (or that the width cuts off), and a float conversion (`%e`, `%f`, `%g`, `%a`) after the `e` of an exponent with no digits (`1e`), where C17 and FreeBSD stop before the `x` or the `e` - [The scanf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - a `q` length is not measured on a console for `vsscanf`, `fscanf`, `vfscanf`, `scanf`, `vscanf` or the `_s` forms. On Linux and macOS the first four read it as `ll`, as the host libc and FreeBSD 11.4's source do; on Windows they end the scan at it. The `_s` forms take `q` as the conversion and pass `%q` to the host libc. `sscanf` ends the scan at the `q` directive with nothing stored on every host, as it does on a PS5 (firmware 12.70) for `%qd`, `%qS` and `%qC`, and `vsscanf` follows it, as it already did on Windows. - [Guest path resolution](../../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied. Explicit native Windows drive and UNC paths bypass guest parent-component validation.. A relinked module's `.guest.prx` is matched the same way when the original module is not beside it: Borderlands 4 (PPSA01494) loads `/app0/prx/libRenoirCore.PS5.prx`, which it ships as `librenoircore.ps5.prx`. A relinked module request that misses both the exact and the case-insensitive `.guest.prx` takes the sibling whose stem (the name without its last extension, ASCII case ignored) equals the requested stem and that ends with `.guest.prx`, shortest name first: Unity IL2CPP titles load `Media/Modules/.prx` and `Media/Plugins/.prx`, and the relinker names the converted module after the ELF it was built from, `.elf.guest.prx`. The console's module namespace is assumed to ignore case and the extension; several stem matches pick the shortest name instead of throwing, which is not measured on a console @@ -258,7 +258,7 @@ - [printf length modifiers](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) - the `_s` forms (`snprintf_s`, `vswprintf_s` and the others) still throw for a length modifier that does not apply to its conversion, unmeasured on the console. The plain forms ignore it as the `libc.prx` of PPSA07923 and PPSA13934 does: `L` on an integer conversion or `%n`, any length but `l` on `%c` and `%s`, any but `L` on a floating conversion, and any on `%p`. - [child process registry](../../../core/libs/prx/libc/src/Process.cpp) (libc) - `RegisterChildProcess_nid_no_patch` and `WaitChildProcess_nid_no_patch` track the native handles of children created by `posix_spawn` ([libSceLibcInternal](../../../core/libs/prx/libSceLibcInternal/Spawn.cpp)) so libkernel can reap them on Windows, where the host has no `waitpid`. The registry lives in libc because libSceLibcInternal imports libc but not libkernel. `RegisterChildProcess_nid_no_patch` stores the process ID and handle; only reaped handles are closed, and a process that exits without waiting leaks them. Registration without a live child is not validated, and a duplicate process ID is accepted. The decoder maps recognized structured-exception codes to guest signals, keeping the guest `exit` byte layout for anything else. These exports are internal glue for libkernel, not console ABI: their behavior is unverified against any PS5 library and a title importing their NIDs directly is not expected. - [fprintf_s, vfprintf_s, vprintf_s, fwprintf_s, wprintf_s, vfwprintf_s and vwprintf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) follow C11 K.3.5.3.1, K.3.5.3.10 and K.3.9.1.1 with the constraints of `snprintf_s` and `swprintf_s`: a null stream, format or argument list, any `%n`, and a null `%s`, `%ls` or `%S` argument return -1 and write nothing, because the output is formatted completely before it is written. A violation only returns: the constraint handler is not invoked, `errno` is untouched, and the value is the one the narrow `snprintf_s` returns on this branch, not a result measured for a stream function on a PS5. A closed stream sets `EBADF` (9) as `fprintf` does. `vprintf_s` and `vwprintf_s` write to the guest standard output stream, and the wide forms count characters while the narrow ones count bytes, as their unchecked siblings do -- [scanf_s, fscanf_s, vscanf_s and vfscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) run the scanner of `sscanf_s` over the stream, taking the size of `%s`, `%c` and `%[` as `unsigned int` after the pointer, so everything listed for `sscanf_s` applies, including the count of items converted before an undersized destination, whose first element is set to zero. Each directive is read from the stream with the host `fscanf`, with one character of look-ahead pushed back, so the stream stops after the last character a conversion used. A null stream, null format or null destination returns `EOF`, a closed stream also sets `EBADF`, and the constraint handler is not invoked. The 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) throw on Linux, as in `fscanf`. None of the stream forms were measured on a PS5, and the size type follows `sscanf_s` rather than `rsize_t` +- [scanf_s, fscanf_s, vscanf_s and vfscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) run the scanner of `sscanf_s` over the stream, taking the size of `%s`, `%c` and `%[` as `unsigned int` after the pointer, so everything listed for `sscanf_s` applies, including the count of items converted before an undersized destination, whose first element is set to zero. Each directive is read from the stream with the host `fscanf`, with one character of look-ahead pushed back, so the stream stops after the last character a conversion used. A null stream, null format or null destination returns `EOF`, a closed stream also sets `EBADF`, and the constraint handler is not invoked. On Linux the 16-bit wide conversions (`%ls`, `%lc`, `%l[`, `%S`, `%C`) are read by the same scanner as in `fscanf`. None of the stream forms were measured on a PS5, and the size type follows `sscanf_s` rather than `rsize_t` - [freopen_s](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) follows C11 K.3.5.2.2 through `freopen`: a null result, mode or stream returns `EINVAL` (22) with `*result` set to null when it exists, and calls the constraint handler with `EINVAL`, a null pointer and the titles' `fopen_s` text: "fopen_s: bad FILE ** argument" for a null result, "fopen_s: bad argument" for a null mode or stream. A null name is passed to `freopen`, which reports mode-only reopening as unsupported (`ENOTSUP`, 45) with `*result` null and no handler call. A successful reopen stores the stream in `*result` and returns 0, and a failed one stores null and returns the errno `freopen` left, closing the stream as `freopen` does. The console's return values are unverified - [_Fltrounds](../../../core/libs/prx/libc/src/Math.cpp) (libc) console execution and unmasked pending exception behavior remain unverified. The x87 rounding query matches PPSA03766 and PPSA07923 bundled libc.prx run on a Linux host. The shared SonyFltrounds helper used by libSceAmpr still follows the host CRT and is not covered by these measurements. - [cbrt](../../../core/libs/prx/libc/src/Math.cpp) (libc) does not compare floating-point exceptions, and its double inputs are checked by sampling, not exhaustively. It is FreeBSD msun's [`s_cbrt.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_cbrt.c), which sets no `errno`, as the titles' `libc.prx` does not. It gives the same bits as the `cbrt` bundled with PPSA07923 and PPSA05916 on 320 million random and 10,305 structured arguments on a Windows host; the FMA-contracted build bundled with PPSA13934 differs on 32 of the random ones.