diff --git a/core/libs/prx/libSceLibcInternal/CMakeLists.txt b/core/libs/prx/libSceLibcInternal/CMakeLists.txt index 2655d73c0b..da60a49d7c 100644 --- a/core/libs/prx/libSceLibcInternal/CMakeLists.txt +++ b/core/libs/prx/libSceLibcInternal/CMakeLists.txt @@ -222,3 +222,40 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +target_sources(libSceLibcInternal PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/Sigvec.cpp) + +if(BUILD_TESTING AND (WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux")) + add_test_executable(guest_sigvec_modules_tests + ${LIBS_INCLUDE_DIR}/tests/GuestSigvecModules.cpp + ${LIBS_INCLUDE_DIR}/nid/src/NidCompute.cpp + ${LIBS_INCLUDE_DIR}/nid/src/Sha1.cpp) + target_include_directories(guest_sigvec_modules_tests PRIVATE ${LIBS_INCLUDE_DIR} ${LIBS_INCLUDE_DIR}/nid/include) + target_link_libraries(guest_sigvec_modules_tests PRIVATE ${CMAKE_DL_LIBS}) + set_source_files_properties(${LIBS_INCLUDE_DIR}/tests/GuestSigvecModules.cpp PROPERTIES COMPILE_OPTIONS "-fno-exceptions;-fno-asynchronous-unwind-tables") + if(MINGW) + target_compile_options(guest_sigvec_modules_tests PRIVATE -fasynchronous-unwind-tables) + target_link_options(guest_sigvec_modules_tests PRIVATE -static -static-libgcc -static-libstdc++) + endif() + add_library(sigvec_modules_exceptions SHARED ${LIBS_INCLUDE_DIR}/tests/SigvecModulesExceptions.cpp) + target_include_directories(sigvec_modules_exceptions PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(sigvec_modules_exceptions PRIVATE libc) + set_target_properties(sigvec_modules_exceptions PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) + configure_windows_unwind(sigvec_modules_exceptions) + add_dependencies(guest_sigvec_modules_tests libc libkernel libSceLibcInternal nid_patcher sigvec_modules_exceptions) + set(sigvecAllocatorArgument) + if(TARGET mimalloc) + set(sigvecAllocatorArgument -DALLOCATOR_DIRECTORY=$) + endif() + add_test(NAME guest_sigvec_modules COMMAND ${CMAKE_COMMAND} + -DEXCEPTION_PROBE=$ + -DPATCHER=$ + -DLIBC=$ + -DKERNEL=$ + -DINTERNAL=$ + ${sigvecAllocatorArgument} + -DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/sigvec-modules + -P ${LIBS_INCLUDE_DIR}/tests/SigvecModules.cmake + -- "$") + set_tests_properties(guest_sigvec_modules PROPERTIES TIMEOUT 20) +endif() diff --git a/core/libs/prx/libSceLibcInternal/Sigvec.cpp b/core/libs/prx/libSceLibcInternal/Sigvec.cpp new file mode 100644 index 0000000000..531b0a0cfb --- /dev/null +++ b/core/libs/prx/libSceLibcInternal/Sigvec.cpp @@ -0,0 +1,5 @@ +#include "prx/libc/include/LegacySignals.hpp" + +extern "C" int APS5_VABI sigvec_nid_postfix(int signal, const GuestSignalVector* replacement, GuestSignalVector* previous) { + return LegacySigvec_nid_no_patch(signal, replacement, previous); +} diff --git a/core/libs/prx/libSceLibcInternal/include/Api.hpp b/core/libs/prx/libSceLibcInternal/include/Api.hpp index a995513d96..97e30f397e 100644 --- a/core/libs/prx/libSceLibcInternal/include/Api.hpp +++ b/core/libs/prx/libSceLibcInternal/include/Api.hpp @@ -4,6 +4,7 @@ #include "prx/libSceLibcInternal/include/Spawn.hpp" #include "prx/libkernel/System/include/UserContext.hpp" #include "prx/libc/include/FileStream.hpp" +#include "prx/libc/include/LegacySignals.hpp" #include "SceTypes.hpp" #include diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 90509b697a..c0db6f2639 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -134,6 +134,20 @@ set_source_files_properties(${CMAKE_CURRENT_SOURCE_DIR}/src/MathExact.cpp TARGET_DIRECTORY libc PROPERTIES COMPILE_OPTIONS "-frounding-math") target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/BinaryWordIo.cpp) +target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/LegacySignals.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/SigvecExport.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_sigvec_tests ${LIBS_INCLUDE_DIR}/tests/GuestSigvec.cpp) + target_include_directories(guest_sigvec_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sigvec_tests PRIVATE libc libkernel) + configure_windows_unwind(guest_sigvec_tests) + add_test(NAME guest_sigvec COMMAND guest_sigvec_tests) + set_tests_properties(guest_sigvec PROPERTIES TIMEOUT 10) + if(WIN32) + set_tests_properties(guest_sigvec PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() target_include_directories(libc PRIVATE ${LIBS_INCLUDE_DIR} diff --git a/core/libs/prx/libc/include/Api.hpp b/core/libs/prx/libc/include/Api.hpp index 2e55965a17..a64e0da5b0 100644 --- a/core/libs/prx/libc/include/Api.hpp +++ b/core/libs/prx/libc/include/Api.hpp @@ -6,9 +6,11 @@ #include "prx/libc/include/GuestTime.hpp" #include "prx/libc/include/GuestLocale.hpp" #include "prx/libc/include/GuestDirectory.hpp" +#include "prx/libc/include/GuestErrno.hpp" #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/exceptions/Unwind.hpp" #include "prx/libc/include/exceptions/Runtime.hpp" +#include "prx/libc/include/LegacySignals.hpp" #include "SceTypes.hpp" #include #include @@ -104,6 +106,7 @@ struct FutureErrorObject { const void* vtable; GuestLocale::String message; Erro extern "C" { + int APS5_VABI std_execute_once_nid_postfix(int* flag, int (APS5_VABI *func)(void*, void*, void**), void* arg); void APS5_VABI LibcHeapGetTraceInfo_nid_postfix(LibcHeapInfo* info); int APS5_VABI _ZSt14_Atomic_assertPKcS0__nid_postfix(); @@ -830,7 +833,6 @@ std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const ch std::size_t APS5_VABI mbstowcs_nid_postfix(std::uint16_t* dest, const char* src, std::size_t size); void APS5_VABI exit_nid_postfix(int code); int* APS5_VABI libc_error_nid_postfix(); -int* APS5_VABI __error_nid_postfix(); int APS5_VABI atexit_nid_postfix(GuestExitCallback func); int APS5_VABI at_quick_exit_nid_postfix(GuestExitCallback func); int APS5_VABI daemon_nid_postfix(int noChdir, int noClose); diff --git a/core/libs/prx/libc/include/GuestErrno.hpp b/core/libs/prx/libc/include/GuestErrno.hpp new file mode 100644 index 0000000000..00d2aa0f77 --- /dev/null +++ b/core/libs/prx/libc/include/GuestErrno.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include "general/VabiMacros.hpp" + +extern "C" int* APS5_VABI __error_nid_postfix(); diff --git a/core/libs/prx/libc/include/LegacySignals.hpp b/core/libs/prx/libc/include/LegacySignals.hpp new file mode 100644 index 0000000000..0f99fb0ef0 --- /dev/null +++ b/core/libs/prx/libc/include/LegacySignals.hpp @@ -0,0 +1,27 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_LEGACYSIGNALS_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_LEGACYSIGNALS_HPP + +#include "prx/libkernel/include/Api.hpp" +#include +#include + +struct GuestSignalVector { + std::uintptr_t handler; + int mask; + int flags; +}; + +static_assert(sizeof(GuestSignalVector) == 16 && offsetof(GuestSignalVector, mask) == 8 && offsetof(GuestSignalVector, flags) == 12); + +using LegacySigaction_nid_no_patch = int (APS5_VABI *)(int, const GuestSigaction*, GuestSigaction*); + +extern "C" { + +void RegisterSigvec_nid_no_patch(LegacySigaction_nid_no_patch action); +int LegacySigvec_nid_no_patch(int signal, const GuestSignalVector* replacement, GuestSignalVector* previous); +int APS5_VABI sigvec_nid_disambig1(int signal, const GuestSignalVector* replacement, GuestSignalVector* previous); +int APS5_VABI sigvec_nid_postfix(int signal, const GuestSignalVector* replacement, GuestSignalVector* previous); + +} + +#endif diff --git a/core/libs/prx/libc/src/LegacySignals.cpp b/core/libs/prx/libc/src/LegacySignals.cpp new file mode 100644 index 0000000000..4e935693fc --- /dev/null +++ b/core/libs/prx/libc/src/LegacySignals.cpp @@ -0,0 +1,47 @@ +#include "prx/libc/libc.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include +#include +#include +#include + +namespace { + +std::atomic signalAction{}; + +} + +extern "C" { + +void RegisterSigvec_nid_no_patch(LegacySigaction_nid_no_patch action) { + signalAction.store(action, std::memory_order_release); +} + +int LegacySigvec_nid_no_patch(int signal, const GuestSignalVector* replacement, GuestSignalVector* previous) { + const auto forward = signalAction.load(std::memory_order_acquire); + if (forward == nullptr) throw std::runtime_error("sigvec: libkernel has not registered sigaction"); + GuestSigaction action{}; + if (replacement != nullptr) { + action.handler = replacement->handler; + action.flags = replacement->flags ^ 2; + action.mask.bits[0] = static_cast(replacement->mask); + } + std::optional access; + if (previous != nullptr) { + access.emplace(previous, sizeof(*previous)); + if (!access->Open()) { + *__error_nid_postfix() = 14; + return -1; + } + } + GuestSigaction old{}; + const int result = forward(signal, replacement == nullptr ? nullptr : &action, previous == nullptr ? nullptr : &old); + if (result == 0 && previous != nullptr) { + previous->handler = old.handler; + previous->mask = std::bit_cast(old.mask.bits[0]); + previous->flags = old.flags ^ 2; + } + return result; +} + +} diff --git a/core/libs/prx/libc/src/SigvecExport.cpp b/core/libs/prx/libc/src/SigvecExport.cpp new file mode 100644 index 0000000000..fcfeed0d19 --- /dev/null +++ b/core/libs/prx/libc/src/SigvecExport.cpp @@ -0,0 +1,5 @@ +#include "prx/libc/include/LegacySignals.hpp" + +extern "C" int APS5_VABI sigvec_nid_disambig1(int signal, const GuestSignalVector* replacement, GuestSignalVector* previous) { + return LegacySigvec_nid_no_patch(signal, replacement, previous); +} diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 79f5520b73..d4b23e646a 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -81,6 +81,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Error.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/ThermalSensors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Signals.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SigvecRegistration.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/SignalSets.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Notification.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/UserContext.cpp diff --git a/core/libs/prx/libkernel/System/src/SigvecRegistration.cpp b/core/libs/prx/libkernel/System/src/SigvecRegistration.cpp new file mode 100644 index 0000000000..495a909494 --- /dev/null +++ b/core/libs/prx/libkernel/System/src/SigvecRegistration.cpp @@ -0,0 +1,13 @@ +#include "prx/libc/include/LegacySignals.hpp" + +namespace { + +struct RegisterSigvecOnLoad { + RegisterSigvecOnLoad() { + RegisterSigvec_nid_no_patch(sigaction_nid_postfix); + } +}; + +const RegisterSigvecOnLoad registerSigvec; + +} diff --git a/core/libs/tests/GuestExitOrder.cpp b/core/libs/tests/GuestExitOrder.cpp index af813531ef..3520fd68fb 100644 --- a/core/libs/tests/GuestExitOrder.cpp +++ b/core/libs/tests/GuestExitOrder.cpp @@ -14,7 +14,7 @@ void Record(char value) { static_cast(!write(events[1], &value, 1)); } -void APS5_VABI GuestHandler(void*) { Record('G'); } +void APS5_VABI RecordGuestExit(void*) { Record('G'); } void HostHandler() { Record('H'); } @@ -46,11 +46,11 @@ bool ExitOrder(Setup setup, const char* expected) { } int main() { - if (!ExitOrder([] { __cxa_atexit_nid_postfix(GuestHandler, nullptr, nullptr); std::atexit(HostHandler); }, "GH")) { + if (!ExitOrder([] { __cxa_atexit_nid_postfix(RecordGuestExit, nullptr, nullptr); std::atexit(HostHandler); }, "GH")) { std::fputs("guest exit handlers must run before the host exit handlers\n", stderr); return 1; } - if (!ExitOrder([] { __cxa_atexit_nid_postfix(GuestHandler, nullptr, nullptr); atexit_nid_postfix(AtexitHandler); }, "AG")) { + if (!ExitOrder([] { __cxa_atexit_nid_postfix(RecordGuestExit, nullptr, nullptr); atexit_nid_postfix(AtexitHandler); }, "AG")) { std::fputs("atexit and __cxa_atexit handlers must run in reverse order of registration\n", stderr); return 1; } diff --git a/core/libs/tests/GuestSigvec.cpp b/core/libs/tests/GuestSigvec.cpp new file mode 100644 index 0000000000..1680cac705 --- /dev/null +++ b/core/libs/tests/GuestSigvec.cpp @@ -0,0 +1,171 @@ +#include "prx/libc/libc.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "sigvec: %s\n", message); + std::abort(); +} + +GuestSignalSet Mask() { + GuestSignalSet value{}; + Require(_sigprocmask_nid_postfix(0, nullptr, &value) == 0, "query mask"); + return value; +} + +bool Equal(const GuestSignalSet& first, const GuestSignalSet& second) { + for (int word = 0; word < 4; ++word) if (first.bits[word] != second.bits[word]) return false; + return true; +} + +bool Equal(const GuestSignalVector& first, const GuestSignalVector& second) { + return first.handler == second.handler && first.mask == second.mask && first.flags == second.flags; +} + +int received = 0; +int signalReceived = 0; + +void APS5_VABI Handler(int signal) { + ++received; + signalReceived = signal; +} + +void Vectors() { + constexpr int signal = 30; + const auto handler = reinterpret_cast(&Handler); + GuestSigaction initial{}; + Require(sigaction_nid_postfix(signal, nullptr, &initial) == 0, "save original action"); + for (int flags = 0; flags < 128; ++flags) { + const GuestSignalVector replacement{handler, std::bit_cast(0x80000005u), flags}; + Require(sigvec_nid_disambig1(signal, &replacement, nullptr) == 0, "install each supported flag combination"); + GuestSigaction actual{}; + Require(sigaction_nid_postfix(signal, nullptr, &actual) == 0, "query modern action"); + Require(actual.handler == handler && actual.flags == (flags ^ 2), "legacy interrupt flag reverses restart flag"); + Require(Equal(actual.mask, GuestSignalSet{{0x80000005, 0, 0, 0}}), "legacy vector zero extends mask"); + GuestSignalVector queried{}; + Require(sigvec_nid_disambig1(signal, nullptr, &queried) == 0 && Equal(queried, replacement), "query converts flags and mask back"); + } + const GuestSigaction extended{handler, 0x13, {{0x87654321, 0x12345678, 0x80000001, 0x7fffffff}}}; + Require(sigaction_nid_postfix(signal, &extended, nullptr) == 0, "install action with extended mask"); + GuestSignalVector queried{}; + *__error_nid_postfix() = 81; + Require(sigvec_nid_disambig1(signal, nullptr, &queried) == 0, "query extended action"); + Require(Equal(queried, GuestSignalVector{handler, std::bit_cast(0x87654321u), 0x11}), "query truncates modern mask to low word"); + Require(*__error_nid_postfix() == 81, "successful vector preserves errno"); + GuestSignalVector alias{handler, 7, 0}; + Require(sigvec_nid_disambig1(signal, &alias, &alias) == 0 && Equal(alias, queried), "aliased input and output returns old action"); + GuestSignalVector newState{}; + Require(sigvec_nid_disambig1(signal, nullptr, &newState) == 0 && Equal(newState, GuestSignalVector{handler, 7, 0}), "alias installs original input"); + const GuestSignalVector sentinel{0x12345678, -123, 0x345}; + const GuestSignalVector invalid{handler, -1, 0x80}; + for (const int number : std::array{-1, 0, 129, signal}) { + GuestSignalVector output = sentinel; + *__error_nid_postfix() = 0; + Require(sigvec_nid_disambig1(number, &invalid, &output) == -1, "invalid signal or flags fail"); + Require(*__error_nid_postfix() == 22 && Equal(output, sentinel), "failure preserves previous output and exposes errno"); + } + Require(sigvec_nid_disambig1(signal, nullptr, &queried) == 0 && Equal(queried, newState), "failed update leaves disposition unchanged"); + for (const int number : std::array{9, 17}) { + GuestSignalVector output = sentinel; + Require(sigvec_nid_disambig1(number, &newState, &output) == -1 && Equal(output, sentinel), "uninstallable handlers preserve output"); + } + Require(sigvec_nid_disambig1(signal, nullptr, nullptr) == 0, "null input and output query is accepted"); + const GuestSignalSet saved = Mask(); + const GuestSignalSet empty{}; + Require(_sigprocmask_nid_postfix(3, &empty, nullptr) == 0, "clear mask for delivery"); + received = 0; + const GuestSignalSet blocked{{1u << (signal - 1), 0, 0, 0}}; + Require(_sigprocmask_nid_postfix(1, &blocked, nullptr) == 0, "block installed handler"); + Require(raise_nid_postfix(signal) == 0 && received == 0, "blocked signal becomes pending"); + GuestSignalSet previous{}; + Require(_sigprocmask_nid_postfix(3, &empty, &previous) == 0 && previous.bits[0] == (1u << (signal - 1)), "unblock returns prior mask"); + Require(received == 1 && signalReceived == signal, "unblock delivers pending signal through guest handler"); + Require(raise_nid_postfix(signal) == 0 && received == 2, "unblocked delivery retains vector handler"); + Require(sigaction_nid_postfix(signal, &initial, nullptr) == 0, "restore disposition"); + Require(_sigprocmask_nid_postfix(3, &saved, nullptr) == 0, "restore mask after delivery"); +} + +void GuestBackedOutput() { + constexpr std::size_t page = 0x4000; + constexpr std::size_t bytes = page * 2; + std::int64_t physical = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, bytes, 0, 0, &physical) == 0, "allocate guest signal storage"); + void* mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, bytes, 3, 0, physical, 0) == 0 && mapping != nullptr, "map guest signal storage"); + Require(GuestArena::GuestArenaContains_nid_postfix(mapping, bytes), "output is not backed by the guest arena"); + auto* memory = static_cast(mapping); + { + GuestArena::HostWrite access(mapping, bytes); + Require(access.Open(), "initialize guest signal storage"); + std::memset(mapping, 0xA5, bytes); + } + constexpr int signal = 30; + GuestSigaction initial{}; + Require(sigaction_nid_postfix(signal, nullptr, &initial) == 0, "save guest-output disposition"); + const GuestSignalVector replacement{reinterpret_cast(&Handler), -123, 0}; + auto* output = reinterpret_cast(memory + page - 8); + Require(sigvec_nid_disambig1(signal, &replacement, output) == 0, "write complete vector across guest pages"); + Require(Equal(*output, GuestSignalVector{initial.handler, std::bit_cast(initial.mask.bits[0]), initial.flags ^ 2}), "guest vector conversion"); + Require(memory[page - 9] == 0xA5 && memory[page + 8] == 0xA5, "guest vector output escaped its bounds"); + *output = replacement; + Require(sigvec_nid_disambig1(signal, output, output) == 0 && Equal(*output, replacement), "guest input-output alias conversion"); +#ifdef _WIN32 + const GuestSignalVector sentinel{0x12345678, -123, 0x345}; + const GuestSignalVector changed{replacement.handler, 5, 2}; + const auto reject = [&](GuestSignalVector* destination, void* protectedPage) { + *destination = sentinel; + Require(sceKernelMprotect(protectedPage, page, 1) == 0, "protect guest vector output"); + { + GuestArena::HostWrite access(destination, sizeof(*destination)); + Require(!access.Open(), "read-only guest output unexpectedly opened"); + } + *__error_nid_postfix() = 73; + Require(sigvec_nid_disambig1(signal, &changed, destination) == -1 && *__error_nid_postfix() == 14, "read-only guest vector was not rejected"); + Require(Equal(*destination, sentinel), "rejected guest vector was partially overwritten"); + GuestSignalVector current{}; + Require(sigvec_nid_disambig1(signal, nullptr, ¤t) == 0 && Equal(current, replacement), "rejected output changed the disposition"); + Require(sigvec_nid_disambig1(signal, destination, destination) == -1 && *__error_nid_postfix() == 14, "read-only aliased vector was not rejected before mutation"); + Require(Equal(*destination, sentinel), "rejected aliased vector was changed"); + Require(sigvec_nid_disambig1(signal, nullptr, ¤t) == 0 && Equal(current, replacement), "rejected alias changed the disposition"); + Require(sceKernelMprotect(protectedPage, page, 3) == 0, "restore guest vector protection"); + }; + reject(reinterpret_cast(memory + 128), mapping); + reject(output, memory + page); +#endif + Require(sigaction_nid_postfix(signal, &initial, nullptr) == 0, "restore guest-output disposition"); + Require(sceKernelMunmap(reinterpret_cast(mapping), bytes) == 0, "unmap guest signal storage"); + Require(sceKernelReleaseDirectMemory(physical, bytes) == 0, "release guest signal storage"); +} + +void MissingBackend() { + const auto saved = Mask(); + const GuestSignalVector sentinel{0x12345678, -123, 0x345}; + GuestSignalVector output = sentinel; + const GuestSignalVector replacement{reinterpret_cast(&Handler), 7, 0}; + RegisterSigvec_nid_no_patch(nullptr); + bool threw = false; + try { + sigvec_nid_disambig1(30, &replacement, &output); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "missing backend throws"); + Require(Equal(output, sentinel) && Equal(Mask(), saved), "missing backend leaves output and masks untouched"); + RegisterSigvec_nid_no_patch(sigaction_nid_postfix); +} + +} + +int main() { + Vectors(); + GuestBackedOutput(); + MissingBackend(); +} diff --git a/core/libs/tests/GuestSigvecModules.cpp b/core/libs/tests/GuestSigvecModules.cpp new file mode 100644 index 0000000000..6a4c33fe01 --- /dev/null +++ b/core/libs/tests/GuestSigvecModules.cpp @@ -0,0 +1,237 @@ +#include "prx/libc/include/LegacySignals.hpp" +#include "prx/libc/include/GuestErrno.hpp" +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +namespace { + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "sigvec modules: %s\n", message); + std::abort(); +} + +class Module { +public: + explicit Module(const std::filesystem::path& path) { +#ifdef _WIN32 + handle = LoadLibraryExW(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); + if (handle == nullptr) std::fprintf(stderr, "LoadLibraryExW(%ls) failed: %lu\n", path.c_str(), GetLastError()); +#else + handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); + if (handle == nullptr) std::fprintf(stderr, "%s\n", dlerror()); +#endif + Require(handle != nullptr, "load actual patched module"); +#ifdef _WIN32 + HMODULE pinned = nullptr; + Require(GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_PIN | GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + reinterpret_cast(handle), &pinned) != 0 && pinned == handle, "pin actual module until process exit"); +#endif + } + + ~Module() { +#ifdef _WIN32 + FreeLibrary(handle); +#else + dlclose(handle); +#endif + } + + Module(const Module&) = delete; + Module& operator=(const Module&) = delete; + + void* Find(const char* name) const { +#ifdef _WIN32 + auto* result = reinterpret_cast(GetProcAddress(handle, name)); +#else + auto* result = dlsym(handle, name); +#endif + return result; + } + + void* Symbol(const char* name) const { + auto* result = Find(name); + Require(result != nullptr, "lookup actual native export"); + return result; + } + + void RequireOwner(void* address) const { +#ifdef _WIN32 + HMODULE owner = nullptr; + Require(GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(address), &owner) != 0 && owner == handle, "NID resolved outside requested DLL"); +#else + link_map* image = nullptr; + Dl_info info{}; + Require(dlinfo(handle, RTLD_DI_LINKMAP, &image) == 0 && image != nullptr, "read requested module identity"); + Require(dladdr(address, &info) != 0 && info.dli_fbase == reinterpret_cast(image->l_addr), "NID resolved through an ELF dependency"); +#endif + } + + template + T Function(const char* name) const { + return reinterpret_cast(Symbol(Nid::ComputeNid(name, "").c_str())); + } + +private: +#ifdef _WIN32 + HMODULE handle = nullptr; +#else + void* handle = nullptr; +#endif +}; + +using Sigvec = decltype(&sigvec_nid_postfix); +GuestSigaction recorded{}; +GuestSigaction answer{0x12345678, 0x13, {{0x87654321, 0x11111111, 0x22222222, 0x33333333}}}; +int calls = 0; +int result = 0; +bool sawReplacement = false; +bool sawPrevious = false; +int requestedSignal = 0; + +int APS5_VABI RecordAction(int signal, const GuestSigaction* replacement, GuestSigaction* previous) { + ++calls; + requestedSignal = signal; + sawReplacement = replacement != nullptr; + sawPrevious = previous != nullptr; + if (replacement != nullptr) recorded = *replacement; + if (result == 0 && previous != nullptr) *previous = answer; + return result; +} + +bool Equal(const GuestSignalVector& first, const GuestSignalVector& second) { + return first.handler == second.handler && first.mask == second.mask && first.flags == second.flags; +} + +void CheckSharedDisposition(const std::array& exports, const Module& libc) { + const auto error = libc.Function("__error"); + GuestSignalVector original{}; + Require(exports[0](30, nullptr, &original) == 0, "save actual kernel disposition"); + const GuestSignalVector first{1, std::bit_cast(0x80000005u), 3}; + const GuestSignalVector second{1, -123, 2}; + GuestSignalVector queried{}; + Require(exports[1](30, &first, nullptr) == 0 && exports[0](30, nullptr, &queried) == 0 && Equal(queried, first), "Internal install is not shared with libc"); + Require(exports[0](30, &second, nullptr) == 0 && exports[1](30, nullptr, &queried) == 0 && Equal(queried, second), "libc install is not shared with Internal"); + for (const auto function : exports) { + queried = first; + Require(function(0, &second, &queried) == -1 && *error() == 22 && Equal(queried, first), "actual backend failure changed module output"); + } + Require(exports[0](30, &original, nullptr) == 0, "restore actual kernel disposition"); +} + +void CheckConversions(const std::array& exports) { + const GuestSignalVector expected{answer.handler, std::bit_cast(answer.mask.bits[0]), answer.flags ^ 2}; + for (const auto function : exports) { + for (const int flags : {0, 1, 2, 3, -1}) { + const GuestSignalVector replacement{0x34567890, -1, flags}; + GuestSignalVector previous{}; + result = 0; + const int before = calls; + Require(function(30, &replacement, &previous) == 0 && calls == before + 1, "module wrapper did not call shared core once"); + Require(requestedSignal == 30 && sawReplacement && sawPrevious, "forwarded argument identity"); + Require(recorded.handler == replacement.handler && recorded.flags == (flags ^ 2), "firmware handler/flag conversion"); + Require(recorded.mask.bits[0] == 0xffffffffu && recorded.mask.bits[1] == 0 && recorded.mask.bits[2] == 0 && recorded.mask.bits[3] == 0, "firmware low mask conversion"); + Require(Equal(previous, expected), "firmware previous vector conversion"); + GuestSignalVector alias = replacement; + Require(function(30, &alias, &alias) == 0 && Equal(alias, expected), "module-owned alias conversion"); + Require(recorded.handler == replacement.handler && recorded.flags == (flags ^ 2) && recorded.mask.bits[0] == 0xffffffffu, "alias input changed before forwarding"); + } + result = 0; + Require(function(30, nullptr, nullptr) == 0 && !sawReplacement && !sawPrevious, "both null pointers were not forwarded"); + GuestSignalVector previous{}; + Require(function(30, nullptr, &previous) == 0 && !sawReplacement && sawPrevious && Equal(previous, expected), "null replacement query"); + const GuestSignalVector replacement{0x34567890, 7, 0}; + Require(function(30, &replacement, nullptr) == 0 && sawReplacement && !sawPrevious, "null previous output"); + for (const int failure : {-7, -1, 1}) { + result = failure; + previous = replacement; + Require(function(17, &replacement, &previous) == failure && Equal(previous, replacement), "forward failure changed output or result"); + } + } +} + +void CheckGuestOutput(const std::array& exports, const Module& kernel, const Module& libc) { + const auto allocate = kernel.Function("sceKernelAllocateDirectMemory"); + const auto map = kernel.Function("sceKernelMapDirectMemory"); + const auto unmap = kernel.Function("sceKernelMunmap"); + const auto release = kernel.Function("sceKernelReleaseDirectMemory"); + constexpr std::size_t page = 0x4000; + std::int64_t physical = 0; + Require(allocate(0, 0x7fffffffffll, page * 2, 0, 0, &physical) == 0, "allocate real guest module output"); + void* mapping = nullptr; + Require(map(&mapping, page * 2, 3, 0, physical, 0) == 0 && mapping != nullptr, "map real guest module output"); + auto* output = reinterpret_cast(static_cast(mapping) + page - 8); + const GuestSignalVector expected{answer.handler, std::bit_cast(answer.mask.bits[0]), answer.flags ^ 2}; + result = 0; + for (const auto function : exports) Require(function(30, nullptr, output) == 0 && Equal(*output, expected), "owned NID guest-backed output"); +#ifdef _WIN32 + const auto protect = kernel.Function("sceKernelMprotect"); + const auto error = libc.Function("__error"); + const GuestSignalVector sentinel{0x23456789, 5, 2}; + *output = sentinel; + Require(protect(static_cast(mapping) + page, page, 1) == 0, "protect module output tail"); + for (const auto function : exports) { + const int before = calls; + Require(function(30, &sentinel, output) == -1 && *error() == 14 && calls == before, "rejected module output mutated backend"); + Require(Equal(*output, sentinel), "rejected module output partially written"); + } + Require(protect(static_cast(mapping) + page, page, 3) == 0, "restore module output protection"); +#else + static_cast(libc); +#endif + Require(unmap(reinterpret_cast(mapping), page * 2) == 0, "unmap module output"); + Require(release(physical, page * 2) == 0, "release module output"); +} + +} + +int main(int argc, char** argv) { + Require(argc == 3 || (argc == 4 && std::string(argv[3]) == "libc-first"), "expected patched module directory, exception probe and optional load order"); + const auto directory = std::filesystem::absolute(argv[1]); + const Module first(directory / (argc == 4 ? "libc.prx" : "libSceLibcInternal.prx")); + const Module libc(directory / "libc.prx"); + const Module kernel(directory / "libkernel.prx"); + const Module internal(directory / "libSceLibcInternal.prx"); + auto* libcAddress = libc.Symbol("yhxKO9LYc8A"); + auto* internalAddress = internal.Symbol("yhxKO9LYc8A"); + libc.RequireOwner(libcAddress); + internal.RequireOwner(internalAddress); + Require(libcAddress != internalAddress, "both modules resolve to the same wrapper"); + std::array exports{reinterpret_cast(libcAddress), reinterpret_cast(internalAddress)}; + for (const auto function : exports) Require(function(30, nullptr, nullptr) == 0, "cold kernel registration missing"); + CheckSharedDisposition(exports, libc); + auto* registrationAddress = libc.Symbol("RegisterSigvec_nid_no_patch"); + libc.RequireOwner(registrationAddress); + auto* coreAddress = libc.Symbol("LegacySigvec_nid_no_patch"); + libc.RequireOwner(coreAddress); +#ifdef _WIN32 + Require(internal.Find("LegacySigvec_nid_no_patch") == nullptr, "Internal duplicated the private core"); +#else + Require(internal.Find("LegacySigvec_nid_no_patch") == coreAddress, "Internal does not use libc private core"); +#endif + const auto registration = reinterpret_cast(registrationAddress); + const Module exceptionProbe(directory / argv[2]); + auto* exceptionAddress = exceptionProbe.Symbol("SigvecMissingBackend_nid_no_patch"); + exceptionProbe.RequireOwner(exceptionAddress); + const auto checkMissingBackend = reinterpret_cast(exceptionAddress); + registration(nullptr); + Require(checkMissingBackend(exports[0], exports[1], coreAddress), "module wrapper has duplicate or stale callback state"); + registration(RecordAction); + CheckConversions(exports); + CheckGuestOutput(exports, kernel, libc); + registration(kernel.Function("sigaction")); + for (const auto function : exports) Require(function(30, nullptr, nullptr) == 0, "actual kernel registration restore"); + std::printf("sigvec NID ownership, shared callback/core, firmware conversions and guest output passed (%s first)\n", argc == 4 ? "libc" : "Internal"); +} diff --git a/core/libs/tests/SigvecModules.cmake b/core/libs/tests/SigvecModules.cmake new file mode 100644 index 0000000000..4a9f172f26 --- /dev/null +++ b/core/libs/tests/SigvecModules.cmake @@ -0,0 +1,59 @@ +cmake_policy(SET CMP0054 NEW) +math(EXPR testExecutableIndex "${CMAKE_ARGC} - 1") +math(EXPR separatorIndex "${CMAKE_ARGC} - 2") +if(NOT CMAKE_ARGV${separatorIndex} STREQUAL "--") + message(FATAL_ERROR "Expected the test executable after --") +endif() +set(TEST_EXECUTABLE "${CMAKE_ARGV${testExecutableIndex}}") + +file(MAKE_DIRECTORY "${WORK_DIR}") +execute_process(COMMAND "${PATCHER}" --write-export-list "${LIBC}" "${WORK_DIR}/libc.exports" + RESULT_VARIABLE status OUTPUT_VARIABLE output ERROR_VARIABLE error) +if(NOT status STREQUAL "0") + message(FATAL_ERROR "libc export list failed: ${output}${error}") +endif() +foreach(module LIBC KERNEL INTERNAL) + get_filename_component(name "${${module}}" NAME) + get_filename_component(library "${${module}}" NAME_WE) + file(COPY_FILE "${${module}}" "${WORK_DIR}/${name}") + if(module STREQUAL "LIBC") + execute_process(COMMAND "${PATCHER}" "${library}" "${WORK_DIR}/${name}" + RESULT_VARIABLE status OUTPUT_VARIABLE output ERROR_VARIABLE error) + else() + execute_process(COMMAND "${PATCHER}" "${library}" --preserve-export-list "${WORK_DIR}/libc.exports" "${WORK_DIR}/${name}" + RESULT_VARIABLE status OUTPUT_VARIABLE output ERROR_VARIABLE error) + endif() + if(NOT status STREQUAL "0") + message(FATAL_ERROR "${library} patch failed: ${output}${error}") + endif() +endforeach() +get_filename_component(probeName "${EXCEPTION_PROBE}" NAME) +file(COPY_FILE "${EXCEPTION_PROBE}" "${WORK_DIR}/${probeName}") +if(WIN32) + set(searchDirectories "${WORK_DIR}") + foreach(binary LIBC KERNEL INTERNAL EXCEPTION_PROBE TEST_EXECUTABLE) + get_filename_component(directory "${${binary}}" DIRECTORY) + list(APPEND searchDirectories "${directory}") + endforeach() + if(ALLOCATOR_DIRECTORY) + list(APPEND searchDirectories "${ALLOCATOR_DIRECTORY}") + endif() + list(REMOVE_DUPLICATES searchDirectories) + list(JOIN searchDirectories ";" searchPath) + set(environment "PATH=${searchPath};$ENV{PATH}") +else() + set(environment "LD_LIBRARY_PATH=${WORK_DIR}:$ENV{LD_LIBRARY_PATH}") +endif() +foreach(order Internal-first libc-first) + set(arguments "${WORK_DIR}" "${probeName}") + if(order STREQUAL "libc-first") + list(APPEND arguments libc-first) + endif() + execute_process(COMMAND "${CMAKE_COMMAND}" -E env "${environment}" + "${TEST_EXECUTABLE}" ${arguments} + RESULT_VARIABLE status OUTPUT_VARIABLE output ERROR_VARIABLE error) + if(NOT status STREQUAL "0") + message(FATAL_ERROR "sigvec ${order} lookup failed: ${output}${error}") + endif() + message("${output}") +endforeach() diff --git a/core/libs/tests/SigvecModulesExceptions.cpp b/core/libs/tests/SigvecModulesExceptions.cpp new file mode 100644 index 0000000000..79f1fb0abd --- /dev/null +++ b/core/libs/tests/SigvecModulesExceptions.cpp @@ -0,0 +1,27 @@ +#include "prx/libc/include/LegacySignals.hpp" +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +extern "C" bool SigvecMissingBackend_nid_no_patch(decltype(&sigvec_nid_postfix) first, + decltype(&sigvec_nid_postfix) second, const void* core) { +#ifdef _WIN32 + const auto libc = GetModuleHandleW(L"libc.prx"); + if (libc == nullptr || reinterpret_cast(GetProcAddress(libc, "LegacySigvec_nid_no_patch")) != core) return false; +#else + if (reinterpret_cast(&LegacySigvec_nid_no_patch) != core) return false; +#endif + for (const auto function : std::array{first, second}) { + bool threw = false; + try { + function(30, nullptr, nullptr); + } catch (const std::runtime_error& error) { + threw = std::strcmp(error.what(), "sigvec: libkernel has not registered sigaction") == 0; + } + if (!threw) return false; + } + return true; +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 0b753e2c18..42a6a7acae 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -9,6 +9,7 @@ - [frexpf](../../../core/libs/prx/libc/src/Math.cpp) (libc) - follows the bundled PPSA03766 implementation: exponent-bit decomposition, conditional single-precision subnormal scaling, zero exponent for zero/NaN/infinity, and unchanged signaling NaNs. PPSA07923 agrees on results/exponents but unconditionally scales finite inputs, additionally raising overflow/inexact on large values; that SDK variant is not reproduced. Masked-exception observations cover rounding, DAZ/FTZ and sticky flags; unmasked traps and other SDK versions are unverified. - [fesetenv](../../../core/libs/prx/libc/src/Math.cpp) (libSceLibcInternal, implemented in libc, NID `Q-bLp+b-RVY`) uses the 32-byte amd64 environment and x87/MXCSR restore sequence in the [public SDK fenv.h](https://github.com/ps5-payload-dev/sdk/blob/a7685ce8e97ce1f8a4f38253a41d54241c9124f1/include/freebsd/fenv.h). The console layout, behavior and module/library versions are unmeasured. Null pointers and MXCSR bits unsupported by the current host's `FXSAVE` mask throw before changing state; a zero mask uses the architectural `0xffbf` fallback. Console fault behavior for invalid environments is unverified. The separate `__fe_dfl_env` object remains missing and its console initializer is unknown. +- [sigvec](../../../core/libs/prx/libc/src/LegacySignals.cpp) (libc and libSceLibcInternal) has not been measured on a physical PS5 or run on macOS. Its Windows module-lookup fixture pins modules until process exit; explicit DLL unloading remains unverified. The cause of the reported unload crash is unknown. A contributor executed the firmware 13.42 export `yhxKO9LYc8A` from libSceLibcInternal in an emulator with recording `_sigaction`/`sigemptyset` imports; layout, conversion, aliasing and forwarded results match this implementation. Its vector ABI, low-word mask conversion and restart/interrupt flag inversion follow FreeBSD 11.4 [`sigcompat.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/compat-43/sigcompat.c) and [`signal.h`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/sys/signal.h). Calls use registered guest `sigaction`; absent registration throws before mutation. It inherits the [guest signal layer's gaps](libkernel.md): action masks, restart/reset flags and `SA_SIGINFO` delivery are not applied. New vectors clear the upper mask words; queries return only the low word. The previous-vector output is guarded before the action changes; a rejected host-write scope returns `EFAULT` without output or disposition mutation. Other invalid caller memory is undefined, as upstream. - [strtonum](../../../core/libs/prx/libc/src/StringExtensions.cpp) (libc) is unverified on the console. Its signature, decimal parsing, inclusive bounds, error strings, error precedence and errno preservation follow [FreeBSD 11.4 strtonum.c](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/stdlib/strtonum.c). Parsing uses the host `strtoll` with base 10. It is not exported by `libSceLibcInternal.sprx` on a PS5 (firmware 13.60), so a payload process cannot call it there - [a64l, l64a and l64a_r](../../../core/libs/prx/libc/src/Radix64.cpp) (libc, NIDs `XR3orje3XCg`, `lF2uY5zV6tg`, `Zv-2rBFdZV0`) are ported from FreeBSD 11.4 [a64l.c](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/libc/stdlib/a64l.c) and [l64a.c](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/lib/libc/stdlib/l64a.c): `a64l` maps every byte of the first six arithmetically, including bytes outside `./0-9A-Za-z`, and sign-extends the low 32 bits; `l64a_r` writes the digits that fit before returning -1 on a short buffer. Deviations from FreeBSD: `a64l(NULL)` returns 0, `l64a_r` returns -1 without writing for a null buffer or `buflen <= 0`, and `l64a` uses a per-thread buffer instead of one shared `static` buffer. The PS5 `libc` storage, parsing of invalid characters and error behaviour are unverified; the tests check the behaviour chosen here, not equivalence with the console.