From e859215335e34c1fd566130ab0c36223864ec064 Mon Sep 17 00:00:00 2001 From: Eduardo Ruiz Date: Sun, 25 Oct 2026 23:26:44 +0100 Subject: [PATCH 1/2] feat(libc): export the error_category members and std::future_error The libc.prx bundled with PPSA02530 keeps error_code values in the second dword: the shared error_category::default_error_condition shifts the value into the high half and the equivalent overloads compare that dword and the category. Export the type info, the base destructor and the three base members, so titles that import them resolve to the same code the generic and system category vtables already use. future_category returns a static _Future_error_category derived from _Generic_error_category and named "future". Its message maps 1..4 to "broken promise", "future already retrieved", "promise already satisfied" and "no state", and falls back to the generic table. future_error is 0x40 bytes: a GuestLocale::String at 0x8 and the 16-byte error_code at 0x30; its 0x30-byte vtable carries the console's _Doraise slot. _Throw_future_error allocates that size and throws it with the console type info, and _Rethrow_future_exception delegates to rethrow_exception. --- core/libs/prx/libc/CMakeLists.txt | 10 ++ core/libs/prx/libc/include/Api.hpp | 10 ++ .../libc/src/exception/StandardExceptions.cpp | 115 ++++++++++++- core/libs/tests/GuestFutureCategory.cpp | 157 ++++++++++++++++++ docs/dev/debt/libc.md | 1 + 5 files changed, 290 insertions(+), 3 deletions(-) create mode 100644 core/libs/tests/GuestFutureCategory.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index bf8e7ab043..93a0a33f12 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -845,6 +845,16 @@ if(BUILD_TESTING) set_tests_properties(guest_generic_category PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_future_category_tests ${LIBS_INCLUDE_DIR}/tests/GuestFutureCategory.cpp) + target_include_directories(guest_future_category_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_future_category_tests PRIVATE libc) + configure_windows_unwind(guest_future_category_tests) + add_test(NAME guest_future_category COMMAND guest_future_category_tests) + set_tests_properties(guest_future_category PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_future_category PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test(NAME guest_new_handler COMMAND guest_new_handler_tests) if(WIN32) set_tests_properties(guest_new_handler PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index b1e66a54b6..9ef236a945 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -98,6 +98,8 @@ struct ErrorCode { int reserved; int value; const CategoryObject* category; }; struct SystemErrorObject { ExceptionObject base; ErrorCode code; }; +struct FutureErrorObject { const void* vtable; GuestLocale::String message; ErrorCode code; }; + } extern "C" { @@ -1150,6 +1152,11 @@ const char* APS5_VABI _ZSt13_Syserror_mapi_nid_postfix(int value); const LibcException::CategoryObject* APS5_VABI _ZSt16generic_categoryv_nid_postfix(); const LibcException::CategoryObject* APS5_VABI _ZSt15system_categoryv_nid_postfix(); GuestLocale::String* APS5_VABI _ZNKSt23_Generic_error_category7messageEi_nid_postfix( GuestLocale::String* result, const LibcException::CategoryObject* self, int value); +const LibcException::CategoryObject* APS5_VABI _ZSt15future_categoryv_nid_postfix(); +LibcException::ErrorCode APS5_VABI _ZNKSt14error_category23default_error_conditionEi_nid_postfix(const LibcException::CategoryObject* self, int value); +bool APS5_VABI _ZNKSt14error_category10equivalentEiRKSt15error_condition_nid_postfix(const LibcException::CategoryObject* self, int value, const LibcException::ErrorCode* condition); +bool APS5_VABI _ZNKSt14error_category10equivalentERKSt10error_codei_nid_postfix(const LibcException::CategoryObject* self, const LibcException::ErrorCode* code, int value); +void APS5_VABI _ZNSt14error_categoryD2Ev_nid_postfix(LibcException::CategoryObject* self); void APS5_VABI _ZNSt9exceptionD1Ev_nid_postfix(LibcException::ExceptionObject* self); void APS5_VABI _ZNSt9exceptionD2Ev_nid_postfix(LibcException::ExceptionObject* self); void APS5_VABI _ZNSt9exceptionD0Ev_nid_postfix(LibcException::ExceptionObject* self); @@ -1251,6 +1258,7 @@ void APS5_VABI _ZNSt12length_errorC1ERKS__nid_postfix(LibcException::ExceptionOb void APS5_VABI _ZNSt12length_errorC2ERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source); LibcException::ExceptionObject* APS5_VABI _ZNSt12length_erroraSERKS__nid_postfix(LibcException::ExceptionObject* self, const LibcException::ExceptionObject* source); void APS5_VABI _ZNSt12system_errorD1Ev_nid_postfix(LibcException::ExceptionObject* self); +void APS5_VABI _ZNSt12future_errorD1Ev_nid_postfix(LibcException::FutureErrorObject* self); void APS5_VABI _ZNSt12system_errorD2Ev_nid_postfix(LibcException::ExceptionObject* self); void APS5_VABI _ZNSt12system_errorD0Ev_nid_postfix(LibcException::ExceptionObject* self); const char* APS5_VABI _ZNKSt12system_error4whatEv_nid_postfix(const LibcException::ExceptionObject* self); @@ -1295,6 +1303,8 @@ int APS5_VABI dl_iterate_phdr_nid_postfix(int (*callback)(dl_phdr_info*, std::si [[noreturn]] void APS5_VABI _ZSt16_Throw_Cpp_errori_nid_postfix(int code); [[noreturn]] void APS5_VABI _Thrd_exit_nid_postfix(int code); [[noreturn]] void APS5_VABI _ZSt17rethrow_exceptionNSt15__exception_ptr13exception_ptrE_nid_postfix(GuestExceptionPointerArgument exception); +[[noreturn]] void APS5_VABI _ZSt19_Throw_future_errorRKSt10error_code_nid_postfix(const LibcException::ErrorCode* code); +[[noreturn]] void APS5_VABI _ZSt25_Rethrow_future_exceptionSt13exception_ptr_nid_postfix(GuestExceptionPointerArgument exception); [[noreturn]] void APS5_VABI _ZSt9terminatev_nid_postfix(); [[noreturn]] void APS5_VABI __cxa_call_terminate_nid_postfix(void* exception); [[noreturn]] void APS5_VABI __cxa_pure_virtual_nid_postfix(); diff --git a/core/libs/prx/libc/src/exception/StandardExceptions.cpp b/core/libs/prx/libc/src/exception/StandardExceptions.cpp index b009e10648..16e28ba4f6 100644 --- a/core/libs/prx/libc/src/exception/StandardExceptions.cpp +++ b/core/libs/prx/libc/src/exception/StandardExceptions.cpp @@ -11,6 +11,8 @@ namespace LibcException { struct TypeRecord { const void* const* vtable; const char* name; const TypeRecord* base; }; +struct ClassTypeRecord { const void* const* vtable; const char* name; }; + struct ExceptionVtable { std::ptrdiff_t offset; const TypeRecord* type; @@ -18,6 +20,17 @@ struct ExceptionVtable { void (APS5_VABI *deleteObject)(ExceptionObject*); const char* (APS5_VABI *what)(const ExceptionObject*); }; + +struct FutureErrorVtable { + std::ptrdiff_t offset; + const TypeRecord* type; + void (APS5_VABI *destroy)(FutureErrorObject*); + void (APS5_VABI *deleteObject)(FutureErrorObject*); + const char* (APS5_VABI *what)(const FutureErrorObject*); + void (APS5_VABI *doraise)(const FutureErrorObject*); +}; +static_assert(sizeof(ClassTypeRecord) == 0x10); +static_assert(sizeof(FutureErrorVtable) == 0x30); struct Message { std::size_t length; std::size_t capacity; @@ -129,6 +142,7 @@ ExceptionObject* Assign(ExceptionObject* object, const ExceptionObject* source) static_assert(sizeof(ErrorCode) == 16 && offsetof(ErrorCode, value) == 4 && offsetof(ErrorCode, category) == 8); static_assert(sizeof(SystemErrorObject) == 32 && offsetof(SystemErrorObject, code) == 16); +static_assert(sizeof(FutureErrorObject) == 0x40 && offsetof(FutureErrorObject, message) == 0x8 && offsetof(FutureErrorObject, code) == 0x30); struct CppError { int value; const char* message; }; static const char* GenericErrorMessage(int value) { @@ -220,10 +234,8 @@ static void APS5_VABI CategoryDelete_nid_no_patch(CategoryObject*) { } static const char* APS5_VABI GenericCategoryName_nid_no_patch(const CategoryObject*) { return "generic"; } static const char* APS5_VABI SystemCategoryName_nid_no_patch(const CategoryObject*) { return "system"; } -static GuestLocale::String* APS5_VABI CategoryMessage_nid_no_patch(GuestLocale::String* result, const CategoryObject*, int value) { +static GuestLocale::String* FillMessage(GuestLocale::String* result, const char* message) { if (!result) throw std::invalid_argument("std::error_category::message: null result"); - const char* message = GenericErrorMessage(value); - if (!message) message = "unknown error"; const auto length = std::strlen(message); const auto capacity = length | std::size_t{15}; char* storage = length <= 15 ? result->buffer : static_cast(_Znwm_nid_postfix(capacity + 1)); @@ -234,6 +246,23 @@ static GuestLocale::String* APS5_VABI CategoryMessage_nid_no_patch(GuestLocale:: result->capacity = capacity; return result; } +static const char* FutureErrorMessage(int value) { + static constexpr const char* messages[] {"broken promise", "future already retrieved", "promise already satisfied", "no state"}; + if (value < 1 || value > 4) return nullptr; + return messages[value - 1]; +} +static GuestLocale::String* APS5_VABI CategoryMessage_nid_no_patch(GuestLocale::String* result, const CategoryObject*, int value) { + const char* message = GenericErrorMessage(value); + if (!message) message = "unknown error"; + return FillMessage(result, message); +} +static GuestLocale::String* APS5_VABI FutureCategoryMessage_nid_no_patch(GuestLocale::String* result, const CategoryObject*, int value) { + const char* message = FutureErrorMessage(value); + if (!message) message = GenericErrorMessage(value); + if (!message) message = "unknown error"; + return FillMessage(result, message); +} +static const char* APS5_VABI FutureCategoryName_nid_no_patch(const CategoryObject*) { return "future"; } static ErrorCode APS5_VABI CategoryCondition_nid_no_patch(const CategoryObject* self, int value) { return {0, value, self}; } static bool APS5_VABI CategoryEquivalentCondition_nid_no_patch(const CategoryObject* self, int value, const ErrorCode* condition) { using Condition = ErrorCode (APS5_VABI *)(const CategoryObject*, int); @@ -633,4 +662,84 @@ const char* APS5_VABI _ZNSt8__sce_v213_Syserror_mapEi_nid_postfix(int value) { const char* message = LibcException::GenericErrorMessage(value); return message ? message : "unknown error"; } +LibcException::ClassTypeRecord _ZTISt14error_category_nid_postfix {LibcException::ClassTypeVtable + 2, "St14error_category"}; +LibcException::TypeRecord _ZTISt22_Future_error_category_nid_postfix {LibcException::SingleTypeVtable + 2, "St22_Future_error_category", &LibcException::GenericCategoryType}; +static void APS5_VABI FutureCategoryDelete_nid_no_patch(LibcException::CategoryObject*) { + throw std::invalid_argument("std::error_category: the future category object is not heap allocated"); +} +extern const void* const _ZTVSt22_Future_error_category_nid_postfix[] { + nullptr, &_ZTISt22_Future_error_category_nid_postfix, + reinterpret_cast(LibcException::CategoryDestroy_nid_no_patch), reinterpret_cast(FutureCategoryDelete_nid_no_patch), + reinterpret_cast(LibcException::FutureCategoryName_nid_no_patch), reinterpret_cast(LibcException::FutureCategoryMessage_nid_no_patch), + reinterpret_cast(LibcException::CategoryCondition_nid_no_patch), + reinterpret_cast(LibcException::CategoryEquivalentCondition_nid_no_patch), + reinterpret_cast(LibcException::CategoryEquivalentCode_nid_no_patch) +}; +static const LibcException::CategoryObject FutureCategory {_ZTVSt22_Future_error_category_nid_postfix + 2}; +const LibcException::CategoryObject* APS5_VABI _ZSt15future_categoryv_nid_postfix() { + return &FutureCategory; +} +LibcException::ErrorCode APS5_VABI _ZNKSt14error_category23default_error_conditionEi_nid_postfix(const LibcException::CategoryObject* self, int value) { + return LibcException::CategoryCondition_nid_no_patch(self, value); +} +bool APS5_VABI _ZNKSt14error_category10equivalentEiRKSt15error_condition_nid_postfix(const LibcException::CategoryObject* self, int value, const LibcException::ErrorCode* condition) { + return LibcException::CategoryEquivalentCondition_nid_no_patch(self, value, condition); +} +bool APS5_VABI _ZNKSt14error_category10equivalentERKSt10error_codei_nid_postfix(const LibcException::CategoryObject* self, const LibcException::ErrorCode* code, int value) { + return LibcException::CategoryEquivalentCode_nid_no_patch(self, code, value); +} +void APS5_VABI _ZNSt14error_categoryD2Ev_nid_postfix(LibcException::CategoryObject*) {} +LibcException::TypeRecord _ZTISt12future_error_nid_postfix {LibcException::SingleTypeVtable + 2, "St12future_error", &_ZTISt11logic_error_nid_postfix}; +static void DestroyFutureErrorString(GuestLocale::String* message) { + if (message->capacity > 15) _ZdlPv_nid_postfix(message->pointer); + message->reserved = 0; + message->size = 0; + message->capacity = 15; + std::memset(message->buffer, 0, sizeof(message->buffer)); +} +static void APS5_VABI DestroyFutureError_nid_no_patch(LibcException::FutureErrorObject* self) { + DestroyFutureErrorString(&self->message); +} +static void APS5_VABI DeleteFutureError_nid_no_patch(LibcException::FutureErrorObject* self) { + DestroyFutureErrorString(&self->message); + std::free(self); +} +static const char* APS5_VABI FutureErrorWhat_nid_no_patch(const LibcException::FutureErrorObject* self) { + return LibcException::FutureErrorMessage(self->code.value); +} +static void CopyFutureError(LibcException::FutureErrorObject* self, const LibcException::FutureErrorObject* source) { + *self = *source; + if (source->message.capacity > 15) { + const auto capacity = source->message.capacity; + auto* storage = static_cast(_Znwm_nid_postfix(capacity + 1)); + std::memcpy(storage, source->message.pointer, source->message.size + 1); + self->message.pointer = storage; + } +} +[[noreturn]] static void APS5_VABI RaiseFutureError_nid_no_patch(const LibcException::FutureErrorObject* self) { + auto* object = static_cast(__cxa_allocate_exception_nid_postfix(sizeof(LibcException::FutureErrorObject))); + CopyFutureError(object, self); + __cxa_throw_nid_postfix(object, reinterpret_cast(&_ZTISt12future_error_nid_postfix), + reinterpret_cast(DestroyFutureError_nid_no_patch)); +} +LibcException::FutureErrorVtable _ZTVSt12future_error_nid_postfix { + 0, &_ZTISt12future_error_nid_postfix, + DestroyFutureError_nid_no_patch, DeleteFutureError_nid_no_patch, FutureErrorWhat_nid_no_patch, RaiseFutureError_nid_no_patch +}; +[[noreturn]] void APS5_VABI _ZSt19_Throw_future_errorRKSt10error_code_nid_postfix(const LibcException::ErrorCode* code) { + auto* object = static_cast(__cxa_allocate_exception_nid_postfix(sizeof(LibcException::FutureErrorObject))); + object->vtable = &_ZTVSt12future_error_nid_postfix.destroy; + object->message = {}; + object->message.capacity = 15; + object->code = *code; + __cxa_throw_nid_postfix(object, reinterpret_cast(&_ZTISt12future_error_nid_postfix), + reinterpret_cast(DestroyFutureError_nid_no_patch)); +} +[[noreturn]] void APS5_VABI _ZSt25_Rethrow_future_exceptionSt13exception_ptr_nid_postfix(GuestExceptionPointerArgument exception) { + _ZSt17rethrow_exceptionNSt15__exception_ptr13exception_ptrE_nid_postfix(exception); +} +void APS5_VABI _ZNSt12future_errorD1Ev_nid_postfix(LibcException::FutureErrorObject* self) { + DestroyFutureErrorString(&self->message); +} + } diff --git a/core/libs/tests/GuestFutureCategory.cpp b/core/libs/tests/GuestFutureCategory.cpp new file mode 100644 index 0000000000..d7c20f4f82 --- /dev/null +++ b/core/libs/tests/GuestFutureCategory.cpp @@ -0,0 +1,157 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/GuestLocale.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using Message = GuestLocale::String* (APS5_VABI *)(GuestLocale::String*, const LibcException::CategoryObject*, int); +using Name = const char* (APS5_VABI *)(const LibcException::CategoryObject*); + +struct TypeRecord { const void* vtable; const char* name; const TypeRecord* base; }; +struct ClassTypeRecord { const void* vtable; const char* name; }; + +extern "C" { +extern TypeRecord _ZTISt12future_error_nid_postfix; +extern TypeRecord _ZTISt22_Future_error_category_nid_postfix; +extern ClassTypeRecord _ZTISt14error_category_nid_postfix; +extern std::uintptr_t _ZTVSt12future_error_nid_postfix[6]; +} + +static void Require(bool value, int line) { + if (value) return; + std::fprintf(stderr, "future category check failed at line %d\n", line); + std::exit(1); +} +#define REQUIRE(value) Require((value), __LINE__) + +static void CheckMessage(Message message, const LibcException::CategoryObject* category, int value, const char* expected) { + constexpr std::uint64_t marker = 0xA55AA55AA55AA55Aull; + struct Guarded { std::uint64_t before; GuestLocale::String result; std::uint64_t after; }; + Guarded guarded {marker, {}, marker}; + std::memset(&guarded.result, 0xcd, sizeof(guarded.result)); + *__error_nid_postfix() = 61; + REQUIRE(message(&guarded.result, category, value) == &guarded.result); + REQUIRE(*__error_nid_postfix() == 61); + REQUIRE(guarded.before == marker && guarded.after == marker); + const auto length = std::strlen(expected); + REQUIRE(guarded.result.reserved == 0 && guarded.result.size == length); + REQUIRE(guarded.result.capacity >= length && (guarded.result.capacity & 15) == 15); + const char* text = guarded.result.capacity <= 15 ? guarded.result.buffer : guarded.result.pointer; + REQUIRE(std::memcmp(text, expected, length + 1) == 0); + REQUIRE((guarded.result.capacity <= 15) == (length <= 15)); + if (length > 15) _ZdlPv_nid_postfix(guarded.result.pointer); +} + +static void CheckBaseCategory() { + REQUIRE(sizeof(ClassTypeRecord) == 0x10); + REQUIRE(std::strcmp(_ZTISt14error_category_nid_postfix.name, "St14error_category") == 0); + const auto* future = _ZSt15future_categoryv_nid_postfix(); + const auto* generic = _ZSt16generic_categoryv_nid_postfix(); + REQUIRE(future != nullptr && generic != nullptr && future != generic); + + const LibcException::ErrorCode condition {0x7fffffff, 22, future}; + REQUIRE(_ZNKSt14error_category10equivalentEiRKSt15error_condition_nid_postfix(future, 22, &condition)); + REQUIRE(!_ZNKSt14error_category10equivalentEiRKSt15error_condition_nid_postfix(future, 23, &condition)); + const LibcException::ErrorCode otherCondition {0, 22, generic}; + REQUIRE(!_ZNKSt14error_category10equivalentEiRKSt15error_condition_nid_postfix(future, 22, &otherCondition)); + + const LibcException::ErrorCode code {0x12345678, 22, future}; + REQUIRE(_ZNKSt14error_category10equivalentERKSt10error_codei_nid_postfix(future, &code, 22)); + REQUIRE(!_ZNKSt14error_category10equivalentERKSt10error_codei_nid_postfix(future, &code, 23)); + const LibcException::ErrorCode otherCode {0, 22, generic}; + REQUIRE(!_ZNKSt14error_category10equivalentERKSt10error_codei_nid_postfix(future, &otherCode, 22)); + + const auto mapped = _ZNKSt14error_category23default_error_conditionEi_nid_postfix(future, 7); + REQUIRE(mapped.reserved == 0 && mapped.value == 7 && mapped.category == future); + _ZNSt14error_categoryD2Ev_nid_postfix(nullptr); +} + +static void CheckFutureCategory() { + const auto* future = _ZSt15future_categoryv_nid_postfix(); + const auto* type = static_cast(static_cast(future->vtable)[-1]); + REQUIRE(std::strcmp(type->name, "St22_Future_error_category") == 0); + REQUIRE(type->base && std::strcmp(type->base->name, "St23_Generic_error_category") == 0); + REQUIRE(std::strcmp(_ZTISt22_Future_error_category_nid_postfix.name, "St22_Future_error_category") == 0); + REQUIRE(_ZTISt22_Future_error_category_nid_postfix.base && std::strcmp(_ZTISt22_Future_error_category_nid_postfix.base->name, "St23_Generic_error_category") == 0); + + const auto name = std::bit_cast(static_cast(future->vtable)[2]); + REQUIRE(std::strcmp(name(future), "future") == 0); + + const auto message = std::bit_cast(static_cast(future->vtable)[3]); + struct Expected { int value; const char* text; }; + static constexpr Expected errors[] { + {1, "broken promise"}, {2, "future already retrieved"}, {3, "promise already satisfied"}, {4, "no state"} + }; + for (const auto& error : errors) CheckMessage(message, future, error.value, error.text); + + for (int value : {0, 5, 22, 99}) { + const char* generic = _ZSt13_Syserror_mapi_nid_postfix(value); + CheckMessage(message, future, value, generic ? generic : "unknown error"); + } + + GuestLocale::String first {}, second {}; + REQUIRE(message(&first, future, 2) == &first && message(&second, future, 3) == &second); + REQUIRE(first.pointer != second.pointer); + _ZdlPv_nid_postfix(second.pointer); + REQUIRE(std::strcmp(first.pointer, "future already retrieved") == 0); + _ZdlPv_nid_postfix(first.pointer); + + bool rejected = false; + try { message(nullptr, future, 2); } + catch (const std::invalid_argument&) { rejected = true; } + REQUIRE(rejected); +} + +static void CheckFutureError() { + REQUIRE(sizeof(LibcException::FutureErrorObject) == 0x40); + REQUIRE(offsetof(LibcException::FutureErrorObject, message) == 0x8); + REQUIRE(offsetof(LibcException::FutureErrorObject, code) == 0x30); + REQUIRE(std::strcmp(_ZTISt12future_error_nid_postfix.name, "St12future_error") == 0); + REQUIRE(_ZTISt12future_error_nid_postfix.base && std::strcmp(_ZTISt12future_error_nid_postfix.base->name, "St11logic_error") == 0); + + REQUIRE(_ZTVSt12future_error_nid_postfix[0] == 0); + REQUIRE(_ZTVSt12future_error_nid_postfix[1] == reinterpret_cast(&_ZTISt12future_error_nid_postfix)); + for (int slot = 2; slot < 6; ++slot) REQUIRE(_ZTVSt12future_error_nid_postfix[slot] != 0); + + const auto* future = _ZSt15future_categoryv_nid_postfix(); + LibcException::ErrorCode code {0x12345678, 2, future}; + bool caught = false; + try { _ZSt19_Throw_future_errorRKSt10error_code_nid_postfix(&code); } + catch (const std::exception& error) { + caught = true; + REQUIRE(error.what() != nullptr && std::strcmp(error.what(), "future already retrieved") == 0); + } + REQUIRE(caught); + + code.value = 99; + caught = false; + try { _ZSt19_Throw_future_errorRKSt10error_code_nid_postfix(&code); } + catch (const std::exception& error) { + caught = true; + REQUIRE(error.what() == nullptr); + } + REQUIRE(caught); + + LibcException::FutureErrorObject object {}; + object.message.capacity = 15; + _ZNSt12future_errorD1Ev_nid_postfix(&object); + REQUIRE(object.message.reserved == 0 && object.message.size == 0 && object.message.capacity == 15); + REQUIRE(object.message.buffer[0] == 0); +} + +int main() { + const std::array api {}; + ApplicationHeapRegister_nid_no_patch(api.data()); + CheckBaseCategory(); + CheckFutureCategory(); + CheckFutureError(); + std::puts("guest future category, error_category base members and future_error layout: PASS"); + return 0; +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 0609afdb86..0499855abe 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -163,6 +163,7 @@ - [daemon](../../../core/libs/prx/libc/src/Process.cpp) (libc) throws: forking into the background is not supported. PPSA85300 imports it and fails to load without it. VLC 3.0 calls it only when its `daemon` option is set ([libvlc.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/libvlc.c#L190-L197)), and the title starts libVLC with `--ignore-config` and without `--daemon` ([player.cc](https://github.com/ZiZc3/VLC/blob/f3253020a64368153233ede29183014c0b6c8979/src/player.cc#L960-L981)) - [_WStoul](../../../core/libs/prx/libc/src/Strings.cpp), [_Iswctype](../../../core/libs/prx/libc/src/RuneLocale.cpp), [swscanf_s and swscanf](../../../core/libs/prx/libc/src/ScanningWide.cpp) (libc) - `_WStoul` is `wcstoul` and throws for a base other than 0 or 2 to 36. `_Iswctype` takes the class numbers of Dinkumware's [xwcc.h](https://github.com/superna9999/ti-cgt6x/blob/cgt6x_7_3_4/include/xwcc.h) (1 `alnum` to 11 `xdigit`, 12 `blank`) and throws for any other value, including the FreeBSD masks `wctype` returns; the numbering is not confirmed on the console's `libc.prx`. `swscanf_s` follows C11 7.29.2.2 and K.3.9.1.5, but reads the size argument as `unsigned int` like `sscanf_s` where K.3.9.1.5 specifies `rsize_t`; the console's width is unverified. A runtime-constraint violation (null buffer, format or receiving pointer) only returns `EOF`, and a too small array gets a null first element as in `sscanf_s`. A literal at end of input returns the number of assignments, including zero, rather than `EOF` as FreeBSD does, as reported from the console scanner in #8479; `%%` at end of input still returns `EOF`. A literal mismatch is a matching failure, where FreeBSD's `vfwscanf` reports an input failure. An incomplete floating exponent such as `1e+` converts its mantissa, as reported from the console scanner in #8479; other incomplete exponent forms are not separately verified. A short `%c` field is currently a matching failure without storing its available prefix, while #9321 reports that the console stores a short `%3lc` field; its return count, partial-output extent and precedence are unverified. An integer field that is only a prefix, such as `0x`, does not convert; FreeBSD's `vfwscanf` pushes back the `x` and converts the `0`. Integers out of range are truncated from `strtoll`/`strtoull`. `%p` follows FreeBSD 11's [vfwscanf.c](https://github.com/freebsd/freebsd-src/blob/4ab2e064d7950be84256d671a7ae93f87cc6aa36/lib/libc/stdio/vfwscanf.c#L610-L615): a hexadecimal integer with an optional `0x`, converted as unsigned and stored as a pointer; a length modifier on it throws, where FreeBSD ignores it. `%S` and `%C`, with any length modifier or `*`, end the scan as a matching failure, as the console's Dinkumware scanner in PPSA21564's `libc.prx` treats them as invalid conversions; this was read from the module, not run on a console, and whether input ending before them is an input failure instead is unverified. It throws for unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert. `swscanf` runs the same scanner without the size arguments of `%s`, `%c` and `%[`, as FreeBSD's [swscanf.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libc/stdio/swscanf.c) passes its arguments to `vswscanf`, so everything above except the sizes applies to it; it also keeps the null buffer, format and receiving pointer checks returning `EOF`, where C leaves those undefined. Neither variant is confirmed on a PS5 title - [flock](../../../core/libs/prx/libc/src/FileLock.cpp) (libkernel) on Windows shares one lock state between duplicated descriptors, following FreeBSD 11.4's [flock(2)](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libc/sys/flock.2). Independent opens have separate states; closing a descriptor or reopening a stream drops only its reference. Lock transitions on one open file description are serialized. The lock is a host [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) lock on one byte at offset 2^63 - 2, which no file reaches, so it excludes other `flock` calls and leaves reads, writes and mapped views alone, like FreeBSD's advisory locks. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. +- [std::error_category, std::future_category and std::future_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) - measured from PPSA02530's `libc.prx`, except that the future category's deleting destructor throws instead of freeing it, the string destructor frees the block instead of the console header path, and a `_Doraise` copy of a heap message and header size are unverified. `error_code` keeps the value in the second dword, so `default_error_condition` shifts it into the high half and `equivalent` compares that dword and the category. `_Future_error_category` derives from `_Generic_error_category`, is `future` and maps 1..4 to `broken promise`, `future already retrieved`, `promise already satisfied` and `no state`; other values use the generic map, but `future_error::what()` returns null outside 1..4. `future_error` is 0x40 bytes (vtable, `GuestLocale::String` at 0x8, `error_code` at 0x30). `_Rethrow_future_exception` delegates to `rethrow_exception`. - [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libc) follows Dinkumware `thread0.cpp` ([VC12](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/thread0.cpp), [STL](https://github.com/microsoft/STL/blob/da52dc0f/stl/src/thread0.cpp)), without console verification. Codes outside 0..6 throw `std::out_of_range`, the deleting destructor throws, and STREAMS errnos absent from FreeBSD 11.4 [`errno.h`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h) (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are omitted. Codes 0..6 throw a 32-byte [VC12 `system_error`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/system_error) with FreeBSD errnos (16, 22, 3, 12, 1, 11, 35), a static `generic_category` (not `_Generic_object`), and unverified 40-byte [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) messages from [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126/src/syserror.cpp) - [Call tracing](../../../core/libs/prx/libc/src/specifics/x86_64/CallTrace.cpp) (`--trace-imports`, `APS5_TRACE_CALLS`) is missing on macOS and records only the entry of a call, not its arguments or result. One process-wide ring keeps the last 65536 calls, so a busy thread pushes the others out; the reporting thread's own calls are listed only while they are still in that ring. Calls are counted per traced target; an import bound to a target already traced under another name is counted under the first name. The thunk saves the integer argument registers and `xmm0` to `xmm7`; wider vector arguments survive only while the recording code avoids AVX, which is untested. Linux has no crash report, so it prints the trace only on `SIGABRT` (guest or host `abort()`), not on `SIGSEGV`; a caller in a module loaded after the last traced import prints without a module name, and a thread aborting while another writes the report waits for it at most 2 seconds - [_Dtest](../../../core/libs/prx/libc/src/Math.cpp) (libc) follows Dinkumware double classification matching _FDtest: finite values return -1, subnormals -2, zeros 0, infinities 1, and NaNs 2. From 45ea48c5aa1d3a612822d373944b5ba9817c1737 Mon Sep 17 00:00:00 2001 From: edubox Date: Mon, 26 Oct 2026 01:15:27 +0100 Subject: [PATCH 2/2] fix(libc): free the future_error block when the message copy throws --- .../libc/src/exception/StandardExceptions.cpp | 7 +++- core/libs/tests/GuestFutureCategory.cpp | 37 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libc/src/exception/StandardExceptions.cpp b/core/libs/prx/libc/src/exception/StandardExceptions.cpp index 16e28ba4f6..a04b42a966 100644 --- a/core/libs/prx/libc/src/exception/StandardExceptions.cpp +++ b/core/libs/prx/libc/src/exception/StandardExceptions.cpp @@ -718,7 +718,12 @@ static void CopyFutureError(LibcException::FutureErrorObject* self, const LibcEx } [[noreturn]] static void APS5_VABI RaiseFutureError_nid_no_patch(const LibcException::FutureErrorObject* self) { auto* object = static_cast(__cxa_allocate_exception_nid_postfix(sizeof(LibcException::FutureErrorObject))); - CopyFutureError(object, self); + try { + CopyFutureError(object, self); + } catch (...) { + __cxa_free_exception_nid_postfix(object); + throw; + } __cxa_throw_nid_postfix(object, reinterpret_cast(&_ZTISt12future_error_nid_postfix), reinterpret_cast(DestroyFutureError_nid_no_patch)); } diff --git a/core/libs/tests/GuestFutureCategory.cpp b/core/libs/tests/GuestFutureCategory.cpp index d7c20f4f82..35e38af6a7 100644 --- a/core/libs/tests/GuestFutureCategory.cpp +++ b/core/libs/tests/GuestFutureCategory.cpp @@ -9,10 +9,25 @@ #include #include #include +#include #include using Message = GuestLocale::String* (APS5_VABI *)(GuestLocale::String*, const LibcException::CategoryObject*, int); using Name = const char* (APS5_VABI *)(const LibcException::CategoryObject*); +using Raise = void (APS5_VABI *)(const LibcException::FutureErrorObject*); + +bool failAllocation = false; + +void* APS5_VABI TestAllocate(std::size_t size) { return failAllocation ? nullptr : std::malloc(size); } +void APS5_VABI TestFree(void* pointer) { std::free(pointer); } +void* APS5_VABI TestCalloc(std::size_t count, std::size_t size) { return std::calloc(count, size); } +void* APS5_VABI TestReallocate(void* pointer, std::size_t size) { return std::realloc(pointer, size); } +void* APS5_VABI TestAlign(std::size_t, std::size_t size) { return std::malloc(size); } +void* APS5_VABI TestRealign(void* pointer, std::size_t size, std::size_t) { return std::realloc(pointer, size); } +int APS5_VABI TestPosixAlign(void** pointer, std::size_t, std::size_t size) { + *pointer = std::malloc(size); + return *pointer ? 0 : 12; +} struct TypeRecord { const void* vtable; const char* name; const TypeRecord* base; }; struct ClassTypeRecord { const void* vtable; const char* name; }; @@ -139,6 +154,22 @@ static void CheckFutureError() { } REQUIRE(caught); + const auto raise = std::bit_cast(_ZTVSt12future_error_nid_postfix[5]); + LibcException::FutureErrorObject source {}; + source.message.capacity = 24; + source.message.size = 24; + source.message.pointer = static_cast(_Znwm_nid_postfix(25)); + std::memcpy(source.message.pointer, "future already retrieved", 25); + source.code = {0x12345678, 2, future}; + failAllocation = true; + bool allocationFailure = false; + try { raise(&source); } + catch (const std::bad_alloc&) { allocationFailure = true; } + failAllocation = false; + REQUIRE(allocationFailure); + REQUIRE(std::strcmp(source.message.pointer, "future already retrieved") == 0); + _ZdlPv_nid_postfix(source.message.pointer); + LibcException::FutureErrorObject object {}; object.message.capacity = 15; _ZNSt12future_errorD1Ev_nid_postfix(&object); @@ -147,7 +178,11 @@ static void CheckFutureError() { } int main() { - const std::array api {}; + const std::array api { + reinterpret_cast(&TestAllocate), reinterpret_cast(&TestFree), + reinterpret_cast(&TestCalloc), reinterpret_cast(&TestReallocate), + reinterpret_cast(&TestAlign), reinterpret_cast(&TestRealign), + reinterpret_cast(&TestPosixAlign), nullptr, nullptr, nullptr}; ApplicationHeapRegister_nid_no_patch(api.data()); CheckBaseCategory(); CheckFutureCategory();