Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions core/libs/prx/libc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
1 change: 1 addition & 0 deletions core/libs/prx/libc/include/Api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,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);
Expand Down
8 changes: 8 additions & 0 deletions core/libs/prx/libc/src/FloatingPoint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
173 changes: 173 additions & 0 deletions core/libs/tests/GuestRaiseExcept.cpp
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions docs/dev/debt/libc.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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.
Expand Down
Loading