diff --git a/core/libs/prx/libSceAmpr/CMakeLists.txt b/core/libs/prx/libSceAmpr/CMakeLists.txt index 35c734c3e..f9898075a 100644 --- a/core/libs/prx/libSceAmpr/CMakeLists.txt +++ b/core/libs/prx/libSceAmpr/CMakeLists.txt @@ -34,7 +34,7 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() - if(MINGW) + if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") enable_language(ASM) add_test_executable(guest_ampr_cpp_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestAmprCppError.cpp @@ -43,7 +43,10 @@ if(BUILD_TESTING) target_link_libraries(guest_ampr_cpp_error_tests PRIVATE libSceAmpr libkernel libc) configure_windows_unwind(guest_ampr_cpp_error_tests) add_test(NAME guest_ampr_cpp_error COMMAND guest_ampr_cpp_error_tests) - set_tests_properties(guest_ampr_cpp_error PROPERTIES TIMEOUT 10 ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") + set_tests_properties(guest_ampr_cpp_error PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_ampr_cpp_error PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() endif() diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ba433b77f..4cfa6a288 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -442,6 +442,16 @@ if(BUILD_TESTING) target_include_directories(guest_new_handler_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_new_handler_tests PRIVATE libc) configure_windows_unwind(guest_new_handler_tests) + add_test_executable(guest_generic_category_tests ${LIBS_INCLUDE_DIR}/tests/GuestGenericCategory.cpp) + target_include_directories(guest_generic_category_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_generic_category_tests PRIVATE libc) + configure_windows_unwind(guest_generic_category_tests) + add_test(NAME guest_generic_category COMMAND guest_generic_category_tests) + set_tests_properties(guest_generic_category PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_generic_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/src/exception/StandardExceptions.cpp b/core/libs/prx/libc/src/exception/StandardExceptions.cpp index 2f290cfd4..fdaca1850 100644 --- a/core/libs/prx/libc/src/exception/StandardExceptions.cpp +++ b/core/libs/prx/libc/src/exception/StandardExceptions.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/exceptions/Runtime.hpp" +#include "prx/libc/include/GuestLocale.hpp" #include #include #include @@ -6,6 +7,8 @@ #include #include +extern "C" void* APS5_VABI _Znwm_nid_postfix(std::size_t size); + namespace LibcException { struct TypeRecord { const void* const* vtable; const char* name; const TypeRecord* base; }; struct ExceptionObject { const void* vtable; const char* message; }; @@ -129,6 +132,86 @@ struct SystemErrorObject { ExceptionObject base; ErrorCode code; }; static_assert(sizeof(ErrorCode) == 16 && sizeof(SystemErrorObject) == 32 && offsetof(SystemErrorObject, code) == 16); struct CppError { int value; const char* message; }; +static const char* GenericErrorMessage(int value) { + static constexpr CppError errors[] { + {1, "operation not permitted"}, + {2, "no such file or directory"}, + {3, "no such process"}, + {4, "interrupted"}, + {5, "io error"}, + {6, "no such device or address"}, + {7, "argument list too long"}, + {8, "executable format error"}, + {9, "bad file descriptor"}, + {10, "no child process"}, + {11, "resource deadlock would occur"}, + {12, "not enough memory"}, + {13, "permission denied"}, + {14, "bad address"}, + {16, "device or resource busy"}, + {17, "file exists"}, + {18, "cross device link"}, + {19, "no such device"}, + {20, "not a directory"}, + {21, "is a directory"}, + {22, "invalid argument"}, + {23, "too many files open in system"}, + {24, "too many files open"}, + {25, "inappropriate io control operation"}, + {26, "text file busy"}, + {27, "file too large"}, + {28, "no space on device"}, + {29, "invalid seek"}, + {30, "read only file system"}, + {31, "too many links"}, + {32, "broken pipe"}, + {33, "argument out of domain"}, + {34, "result out of range"}, + {35, "operation would block"}, + {36, "operation in progress"}, + {37, "connection already in progress"}, + {38, "not a socket"}, + {39, "destination address required"}, + {40, "message size"}, + {41, "wrong protocol type"}, + {42, "no protocol option"}, + {43, "protocol not supported"}, + {45, "not supported"}, + {47, "address family not supported"}, + {48, "address in use"}, + {49, "address not available"}, + {50, "network down"}, + {51, "network unreachable"}, + {52, "network reset"}, + {53, "connection aborted"}, + {54, "connection reset"}, + {55, "no buffer space"}, + {56, "already connected"}, + {57, "not connected"}, + {60, "timed out"}, + {61, "connection refused"}, + {62, "too many symbolic link levels"}, + {63, "filename too long"}, + {65, "host unreachable"}, + {66, "directory not empty"}, + {77, "no lock available"}, + {78, "function not supported"}, + {82, "identifier removed"}, + {83, "no message"}, + {84, "value too large"}, + {85, "operation canceled"}, + {86, "illegal byte sequence"}, + {89, "bad message"}, + {91, "no link"}, + {92, "protocol error"}, + {95, "state not recoverable"}, + {96, "owner dead"} + }; + for (const auto& error : errors) + if (error.value == value) return error.message; + return nullptr; +} + const TypeRecord ErrorCategoryType {ClassTypeVtable + 2, "St14error_category", nullptr}; const TypeRecord GenericCategoryType {SingleTypeVtable + 2, "St23_Generic_error_category", &ErrorCategoryType}; static void APS5_VABI GenericCategoryDestroy_nid_no_patch(CategoryObject*) {} @@ -136,8 +219,18 @@ static void APS5_VABI GenericCategoryDelete_nid_no_patch(CategoryObject*) { throw std::invalid_argument("std::_Generic_error_category: the generic category object is not heap allocated"); } static const char* APS5_VABI GenericCategoryName_nid_no_patch(const CategoryObject*) { return "generic"; } -static void* APS5_VABI GenericCategoryMessage_nid_no_patch(void* result, const CategoryObject*, int) { - NotImplemented_nid_no_patch("std::_Generic_error_category::message"); +static GuestLocale::String* APS5_VABI GenericCategoryMessage_nid_no_patch(GuestLocale::String* result, const CategoryObject*, int value) { + if (!result) throw std::invalid_argument("std::_Generic_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)); + std::memcpy(storage, message, length + 1); + result->reserved = 0; + if (length > 15) result->pointer = storage; + result->size = length; + result->capacity = capacity; return result; } static ErrorCode APS5_VABI GenericCategoryCondition_nid_no_patch(const CategoryObject* self, int value) { return {value, self}; } @@ -159,6 +252,16 @@ const CategoryObject GenericCategory {GenericCategoryVtable + 2}; } extern "C" { +const char* APS5_VABI _ZSt13_Syserror_mapi_nid_postfix(int value) { + return LibcException::GenericErrorMessage(value); +} +const LibcException::CategoryObject* APS5_VABI _ZSt16generic_categoryv_nid_postfix() { + return &LibcException::GenericCategory; +} +GuestLocale::String* APS5_VABI _ZNKSt23_Generic_error_category7messageEi_nid_postfix( + GuestLocale::String* result, const LibcException::CategoryObject* self, int value) { + return LibcException::GenericCategoryMessage_nid_no_patch(result, self, value); +} LibcException::TypeRecord _ZTISt8ios_base_nid_postfix {LibcException::ClassTypeVtable + 2, "St8ios_base", nullptr}; LibcException::TypeRecord _ZTISt9exception_nid_postfix {LibcException::ClassTypeVtable + 2, "St9exception", nullptr}; LibcException::ExceptionVtable _ZTVSt9exception_nid_postfix {0, &_ZTISt9exception_nid_postfix, LibcException::GuestDestroyPlain_nid_no_patch, LibcException::GuestDeletePlain_nid_no_patch, LibcException::GuestPlainWhat_nid_no_patch}; @@ -382,7 +485,7 @@ const char* APS5_VABI _ZNKSt8ios_base7failure4whatEv_nid_postfix(const LibcExcep throw std::out_of_range("_Throw_Cpp_error: " + std::to_string(code) + " is not a _Cpp_error code"); const auto& error = errors[code]; char message[128]; - std::snprintf(message, sizeof(message), "%s: %s", error.message, error.message); + std::snprintf(message, sizeof(message), "%s: %s", error.message, LibcException::GenericErrorMessage(error.value)); auto* object = static_cast(__cxa_allocate_exception_nid_postfix(sizeof(LibcException::SystemErrorObject))); LibcException::Construct(&object->base, _ZTVSt12system_error_nid_postfix, message); object->code = {error.value, &LibcException::GenericCategory}; diff --git a/core/libs/tests/GuestAmprCppError.S b/core/libs/tests/GuestAmprCppError.S index c9988cf44..024758cd4 100644 --- a/core/libs/tests/GuestAmprCppError.S +++ b/core/libs/tests/GuestAmprCppError.S @@ -31,7 +31,11 @@ GuestCatch: call FixtureFail ud2 .cfi_endproc +#ifdef _WIN32 .section .rdata,"dr" +#else +.section .rodata +#endif .Lcatch_lsda: .byte 0xff, 0x80 .uleb128 .Lcatch_types_end-.Lcatch_types_offset @@ -48,3 +52,6 @@ GuestCatch: .quad FixtureCatchType .quad FixtureOtherType .Lcatch_types_end: +#ifndef _WIN32 +.section .note.GNU-stack,"",@progbits +#endif diff --git a/core/libs/tests/GuestAmprCppError.cpp b/core/libs/tests/GuestAmprCppError.cpp index f16fe3bff..8e2407c19 100644 --- a/core/libs/tests/GuestAmprCppError.cpp +++ b/core/libs/tests/GuestAmprCppError.cpp @@ -1,4 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/GuestLocale.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include #include #include #include @@ -29,7 +32,7 @@ static constexpr Expected Errors[] { {12, "not enough memory: not enough memory"}, {1, "operation not permitted: operation not permitted"}, {11, "resource deadlock would occur: resource deadlock would occur"}, - {35, "resource unavailable try again: resource unavailable try again"} + {35, "resource unavailable try again: operation would block"} }; extern "C" { @@ -41,6 +44,7 @@ extern const unsigned char _ZTISt12system_error_nid_postfix[]; void APS5_VABI _ZNSt12system_errorC1ERKS__nid_postfix(SystemError* self, const SystemError* source); SystemError* APS5_VABI _ZNSt12system_erroraSERKS__nid_postfix(SystemError* self, const SystemError* source); void APS5_VABI _ZNSt12system_errorD1Ev_nid_postfix(SystemError* self); +void APS5_VABI _ZdlPv_nid_postfix(void* pointer); void APS5_VABI GuestCatch(int code); const void* FixtureCatchType = nullptr; @@ -86,14 +90,11 @@ static void CheckCategory(const SystemError& error) { Require(equivalentCode(category, &error.code, error.code.value), 28); Require(!equivalentCode(category, &error.code, error.code.value + 1), 29); Require(!equivalentCode(category, &foreign, error.code.value), 30); - bool messageThrew = false; - try { - unsigned char result[32] {}; - Slot(category->vtable, 3)(result, category, error.code.value); - } catch (const std::runtime_error&) { - messageThrew = true; - } - Require(messageThrew, 31); + GuestLocale::String result {}; + Require(Slot(category->vtable, 3)(&result, category, error.code.value) == &result, 33); + const char* data = result.capacity <= 15 ? result.buffer : result.pointer; + Require(std::strcmp(data, std::strstr(FixtureExpected->what, ": ") + 2) == 0, 34); + if (result.capacity > 15) _ZdlPv_nid_postfix(result.pointer); bool deleteThrew = false; try { Slot(category->vtable, 1)(category); @@ -148,6 +149,8 @@ static void CheckOutOfRange(int code) { } int main() { + const std::array api {}; + ApplicationHeapRegister_nid_no_patch(api.data()); FixtureOtherType = _ZTISt11logic_error_nid_postfix; for (int code = 0; code < static_cast(std::size(Errors)); ++code) Catch(_ZTISt12system_error_nid_postfix, code); Catch(_ZTISt13runtime_error_nid_postfix, 5); diff --git a/core/libs/tests/GuestGenericCategory.cpp b/core/libs/tests/GuestGenericCategory.cpp new file mode 100644 index 000000000..e42958827 --- /dev/null +++ b/core/libs/tests/GuestGenericCategory.cpp @@ -0,0 +1,99 @@ +#include "prx/libc/include/GuestLocale.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include + +struct Category { const void* const* vtable; }; +struct ErrorCode { int value; const Category* category; }; +using Message = GuestLocale::String* (APS5_VABI *)(GuestLocale::String*, const Category*, int); +using Name = const char* (APS5_VABI *)(const Category*); +using Condition = ErrorCode (APS5_VABI *)(const Category*, int); + +extern "C" { +const char* APS5_VABI _ZSt13_Syserror_mapi_nid_postfix(int value); +const Category* APS5_VABI _ZSt16generic_categoryv_nid_postfix(); +GuestLocale::String* APS5_VABI _ZNKSt23_Generic_error_category7messageEi_nid_postfix( + GuestLocale::String* result, const Category* self, int value); +int* APS5_VABI __error_nid_postfix(); +void APS5_VABI _ZdlPv_nid_postfix(void* pointer); +} + +static void Require(bool value, int line) { + if (value) return; + std::fprintf(stderr, "generic category check failed at line %d\n", line); + std::exit(1); +} +#define REQUIRE(value) Require((value), __LINE__) + +static void CheckMessage(Message message, const Category* 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); +} + +int main() { + const std::array api {}; + ApplicationHeapRegister_nid_no_patch(api.data()); + const Category* category = _ZSt16generic_categoryv_nid_postfix(); + REQUIRE(category && category == _ZSt16generic_categoryv_nid_postfix()); + REQUIRE(std::strcmp(reinterpret_cast(category->vtable[2])(category), "generic") == 0); + const auto message = reinterpret_cast(category->vtable[3]); + const auto condition = reinterpret_cast(category->vtable[4])(category, 35); + REQUIRE(condition.value == 35 && condition.category == category); + + struct Expected { int value; const char* text; }; + const Expected expected[] { + {3, "no such process"}, {14, "bad address"}, {13, "permission denied"}, + {25, "inappropriate io control operation"}, {35, "operation would block"}, + {45, "not supported"}, {47, "address family not supported"}, {60, "timed out"}, + {78, "function not supported"}, {95, "state not recoverable"}, {96, "owner dead"} + }; + for (const auto& error : expected) { + *__error_nid_postfix() = 61; + const char* mapped = _ZSt13_Syserror_mapi_nid_postfix(error.value); + REQUIRE(mapped && std::strcmp(mapped, error.text) == 0 && *__error_nid_postfix() == 61); + CheckMessage(message, category, error.value, error.text); + CheckMessage(_ZNKSt23_Generic_error_category7messageEi_nid_postfix, category, error.value, error.text); + } + + unsigned known = 0; + for (int value = 1; value <= 96; ++value) { + const char* mapped = _ZSt13_Syserror_mapi_nid_postfix(value); + if (mapped) ++known; + CheckMessage(message, category, value, mapped ? mapped : "unknown error"); + } + REQUIRE(known == 72); + for (int value : {std::numeric_limits::min(), -1, 0, 15, 44, 97, std::numeric_limits::max()}) { + *__error_nid_postfix() = 61; + REQUIRE(_ZSt13_Syserror_mapi_nid_postfix(value) == nullptr && *__error_nid_postfix() == 61); + CheckMessage(message, category, value, "unknown error"); + } + GuestLocale::String first {}, second {}; + REQUIRE(message(&first, category, 25) == &first && message(&second, category, 11) == &second); + REQUIRE(first.pointer != second.pointer); + _ZdlPv_nid_postfix(second.pointer); + REQUIRE(std::strcmp(first.pointer, "inappropriate io control operation") == 0); + _ZdlPv_nid_postfix(first.pointer); + bool rejected = false; + try { message(nullptr, category, 22); } + catch (const std::invalid_argument&) { rejected = true; } + REQUIRE(rejected); + std::puts("guest generic category, FreeBSD error map and message ownership: PASS"); +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 08ed06628..a37693bde 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -62,4 +62,4 @@ - [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) and [swscanf_s](../../../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 mismatch is a matching failure and a field that is only a prefix, such as `0x` or `1e`, does not convert, as C11 requires; FreeBSD's `vfwscanf` reports an input failure for the first and converts the second. Integers out of range are truncated from `strtoll`/`strtoull`. It throws for `%p`, `%S`, `%C`, unknown length modifiers, scanset ranges, `%n` with suppression or width, and narrow `%s`, `%c` or `%[` input that the C-locale `wcrtomb` cannot convert - [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 host's [LockFileEx](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex) locks also restrict ordinary reads and writes through other handles, unlike FreeBSD's advisory locks, and do not restrict mapped views. Console behavior, signal interruption and concurrent closure of a descriptor while its lock call is pending are unverified. -- [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, taken to be the same text as in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and no `generic_category()` export returns it, so it compares unequal to a title's own `generic_category()`. Its `message` throws, as the guest `std::string` layout is not modelled, and so does its deleting destructor +- [std::_Throw_Cpp_error](../../../core/libs/prx/libc/src/exception/StandardExceptions.cpp) (libSceAmpr) follows Dinkumware's `thread0.cpp` as shipped with [Visual C++ 2013](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/thread0.cpp) and in [microsoft/STL](https://github.com/microsoft/STL/blob/da52dc0fcadbd83690cfd1ed32408d2847439493/stl/src/thread0.cpp), without console verification. `_Cpp_error` codes 0 to 6 throw a guest `std::system_error` whose `error_code` holds the FreeBSD errno of the `errc` value (EBUSY 16, EINVAL 22, ESRCH 3, ENOMEM 12, EPERM 1, EDEADLK 11, EAGAIN 35) and the generic category, and whose `what()` is the table message, `": "` and the generic category's message for that errno, selected by the first matching errno entry in VC12's [`syserror.cpp`](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/syserror.cpp); another code throws `std::out_of_range` on the host instead of reading past the table. The guest `system_error` is taken to be [VC12's](https://github.com/ganboing/crt.vc12/blob/f1d47126bf13d1c9dccaa6c53b72217fd0474968/src/system_error): the 16-byte `runtime_error` followed by `error_code {int, const error_category*}`, 32 bytes, so its copy constructor and assignment copy the code too. The generic category is a static libc object with VC12's `error_category` virtual order (destructors, `name`, `message`, `default_error_condition`, the two `equivalent`), not the console's `_Error_objects::_Generic_object`, and the new `generic_category()` export returns that object; a title's inline category object in a bundled libc can still have a different identity. Its `message` and direct message export construct the existing [GuestLocale::String](../../../core/libs/prx/libc/include/GuestLocale.hpp) model: a reserved eight-byte word, 16-byte inline buffer, length and capacity, with guest `operator new` storage above 15 bytes. This 40-byte string layout and virtual return ABI have not been verified against a console library. The deleting destructor still throws. `_Syserror_map` covers the 72 distinct Dinkumware `errc` values present in FreeBSD 11.4 [errno.h](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/sys/errno.h), returning null for other values; category messages use `unknown error` for those. First-match aliases are `EAGAIN`/`EWOULDBLOCK` -> `operation would block` and `ENOTSUP`/`EOPNOTSUPP` -> `not supported`. STREAMS-only errno assignments absent from that header (`ENODATA`, `ENOSR`, `ENOSTR`, `ETIME`) are not inferred