diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 56faa5a814..2cb3007787 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -140,6 +140,21 @@ endif() target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/FloatingPoint.cpp) if(BUILD_TESTING) + add_test_executable(guest_raise_except_tests ${LIBS_INCLUDE_DIR}/tests/GuestRaiseExcept.cpp) + target_include_directories(guest_raise_except_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_raise_except_tests PRIVATE libc ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_raise_except_tests) + add_test(NAME guest_raise_except COMMAND guest_raise_except_tests $) + set(raiseExceptTests guest_raise_except) + foreach(exceptionBit RANGE 0 5) + add_test(NAME guest_raise_except_trap_${exceptionBit} COMMAND guest_raise_except_tests $ ${exceptionBit}) + list(APPEND raiseExceptTests guest_raise_except_trap_${exceptionBit}) + endforeach() + set_tests_properties(${raiseExceptTests} PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(${raiseExceptTests} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_floating_point_tests ${LIBS_INCLUDE_DIR}/tests/GuestFloatingPoint.cpp) target_include_directories(guest_floating_point_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_floating_point_tests PRIVATE libc ${CMAKE_DL_LIBS}) diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index e54cf4ce73..a45fb9037d 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -334,6 +334,7 @@ int APS5_VABI fegetexceptflag_nid_postfix(std::uint16_t* flags, int exceptions); void APS5_VABI _Feraise_nid_postfix(int exceptions); int APS5_VABI fesetexceptflag_nid_postfix(const std::uint16_t* flags, int exceptions); int APS5_VABI feclearexcept_nid_postfix(int exceptions); +int APS5_VABI feraiseexcept_nid_postfix(int exceptions); int APS5_VABI feenableexcept_nid_postfix(int exceptions); int APS5_VABI fedisableexcept_nid_postfix(int exceptions); int APS5_VABI __feenableexcept_nid_postfix(int exceptions); diff --git a/core/libs/prx/libc/src/FloatingPoint.cpp b/core/libs/prx/libc/src/FloatingPoint.cpp index fa29724c23..9ba5350159 100644 --- a/core/libs/prx/libc/src/FloatingPoint.cpp +++ b/core/libs/prx/libc/src/FloatingPoint.cpp @@ -84,6 +84,14 @@ int APS5_VABI fesetexceptflag_nid_postfix(const std::uint16_t* flags, int except return 0; } +int APS5_VABI feraiseexcept_nid_postfix(int exceptions) { + const auto raised = static_cast(exceptions); + if ((raised & ~Exceptions) != 0) throw std::invalid_argument("feraiseexcept: unsupported exception bits"); + ReplaceFlags(raised, raised); + asm volatile("fwait"); + return 0; +} + int APS5_VABI feclearexcept_nid_postfix(int exceptions) { const auto mask = static_cast(exceptions) & Exceptions; if (mask == Exceptions) { diff --git a/core/libs/tests/GuestRaiseExcept.cpp b/core/libs/tests/GuestRaiseExcept.cpp new file mode 100644 index 0000000000..446a772297 --- /dev/null +++ b/core/libs/tests/GuestRaiseExcept.cpp @@ -0,0 +1,173 @@ +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +using Raise = int (APS5_VABI*)(int); +Raise raiseFlags; + +void Require(bool condition, const char* message) { + if (!condition) { std::fprintf(stderr,"feraiseexcept: %s\n",message); std::abort(); } +} + +struct Environment { std::array x87; std::uint32_t sse; }; +Environment Read() { + Environment state; + asm volatile("fnstenv %0\n\tfldenv %0" : "=m"(state.x87)); + state.sse=_mm_getcsr(); + return state; +} +void Write(const Environment& state) { + asm volatile("fldenv %0" : : "m"(state.x87)); + _mm_setcsr(state.sse); +} + +void Flags() { + const auto original=Read(); + for (const unsigned precision: {0u,0x200u,0x300u}) + for (unsigned rounding=0;rounding<16;++rounding) + for (unsigned old=0;old<64;++old) + for (unsigned raised=0;raised<64;++raised) { + auto before=original; + before.x87[0]=(before.x87[0]&~0xf3fu)|0x3f|precision|((rounding&3)<<10); + before.x87[1]=(before.x87[1]&~0xffu)|old; + before.sse=(before.sse&~0x603fu)|(0x3f<<7)|((rounding>>2)<<13)|(old^0x3f); + Write(before); + before=Read(); + *__error_nid_postfix()=123; + Require(raiseFlags(static_cast(raised))==0,"return value"); + const auto after=Read(); + Require(*__error_nid_postfix()==123,"errno preserved"); + Require((after.x87[1]&0x3f)==(old|raised),"x87 flags accumulate"); + Require((after.sse&0x3f)==((old^0x3f)|raised),"SSE flags accumulate independently"); + Require((after.x87[1]&~0x3fu)==(before.x87[1]&~0x3fu),"other x87 status preserved"); + Require((after.sse&~0x3fu)==(before.sse&~0x3fu),"SSE controls preserved"); + for (unsigned word: {0u,2u,3u,4u,5u,6u}) + Require(after.x87[word]==before.x87[word],"x87 controls, tags and pointers preserved"); + } + Write(original); + for (const int invalid: {-1,0x40,0x10000,std::numeric_limits::min(),std::numeric_limits::max()}) { + bool rejected=false; + try { raiseFlags(invalid); } catch (const std::exception&) { rejected=true; } + Require(rejected,"unverified exception bits rejected"); + const auto after=Read(); + Require(after.x87==original.x87 && after.sse==original.sse,"rejected mask preserves environment"); + } +} + +void DifferentMasks() { + const auto original=Read(); + for (unsigned bit=0;bit<6;++bit) { + auto seed=original; + seed.x87[0]|=0x3f; + seed.x87[1]&=~0xffu; + seed.sse=(seed.sse&~0x3fu)|(0x3f<<7); + seed.sse&=~(1u<<(bit+7)); + Write(seed); + const int result=raiseFlags(static_cast(1u<(100+flag); + Require(raiseFlags(static_cast(flag))==0,"thread return"); + const int raisedError=*__error_nid_postfix(); + ready.arrive_and_wait(); + const auto after=Read(); + Require((after.x87[1]&0x3f)==flag && (after.sse&0x3f)==flag,"thread-local exception flags"); + Require(raisedError==static_cast(100+flag),"thread-local errno"); + Write(original); + }; + std::thread a(worker,1),b(worker,4); + a.join(); b.join(); +} + +#ifdef _WIN32 +DWORD expectedException; +unsigned expectedFlag; +LONG CALLBACK Trap(EXCEPTION_POINTERS* exception) { + const auto code=exception->ExceptionRecord->ExceptionCode; + const auto& state=exception->ContextRecord->FltSave; + const auto active=state.StatusWord & ~state.ControlWord & 0x3f; + const bool denormalCode=expectedFlag==2 && code==EXCEPTION_FLT_INVALID_OPERATION; + if ((code==expectedException || denormalCode) && active==expectedFlag) std::_Exit(0); + return EXCEPTION_CONTINUE_SEARCH; +} +#else +void Trap(int) { std::_Exit(0); } +#endif + +void CheckTrap(unsigned bit) { + Require(bit<6,"trap bit"); +#ifdef _WIN32 + constexpr DWORD codes[]{EXCEPTION_FLT_INVALID_OPERATION,EXCEPTION_FLT_DENORMAL_OPERAND,EXCEPTION_FLT_DIVIDE_BY_ZERO,EXCEPTION_FLT_OVERFLOW,EXCEPTION_FLT_UNDERFLOW,EXCEPTION_FLT_INEXACT_RESULT}; + expectedException=codes[bit]; + expectedFlag=1u<(1u<(GetProcAddress(library,"feraiseexcept_nid_postfix")); +#else + const auto library=dlopen(argv[1],RTLD_NOW); + Require(library!=nullptr,"load libc"); + raiseFlags=reinterpret_cast(dlsym(library,"feraiseexcept_nid_postfix")); +#endif + Require(raiseFlags!=nullptr,"feraiseexcept export is missing"); + if (argc==3) CheckTrap(static_cast(std::atoi(argv[2]))); + Flags(); DifferentMasks(); Threads(); +#ifdef _WIN32 + Require(FreeLibrary(library)!=0,"release lookup handle"); +#else + Require(dlclose(library)==0,"release lookup handle"); +#endif + std::puts("feraiseexcept checks passed"); +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 0d77c17079..1230757d84 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -43,6 +43,7 @@ - [__atomic_load, __atomic_store, __atomic_exchange and __atomic_compare_exchange](../../../core/libs/prx/libc/src/AtomicBuiltins.cpp) (libc) - the generic, size-parameterised libatomic entry points are a single `ud2` in the `libc.prx` bundled with PPSA08427 (disassembly), so the console traps on any call; these throw instead - [Sized __atomic calls](../../../core/libs/prx/libc/src/DinkumwareAtomics.cpp) (libc) - the 36 `__atomic_load_N`, `__atomic_store_N`, `__atomic_exchange_N`, `__atomic_compare_exchange_N` and `__atomic_fetch_{add,sub,and,or,xor}_N` calls (N = 1, 2, 4, 8) are sequentially consistent whatever order they are given. The `libc.prx` bundled with PPSA08427, run side by side on random values and on every order from -1 to 7, switches on the order but always uses a locked instruction or CAS loop, except that a store with an order other than `memory_order_seq_cst` is a plain store, so these are never weaker than the console. Unlike `_Atomic_load_8` and the 8-byte `_Atomic_*` exchange and fetch calls above, the console accepts every order here, including release, acq_rel and invalid ones, and these do the same - [Dinkumware mutexes and conditions](../../../core/libs/prx/libc/src/Threads.cpp) (libc) use pointer-to-handle arguments, matching the console aerolib signatures reported in [#3079](https://github.com/boykopovar/AnyPS5/pull/3079). The title's `_Mtx_lock` maps `0` to `_Thrd_success` (0), `0x8002000b` to `_Thrd_busy` (3), and other failures to `_Thrd_error` (4); its internal mutex init recognizes recursive flag `0x100`, while every other type uses default attributes. The observed relock failure for a plain mutex suggests those defaults are error-checking, but that has not been confirmed on hardware. The title's `_Mtx_unlock` and `_Cnd_wait` return success regardless of their internal result. The host implementation treats invalid ownership preconditions as no-ops, and throws for invalid handles, host wait errors and destroying a locked mutex as debug guards; those cases are not confirmed against the console. +- [feraiseexcept](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc, NID `NIfFNcyeCTo`) - console behavior and module/library versions have not been measured. The signature follows the [public PS5 SDK](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/fenv.h); flag updates and x87 trap delivery follow [FreeBSD amd64 fenv.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/msun/amd64/fenv.c). All six defined exception bits are supported. Unlike the FreeBSD reference, which does not mask or validate the requested bits, this implementation rejects other bits with `std::invalid_argument` before modifying state; console behavior for those bits is unknown. This is distinct from `_Feraise`, which also modifies errno and adds inexact for range exceptions. - [_Mtx_timedlock](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `hPzYSd5Nasc`) follows the console's `libc.prx` at `0x5d50` (PPSA02530): it takes an absolute xtime and returns `_Thrd_timedout` (2) on expiry (`0x8002003c`) or 5 on other failures. On non-Windows hosts the native mutex and condition are `std::timed_mutex` and `std::condition_variable_any`; Windows keeps the `CRITICAL_SECTION`/`CONDITION_VARIABLE` pair because `std::condition_variable_any` failed `guest_libc_cnd_timed_wait` on the Windows runner, and its `_Mtx_timedlock` polls `TryEnterCriticalSection` until the deadline. Windows timing is not measured on the console. - [Floating-point compiler-rt builtins](../../../core/libs/prx/libc/src/FloatBuiltins.cpp) (libc) follow the `libc.prx` bundled with PPSA08427 (disassembly, and that binary run side by side on random inputs) where it differs from current compiler-rt: the soft-float `__add`, `__sub`, `__mul` and `__div` (`sf3`, `df3`) round to nearest, raise no exception flags and ignore MXCSR, here done with SSE under a default MXCSR that is restored afterwards; division flushes a quotient below the smallest normal to a signed zero; `__extendsfdf2` and `__truncdfsf2` copy NaN payloads unshifted; float-to-integer conversions do not saturate but shift the significand by a count masked to the shift width. `__mulsc3`, `__muldc3`, `__divsc3` and `__divdc3` fuse the same products as the console with `fma` and scale through the x87 like its `scalbn`, so their values match, but NaN payloads and exception flags can differ, as the host `fma` and `logb` are not the console's VEX FMA code and libm - [random, srandom, initstate, setstate](../../../core/libs/prx/libc/src/Random.cpp) (libc) - not measured: the state `setstate` returns and its null return for a rear position past the degree, `errno` after `initstate` below 8 bytes, a null state (throws here), an `initstate` seed other than 1, concurrent callers (one lock serialises every call here) and a title's own libc. Measured in a payload process on a PS5 (firmware 12.70): a fresh process draws the `srandom(1)` sequence; `srandom` seeds an additive feedback generator through a `good_rand` that reads its argument as an int32, turns 0 into 123459876 and applies `x = 16807 * (x % 127773) - 2836 * (x / 127773)`, adding 0x7fffffff to a negative result, which reproduces seeds 0, 1, 2, 12345, 0xffffffff and 0x80000000; `initstate` returns null at 0 and 7 bytes and selects types 0 to 4 by size as measured at 8, 15, 31, 32, 63, 64, 127, 128, 129, 255, 256, 257 and 1000 bytes.