From a3186c59ef22bbb1816d968d8e26fcdda7cecd2a Mon Sep 17 00:00:00 2001 From: turkishcuber Date: Thu, 8 Oct 2026 13:57:37 +0300 Subject: [PATCH] fix(libc): format guest wide strings in swprintf as 16-bit units The padded, exact-fit and invalid-conversion swprintf checks come from #1512. Co-Authored-By: Noface07 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01ULF8vCbJFyzxVsW3SfiWC5 --- .../libc/include/WindowsWideFormatting.hpp | 99 ------------------- core/libs/prx/libc/src/Formatting.cpp | 16 --- core/libs/prx/libc/src/FormattingWide.cpp | 7 ++ core/libs/tests/GuestWideFormatting.cpp | 26 +++++ docs/dev/TechnicalDebt.md | 2 +- 5 files changed, 34 insertions(+), 116 deletions(-) delete mode 100644 core/libs/prx/libc/include/WindowsWideFormatting.hpp diff --git a/core/libs/prx/libc/include/WindowsWideFormatting.hpp b/core/libs/prx/libc/include/WindowsWideFormatting.hpp deleted file mode 100644 index 8d4baba34..000000000 --- a/core/libs/prx/libc/include/WindowsWideFormatting.hpp +++ /dev/null @@ -1,99 +0,0 @@ -#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSWIDEFORMATTING_HPP -#define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSWIDEFORMATTING_HPP - -#include "WindowsFormatting.hpp" -#include "General.hpp" -#include -#include -#include - -namespace LibcDetail { - -inline int FormatWideWindows(wchar_t* output, size_t capacity, const wchar_t* format, const void* source) { - if (!capacity) return -1; - if (!output || !format || !source) { errno = 22; return -1; } - static_assert(sizeof(wchar_t) == 2); - FormatArguments args(source); - std::wstring translated; - std::wstring result; - std::vector slots; - std::deque extended; - auto append = [&slots](auto value) { - static_assert(sizeof(value) <= 8); - std::uint64_t bits = 0; - std::memcpy(&bits, &value, sizeof(value)); - slots.push_back(bits); - }; - while (*format) { - const wchar_t value = *format++; - if (value != L'%') { result += value; continue; } - if (*format == L'%') { result += *format++; continue; } - translated = L"%"; - slots.clear(); - extended.clear(); - bool nullCharacter = false; - while (*format && std::wcschr(L"-+ #0", *format)) translated += *format++; - if (*format == L'*') { translated += *format++; append(args.Next()); } - else while (*format >= L'0' && *format <= L'9') translated += *format++; - if (*format == L'.') { - translated += *format++; - if (*format == L'*') { translated += *format++; append(args.Next()); } - else while (*format >= L'0' && *format <= L'9') translated += *format++; - } - std::wstring length; - if (*format && std::wcschr(L"hljztL", *format)) { - length += *format++; - if ((length == L"h" && *format == L'h') || (length == L"l" && *format == L'l')) length += *format++; - } - const wchar_t conversion = *format; - if (!conversion) { errno = 22; return -1; } - if (!std::wcschr(L"diouxXaAeEfFgGcspn", conversion)) { - NotImplemented_nid_no_patch("swprintf format conversion"); - return -1; - } - ++format; - if (std::wcschr(L"diouxXn", conversion) && (length == L"l" || length == L"j" || length == L"z" || length == L"t")) - translated += L"ll"; - else if (conversion == L's' && length.empty()) translated += L"h"; - else if (conversion == L'c' && length.empty()) translated += L"l"; - else translated += length; - translated += conversion; - if (conversion == L'n') { - void* pointer = args.Next(); - if (length == L"hh") *static_cast(pointer) = static_cast(result.size()); - else if (length == L"h") *static_cast(pointer) = static_cast(result.size()); - else if (length.empty()) *static_cast(pointer) = static_cast(result.size()); - else *static_cast(pointer) = static_cast(result.size()); - continue; - } - if (conversion == L's' || conversion == L'p') append(args.Next()); - else if (conversion == L'c') { - const int character = args.Next(); - const int converted = length.empty() ? static_cast(static_cast(character)) : character; - nullCharacter = converted == 0; - append(nullCharacter ? 1 : converted); - } - else if (std::wcschr(L"aAeEfFgG", conversion)) { - if (length == L"L") { - extended.push_back(args.Next()); - append(&extended.back()); - } else append(args.Next()); - } else if (length.empty() || length == L"h" || length == L"hh") append(args.Next()); - else append(args.Next()); - if (result.size() >= capacity) return -1; - std::vector part(capacity - result.size()); - const int count = std::vswprintf(part.data(), part.size(), translated.c_str(), reinterpret_cast(slots.data())); - if (count < 0 || static_cast(count) >= part.size()) return -1; - if (nullCharacter) { - for (int i = 0; i < count; ++i) if (part[i] == 1) part[i] = 0; - } - result.append(part.data(), static_cast(count)); - } - if (result.size() >= capacity || result.size() > INT_MAX) return -1; - std::memcpy(output, result.c_str(), (result.size() + 1) * sizeof(wchar_t)); - return static_cast(result.size()); -} - -} - -#endif diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index f09262076..37a59d7d2 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -19,7 +19,6 @@ #ifdef _WIN32 #include "prx/libc/include/WindowsScanning.hpp" -#include "prx/libc/include/WindowsWideFormatting.hpp" #endif namespace { @@ -157,21 +156,6 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n extern "C" { -int APS5_VABI swprintf_nid_postfix(wchar_t* output, size_t capacity, const wchar_t* format, ...) { -#ifdef _WIN32 - __builtin_sysv_va_list args; - __builtin_sysv_va_start(args, format); - const int result = LibcDetail::FormatWideWindows(output, capacity, format, args); - __builtin_sysv_va_end(args); -#else - std::va_list args; - va_start(args, format); - const int result = std::vswprintf(output, capacity, format, args); - va_end(args); -#endif - return result; -} - int APS5_VABI vasprintf_nid_postfix(char** destination, const char* format, VaList* args) { if (!destination) { errno = 22; return -1; } *destination = nullptr; diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index 7808ba4ba..e3a18f1b6 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -214,6 +214,13 @@ int APS5_VABI vswprintf_nid_postfix(char16_t* buffer, std::size_t size, const ch } } +int APS5_VABI swprintf_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, ...) { + APS5_VA_BEGIN(format); + const int result = vswprintf_nid_postfix(buffer, size, format, reinterpret_cast(args)); + APS5_VA_END(); + return result; +} + int APS5_VABI snwprintf_s_nid_postfix(char16_t* buffer, std::size_t size, const char16_t* format, ...) { constexpr std::size_t RsizeMax = SIZE_MAX >> 1; int result = -1; diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index c758ba10b..facf372a6 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -8,6 +8,7 @@ extern "C" int APS5_VABI vswprintf_nid_postfix(char16_t*, std::size_t, const char16_t*, VaList*); extern "C" int APS5_VABI snwprintf_s_nid_postfix(char16_t*, std::size_t, const char16_t*, ...); +extern "C" int APS5_VABI swprintf_nid_postfix(char16_t*, std::size_t, const char16_t*, ...); static int APS5_VABI Format(char16_t* buffer, std::size_t size, const char16_t* format, ...) { #ifdef _WIN32 @@ -111,9 +112,34 @@ static void CheckCount() { Require(Format(buffer, 16, u"a%n", nullCount) < 0, "vswprintf null %n argument"); } +static void CheckSwprintf() { + char16_t buffer[8]; + for (auto& unit : buffer) unit = u'x'; + Require(swprintf_nid_postfix(buffer, 8, u"%d-%ls", 42, u"ab") == 5, "swprintf length"); + Require(buffer == std::u16string(u"42-ab") && buffer[6] == u'x', "swprintf output"); + Require(swprintf_nid_postfix(buffer, 8, u"中%ls%s", u"文", "\xc3\xa9") == 3, "swprintf non-ASCII length"); + Require(buffer == std::u16string(u"中文é"), "swprintf non-ASCII output"); + Require(swprintf_nid_postfix(buffer, 8, u"%s", "\xf0\x9f\x98\x80") == 2, "swprintf surrogate pair length"); + Require(buffer == std::u16string(u"\U0001f600"), "swprintf surrogate pair output"); + Require(swprintf_nid_postfix(buffer, 8, u"") == 0 && buffer[0] == 0, "swprintf empty format"); + for (auto& unit : buffer) unit = u'x'; + Require(swprintf_nid_postfix(buffer, 3, u"%ls", u"abcd") < 0, "swprintf truncation result"); + Require(buffer == std::u16string(u"ab") && buffer[3] == u'x', "swprintf truncation output"); + Require(swprintf_nid_postfix(buffer, 3, u"abc") < 0, "swprintf terminator does not fit"); + buffer[0] = u'x'; + Require(swprintf_nid_postfix(buffer, 0, u"a") < 0 && buffer[0] == u'x', "swprintf zero size"); + Require(swprintf_nid_postfix(nullptr, 8, u"a") < 0, "swprintf null buffer"); + Require(swprintf_nid_postfix(buffer, 5, u"%ls", u"abcd") == 4 && buffer == std::u16string(u"abcd"), "swprintf exact fit"); + char16_t wide[16]; + Require(swprintf_nid_postfix(wide, 16, u"%5ls|%-3c|%lc", u"wide", 'z', 0x20ac) == 11, "swprintf padded length"); + Require(wide == std::u16string(u" wide|z |€"), "swprintf padded output"); + Require(swprintf_nid_postfix(wide, 16, u"%q") < 0 && wide[0] == 0, "swprintf invalid conversion"); +} + int main() { CheckBounded(); CheckCount(); + CheckSwprintf(); Check(u"%.2s", "\xc3\xa9\xc3\xa8", u"\u00e9\u00e8"); Check(u"%.1s", "\xc3\xa9\xc3\xa8", u"\u00e9"); Check(u"%.0s", "\xc3\xa9", u""); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index b396c1fce..0d7595779 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -264,7 +264,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown -- [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - [`swprintf`](../../core/libs/prx/libc/src/Formatting.cpp) still passes guest strings to the host's `wchar_t` functions. The guest's `wchar_t` is 16 bits, so on Linux, where the host's is 32 bits, it misreads and miswrites guest strings. The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported +- [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported - [SPIRV-Tools optimization](../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. - On macOS the [write watch](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: - a system call that writes into a protected page fails with `EFAULT` instead of faulting. `GuestArena::HostWrite` lifts the protection for the calls it brackets (the file reads); a host call that writes guest memory without it (sockets, `clock_gettime` and the like into a watched page) fails;