From c66f61ff5259e3d05eab0e35c99b948d2b62903b Mon Sep 17 00:00:00 2001 From: Disaster Date: Sun, 25 Oct 2026 23:21:45 +0300 Subject: [PATCH] feat(libc): wire the collate facet vtable The nine-slot _ZTVSt7collateIcE export added by #9979 was still zeroed, so a guest that called through the exported vtable faulted. Fill slots 2 through 8 with the same facet methods that g_collateVtable uses, leave the offset-to-top and typeinfo slots zero, and cover compare, transform, hash, retain and release through the exported object. --- core/libs/prx/libc/src/LocaleSupport.cpp | 12 +++++++++++- core/libs/tests/GuestLocale.cpp | 23 +++++++++++++++++------ docs/dev/debt/libc.md | 2 +- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index a838c65929..7a9cb63322 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -370,7 +370,17 @@ std::uint64_t _ZNSt5ctypeIcE2idE_nid_postfix = 0; std::uint64_t _ZNSt5ctypeIwE2idE_nid_postfix = 0; std::uint64_t _ZNSt7collateIwE2idE_nid_postfix = 0; std::uint64_t _ZNSt7collateIcE2idE_nid_postfix = 0; -std::uintptr_t _ZTVSt7collateIcE_nid_postfix[9] {}; +std::uintptr_t _ZTVSt7collateIcE_nid_postfix[9] { + 0, + 0, + reinterpret_cast(&DestroyCollate), + reinterpret_cast(&DeleteCollate), + reinterpret_cast(&RetainFacet), + reinterpret_cast(&ReleaseFacet), + reinterpret_cast(&CollateCompare), + reinterpret_cast(&CollateTransform), + reinterpret_cast(&CollateHash), +}; std::uint64_t _ZNSt7codecvtIcc9_MbstatetE2idE_nid_postfix = 0; std::uintptr_t _ZTVSt7codecvtIcc9_MbstatetE_nid_postfix[16] {}; std::uintptr_t _ZTVSt7codecvtIwc9_MbstatetE_nid_postfix[13] {}; diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index 64a41353cf..16dafe2671 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -72,12 +73,22 @@ static void CheckCodecvtWideVtableObject() { } static void CheckCollateVtableObject() { - for (std::uintptr_t& slot : _ZTVSt7collateIcE_nid_postfix) { - Require(slot == 0); - slot = 0x1111; - Require(slot == 0x1111); - slot = 0; - } + Require(_ZTVSt7collateIcE_nid_postfix[0] == 0 && _ZTVSt7collateIcE_nid_postfix[1] == 0); + for (std::size_t index = 2; index < std::size(_ZTVSt7collateIcE_nid_postfix); ++index) Require(_ZTVSt7collateIcE_nid_postfix[index] != 0); + const auto* vtable = reinterpret_cast(&_ZTVSt7collateIcE_nid_postfix[2]); + GuestLocale::CollateFacet facet{}; + facet.base.references = 0; + vtable->facet.destroy(&facet.base); + vtable->facet.retain(&facet.base); + Require(facet.base.references == 1); + Require(vtable->facet.release(&facet.base) == &facet.base && facet.base.references == 0); + Require(vtable->compare(&facet, "abc", "abc" + 3, "abd", "abd" + 3) < 0); + Require(vtable->compare(&facet, "abc", "abc" + 3, "abc", "abc" + 3) == 0); + Require(vtable->compare(&facet, "abd", "abd" + 3, "abc", "abc" + 3) > 0); + GuestLocale::String result{}; + Require(vtable->transform(&result, &facet, "abc", "abc" + 3) == &result); + Require(result.size == 3 && result.capacity == 15 && std::memcmp(result.buffer, "abc", 3) == 0); + Require(vtable->hash(&facet, "abc", "abc" + 3) == -1792535898324117685LL); } static void CheckGuestCalls() { diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 534052f5a8..3c4c9557ba 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -172,7 +172,7 @@ - [ios_base standard streams](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) `_Addstd` queues the stream in the standard stream table of Dinkumware [ios.cpp](https://github.com/ganboing/crt.vc12/blob/master/src/ios.cpp) (`stdstr`, `stdopens`, `_Addstd`, `_Ios_base_dtor`): it takes the first of slots 1 to 7 that is free or already holds the stream, and slot 8 (`NSTDSTR`) when none is, so slot 8 is shared by every stream past the first seven and registration never fails. It stores the 1-based slot index in the guest `IosBase::standardStream` (Dinkumware's `_Stdstr`, where 0 means not a standard stream) and counts the stream objects registered per slot; registering the same stream again reuses its slot and raises its count. Measured on a PS5 (firmware 13.60) from a homebrew payload calling the system `libSceLibcInternal` export: in a process whose own standard streams already hold slots 1 to 7, twelve distinct streams and a repeated one all get 8, written as a 64-bit value at offset 0x8 with no other byte of the object touched; the assignment of slots 1 to 7 to fresh streams is not observable there and follows VC12. The counts are `int`, not VC12's `char`, and the registration and the destructor lock the table where VC12 locks only `_Addstd`; a slot index past the table throws in the destructor instead of indexing out of bounds. The destructor decrements its slot's count and suppresses the rest of the destruction while the count stays positive, as `_Ios_base_dtor` does, and the slots of destroyed streams keep their pointer and stay reserved for that stream, as `stdstr` does. The streams themselves are destroyed by the title's own code as it registers them through `__cxa_atexit`; when the last reference closes, the unsupported stream storage and callback cleanup is still the throw it was and the locale is released as for a non-standard stream. - [std::_Pad, _Thrd_join, _Thrd_detach, _Thrd_id](../../../core/libs/prx/libc/src/Threads.cpp) (libc) follow Dinkumware V6.00 `thr/xthread` and `thr/threadcall.cpp` ([crt.vc12 `f1d47126`](https://github.com/ganboing/crt.vc12/tree/f1d47126/src/thr)), otherwise without console verification. `_Thrd_id` returns `scePthreadSelf()` unchanged, as measured on a PS5 (firmware 13.40) in two threads (#4164, #8711); `_Launch` omits `_Cnd_do_broadcast_at_thread_exit` and calls `pthread_create` registered by libkernel with default attributes. The 32-byte pad layout (`_Go()` vtable at 0x0, `_Cnd_t` at 0x8, `_Mtx_t` at 0x10, `bool _Started` at 0x18) is assumed from VC12, unverified. Failing primitives or thread start throw the guest `std::system_error`; null arguments throw. `_Thrd_join` returns `_Thrd_error` (4) on `pthread_join` failure and truncates the exit code to `int`; `_Thrd_detach` returns `_Thrd_error` (4) on `pthread_detach` failure and throws when none is registered - [siginterrupt](../../../core/libs/prx/libc/src/Process.cpp) (libc) runs FreeBSD 11.4's `siginterrupt.c` sequence through the `sigaction` and the `_sigintr` set that libkernel registers at load: it reads the signal's disposition, sets or clears the signal's bit and `SA_RESTART`, and installs the disposition again. The set is written outside libkernel's registration lock, as FreeBSD's libc writes it without one; a call before libkernel has registered throws. -- [collate vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `ruZtIwbCFjk`) is nine zeroed `uintptr_t` slots matching `st_size` 72 on the title `libc.prx` from PPSA16833 (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and three `do_*` virtuals). Facet calls through the object fault until those methods are wired; `_Getcat` still installs the host `g_collateVtable` for facets it builds. +- [collate vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc, NID `ruZtIwbCFjk`) is nine `uintptr_t` slots matching `st_size` 72 on the title `libc.prx` from PPSA16833 (offset-to-top, typeinfo, two destructors, `_Incref`/`_Decref`, and three `do_*` virtuals). Slots 2 through 8 hold the same `DestroyCollate`, `DeleteCollate`, `RetainFacet`, `ReleaseFacet`, `CollateCompare`, `CollateTransform` and `CollateHash` functions that `g_collateVtable` uses, so facet calls through the exported object work; offset-to-top and typeinfo remain zero, unlike the console's relocated entries, and `_Getcat` still installs `g_collateVtable` for facets it builds. - [num_get::id, time_get::id and the num_get vtable](../../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) are data objects, as the public SDK's libSceLibcInternal table declares them and as the other facet ids and the `num_put` and `codecvt` vtables are kept: the ids start at zero and the vtable is 17 zeroed slots, the 136-byte size of `KfcTPbeaOqg` in the console's libSceLibcInternal, whose relocations fill the offset-to-top and typeinfo entries, the two destructors, `locale::facet::_Incref` and `_Decref`, and the eleven `do_get` of Dinkumware's crt.vc12 `xlocnum`, so a guest resolves the symbols and reads data, and a facet call through the vtable faults. libSceAmpr exported the three as throwing functions, which a data consumer reads as code; its qualified imports fall back to libc. - [printf family conversions](../../../core/libs/prx/libc/include/WindowsFormatting.hpp) (libc) - Windows `FormatWindows` follows the title `libc.prx` `_Printf` measured from PPSA13934, PPSA07923, PPSA05916 and PPSA04158: `%q`, `%D`, `%O` and `%U` are unknown conversions; an unknown character is printed with its leading `%`, and width, `-` and discarded length modifiers behave as measured (`%5y` is ` %y`, `%hhz` is `%z`). `%%` takes a width. A trailing `%` reads past the format's NUL and is not reproduced; it throws. `#`, precision and a `0` without a width are ignored, as a payload process on a PS5 (firmware 13.60) printed `%#k` and `%.2k` as `%k`; `+`, space, `'` and `0` with a width on an unknown conversion are unverified and throw. On Linux a format with an unknown conversion also goes to this formatter - [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.