From ce32d855ba3817332ab44e454f95f6257836943d Mon Sep 17 00:00:00 2001 From: JJenkx Date: Thu, 29 Oct 2026 19:00:11 -0400 Subject: [PATCH] fix(libc): ignore non-wide lengths on character scans --- core/libs/prx/libc/src/Formatting.cpp | 42 +++++++++++++++++++++++++++ core/libs/tests/FileStreams.cpp | 40 +++++++++++++++++++++++++ core/libs/tests/Formatting.cpp | 38 ++++++++++++++++++++++++ docs/dev/debt/libc.md | 1 + 4 files changed, 121 insertions(+) diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 0f5176f7fc2c..e0fc7bc3cfe6 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -102,6 +102,7 @@ const char* CutAtQuadScanDirective(const char* format, std::string& head) { #endif std::string HostLength(const std::string& length, char conversion) { + if (std::strchr("sc[", conversion) && length != "l") return {}; #ifdef _WIN32 if (length == "l" && std::strchr("diouxXn", conversion) != nullptr) return "ll"; #else @@ -110,7 +111,46 @@ std::string HostLength(const std::string& length, char conversion) { return length; } +const char* ScanLengthEnd(const char* cursor) { + if (*cursor == 'h' || *cursor == 'l') { + const char first = *cursor++; + if (*cursor == first) ++cursor; + } else if (*cursor != '\0' && std::strchr("jztL", *cursor)) ++cursor; + return cursor; +} + +const char* HostScanFormat(const char* format, std::string& normalized) { + if (format == nullptr) return format; + const char* copied = format; + const char* cursor = format; + while ((cursor = std::strchr(cursor, '%')) != nullptr) { + ++cursor; + if (*cursor == '%') { ++cursor; continue; } + while (*cursor && std::strchr("-+ #0'0123456789.*$", *cursor)) ++cursor; + const char* const length = cursor; + cursor = ScanLengthEnd(cursor); + const bool wide = cursor == length + 1 && *length == 'l'; + if (cursor != length && !wide && *cursor && std::strchr("sc[", *cursor)) { + normalized.append(copied, length); + copied = cursor; + } + if (*cursor == '[') { + ++cursor; + if (*cursor == '^') ++cursor; + if (*cursor == ']') ++cursor; + while (*cursor && *cursor != ']') ++cursor; + } + if (*cursor) ++cursor; + else break; + } + if (copied == format) return format; + normalized.append(copied); + return normalized.c_str(); +} + int HostVsscanf(const char* input, const char* format, std::va_list args) { + std::string normalized; + format = HostScanFormat(format, normalized); #if defined(__GLIBC__) return __isoc99_vsscanf(input, format, args); #else @@ -128,6 +168,8 @@ int HostSscanf(const char* input, const char* format, ...) { #ifndef _WIN32 int HostVfscanf(std::FILE* stream, const char* format, std::va_list args) { + std::string normalized; + format = HostScanFormat(format, normalized); #if defined(__GLIBC__) return __isoc99_vfscanf(stream, format, args); #else diff --git a/core/libs/tests/FileStreams.cpp b/core/libs/tests/FileStreams.cpp index 02eed2f0a344..a1986db18866 100644 --- a/core/libs/tests/FileStreams.cpp +++ b/core/libs/tests/FileStreams.cpp @@ -2,6 +2,8 @@ #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include +#include +#include #include #include #include @@ -129,6 +131,43 @@ static std::string ReadWhole(const std::string& path) { return std::string(buffer.data(), size); } +static int APS5_VABI ScanSafeList(FileStream* stream, const char* format, ...) { + __builtin_sysv_va_list args; + __builtin_sysv_va_start(args, format); + const int result = vfscanf_s_nid_postfix(stream, format, reinterpret_cast(args)); + __builtin_sysv_va_end(args); + return result; +} + +static void CheckCharacterScanLengths() { + using Scanner = int (APS5_VABI*)(FileStream*, const char*, ...); + const Scanner scanners[]{fscanf_nid_postfix, ScanList, fscanf_s_nid_postfix, ScanSafeList}; + for (std::size_t api = 0; api < std::size(scanners); ++api) { +#ifdef _WIN32 + if (api < 2) continue; +#endif + for (const char* length : {"hh", "h", "ll", "j", "z", "t", "L"}) { + for (const char* conversion : {"s", "c", "[a-z]"}) { + auto* native = std::tmpfile(); + Require(native != nullptr && std::fputs("ab", native) >= 0); + std::rewind(native); + FileStream stream(native); + std::array output; + output.fill('Z'); + int consumed = -1; + const std::string format = std::string("%2") + length + conversion + "%n"; + const auto scan = scanners[api]; + const int result = api >= 2 ? scan(&stream, format.c_str(), output.data(), 32u, &consumed) : scan(&stream, format.c_str(), output.data(), &consumed); + Require(result == 1 && consumed == 2, "Stream character scan ignored-length consumption"); + const bool single = conversion[0] == 'c'; + Require(output[0] == 'a' && output[1] == 'b' && (single || output[2] == 0), "Stream character scan ignored length stored wide data"); + Require(std::all_of(output.begin() + (single ? 2 : 3), output.end(), [](char value) { return value == 'Z'; })); + Require(std::fclose(native) == 0); + } + } + } +} + template static void CheckReadAfterWrite(const std::string& path, const char* mode, const char* expected, TRead read) { auto* stream = fopen_nid_postfix(path.c_str(), "wb"); @@ -210,6 +249,7 @@ static void CheckWriteAfterRead(const std::string& path, const char* mode, const } static void CheckStreamWritesAfterReads(const std::string& base) { + CheckCharacterScanLengths(); const std::string path = base + ".written"; constexpr char appended[] = "0123456789XY"; #if defined(__GLIBC__) diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index ed534f18d731..46e9914b452d 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -5,6 +5,8 @@ #include "prx/libc/include/ScanQuadLength.hpp" #include #include +#include +#include #include #include #include @@ -67,6 +69,41 @@ static int APS5_VABI ScanListS(const char* buffer, const char* format, ...) { return result; } +static void CheckCharacterScanLengths() { + const auto check = [](bool condition, const char* message = "Character scan length check failed") { + if (!condition) throw std::runtime_error(message); + }; + using Scanner = int (APS5_VABI*)(const char*, const char*, ...); + const Scanner scanners[]{sscanf_nid_postfix, ScanList, sscanf_s_nid_postfix, ScanListS}; + for (std::size_t api = 0; api < std::size(scanners); ++api) { + const auto scan = scanners[api]; + const bool secure = api >= 2; +#ifdef _WIN32 + if (!secure) continue; +#endif + for (const char* length : {"hh", "h", "ll", "j", "z", "t", "L"}) { + for (const char* conversion : {"s", "c", "[a-z]"}) { + const std::string format = std::string("%2") + length + conversion + "%n"; + std::array output; + output.fill('Z'); + int consumed = -1; + const int result = secure ? scan("ab", format.c_str(), output.data(), 32u, &consumed) : scan("ab", format.c_str(), output.data(), &consumed); + check(result == 1 && consumed == 2, "Character scan ignored-length consumption"); + const bool single = conversion[0] == 'c'; + check(output[0] == 'a' && output[1] == 'b' && (single || output[2] == '\0'), "Character scan ignored length stored wide data"); + check(std::all_of(output.begin() + (single ? 2 : 3), output.end(), [](char value) { return value == 'Z'; }), "Character scan changed its destination guard"); + } + } + for (const auto& example : {std::pair{"%lls", "%5[%lls]%n"}, std::pair{"% ab", "%% %2lls%n"}}) { + std::array output{}; + int consumed = -1; + const int result = secure ? scan(example.first, example.second, output.data(), 32u, &consumed) : scan(example.first, example.second, output.data(), &consumed); + check(result == 1 && consumed == 4); + check(std::strcmp(output.data(), example.first[1] == ' ' ? "ab" : "%lls") == 0, "Character scan normalization changed a literal or scanset"); + } + } +} + static int APS5_VABI PrintList(const char* format, ...) { __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); @@ -78,6 +115,7 @@ static int APS5_VABI PrintList(const char* format, ...) { } static void CheckIgnoredLengths() { + CheckCharacterScanLengths(); char output[96]; Require(snprintf_nid_postfix(output, sizeof(output), "%Ld|%Lx|%Lu|%Lo", -1985229329, 0x89abcdefu, 0x89abcdefu, 0x89abcdefu) == 43 && std::strcmp(output, "-1985229329|89abcdef|2309737967|21152746757") == 0); diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 82a66c7628f4..b1ed721fd4eb 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -107,6 +107,7 @@ - [sscanf_s and vsscanf_s](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) return `EOF` when a reached conversion has a null destination, stopping before further assignments, following C11 K.3.5.3.7 and K.3.5.3.14. Earlier assignments are retained. The constraint handler is not invoked, and console behaviour is unverified. - [memmem and memccpy](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `memmem` searches for needle occurrences in memory buffers and `memccpy` copies buffers until a character match, following FreeBSD and POSIX specifications; behavior is unverified on console. Neither is exported by `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), so a payload process cannot call them there. - [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) round to the nearest float, where the console's `_Stof` in `libSceLibcInternal.sprx` (0x734a0) does not always: about a third of decimal inputs within a relative 2^-51 of a point halfway between two floats, and some hexadecimal inputs with more than 24 significant bits, come out one unit apart in the last place (`1e-40` is `0x1.16c3p-133` there and `0x1.16c2p-133` here). The console's own rounding is not reproduced +- [Short narrow character fields](../../../core/libs/prx/libc/src/Formatting.cpp) (libc) - on glibc-backed scan paths, `%2c` with one available input byte can return `EOF` after storing that byte, while the bundled modules in PPSA03766 and PPSA07923 count one assignment. This is a CPU-only module comparison, not a console run. - [strtof and wcstof](../../../core/libs/prx/libc/src/Strings.cpp) (libc) - `ERANGE` follows the `libc.prx` bundled with PPSA08427 (J03547016) and PPSA03671 (J03848509), both run side by side with this libc on a Linux host: a decimal input whose result is a nonzero subnormal sets it only when inexact, a hexadecimal one only when it lies below the smallest subnormal and rounds up to it, and underflow to zero and overflow set it. Both builds also take a `NAN(...)` payload in `strtof` from the sequence's leading digits (`nan(0x123)` is `0x7fc00123`, `-nan(5)` is `0xffc00005`); the sign is kept here, the payload is not. `libSceLibcInternal`'s `_Stof` differs: no `ERANGE` for hexadecimal input, a positive `nan`, and after a minus sign a failed parse is `-0.0` for a lone sign and `+0.0` before an `inf`/`nan` spelling, the reverse of `libc.prx`. Decimal `strtod` sets `ERANGE` for a nonzero subnormal in J03547016 but not in J03848509 or `libSceLibcInternal`, whose result is kept. - [operator new](../../../core/libs/prx/libc/src/StdioExtras.cpp) (libc) - the `nothrow` forms return null only for `std::bad_alloc`, where the `libc.prx` bundled with PPSA21564 (build `J03093718`) catches every exception there, so a new handler that throws anything else ends the process instead of returning null. The aligned forms throw `std::bad_alloc` for an alignment of zero or one that is not a power of two, where that binary calls `abort`.