Merge pull request #8903 from UnhealthyKraken/feat/libc-feraiseexcept

feat(libc): implement feraiseexcept
This commit is contained in:
Dean authored and GitHub committed 2026-10-27 10:52:51 +13:00
commit c74530e615
5 files changed
+198

No files matched your search

+15
View File
@@ -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 $<TARGET_FILE:libc>)
set(raiseExceptTests guest_raise_except)
foreach(exceptionBit RANGE 0 5)
add_test(NAME guest_raise_except_trap_${exceptionBit} COMMAND guest_raise_except_tests $<TARGET_FILE:libc> ${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:$<TARGET_FILE_DIR:libc>")
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})
+1
View File
@@ -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);
+8
View File
@@ -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<unsigned>(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<unsigned>(exceptions) & Exceptions;
if (mask == Exceptions) {
+173
View File
@@ -0,0 +1,173 @@
#include "prx/libc/include/Api.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <barrier>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <limits>
#include <stdexcept>
#include <thread>
#include <xmmintrin.h>
#ifdef _WIN32
#include <windows.h>
#else
#include <csignal>
#include <dlfcn.h>
#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<std::uint32_t,7> 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<int>(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<int>::min(),std::numeric_limits<int>::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<int>(1u<<bit));
const auto after=Read();
Write(original);
Require(result==0,"SSE-only unmask does not deliver an x87 trap");
Require((after.x87[1]&0x3f)==(1u<<bit) && (after.sse&0x3f)==(1u<<bit),"flags with differing exception masks");
Require(after.x87[0]==seed.x87[0] && (after.sse&~0x3fu)==(seed.sse&~0x3fu),"differing masks preserved");
seed.x87[0]&=~(1u<<bit);
seed.sse|=0x3f<<7;
Write(seed);
const int noFlags=raiseFlags(0);
const auto unchanged=Read();
Write(original);
Require(noFlags==0 && (unchanged.x87[1]&0x3f)==0 && (unchanged.sse&0x3f)==0,"empty mask does not trap with x87 enabled");
}
}
void Threads() {
std::barrier ready(2);
auto worker=[&](unsigned flag) {
const auto original=Read();
auto seed=original;
seed.x87[0]|=0x3f;
seed.x87[1]&=~0xffu;
seed.sse=(seed.sse&~0x3fu)|(0x3f<<7);
Write(seed);
ready.arrive_and_wait();
*__error_nid_postfix()=static_cast<int>(100+flag);
Require(raiseFlags(static_cast<int>(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<int>(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<<bit;
Require(AddVectoredExceptionHandler(1,Trap)!=nullptr,"install trap handler");
#else
Require(std::signal(SIGFPE,Trap)!=SIG_ERR,"install trap handler");
#endif
auto state=Read();
state.x87[0]=(state.x87[0]|0x3f)&~(1u<<bit);
state.x87[1]&=~0xffu;
state.sse=(state.sse&~0x3fu)|(0x3f<<7);
Write(state);
raiseFlags(static_cast<int>(1u<<bit));
Require(false,"enabled x87 exception did not trap");
}
int main(int argc,char** argv) {
Require(argc==2 || argc==3,"expected library path and optional trap bit");
#ifdef _WIN32
const auto library=LoadLibraryA(argv[1]);
Require(library!=nullptr,"load libc");
raiseFlags=reinterpret_cast<Raise>(GetProcAddress(library,"feraiseexcept_nid_postfix"));
#else
const auto library=dlopen(argv[1],RTLD_NOW);
Require(library!=nullptr,"load libc");
raiseFlags=reinterpret_cast<Raise>(dlsym(library,"feraiseexcept_nid_postfix"));
#endif
Require(raiseFlags!=nullptr,"feraiseexcept export is missing");
if (argc==3) CheckTrap(static_cast<unsigned>(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");
}
+1
View File
@@ -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.