diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 02a5c831fc..d3e9bed678 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -142,9 +142,9 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/FloatingPoint.cpp) if(BUILD_TESTING) 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) + target_link_libraries(guest_floating_point_tests PRIVATE libc ${CMAKE_DL_LIBS}) configure_windows_unwind(guest_floating_point_tests) - add_test(NAME guest_floating_point COMMAND guest_floating_point_tests) + add_test(NAME guest_floating_point COMMAND guest_floating_point_tests $) set_tests_properties(guest_floating_point PROPERTIES TIMEOUT 10) if(WIN32) set_tests_properties(guest_floating_point PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libc/src/Math.cpp b/core/libs/prx/libc/src/Math.cpp index c7541d4bc3..84177877d1 100644 --- a/core/libs/prx/libc/src/Math.cpp +++ b/core/libs/prx/libc/src/Math.cpp @@ -1313,7 +1313,7 @@ struct GuestFloatingPointEnvironment { unsigned char other[16]; std::uint32_t mxcsr; }; -static_assert(sizeof(GuestFloatingPointEnvironment) == 32 && offsetof(GuestFloatingPointEnvironment, mxcsr) == 28); +static_assert(sizeof(GuestFloatingPointEnvironment) == 32 && alignof(GuestFloatingPointEnvironment) == 4 && offsetof(GuestFloatingPointEnvironment, mxcsr) == 28); int APS5_VABI feupdateenv_nid_postfix(const GuestFloatingPointEnvironment* environment) { if (environment == nullptr) throw std::invalid_argument("feupdateenv: null environment"); @@ -1344,6 +1344,15 @@ int APS5_VABI fegetenv_nid_postfix(GuestFloatingPointEnvironment* environment) { return 0; } +int APS5_VABI feholdexcept_nid_postfix(GuestFloatingPointEnvironment* environment) { + if (environment == nullptr) throw std::invalid_argument("feholdexcept: null environment"); + const auto csr = _mm_getcsr(); + asm volatile("fnstenv %0\n\tfnclex" : "=m"(*environment)); + environment->mxcsr = csr; + _mm_setcsr((csr & ~0x3fu) | 0x1f80u); + return 0; +} + union SonyDconst { unsigned short _Word[8]; float _Float; diff --git a/core/libs/tests/GuestFloatingPoint.cpp b/core/libs/tests/GuestFloatingPoint.cpp index edfaeddf11..9d0de18775 100644 --- a/core/libs/tests/GuestFloatingPoint.cpp +++ b/core/libs/tests/GuestFloatingPoint.cpp @@ -2,11 +2,20 @@ #include #include #include +#include #include #include +#include #include #include #include +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif extern "C" { int* APS5_VABI __error_nid_postfix(); @@ -245,13 +254,122 @@ void CheckTraps() { WriteEnvironment(seeded); } -int main() { +using HoldExcept = int (APS5_VABI*)(Environment*); + +void CheckHold(HoldExcept hold, Environment* saved) { + const auto before = ReadEnvironment(); + *__error_nid_postfix() = 123; + Require(hold(saved) == 0, "feholdexcept succeeds"); + const auto after = ReadEnvironment(); + Require(std::memcmp(saved, &before, sizeof(before)) == 0, "feholdexcept saves the full original environment"); + Require((after.x87[0] & 0x3f) == 0x3f && (after.sse & 0x1f80) == 0x1f80, "feholdexcept masks both units"); + Require((after.x87[1] & 0x80ff) == 0 && (after.sse & 0x3f) == 0, "feholdexcept clears pending exceptions"); + Require((after.x87[1] & ~0x80ffu) == (before.x87[1] & ~0x80ffu), "feholdexcept preserves other x87 status bits"); + Require((after.x87[0] & ~0x3fu) == (before.x87[0] & ~0x3fu), "feholdexcept preserves x87 control settings"); + Require((after.sse & ~0x1fbfu) == (before.sse & ~0x1fbfu), "feholdexcept preserves SSE control settings"); + Require(after.x87[2] == before.x87[2] && std::memcmp(after.x87.data() + 3, before.x87.data() + 3, 16) == 0, "feholdexcept preserves x87 tags and instruction/data state"); + Require(*__error_nid_postfix() == 123, "feholdexcept preserves errno"); +} + +void CheckHoldExcept(HoldExcept hold) { + Require(hold != nullptr, "feholdexcept export is missing"); + static_assert(sizeof(Environment) == 32 && alignof(Environment) == 4 && offsetof(Environment, sse) == 28); + const auto original = ReadEnvironment(); + struct { + std::array before; + Environment value; + std::array after; + } output; + for (unsigned rounding = 0; rounding < 4; ++rounding) { + for (unsigned modes = 0; modes < 4; ++modes) { + for (unsigned mask = 0; mask < 64; ++mask) { + auto state = original; + const unsigned sseMask = (mask * 13 + 7) & 0x3f; + state.x87[0] = (state.x87[0] & ~0xc3fu) | (rounding << 10) | mask; + state.x87[1] = (state.x87[1] & ~0x80ffu) | (mask & 0x25); + state.sse = (state.sse & ~0xffffu) | (sseMask << 7) | (sseMask & 0x1a) | (((rounding + 1) & 3) << 13) | ((modes & 1) << 6) | ((modes & 2) << 14); + WriteEnvironment(state); + std::memset(&output, 0xa5, sizeof(output)); + CheckHold(hold, &output.value); + for (const auto byte : output.before) Require(byte == 0xa5, "feholdexcept underwrites output"); + for (const auto byte : output.after) Require(byte == 0xa5, "feholdexcept overwrites output"); + } + } + } + WriteEnvironment(original); + bool threw = false; + try { hold(nullptr); } catch (const std::invalid_argument&) { threw = true; } + Require(threw, "feholdexcept rejects null storage"); + const auto afterNull = ReadEnvironment(); + Require(std::memcmp(&original, &afterNull, sizeof(original)) == 0, "null feholdexcept preserves the environment"); +#ifdef _WIN32 + SYSTEM_INFO info{}; + GetSystemInfo(&info); + const std::size_t page = info.dwPageSize; + auto* memory = static_cast(VirtualAlloc(nullptr, page * 2, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(memory != nullptr, "allocate feholdexcept guard pages"); + DWORD oldProtection; + Require(VirtualProtect(memory + page, page, PAGE_NOACCESS, &oldProtection) != 0, "protect feholdexcept trailing page"); +#else + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + auto* memory = static_cast(mmap(nullptr, page * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(memory != MAP_FAILED, "allocate feholdexcept guard pages"); + Require(mprotect(memory + page, page, PROT_NONE) == 0, "protect feholdexcept trailing page"); +#endif + CheckHold(hold, reinterpret_cast(memory + page - sizeof(Environment))); +#ifdef _WIN32 + Require(VirtualFree(memory, 0, MEM_RELEASE) != 0, "release feholdexcept guard pages"); +#else + Require(munmap(memory, page * 2) == 0, "release feholdexcept guard pages"); +#endif + WriteEnvironment(original); + std::barrier ready(2); + const auto worker = [&](unsigned mode) { + const auto saved = ReadEnvironment(); + auto state = saved; + state.x87[0] = (state.x87[0] & ~0xc00u) | (mode << 10) | 0x3f; + state.x87[1] &= ~0x80ffu; + state.sse |= 0x1f80; + state.sse &= ~0x3fu; + WriteEnvironment(state); + ready.arrive_and_wait(); + Environment output; + CheckHold(hold, &output); + ready.arrive_and_wait(); + Require(fegetround_nid_postfix() == static_cast(mode << 10), "feholdexcept keeps thread-local rounding"); + WriteEnvironment(saved); + }; + std::thread first(worker, 1); + std::thread second(worker, 2); + first.join(); + second.join(); + const auto afterThreads = ReadEnvironment(); + Require(std::memcmp(&original, &afterThreads, sizeof(original)) == 0, "feholdexcept workers preserve the main environment"); +} + +int main(int argc, char** argv) { + Require(argc == 2, "expected libc path"); +#ifdef _WIN32 + const auto library = LoadLibraryA(argv[1]); + Require(library != nullptr, "load libc for feholdexcept"); + const auto hold = reinterpret_cast(GetProcAddress(library, "feholdexcept_nid_postfix")); +#else + const auto library = dlopen(argv[1], RTLD_NOW); + Require(library != nullptr, "load libc for feholdexcept"); + const auto hold = reinterpret_cast(dlsym(library, "feholdexcept_nid_postfix")); +#endif const auto original = ReadEnvironment(); CheckRounding(); CheckEnabledExceptions(); CheckFlags(); CheckTraps(); CheckThreads(); + CheckHoldExcept(hold); WriteEnvironment(original); +#ifdef _WIN32 + FreeLibrary(library); +#else + dlclose(library); +#endif std::puts("floating-point environment checks passed"); } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 86d761979d..2d4e0fc01d 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -19,6 +19,7 @@ - [fegetround, fesetround, fetestexcept, feclearexcept, fegetexceptflag and fesetexceptflag](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libSceLibcInternal, implemented in libc) follow FreeBSD 11.4 amd64 [fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): rounding modes use x87 control bits 10-11, exception flags include denormals (`0x3f`), and `fexcept_t` is 16 bits. They access the current host thread's x87 and MXCSR state. Under Rosetta 2 the six MXCSR exception masks always read back set, so an SSE exception the guest unmasks stays masked; the x87 masks and the other MXCSR bits are kept. This console ABI is assumed, as for `fegetenv`, not measured on hardware. Null flag pointers throw instead of faulting. - [feenableexcept, fedisableexcept, __feenableexcept, __fedisableexcept, fegettrapenable and fesettrapenable](../../../core/libs/prx/libc/src/FloatingPoint.cpp) (libc) change only the x87 masks under Rosetta 2: MXCSR keeps its six exception masks set there, so no SSE trap is enabled, and the enable and disable results, which report the traps enabled on both units, report none - [fegetenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `psx0YiAKm7k`) follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c) with the 32-byte `fenv_t` of [x86/fenv.h](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/x86/fenv.h): `fnstenv` fills the 28-byte x87 part, MXCSR follows at 0x1c, and the x87 control word is reloaded because `fnstenv` masks every x87 exception. That the console's `libSceLibcInternal` has the same code and layout is assumed, not read from its binary. A null pointer throws instead of faulting +- [feholdexcept](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `1kduKXMqx7k`) - console equivalence and module/library versions are unverified. The signature and 32-byte amd64 environment layout follow the public SDK [fenv.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/fenv.h); saving both units, masking exceptions and clearing pending flags follows FreeBSD 11 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/4554bee35405b36ea19e9e60d6abe6ea534e36e0/lib/msun/amd64/fenv.c). The NID comes from the public 3.20 stub inventory, not a module examined here. Null storage throws before modifying the environment, where the reference dereferences it - [feupdateenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `YXQ4gXamCrY`) follows FreeBSD 11.4 amd64 [fenv.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/msun/amd64/fenv.c): capture the old x87/MXCSR exception union (`0x3f`), restore the saved 32-byte environment, raise those flags in both units, then `fwait`. The console's layout, behavior and module versions are unmeasured; state belongs to the current host thread. A null input or MXCSR bits unsupported by the host's `FXSAVE` mask throw before changing floating-point state; the mask's architectural zero fallback is `0xffbf`. Console fault behavior for malformed environments is unknown. The separate `__fe_dfl_env` object remains unimplemented; no host `FE_DFL_ENV` sentinel is substituted. - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws. The console's low-address layout is not measured. Linux and Windows reserve free addresses from 64 KiB, except host mappings and low chunks the Linux address policy refuses; null hints still allocate from 8 GiB. macOS and ASan builds still start the arena at 8 GiB and cannot supply low 32-bit addresses. Guest thread creation ignores a stack address set in its attributes. A host-loaded executable is not moved, so a PIE above the arena gets no memory within 2 GiB of its code ([#5217](https://github.com/boykopovar/AnyPS5/issues/5217)) - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error