From 20e06ec7beb15674a2d550a164c09c85c432fa79 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 28 Oct 2026 18:09:10 -0400 Subject: [PATCH] fix(libc): report wcstombs_s and wcsrtombs_s violations as titles' libc.prx does The libc.prx that titles ship (PPSA07923, PPSA05916) treats a size or limit above SIZE_MAX >> 3 as a bad argument and calls the constraint handler with EINVAL or ERANGE and a null pointer for each violation: "wcsrtombs_s: bad argument" or "wcstombs_s: bad argument", and for a buffer that is too small "wcstombs_s: buffer too small" from wcsrtombs_s and "wcstombs_s: buffer overflow" from wcstombs_s. main only returned the error and accepted sizes and limits up to 2^63. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Multibyte.cpp | 24 ++++++++++++--------- core/libs/tests/GuestWcsrtombsS.cpp | 32 ++++++++++++++++++++++++++++ docs/dev/debt/libc.md | 2 +- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/core/libs/prx/libc/src/Multibyte.cpp b/core/libs/prx/libc/src/Multibyte.cpp index 330701624b..b0b934016b 100644 --- a/core/libs/prx/libc/src/Multibyte.cpp +++ b/core/libs/prx/libc/src/Multibyte.cpp @@ -290,21 +290,25 @@ std::size_t APS5_VABI wcsrtombs_nid_postfix(char* destination, const std::uint16 static int WideToLocaleChecked( std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t** source, std::size_t limit, - void* state, bool stopAtLimit + void* state, bool stopAtLimit, const char* badArgument, const char* bufferTooSmall ) { constexpr int GuestEinval = 22; constexpr int GuestErange = 34; constexpr int GuestEilseq = 86; constexpr auto Failed = static_cast(-1); - if (!result || !source || !state || !*source || (destination && (static_cast(capacity) <= 0 - || static_cast(limit) < 0))) { - if (destination && static_cast(capacity) > 0) destination[0] = '\0'; + constexpr std::size_t RsizeMax = SIZE_MAX >> 3; + const auto violation = [](const char* message, int error) { + SonyInvokeConstraintHandler(message, nullptr, error); + return error; + }; + if (!result || !source || !state || !*source || (destination && (capacity == 0 || capacity > RsizeMax || limit > RsizeMax))) { + if (destination && capacity != 0 && capacity <= RsizeMax) destination[0] = '\0'; if (result) *result = Failed; - return GuestEinval; + return violation(badArgument, GuestEinval); } if (!destination && capacity) { *result = Failed; - return GuestEinval; + return violation(badArgument, GuestEinval); } const auto* wide = *source; char bytes[MB_LEN_MAX]; @@ -336,7 +340,7 @@ static int WideToLocaleChecked( *source = wide; destination[0] = '\0'; *result = Failed; - return GuestErange; + return violation(bufferTooSmall, GuestErange); } std::memcpy(destination + written, bytes, length); written += length; @@ -351,7 +355,7 @@ static int WideToLocaleChecked( *source = wide; destination[0] = '\0'; *result = Failed; - return GuestErange; + return violation(bufferTooSmall, GuestErange); } limit -= length; } @@ -365,14 +369,14 @@ int APS5_VABI wcsrtombs_s_nid_postfix( std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t** source, std::size_t limit, void* state ) { - return WideToLocaleChecked(result, destination, capacity, source, limit, state, false); + return WideToLocaleChecked(result, destination, capacity, source, limit, state, false, "wcsrtombs_s: bad argument", "wcstombs_s: buffer too small"); } int APS5_VABI wcstombs_s_nid_postfix( std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t* source, std::size_t limit ) { std::array state{}; - return WideToLocaleChecked(result, destination, capacity, &source, limit, state.data(), limit < capacity); + return WideToLocaleChecked(result, destination, capacity, &source, limit, state.data(), limit < capacity, "wcstombs_s: bad argument", "wcstombs_s: buffer overflow"); } std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const char** source, std::size_t capacity, void* state) { diff --git a/core/libs/tests/GuestWcsrtombsS.cpp b/core/libs/tests/GuestWcsrtombsS.cpp index 2169489807..f6b24004b0 100644 --- a/core/libs/tests/GuestWcsrtombsS.cpp +++ b/core/libs/tests/GuestWcsrtombsS.cpp @@ -3,11 +3,28 @@ #include #include #include +#include static void Require(bool value) { if (!value) std::abort(); } constexpr auto Failed = static_cast(-1); constexpr auto Huge = std::size_t{1} << 63; +constexpr auto Limit = (std::size_t{1} << 61) - 1; +static int violations = 0; +static std::string lastMessage; + +static void APS5_VABI RecordViolation(const char* message, void* pointer, int) { + Require(pointer == nullptr); + ++violations; + lastMessage = message; +} + +static bool Reported(const char* message) { + const bool matched = violations == 1 && lastMessage == message; + violations = 0; + lastMessage.clear(); + return matched; +} static const std::uint16_t text[] = {'a', 'b', 'c', 0}; static const std::uint16_t wide[] = {'a', 0x100, 'b', 0}; @@ -28,12 +45,14 @@ static Run Convert(const std::uint16_t* input, std::size_t capacity, std::size_t } int main() { + set_constraint_handler_s_nid_postfix(reinterpret_cast(&RecordViolation)); auto run = Convert(text, 0, 0, false); Require(run.status == 0 && run.result == 3 && run.source == text); run = Convert(wide, 0, Huge, false); Require(run.status == 0 && run.result == 4 && run.source == wide); run = Convert(text, 5, 1, false); Require(run.status == 22 && run.result == Failed && run.source == text); + Require(Reported("wcsrtombs_s: bad argument")); run = Convert(text, 8, 8); Require(run.status == 0 && run.result == 3 && run.source == nullptr && std::memcmp(run.out, "abc\0x", 5) == 0); @@ -46,6 +65,7 @@ int main() { run = Convert(text, 3, 8); Require(run.status == 34 && run.result == Failed && run.source == text + 3 && std::memcmp(run.out, "\0bcx", 4) == 0); + Require(Reported("wcstombs_s: buffer too small")); run = Convert(text, 3, 3); Require(run.status == 34 && run.result == Failed && run.source == text + 3 && std::memcmp(run.out, "\0bcx", 4) == 0); run = Convert(text, 1, 8); @@ -53,6 +73,15 @@ int main() { run = Convert(wide, 8, 8); Require(run.status == 0 && run.result == 4 && run.source == nullptr && std::memcmp(run.out, "a\xc4\x80" "b\0", 5) == 0); + violations = 0; + run = Convert(text, Limit, 8); + Require(run.status == 0 && run.result == 3 && violations == 0); + run = Convert(text, 8, Limit); + Require(run.status == 0 && run.result == 3 && violations == 0); + run = Convert(text, Limit + 1, 8); + Require(run.status == 22 && run.result == Failed && run.out[0] == 'x' && Reported("wcsrtombs_s: bad argument")); + run = Convert(text, 8, Limit + 1); + Require(run.status == 22 && run.result == Failed && run.source == text && run.out[0] == '\0' && Reported("wcsrtombs_s: bad argument")); run = Convert(text, 0, 8); Require(run.status == 22 && run.result == Failed && run.source == text && run.out[0] == 'x'); @@ -84,8 +113,11 @@ int main() { Require(std::memcmp(narrow, "ab\0x", 4) == 0); Require(wcstombs_s_nid_postfix(&result, nullptr, 0, text, 0) == 0 && result == 3); std::memset(narrow, 'x', sizeof(narrow)); + violations = 0; Require(wcstombs_s_nid_postfix(&result, narrow, 3, text, 3) == 34 && result == Failed && narrow[0] == '\0'); + Require(Reported("wcstombs_s: buffer overflow")); Require(wcstombs_s_nid_postfix(&result, nullptr, 4, text, 4) == 22 && result == Failed); + Require(Reported("wcstombs_s: bad argument")); std::memset(narrow, 'x', sizeof(narrow)); Require(wcstombs_s_nid_postfix(&result, narrow, sizeof(narrow), nullptr, sizeof(narrow)) == 22); Require(result == Failed && narrow[0] == '\0'); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ff23d63ebf..6cff36bc5e 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -174,7 +174,7 @@ - [gethostname](../../../core/libs/prx/libc/src/Hostname.cpp) (libc) reports an empty name without querying the host machine name, as a payload process read on a PS5 (firmware 13.60): 0 with only the terminator for a 256-byte and a 1-byte buffer. Buffer copying, null queries and `ENAMETOOLONG` follow [FreeBSD gethostname](https://github.com/freebsd/freebsd-src/blob/main/lib/libc/gen/gethostname.c); whether a title process or a console with a configured name reads another name, and the integration with `sysctl` and name resolution, are unverified and not implemented. - [sinh](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`e_sinh.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/e_sinh.c) over this file's msun `Expm1` for |x| < 22, where it gives msun's bits (0 of 2,000,000 random arguments in [-20, 20] differ from a locally built msun on a Windows host). From 22 on it calls this file's `exp`, which is still the host's, so those results can differ from msun in the last bit until `exp` follows msun; the last step before the overflow threshold scales that `exp` as msun's `__ldexp_exp` does. On overflow it sets `errno` to `ERANGE`, as the host's `sinh` did and `sinhf` does; msun itself sets none. It was compared with msun built on the host, not with a console: the titles' `libc.prx` and the system `libSceLibcInternal` are unmeasured. - [wctomb_s, wcrtomb_s, mbstowcs_s and mbsrtowcs_s](../../../core/libs/prx/libc/src/BoundsChecking.cpp) (libc) follow the console's `libc.prx`, but convert in the single-byte C locale of `wctomb`, `wcrtomb` and `mbrtowc` (`MB_CUR_MAX` 1): a character above 0xFF is `EILSEQ` in `wctomb_s` and `wcrtomb_s`, and each byte is one wide character in `mbstowcs_s` and `mbsrtowcs_s`. The locale of a title's `libc.prx` was not measured. The `wcrtomb_s` path where a character does not fit `smax` and the `EILSEQ` paths of `mbstowcs_s` and `mbsrtowcs_s` are not reached -- [wcsrtombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached +- [wcsrtombs_s and wcstombs_s](../../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follow the `libc.prx` PPSA07923 and PPSA05916 ship: a size or limit above `SIZE_MAX >> 3` is a bad argument, and each violation calls the constraint handler with a null pointer and the titles' message ("wcsrtombs_s: bad argument" or "wcstombs_s: bad argument"; for a short buffer, "wcstombs_s: buffer too small" from `wcsrtombs_s` and "wcstombs_s: buffer overflow" from `wcstombs_s`). Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached - [c16rtomb, c32rtomb, mbrtoc16 and mbrtoc32](../../../core/libs/prx/libc/src/Multibyte.cpp) (libSceLibcInternal, implemented in libc) - a null state, which the console dereferences, throws. Otherwise they follow the console's `libSceLibcInternal.sprx` (0x7D480 to 0x7D920), read from disassembly: UTF-8 whatever the locale, up to six bytes per sequence, with no surrogate or 0x10FFFF check when encoding. The state holds the value in its first eight bytes and the expected byte count (0x10 for a pending low surrogate, 1 for a stored high one) as 16 bits at 0xA. `mbrtoc16` builds a low surrogate from the value the state last held, `c16rtomb` returns -1 without `errno` for an unpaired high surrogate, and `EILSEQ` is 86. On a PS5 (13.60) the system library's default locale converts UTF-8 in `wcrtomb`, `wctomb`, `mbrtowc` and `mblen`; a title's bundled `libc.prx` does too until `setlocale` installs a table ([disassembly](https://github.com/boykopovar/AnyPS5/pull/8855#issuecomment-6335267758)) - [The narrow printf family](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) follows a payload process on a PS5 (firmware 13.60): each 16-bit unit of a `%ls` or `%lc` argument is encoded alone, so a surrogate pair gives two 3-byte sequences; a NaN keeps its sign and the `+` and space flags, and `0` pads it with spaces; argument positions above 99 work (up to 400 measured; above 65535 throw here); an unknown conversion such as `%m` or `%k` prints `%` and its letter, padded to the width, without taking an argument. The guest's `wcrtomb` and locales are not used. On Linux the formats without those conversions or a floating-point conversion go to glibc, which prints NaN the same way. The `_s` forms throw for an unknown conversion, `%m` included, which was not measured - [asinh](../../../core/libs/prx/libc/src/Math.cpp) (libc) is FreeBSD msun's [`s_asinh.c`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/msun/src/s_asinh.c): below 2^-28 and up to |x| = 2 it runs over this file's msun `log1p` and gives msun's bits (0 of 2,000,000 random arguments in each of those ranges differ from a locally built msun on a Windows host). Above 2 it calls this file's `log`, which is still the host's, so those results can differ from msun in the last bit until `log` follows msun: 38,719 of 2,000,000 random arguments in (2, 2^28] differ with the Windows UCRT, none above 2^28 there (not measured with glibc). It sets no `errno`, as msun's does not, and was compared with msun built on the host, not with a console: the titles' `libc.prx` and the system `libSceLibcInternal` are unmeasured.