diff --git a/.github/sanitizer-known-failures.txt b/.github/sanitizer-known-failures.txt index 0d7883838c03..af16c45fd77c 100644 --- a/.github/sanitizer-known-failures.txt +++ b/.github/sanitizer-known-failures.txt @@ -5,4 +5,5 @@ agc_performance_overlay address,undefined Runs past its 30 s ctest TIMEOUT in th guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first frame is not in the calling module', because the sanitizer adds a frame to backtrace(); #9240 starts the frames at the caller. guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock'). guest_math address,undefined ubuntu-latest's GCC 15 targets x86-64-v3 and fuses libc's float multiply-adds into FMA, so atanf misses the console bits by one ulp; #11059 builds libc with -ffp-contract=off. +guest_pthread_provided_stack address A thread on a caller stack returns through uc_link from a makecontext context on a larger stack that contains its own, and ASan, which still takes the caller stack for the current one, ignores the __asan_handle_no_return in setcontext_nid_postfix and reports a stack-buffer-overflow on the stale redzones, because makecontext and setcontext have no fiber annotations. guest_vorbis_dec undefined The bundled libvorbis left-shifts a negative value (3rdparty/vorbis/lib/psy.c:319); #11480 updates libvorbis to the upstream fix. diff --git a/core/libs/prx/libc/include/GuestStacks.hpp b/core/libs/prx/libc/include/GuestStacks.hpp index 2b729a9bd57a..2ceb05183e69 100644 --- a/core/libs/prx/libc/include/GuestStacks.hpp +++ b/core/libs/prx/libc/include/GuestStacks.hpp @@ -10,6 +10,8 @@ namespace GuestStacks { extern "C" { void GuestStackRegister_nid_no_patch(const void* pointer, std::size_t bytes); +void GuestStackEnter_nid_no_patch(const void* pointer, std::size_t bytes); +void GuestStackLeave_nid_no_patch(); void APS5_VABI GuestStackSwitch_nid_no_patch(std::uintptr_t stackPointer); } diff --git a/core/libs/prx/libc/src/GuestStacks.cpp b/core/libs/prx/libc/src/GuestStacks.cpp index 9b24019938c7..c4130d767e46 100644 --- a/core/libs/prx/libc/src/GuestStacks.cpp +++ b/core/libs/prx/libc/src/GuestStacks.cpp @@ -22,6 +22,8 @@ constexpr std::size_t DeallocationStackOffset = 0x1478; std::mutex rangesLock; std::map ranges; +thread_local StackBounds threadStack{}; +thread_local StackBounds hostStack{}; StackBounds CurrentBounds() { auto* tib = reinterpret_cast(NtCurrentTeb()); @@ -38,6 +40,12 @@ void ApplyBounds(const StackBounds& bounds) { *reinterpret_cast(teb + DeallocationStackOffset) = bounds.deallocation; } +bool ThreadBounds(std::uintptr_t stackPointer, StackBounds& bounds) { + if (stackPointer < threadStack.limit || stackPointer >= threadStack.base) return false; + bounds = threadStack; + return true; +} + bool RegisteredBounds(std::uintptr_t stackPointer, StackBounds& bounds) { std::lock_guard lock(rangesLock); const auto next = ranges.upper_bound(stackPointer); @@ -80,11 +88,36 @@ void GuestStackRegister_nid_no_patch(const void* pointer, std::size_t bytes) { #endif } +void GuestStackEnter_nid_no_patch(const void* pointer, std::size_t bytes) { +#ifdef _WIN32 + const auto low = reinterpret_cast(pointer); + hostStack = CurrentBounds(); + threadStack = {low + bytes, low, low}; + GuestArena::GuestArenaPinWritable_nid_postfix(pointer, bytes); + ApplyBounds(threadStack); +#else + (void)pointer; + (void)bytes; +#endif +} + +void GuestStackLeave_nid_no_patch() { +#ifdef _WIN32 + ApplyBounds(hostStack); + GuestArena::GuestArenaUnpinWritable_nid_postfix(reinterpret_cast(threadStack.limit), threadStack.base - threadStack.limit); + threadStack = {}; +#endif +} + void APS5_VABI GuestStackSwitch_nid_no_patch(std::uintptr_t stackPointer) { #ifdef _WIN32 + StackBounds target{}; + if (ThreadBounds(stackPointer, target)) { + ApplyBounds(target); + return; + } const auto current = CurrentBounds(); if (stackPointer >= current.deallocation && stackPointer < current.base) return; - StackBounds target{}; if (RegisteredBounds(stackPointer, target) || HostBounds(stackPointer, target)) ApplyBounds(target); #else (void)stackPointer; diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index f4b59fbaee71..0c3598946488 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -887,6 +887,21 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_stack PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(WIN32 OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_pthread_provided_stack_tests ${LIBS_INCLUDE_DIR}/tests/GuestPthreadProvidedStack.cpp) + target_include_directories(guest_pthread_provided_stack_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_compile_options(guest_pthread_provided_stack_tests PRIVATE -fno-omit-frame-pointer) + target_link_libraries(guest_pthread_provided_stack_tests PRIVATE libSceLibcInternal libkernel libc) + configure_windows_unwind(guest_pthread_provided_stack_tests) + add_test(NAME guest_pthread_provided_stack COMMAND guest_pthread_provided_stack_tests) + set_tests_properties(guest_pthread_provided_stack PROPERTIES TIMEOUT 20) + add_asan_test_options(guest_pthread_provided_stack detect_stack_use_after_return=0) + if(WIN32) + target_compile_definitions(guest_pthread_provided_stack_tests PRIVATE _WIN32_WINNT=0x0602) + set_tests_properties(guest_pthread_provided_stack PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endif() add_test_executable(guest_cond_timedwait_tests ${LIBS_INCLUDE_DIR}/tests/GuestCondTimedwait.cpp) target_include_directories(guest_cond_timedwait_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_cond_timedwait_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index d38a696c266c..8581a70f86ab 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -27,6 +27,7 @@ int APS5_VABI pthread_attr_get_np_nid_postfix(Pthread thread, PthreadAttr* attr) auto* p = *attr; p->_stacksize = thread->stackSize; p->stackAddress = thread->stackAddress; + if (thread->callerStack) p->_guardsize = 0; p->_detachstate = thread->_detached ? 1 : 0; p->_schedpriority = thread->priority.load(std::memory_order_relaxed); p->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed); diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index 46e1991860db..78a82cb2ba0b 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -145,6 +145,7 @@ struct PthreadPrivate { void* wakeEvent = nullptr; void* stackAddress = nullptr; std::size_t stackSize = 0; + bool callerStack = false; std::atomic waitCount{0}; std::atomic affinity{DEFAULT_THREAD_AFFINITY}; std::atomic priority{DEFAULT_THREAD_PRIORITY}; diff --git a/core/libs/prx/libkernel/Pthread/src/Attr.cpp b/core/libs/prx/libkernel/Pthread/src/Attr.cpp index 7a7d3371af22..ec7b582ff1a5 100644 --- a/core/libs/prx/libkernel/Pthread/src/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Attr.cpp @@ -94,6 +94,7 @@ int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr) { if (!live) return SCE_KERNEL_ERROR_ESRCH; (*attr)->_stacksize = thread->stackSize; (*attr)->stackAddress = thread->stackAddress; + if (thread->callerStack) (*attr)->_guardsize = 0; (*attr)->_detachstate = thread->_detached ? DETACH_DETACHED : DETACH_JOINABLE; (*attr)->_schedpriority = thread->priority.load(std::memory_order_relaxed); (*attr)->_schedpolicy = thread->schedPolicy.load(std::memory_order_relaxed); diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index f77d9f844609..6526e8a31395 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -13,6 +13,7 @@ #include "prx/libkernel/KernelErrors.hpp" #include "prx/libc/include/CpuTopology.hpp" #include "prx/libc/include/GuestAllocations.hpp" +#include "prx/libc/include/GuestStacks.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" #include #include @@ -53,6 +54,10 @@ extern "C" bool IsInitialPthread_nid_no_patch() { #endif } +#ifdef ANYPS5_ENABLE_ASAN +#include +#endif + static constexpr int SCE_OK = 0; static constexpr std::size_t GUEST_THREAD_NAME_CAPACITY = 32; static constexpr int GUEST_EINVAL = 22; @@ -94,6 +99,9 @@ struct GuestThreadState { int cancelState = GUEST_CANCEL_ENABLE; int cancelType = GUEST_CANCEL_DEFERRED; CleanupHandler* cleanupHandlers = nullptr; +#ifdef _WIN32 + std::uint64_t* exitJump = nullptr; +#endif }; static GuestThreadState& CurrentGuestState() { #ifdef _WIN32 @@ -117,9 +125,11 @@ static auto& guestStackMappings = *new std::map(thread->guestStack); - const auto stackStart = reinterpret_cast(thread->stackAddress); + const auto mappingEnd = mappingStart + thread->guestStackBytes; + const auto stackStart = thread->callerStack ? mappingEnd : reinterpret_cast(thread->stackAddress); + const auto stackEnd = thread->callerStack ? mappingEnd : stackStart + thread->stackSize; std::lock_guard lock(guestStackMappingsLock); - guestStackMappings[mappingStart] = {mappingStart + thread->guestStackBytes, stackStart, stackStart + thread->stackSize}; + guestStackMappings[mappingStart] = {mappingEnd, stackStart, stackEnd}; } static void UnregisterGuestStackMapping(void* mapping) { @@ -431,7 +441,7 @@ static void UnregisterStack(PthreadPrivate* self) { const auto [low, high] = found->second; for (auto it = protectedStackPages.begin(); it != protectedStackPages.end();) { if (it->first < high && it->first + it->second > low) { - RestoreStackPages(it->first, it->second); + if (!self->callerStack) RestoreStackPages(it->first, it->second); it = protectedStackPages.erase(it); } else { ++it; @@ -793,7 +803,7 @@ void ThreadCancel::EndWait() { BeginWait(nullptr); } -extern "C" void* APS5_VABI Aps5CallGuestEntry_nid_no_patch(PthreadEntry entry, void* arg); +extern "C" void* APS5_VABI Aps5CallGuestEntry_nid_no_patch(PthreadEntry entry, void* arg, std::uintptr_t stackTop); asm(".text\n" APS5_ASM_FUNCTION("Aps5CallGuestEntry_nid_no_patch") R"( @@ -801,16 +811,24 @@ asm(".text\n" push %rbp .cfi_adjust_cfa_offset 8 .cfi_rel_offset %rbp, 0 + mov %rsp, %rbp + .cfi_def_cfa_register %rbp + test %rdx, %rdx + jz 1f + mov %rdx, %rsp + and $-16, %rsp +1: + push %rbp + push %rbp push $0 - .cfi_adjust_cfa_offset 8 push $0 - .cfi_adjust_cfa_offset 8 mov %rsp, %rbp + .cfi_escape 0x0f, 0x05, 0x76, 0x10, 0x06, 0x23, 0x10 mov %rdi, %rax mov %rsi, %rdi call *%rax - add $16, %rsp - .cfi_adjust_cfa_offset -16 + mov 16(%rbp), %rsp + .cfi_def_cfa %rsp, 16 pop %rbp .cfi_adjust_cfa_offset -8 .cfi_restore %rbp @@ -844,7 +862,7 @@ static void RunThread(std::unique_ptr args) { pthread_sigmask(SIG_SETMASK, &signalMask, nullptr); #endif #endif - finishThread(self, Aps5CallGuestEntry_nid_no_patch(entry, arg)); + finishThread(self, Aps5CallGuestEntry_nid_no_patch(entry, arg, 0)); CurrentGuestState().current = nullptr; } @@ -970,18 +988,30 @@ static std::wstring ThreadDescription(const std::string& name) { return description; } +static void* APS5_VABI RunGuestThreadOnCallerStack_nid_no_patch(void* opaque) { + std::uint64_t exitJump[12]; + if (setjmp_nid_postfix(exitJump) == 0) { + CurrentGuestState().exitJump = exitJump; + RunThread(std::unique_ptr(static_cast(opaque))); + } + CurrentGuestState().exitJump = nullptr; + return nullptr; +} + static unsigned __stdcall StartNativeThread(void* opaque) { std::unique_ptr args(static_cast(opaque)); auto* self = args->guest->self; try { - ULONG_PTR low = 0; - ULONG_PTR high = 0; - GetCurrentThreadStackLimits(&low, &high); - if (high <= low || high - low < self->stackSize) - throw std::runtime_error("Cannot query guest thread stack"); - self->stackAddress = reinterpret_cast(high - self->stackSize); - if (!CommittedReadWrite(high - self->stackSize, high)) - throw std::runtime_error("Guest thread stack is not fully committed"); + if (!self->callerStack) { + ULONG_PTR low = 0; + ULONG_PTR high = 0; + GetCurrentThreadStackLimits(&low, &high); + if (high <= low || high - low < self->stackSize) + throw std::runtime_error("Cannot query guest thread stack"); + self->stackAddress = reinterpret_cast(high - self->stackSize); + if (!CommittedReadWrite(high - self->stackSize, high)) + throw std::runtime_error("Guest thread stack is not fully committed"); + } self->threadId = std::this_thread::get_id(); CurrentGuestState().current = self; if (!self->name.empty()) { @@ -997,22 +1027,51 @@ static unsigned __stdcall StartNativeThread(void* opaque) { return 0; auto guest = std::move(args->guest); args.reset(); - RunThread(std::move(guest)); + if (self->callerStack) { + GuestStacks::GuestStackEnter_nid_no_patch(self->stackAddress, self->stackSize); + Aps5CallGuestEntry_nid_no_patch(RunGuestThreadOnCallerStack_nid_no_patch, guest.release(), reinterpret_cast(self->stackAddress) + self->stackSize); + GuestStacks::GuestStackLeave_nid_no_patch(); + } else { + RunThread(std::move(guest)); + } CurrentGuestState().current = nullptr; ReleaseThread(self); return 0; } #else +static void* APS5_VABI RunGuestThread_nid_no_patch(void* opaque) { + std::jmp_buf exitJump; + if (_setjmp(exitJump) == 0) { + threadExitJump = &exitJump; + RunThread(std::unique_ptr(static_cast(opaque))); + } + threadExitJump = nullptr; + return nullptr; +} + +#ifdef ANYPS5_ENABLE_ASAN +static void* APS5_VABI RunGuestThreadOnCallerStack_nid_no_patch(void* opaque) { + const void* nativeStack = nullptr; + std::size_t nativeStackSize = 0; + __sanitizer_finish_switch_fiber(nullptr, &nativeStack, &nativeStackSize); + RunGuestThread_nid_no_patch(opaque); + __sanitizer_start_switch_fiber(nullptr, nativeStack, nativeStackSize); + return nullptr; +} +#endif + static void* StartNativeThread(void* opaque) { std::unique_ptr args(static_cast(opaque)); auto* self = args->guest->self; try { - std::uintptr_t low = 0; - std::uintptr_t high = 0; - const auto reserve = HostStackReserve(); - if (!HostStackLimits(&low, &high) || high - low < reserve + self->stackSize) - throw std::runtime_error("Cannot query guest thread stack"); - self->stackAddress = reinterpret_cast(high - reserve - self->stackSize); + if (!self->callerStack) { + std::uintptr_t low = 0; + std::uintptr_t high = 0; + const auto reserve = HostStackReserve(); + if (!HostStackLimits(&low, &high) || high - low < reserve + self->stackSize) + throw std::runtime_error("Cannot query guest thread stack"); + self->stackAddress = reinterpret_cast(high - reserve - self->stackSize); + } self->threadId = std::this_thread::get_id(); #if defined(__linux__) self->hostTid = static_cast(gettid()); @@ -1034,12 +1093,19 @@ static void* StartNativeThread(void* opaque) { ReleaseThread(self); } } guard{self}; - std::jmp_buf exitJump; - if (_setjmp(exitJump) == 0) { - threadExitJump = &exitJump; - RunThread(std::unique_ptr(guest)); + if (self->callerStack) { + const auto top = reinterpret_cast(self->stackAddress) + self->stackSize; +#ifdef ANYPS5_ENABLE_ASAN + void* fakeStack = nullptr; + __sanitizer_start_switch_fiber(&fakeStack, self->stackAddress, self->stackSize); + Aps5CallGuestEntry_nid_no_patch(RunGuestThreadOnCallerStack_nid_no_patch, guest, top); + __sanitizer_finish_switch_fiber(fakeStack, nullptr, nullptr); +#else + Aps5CallGuestEntry_nid_no_patch(RunGuestThread_nid_no_patch, guest, top); +#endif + } else { + RunGuestThread_nid_no_patch(guest); } - threadExitJump = nullptr; return nullptr; } #endif @@ -1070,14 +1136,20 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread p->createdSuspended = attr && (*attr)->_createSuspended; p->suspended.store(p->createdSuspended, std::memory_order_relaxed); p->pendingException.store(0, std::memory_order_relaxed); + if (attr && (*attr)->stackAddress) { + p->stackAddress = (*attr)->stackAddress; + p->callerStack = true; + } + if (p->callerStack && reinterpret_cast(p->stackAddress) > std::numeric_limits::max() - p->stackSize) + throw std::runtime_error("scePthreadCreate: caller stack range overflows the address space"); std::promise start; auto args = std::make_unique(ThreadArgs{entry, arg, p.get()}); #ifdef _WIN32 SYSTEM_INFO system{}; GetSystemInfo(&system); - if (p->stackSize < 16384 || p->stackSize > std::numeric_limits::max() - HOST_STACK_MARGIN - system.dwPageSize) + if (p->stackSize < 16384 || (!p->callerStack && p->stackSize > std::numeric_limits::max() - HOST_STACK_MARGIN - system.dwPageSize)) throw std::runtime_error("scePthreadCreate: invalid Windows stack size"); - const std::size_t nativeStack = (p->stackSize + HOST_STACK_MARGIN + system.dwPageSize - 1) / system.dwPageSize * system.dwPageSize; + const std::size_t nativeStack = ((p->callerStack ? 0 : p->stackSize) + HOST_STACK_MARGIN + system.dwPageSize - 1) / system.dwPageSize * system.dwPageSize; auto native = std::make_unique(NativeThreadArgs{std::move(args), start.get_future(), {}}); auto initialized = native->initialized.get_future(); const auto handle = _beginthreadex(nullptr, static_cast(nativeStack), StartNativeThread, native.get(), 0, nullptr); @@ -1105,7 +1177,7 @@ int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, Pthread const auto page = static_cast(sysconf(_SC_PAGESIZE)); if (p->stackSize < 16384 || p->stackSize > std::numeric_limits::max() - reserve - HOST_STACK_MARGIN - page) throw std::runtime_error("scePthreadCreate: invalid stack size"); - const std::size_t nativeStack = (p->stackSize + reserve + HOST_STACK_MARGIN + page - 1) / page * page; + const std::size_t nativeStack = ((p->callerStack ? 0 : p->stackSize) + reserve + HOST_STACK_MARGIN + page - 1) / page * page; sigset_t creatorMask; pthread_sigmask(SIG_SETMASK, nullptr, &creatorMask); args->signalMask = creatorMask; @@ -1266,6 +1338,8 @@ void APS5_VABI scePthreadExit(void* retval) { finishThread(self, retval); CurrentGuestState().current = nullptr; #ifdef _WIN32 + if (auto* jump = CurrentGuestState().exitJump) + longjmp_nid_postfix(jump, 1); ReleaseThread(self); _endthreadex(0); #else diff --git a/core/libs/tests/GuestPthreadCreateSuspended.cpp b/core/libs/tests/GuestPthreadCreateSuspended.cpp index 252c757579bc..85144907cf10 100644 --- a/core/libs/tests/GuestPthreadCreateSuspended.cpp +++ b/core/libs/tests/GuestPthreadCreateSuspended.cpp @@ -19,6 +19,7 @@ static constexpr int GUEST_SIGUSR1 = 30; static constexpr int GUEST_CREATE_DETACHED = 1; static constexpr int DETACHED_ROUNDS = 200; static constexpr auto SUSPENDED_WINDOW = std::chrono::milliseconds(50); +static constexpr std::size_t THREAD_STACK_SIZE = 256 * 1024; static void* const CANCELED = reinterpret_cast(std::uintptr_t{1}); static void* const FINISHED = reinterpret_cast(std::uintptr_t{7}); @@ -38,6 +39,7 @@ static std::atomic release{false}; static std::atomic stopResuming{false}; static std::atomic raised{0}; static std::atomic raisedOn{nullptr}; +alignas(16) static unsigned char threadStack[THREAD_STACK_SIZE]; static void* APS5_VABI Record(void* opaque) { static_cast(opaque)->ran.store(true); @@ -178,6 +180,7 @@ static void ReportsCreateSuspendedInAttrGet(int (APS5_VABI* get)(Pthread, Pthrea Require(pthread_attr_init_nid_postfix(&attr) == 0, "pthread_attr_init failed"); Require(get(suspended, &attr) == 0, "getting the attributes of a suspended thread failed"); Pthread copy = nullptr; + Require(pthread_attr_setstack_nid_postfix(&attr, threadStack, sizeof(threadStack)) == 0, "pthread_attr_setstack failed"); Require(pthread_create_nid_postfix(©, &attr, Record, &second) == 0, "pthread_create with the attributes of a suspended thread failed"); std::this_thread::sleep_for(SUSPENDED_WINDOW); Require(!second.ran.load(), "a thread created with the attributes of a suspended thread ran before it was resumed"); @@ -192,6 +195,7 @@ static void ReportsCreateSuspendedInAttrGet(int (APS5_VABI* get)(Pthread, Pthrea Require(get(running, &attr) == 0, "getting the attributes of a running thread failed"); Started third; Pthread plain = nullptr; + Require(pthread_attr_setstack_nid_postfix(&attr, threadStack, sizeof(threadStack)) == 0, "pthread_attr_setstack failed"); Require(pthread_create_nid_postfix(&plain, &attr, Record, &third) == 0, "pthread_create with the attributes of a running thread failed"); Require(pthread_join_nid_postfix(plain, &result) == 0 && result == FINISHED && third.ran.load(), "a thread created with the attributes of a running thread did not run"); release.store(true); diff --git a/core/libs/tests/GuestPthreadProvidedStack.cpp b/core/libs/tests/GuestPthreadProvidedStack.cpp new file mode 100644 index 000000000000..bddb9fd70545 --- /dev/null +++ b/core/libs/tests/GuestPthreadProvidedStack.cpp @@ -0,0 +1,655 @@ +#include "prx/libkernel/include/Api.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libSceLibcInternal/include/Api.hpp" +#include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp" +#include "prx/libkernel/Time/include/Time.hpp" +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include "prx/libc/include/GuestArena.hpp" +#include +#else +#include +#endif +#include "SceTypes.hpp" +#include "prx/libc/include/GuestStacks.hpp" +#include "prx/libc/include/specifics/itanium/UnwindAbi.hpp" +#include "prx/libkernel/Pthread/include/Cancel.hpp" +#include "prx/libkernel/System/include/UserContext.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +void* APS5_VABI Aps5CallGuestEntry_nid_no_patch(PthreadEntry entry, void* arg, std::uintptr_t stackTop); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_ESRCH = static_cast(0x80020003); +static constexpr int GUEST_ESRCH = 3; +static constexpr int DETACH_DETACHED = 1; +static constexpr std::size_t MINIMUM_STACK = 16384; +static constexpr std::size_t STACK_SIZES[] = {MINIMUM_STACK, 65536, 1u << 20}; +static constexpr std::size_t CONTEXT_STACK = 256 * 1024; +static constexpr int LIBC_EXIT_CODE = 0x5ce; +static constexpr int CLEANUP_ORDER[] = {3, 5, 4, 1}; + +thread_local unsigned char threadLocalBlock[512 * 1024]; +static PthreadKey key = 0; + +static void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "guest pthread provided stack: %s\n", what); + std::abort(); +} + +struct CallerStack { + std::unique_ptr memory; + void* base; + std::size_t size; + + explicit CallerStack(std::size_t bytes, std::size_t offset = 0) : memory(new unsigned char[bytes + offset]), base(memory.get() + offset), size(bytes) {} + CallerStack(void* address, std::size_t bytes) : base(address), size(bytes) {} + + bool Contains(std::uintptr_t address) const { + const auto start = reinterpret_cast(base); + return address >= start && address - start < size; + } + + void RequireWritable() const { + std::memset(base, 0x5a, size); + auto* bytes = static_cast(base); + for (std::size_t offset = 0; offset < size; ++offset) + Require(bytes[offset] == 0x5a, "every byte of the caller's stack stays writable and readable after the thread ends"); + } +}; + +static bool OnCallerBounds(const CallerStack& stack) { + const bool framed = stack.Contains(reinterpret_cast(__builtin_frame_address(0))); +#ifdef _WIN32 + ULONG_PTR low = 0; + ULONG_PTR high = 0; + GetCurrentThreadStackLimits(&low, &high); + return framed && low == reinterpret_cast(stack.base) && high == low + stack.size; +#else + return framed; +#endif +} + +enum class Setter { SceStack, SceAddressAndSize, PosixStack }; +enum class Exit { Return, Sce, Libc }; + +struct Worker { + const CallerStack* stack = nullptr; + const CallerStack* detour = nullptr; + Setter setter = Setter::SceStack; + Exit exit = Exit::Return; + std::uintptr_t local = 0; +#ifdef _WIN32 + void* hostThread = nullptr; +#else + int hostTid = 0; +#endif + int cleanups = 0; + int order[std::size(CLEANUP_ORDER)] = {}; + Pthread self = nullptr; + std::uintptr_t destructorLocal = 0; + int destructors = 0; + bool destructorIdentity = false; + std::atomic ready{false}; + + Worker() = default; + Worker(const Worker&) = delete; + Worker& operator=(const Worker&) = delete; + ~Worker() { +#ifdef _WIN32 + if (hostThread) CloseHandle(hostThread); +#endif + } +}; + +static void RequireReported(Pthread thread, const CallerStack& stack, bool live) { + PthreadAttr attr = nullptr; + void* address = nullptr; + std::size_t size = 0; + std::size_t guard = 0; + Require(scePthreadAttrInit(&attr) == SCE_OK && scePthreadAttrSetguardsize(&attr, 8192) == SCE_OK, "initialize a query attribute with a guard"); + if (live) { + Require(scePthreadAttrGet(thread, &attr) == SCE_OK, "scePthreadAttrGet on a live thread with a caller stack"); + Require(scePthreadAttrGetstack(&attr, &address, &size) == SCE_OK && address == stack.base && size == stack.size, "scePthreadAttrGet reports the caller's base and size unchanged"); + Require(scePthreadAttrGetguardsize(&attr, &guard) == SCE_OK && guard == 0, "scePthreadAttrGet reports no guard for a caller stack"); + Require(scePthreadAttrSetguardsize(&attr, 12288) == SCE_OK, "preseed the query attribute's guard again"); + address = nullptr; + size = 0; + Require(pthread_attr_get_np_nid_postfix(thread, &attr) == 0, "pthread_attr_get_np on a thread with a caller stack"); + Require(pthread_attr_getstack_nid_postfix(&attr, &address, &size) == 0 && address == stack.base && size == stack.size, "pthread_attr_get_np reports the caller's base and size unchanged"); + Require(pthread_attr_getguardsize_nid_postfix(&attr, &guard) == 0 && guard == 0, "pthread_attr_get_np reports no guard for a caller stack"); + } else { + Require(scePthreadAttrGet(thread, &attr) == SCE_KERNEL_ERROR_ESRCH, "scePthreadAttrGet answers ESRCH for a joined thread with a caller stack"); + Require(pthread_attr_get_np_nid_postfix(thread, &attr) == GUEST_ESRCH, "pthread_attr_get_np answers ESRCH for a joined thread with a caller stack"); + } + Require(scePthreadAttrDestroy(&attr) == SCE_OK, "destroy the query attribute"); +} + +[[gnu::noinline]] static bool ChainEndsOnStack(const CallerStack& stack) { + auto* frame = static_cast(__builtin_frame_address(0)); + for (int depth = 0; depth < 100; ++depth) { + const auto address = reinterpret_cast(frame); + if (!stack.Contains(address) || !stack.Contains(address + 2 * sizeof(std::uintptr_t) - 1)) return false; + if (frame[1] == 0) return frame[0] == 0; + if (frame[0] <= address) return false; + frame = reinterpret_cast(frame[0]); + } + return false; +} + +static void WaitHostThreadGone(const Worker& worker) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!worker.ready.load(std::memory_order_acquire)) { + Require(std::chrono::steady_clock::now() < deadline, "a detached worker starts on the caller's stack"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +#ifdef _WIN32 + const auto remaining = std::chrono::duration_cast(deadline - std::chrono::steady_clock::now()).count(); + Require(WaitForSingleObject(worker.hostThread, static_cast(std::max(remaining, 0))) == WAIT_OBJECT_0, "a detached worker's host thread ends"); +#else + const auto task = "/proc/self/task/" + std::to_string(worker.hostTid); + while (access(task.c_str(), F_OK) == 0) { + Require(std::chrono::steady_clock::now() < deadline, "a detached worker's host thread ends"); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +#endif +} + +static void PublishHostThread(Worker& worker) { +#ifdef _WIN32 + Require(DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &worker.hostThread, SYNCHRONIZE, FALSE, 0) != 0, "publish the worker's native thread handle"); +#else + worker.hostTid = static_cast(gettid()); +#endif +} + +static void APS5_VABI DestroyValue(void* opaque) { + auto& worker = *static_cast(opaque); + volatile unsigned char local = 0x24; + Require(OnCallerBounds(*worker.stack), "the key destructor runs inside the caller's exact native stack bounds"); + worker.destructorLocal = reinterpret_cast(&local); + worker.destructorIdentity = scePthreadSelf() == worker.self && scePthreadGetspecific(key) == nullptr && local == 0x24; + ++worker.destructors; +} + +static void BindKey(Worker& worker, void* opaque) { + worker.self = scePthreadSelf(); + if (worker.exit != Exit::Libc) { + Require(scePthreadSetspecific(key, opaque) == SCE_OK, "bind the worker to its key on the caller's stack"); + return; + } + Require(_Thrd_current_nid_postfix() == worker.self, "_Thrd_current forwards to the thread on the caller's stack"); + Require(_Tss_set_nid_postfix(key, opaque) == 0 && _Tss_get_nid_postfix(key) == opaque, "_Tss_set and _Tss_get forward on the caller's stack"); +} + +struct CleanupTag { + Worker* worker; + int value; +}; + +static void APS5_VABI RecordCleanup(void* opaque) { + auto& tag = *static_cast(opaque); + Require(OnCallerBounds(*tag.worker->stack), "a cleanup handler runs inside the caller's exact native stack bounds"); + if (tag.worker->cleanups < static_cast(std::size(CLEANUP_ORDER))) tag.worker->order[tag.worker->cleanups] = tag.value; + ++tag.worker->cleanups; +} + +static void PushMixedCleanups(Worker& worker, CleanupTag (&tags)[5], CleanupHandler (&infos)[2]) { + for (int index = 0; index < 5; ++index) tags[index] = {&worker, index + 1}; + __pthread_cleanup_push_imp_nid_postfix(RecordCleanup, &tags[0], &infos[0]); + pthread_cleanup_push_nid_postfix(RecordCleanup, &tags[1]); + pthread_cleanup_pop_nid_postfix(0); + pthread_cleanup_push_nid_postfix(RecordCleanup, &tags[2]); + __pthread_cleanup_pop_imp_nid_postfix(1); + pthread_cleanup_push_nid_postfix(RecordCleanup, &tags[3]); + __pthread_cleanup_push_imp_nid_postfix(RecordCleanup, &tags[4], &infos[1]); + pthread_cleanup_pop_nid_postfix(1); +} + +static void RequireCleanups(const Worker& worker) { + Require(worker.cleanups == static_cast(std::size(CLEANUP_ORDER)), "the stack and heap cleanup handlers left on a caller stack ran exactly once each"); + Require(std::equal(std::begin(CLEANUP_ORDER), std::end(CLEANUP_ORDER), worker.order), "the stack and heap cleanup handlers ran in LIFO order across both push and pop interfaces"); +} + +static void Detour(Worker& worker); + +static void* APS5_VABI Entry(void* opaque) { + auto& worker = *static_cast(opaque); + threadLocalBlock[0] = 1; + threadLocalBlock[sizeof(threadLocalBlock) - 1] = 2; + volatile unsigned char local = 0x42; + worker.local = reinterpret_cast(&local); + Require(OnCallerBounds(*worker.stack), "the entry runs inside the caller's exact native stack bounds"); + BindKey(worker, opaque); + RequireReported(worker.self, *worker.stack, true); + Require(ChainEndsOnStack(*worker.stack), "the frame chain stays on the caller's stack and ends at a zero record"); + Require(threadLocalBlock[0] == 1 && threadLocalBlock[sizeof(threadLocalBlock) - 1] == 2 && local == 0x42, "static TLS and the entry's locals survive on a caller stack"); + if (worker.detour) Detour(worker); + PublishHostThread(worker); + worker.ready.store(true, std::memory_order_release); + if (worker.exit == Exit::Return) return opaque; + CleanupTag tags[5]; + CleanupHandler infos[2]{}; + PushMixedCleanups(worker, tags, infos); + if (worker.exit == Exit::Sce) scePthreadExit(opaque); + _Thrd_exit_nid_postfix(LIBC_EXIT_CODE); +} + +static void* APS5_VABI CancelEntry(void* opaque) { + auto& worker = *static_cast(opaque); + CleanupTag tags[5]; + CleanupHandler infos[2]{}; + PushMixedCleanups(worker, tags, infos); + volatile unsigned char local = 0; + worker.local = reinterpret_cast(&local); + Require(OnCallerBounds(*worker.stack), "the cancelable entry runs inside the caller's exact native stack bounds"); + BindKey(worker, opaque); + worker.ready.store(true, std::memory_order_release); + for (;;) sceKernelUsleep_nid_postfix(1000); +} + +static Pthread Start(Worker& worker, PthreadEntry entry, bool detached) { + const auto& stack = *worker.stack; + PthreadAttr attr = nullptr; + std::size_t guard = 0; + Require(scePthreadAttrInit(&attr) == SCE_OK && scePthreadAttrSetguardsize(&attr, 65536) == SCE_OK, "initialize the creation attribute with a guard"); + switch (worker.setter) { + case Setter::SceStack: + Require(scePthreadAttrSetstack(&attr, stack.base, stack.size) == SCE_OK, "scePthreadAttrSetstack accepts the caller's stack"); + break; + case Setter::SceAddressAndSize: + Require(scePthreadAttrSetstackaddr(&attr, stack.base) == SCE_OK, "scePthreadAttrSetstackaddr accepts the caller's address"); + Require(scePthreadAttrSetstacksize(&attr, stack.size) == SCE_OK, "scePthreadAttrSetstacksize accepts the caller's size"); + break; + case Setter::PosixStack: + Require(pthread_attr_setstack_nid_postfix(&attr, stack.base, stack.size) == 0, "pthread_attr_setstack accepts the caller's stack"); + break; + } + if (detached) Require(scePthreadAttrSetdetachstate(&attr, DETACH_DETACHED) == SCE_OK, "request a detached thread"); + Pthread thread = nullptr; + if (worker.setter == Setter::PosixStack) + Require(pthread_create_nid_postfix(&thread, &attr, entry, &worker) == 0, "pthread_create on the caller's stack"); + else + Require(scePthreadCreate(&thread, &attr, entry, &worker, "provided stack") == SCE_OK, "scePthreadCreate on the caller's stack"); + Require(scePthreadAttrGetguardsize(&attr, &guard) == SCE_OK && guard == 65536, "the creation attribute keeps its configured guard after create"); + Require(scePthreadAttrDestroy(&attr) == SCE_OK, "destroy the creation attribute right after create"); + return thread; +} + +static void* Join(const Worker& worker, Pthread thread) { + void* result = nullptr; + if (worker.setter == Setter::PosixStack) + Require(pthread_join_nid_postfix(thread, &result) == 0, "pthread_join a thread on the caller's stack"); + else + Require(scePthreadJoin(thread, &result) == SCE_OK, "scePthreadJoin a thread on the caller's stack"); + return result; +} + +static void RequireDestroyed(const Worker& worker, const CallerStack& stack) { + Require(worker.destructors == 1, "the key destructor ran once for a thread on the caller's stack"); + Require(worker.destructorIdentity, "the key destructor ran on the ending thread with its value cleared"); + Require(stack.Contains(worker.destructorLocal), "the key destructor's local lies inside the caller's stack"); +} + +static void RunJoined(const CallerStack& stack, Setter setter, Exit exit, const CallerStack* detour = nullptr) { + Worker worker; + worker.stack = &stack; + worker.detour = detour; + worker.setter = setter; + worker.exit = exit; + Pthread thread = Start(worker, Entry, false); + void* const expected = exit == Exit::Libc ? reinterpret_cast(std::uintptr_t{LIBC_EXIT_CODE}) : &worker; + Require(Join(worker, thread) == expected, "the entry's return value, scePthreadExit value or _Thrd_exit code reaches join"); + Require(stack.Contains(worker.local), "the entry's local lies inside the caller's stack"); + if (exit == Exit::Return) Require(worker.cleanups == 0, "a returning entry runs no cleanup handler"); + else RequireCleanups(worker); + RequireDestroyed(worker, stack); + RequireReported(thread, stack, false); + stack.RequireWritable(); +} + +static void RunDetached(const CallerStack& stack, Setter setter, bool detachAtCreate) { + Worker worker; + worker.stack = &stack; + worker.setter = setter; + Pthread thread = Start(worker, Entry, detachAtCreate); + if (!detachAtCreate) Require(scePthreadDetach(thread) == SCE_OK, "scePthreadDetach a thread on the caller's stack"); + WaitHostThreadGone(worker); + Require(stack.Contains(worker.local), "a detached entry's local lies inside the caller's stack"); + RequireDestroyed(worker, stack); + stack.RequireWritable(); +} + +static void RunCanceled(const CallerStack& stack, Setter setter) { + Worker worker; + worker.stack = &stack; + worker.setter = setter; + Pthread thread = Start(worker, CancelEntry, false); + while (!worker.ready.load(std::memory_order_acquire)) std::this_thread::yield(); + Require(scePthreadCancel(thread) == SCE_OK, "cancel a thread on the caller's stack"); + Require(Join(worker, thread) == GUEST_CANCELED, "a canceled thread on the caller's stack joins with PTHREAD_CANCELED"); + RequireCleanups(worker); + Require(stack.Contains(worker.local), "the canceled entry's local lies inside the caller's stack"); + RequireDestroyed(worker, stack); + stack.RequireWritable(); +} + +static void RunConcurrent() { + constexpr std::size_t Count = 4; + std::unique_ptr stacks[Count]; + Worker workers[Count]; + Pthread threads[Count]; + for (std::size_t index = 0; index < Count; ++index) { + stacks[index] = std::make_unique(65536); + workers[index].stack = stacks[index].get(); + workers[index].setter = static_cast(index % 3); + threads[index] = Start(workers[index], Entry, false); + } + for (std::size_t index = 0; index < Count; ++index) { + Require(Join(workers[index], threads[index]) == &workers[index], "concurrent threads on caller stacks join with their own values"); + for (std::size_t other = 0; other < Count; ++other) + Require(stacks[other]->Contains(workers[index].local) == (other == index), "concurrent threads run on their own caller stacks only"); + RequireDestroyed(workers[index], *stacks[index]); + stacks[index]->RequireWritable(); + } +} + +static void RunOverflowRefused() { + PthreadAttr attr = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK, "initialize the overflow attribute"); + auto* wrapping = reinterpret_cast(std::numeric_limits::max() - MINIMUM_STACK + 2); + Require(scePthreadAttrSetstack(&attr, wrapping, MINIMUM_STACK) == SCE_OK, "the setter keeps accepting any non-null address"); + Worker worker; + Pthread thread = nullptr; + bool threw = false; + try { + scePthreadCreate(&thread, &attr, Entry, &worker, nullptr); + } catch (const std::exception&) { + threw = true; + } + Require(threw && thread == nullptr, "a caller stack whose end wraps around the address space is refused at create"); + Require(scePthreadAttrDestroy(&attr) == SCE_OK, "destroy the overflow attribute"); +} + +#ifdef _WIN32 +static std::size_t CollectSharedWrites(const CallerStack& stack) { + std::vector pages(stack.size / 4096 + 1); + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(stack.base), stack.size, pages.data(), &count, true), "collect the writes to the shared stack"); + return count; +} + +static bool SharedPagesProtected(const CallerStack& stack, DWORD protection) { + for (std::size_t offset = 0; offset < stack.size; offset += 4096) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(static_cast(stack.base) + offset, &info, sizeof(info)) == sizeof(info), "query a page of the shared stack"); + if (info.Protect != protection) return false; + } + return true; +} + +static bool SharedPagesPinned(const CallerStack& stack) { + CollectSharedWrites(stack); + return SharedPagesProtected(stack, PAGE_READWRITE); +} + +static bool SharedPagesRearmed(const CallerStack& stack) { + Require(CollectSharedWrites(stack) == stack.size / 4096, "the collector reports every page of the unpinned shared stack before arming it"); + return CollectSharedWrites(stack) == 0 && SharedPagesProtected(stack, PAGE_READONLY); +} + +static void RunPinLifecycle(const CallerStack& stack) { + Require(SharedPagesRearmed(stack), "fresh shared pages arm read-only before any thread used them as a caller stack"); + Worker worker; + worker.stack = &stack; + Pthread thread = Start(worker, CancelEntry, false); + while (!worker.ready.load(std::memory_order_acquire)) std::this_thread::yield(); + Require(SharedPagesPinned(stack), "a live thread keeps the shared pages of its caller stack writable through a collection"); + Require(scePthreadCancel(thread) == SCE_OK, "cancel the live thread on the shared stack"); + Require(Join(worker, thread) == GUEST_CANCELED, "join the canceled thread on the shared stack after its native thread ended"); + RequireCleanups(worker); + RequireDestroyed(worker, stack); + stack.RequireWritable(); + Require(SharedPagesRearmed(stack), "the shared pages arm read-only again after the canceled native thread ended"); + RunJoined(stack, Setter::SceAddressAndSize, Exit::Sce); + Require(SharedPagesRearmed(stack), "the shared pages arm read-only again after scePthreadExit ended the native thread"); + RunJoined(stack, Setter::PosixStack, Exit::Libc); + Require(SharedPagesRearmed(stack), "the shared pages arm read-only again after _Thrd_exit ended the native thread"); + RunJoined(stack, Setter::SceStack, Exit::Return); + Require(SharedPagesRearmed(stack), "the shared pages arm read-only again after a returning entry ended the native thread"); +} +#endif + +static const CallerStack* contextStack = nullptr; +static std::uintptr_t contextLocal = 0; +static bool contextBounds = false; +static int contextEntries = 0; + +static void APS5_VABI ContextEntry() { + volatile unsigned char local = 0x7e; + contextLocal = reinterpret_cast(&local); + contextBounds = OnCallerBounds(*contextStack) && local == 0x7e; + ++contextEntries; +} + +static void Detour(Worker& worker) { + UserUcontext context{}; + UserUcontext back{}; + volatile int phase = 0; + const int entries = contextEntries; + Require(OnCallerBounds(*worker.stack), "the worker starts its detour from its exact caller bounds"); + Require(getcontext_nid_postfix(&back) == 0, "capture the worker before its detour"); + if (phase == 0) { + phase = 1; + Require(getcontext_nid_postfix(&context) == 0, "capture the detour template"); + context.stackPointer = worker.detour->base; + context.stackSize = worker.detour->size; + context.link = &back; + contextStack = worker.detour; + makecontext_nid_postfix(&context, &ContextEntry, 0); + Require(context.mcontext.len == sizeof(UserMcontext), "makecontext accepts the detour stack"); + setcontext_nid_postfix(&context); + Require(false, "setcontext into the detour returned"); + } + Require(contextEntries == entries + 1 && worker.detour->Contains(contextLocal) && !worker.stack->Contains(contextLocal) && contextBounds, "the detour ran once on its own stack, clear of the worker's caller stack, with its own bounds"); + Require(scePthreadSelf() == worker.self, "uc_link resumes the detour's worker on the same thread"); + Require(OnCallerBounds(*worker.stack), "uc_link restores the worker's exact caller bounds rather than the larger registered range"); +} + +static void RunInsideContextStack(const CallerStack& outer) { + const CallerStack detour(CONTEXT_STACK); + UserUcontext context{}; + UserUcontext caller{}; + volatile int phase = 0; + Require(getcontext_nid_postfix(&caller) == 0, "capture the caller of the context"); + if (phase == 0) { + phase = 1; + Require(getcontext_nid_postfix(&context) == 0, "capture the context template"); + context.stackPointer = outer.base; + context.stackSize = outer.size; + context.link = &caller; + makecontext_nid_postfix(&context, &ContextEntry, 0); + Require(context.mcontext.len == sizeof(UserMcontext), "makecontext accepts the context stack"); +#ifdef _WIN32 + Require(SharedPagesPinned(outer), "makecontext pins the shared pages of its stack"); +#endif + const CallerStack inner(static_cast(outer.base) + outer.size / 4, outer.size / 4); + RunJoined(inner, Setter::SceStack, Exit::Return); + RunJoined(inner, Setter::PosixStack, Exit::Sce); + RunJoined(inner, Setter::SceAddressAndSize, Exit::Sce, &detour); + RunJoined(inner, Setter::PosixStack, Exit::Libc, &outer); +#ifdef _WIN32 + Require(SharedPagesPinned(outer), "threads on part of a context stack leave its pages pinned"); +#endif + contextStack = &outer; + setcontext_nid_postfix(&context); + Require(false, "setcontext into the context stack returned"); + } + Require(outer.Contains(contextLocal), "the context runs on its stack after threads used part of it"); + Require(contextBounds, "the context keeps its stack bounds after threads used part of its stack"); +} + +static std::uintptr_t helperLocal = 0; +#ifdef _WIN32 +static ULONG_PTR helperLow = 0; +static ULONG_PTR helperHigh = 0; +#endif + +static void* APS5_VABI RecordBounds(void* opaque) { + volatile unsigned char local = 0x19; + helperLocal = reinterpret_cast(&local); +#ifdef _WIN32 + GetCurrentThreadStackLimits(&helperLow, &helperHigh); +#endif + return local == 0x19 ? opaque : nullptr; +} + +static void RunHostBoundsRestored(const CallerStack& stack) { + int marker = 0; +#ifdef _WIN32 + ULONG_PTR low = 0; + ULONG_PTR high = 0; + GetCurrentThreadStackLimits(&low, &high); +#endif + GuestStacks::GuestStackEnter_nid_no_patch(stack.base, stack.size); + void* const result = Aps5CallGuestEntry_nid_no_patch(RecordBounds, &marker, reinterpret_cast(stack.base) + stack.size); + GuestStacks::GuestStackLeave_nid_no_patch(); + Require(result == &marker && stack.Contains(helperLocal), "the trampoline runs its entry on the caller's stack and returns its value"); +#ifdef _WIN32 + ULONG_PTR restoredLow = 0; + ULONG_PTR restoredHigh = 0; + GetCurrentThreadStackLimits(&restoredLow, &restoredHigh); + Require(helperLow == reinterpret_cast(stack.base) && helperHigh == helperLow + stack.size, "GuestStackEnter publishes the caller's exact bounds to the trampoline's entry"); + Require(restoredLow == low && restoredHigh == high, "GuestStackLeave restores the host thread's native stack bounds after the trampoline returned"); +#endif +} + +struct UnwoundFrame { + std::uintptr_t ip; + std::uintptr_t cfa; +}; + +struct Unwound { + std::uintptr_t trampoline = 0; + bool caught = false; + UnwoundFrame frames[64] = {}; + std::size_t count = 0; + _Unwind_Reason_Code result = _URC_NO_REASON; +}; + +static _Unwind_Reason_Code APS5_VABI RecordFrame(_Unwind_Context* context, void* opaque) { + auto& unwound = *static_cast(opaque); + if (unwound.count == std::size(unwound.frames)) return _URC_NORMAL_STOP; + unwound.frames[unwound.count++] = {_Unwind_GetIP_nid_postfix(context), _Unwind_GetCFA_nid_postfix(context)}; + return _URC_NO_REASON; +} + +[[gnu::noinline]] static void Backtrace(Unwound& unwound) { + unwound.result = _Unwind_Backtrace_nid_postfix(RecordFrame, &unwound); +} + +static void* APS5_VABI UnwoundEntry(void* opaque) { + auto& unwound = *static_cast(opaque); + unwound.trampoline = reinterpret_cast(__builtin_return_address(0)); + try { + throw std::runtime_error("host exception on a guest thread"); + } catch (const std::runtime_error&) { + unwound.caught = true; + } + Backtrace(unwound); + return opaque; +} + +static bool CfaRises(const UnwoundFrame* begin, const UnwoundFrame* end) { + return std::adjacent_find(begin, end, [](const UnwoundFrame& callee, const UnwoundFrame& caller) { return caller.cfa <= callee.cfa; }) == end; +} + +static void RunUnwound(const CallerStack* stack) { + Unwound unwound; + PthreadAttr attr = nullptr; + Pthread thread = nullptr; + void* result = nullptr; + Require(scePthreadAttrInit(&attr) == SCE_OK, "initialize the attribute of the unwound thread"); + if (stack) Require(scePthreadAttrSetstack(&attr, stack->base, stack->size) == SCE_OK, "scePthreadAttrSetstack accepts the unwound thread's caller stack"); + Require(scePthreadCreate(&thread, &attr, UnwoundEntry, &unwound, "unwound") == SCE_OK, "scePthreadCreate starts the unwound thread"); + Require(scePthreadAttrDestroy(&attr) == SCE_OK, "destroy the attribute of the unwound thread"); + Require(scePthreadJoin(thread, &result) == SCE_OK && result == &unwound, "the unwound thread joins with its entry's return value"); + Require(unwound.caught, "a host exception thrown inside the entry is caught there"); + const UnwoundFrame* const begin = unwound.frames; + const UnwoundFrame* const end = begin + unwound.count; + Require(unwound.result == _URC_END_OF_STACK, "_Unwind_Backtrace walks the thread until it finds no further caller"); + Require(std::none_of(begin, end, [](const UnwoundFrame& frame) { return frame.ip == 0; }), "_Unwind_Backtrace reports only nonzero instruction pointers"); + Require(unwound.count > 2 && begin[2].ip == unwound.trampoline, "_Unwind_Backtrace reports the walker, then the entry, then the trampoline the entry returns to"); + Require(std::count_if(begin, end, [&](const UnwoundFrame& frame) { return frame.ip == unwound.trampoline; }) == (stack ? 2 : 1), "the walk passes the trampoline once for the entry and once more where it switched onto a caller stack"); + const UnwoundFrame* host = begin; + if (stack) { + host = std::find_if(begin, end, [stack](const UnwoundFrame& frame) { return !stack->Contains(frame.cfa); }); + Require(host - begin > 2, "the walker, the entry and the trampoline that called the entry have their CFA on the caller's stack"); + Require(host < end && host->ip == unwound.trampoline, "the first frame whose CFA leaves the caller's stack is the trampoline that switched onto it"); + Require(std::none_of(host, end, [stack](const UnwoundFrame& frame) { return stack->Contains(frame.cfa); }), "no frame beyond the switching trampoline has its CFA on the caller's stack"); + } + const UnwoundFrame* const last = stack ? host : begin + 2; + Require(end - last > 1, "the walk recovers the last trampoline's CFA and return address and reports a frame beyond it"); + Require(CfaRises(begin, host) && CfaRises(host, end), "the CFA rises from each frame to its caller on the same stack"); +} + +int main() { + Require(scePthreadKeyCreate(&key, reinterpret_cast(DestroyValue)) == SCE_OK, "create the key whose destructor runs on caller stacks"); + for (auto size : STACK_SIZES) { + CallerStack stack(size); + RunJoined(stack, Setter::SceStack, Exit::Return); + RunJoined(stack, Setter::SceAddressAndSize, Exit::Sce); + RunJoined(stack, Setter::PosixStack, Exit::Return); + RunJoined(stack, Setter::SceStack, Exit::Sce); + RunJoined(stack, Setter::PosixStack, Exit::Libc); + } + CallerStack unaligned(MINIMUM_STACK + 3, 8); + RunJoined(unaligned, Setter::SceStack, Exit::Return); + RunJoined(unaligned, Setter::PosixStack, Exit::Sce); + RunJoined(unaligned, Setter::SceAddressAndSize, Exit::Libc); + CallerStack shared(1u << 20); + RunCanceled(shared, Setter::SceStack); + RunCanceled(shared, Setter::PosixStack); + RunDetached(shared, Setter::SceStack, true); + RunDetached(shared, Setter::SceAddressAndSize, false); + RunDetached(shared, Setter::PosixStack, true); + RunJoined(shared, Setter::SceAddressAndSize, Exit::Return); + RunConcurrent(); + RunHostBoundsRestored(shared); + RunUnwound(&shared); + RunUnwound(nullptr); +#ifdef _WIN32 + std::int64_t physical = 0; + void* mapped = nullptr; + void* alias = nullptr; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, CONTEXT_STACK, 0, 0, &physical) == SCE_OK, "allocate direct memory for a context stack"); + Require(sceKernelMapDirectMemory(&mapped, CONTEXT_STACK, 3, 0, physical, 0) == SCE_OK && sceKernelMapDirectMemory(&alias, CONTEXT_STACK, 3, 0, physical, 0) == SCE_OK, "map the context stack twice so its pages are shared"); + RunPinLifecycle(CallerStack(mapped, CONTEXT_STACK)); + RunInsideContextStack(CallerStack(mapped, CONTEXT_STACK)); + Require(sceKernelMunmap(reinterpret_cast(alias), CONTEXT_STACK) == SCE_OK && sceKernelMunmap(reinterpret_cast(mapped), CONTEXT_STACK) == SCE_OK && sceKernelReleaseDirectMemory(physical, CONTEXT_STACK) == SCE_OK, "release the context stack"); +#else + RunInsideContextStack(CallerStack(CONTEXT_STACK)); +#endif + RunOverflowRefused(); + std::puts("guest pthread provided stack tests passed"); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index c9d74c1db70a..0f3158190c61 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -223,9 +223,10 @@ - [Direct memory unmap](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (WebAssembly) loses the contents of a direct-memory range when it is unmapped and mapped again: the pages come back zero-filled, where FreeBSD keeps them - [EVFILT_VNODE](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (WebAssembly) throws because host file-change notification is not implemented - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them -- [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only +- [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack or a caller-provided thread stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only - [scePthreadAttrGet](../../../core/libs/prx/libkernel/Pthread/src/Attr.cpp) and [pthread_attr_get_np](../../../core/libs/prx/libkernel/Pthread/Posix/Attr.cpp) (libkernel) - the answer for a thread that has ended is not measured on the console; the implementation follows FreeBSD 11.4's [`_pthread_attr_get_np`](https://github.com/freebsd/freebsd-src/blob/releng/11.4/lib/libthr/thread/thr_attr.c#L139-L172) through `_thr_find_thread`. On Windows a host thread adopted by `scePthreadSelf` that ends without `scePthreadExit` keeps answering 0. Monster Hunter Wilds (PPSA24191) reuses a slot of its thread table only when `scePthreadAttrGet` returns `0x80020003` - [scePthreadAttrGet, pthread_attr_get_np and sceKernelIsStack](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) report the stack of a host thread the library did not create, on Windows, from one allocation granule (64 KiB) above the bottom of its reservation when everything above that is committed read-write, as in a relinked executable's main thread and its default-size host threads (0x200000 committed of 0x210000 reserved), so the guest can protect the lowest reported pages, as HotSpot does for its stack guard pages in Java 25 LAN / DNS (PPSA99869). Any other such thread, and every such thread on Linux, gets its whole host stack: on Windows `mprotect` commits its reserved lower pages before protecting them and drops the guard flag of Windows' guard pages, and on Linux it cannot change the unmapped lower pages +- [scePthreadCreate](../../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) runs a thread whose attribute names a stack on that memory with no guard page and no margin below it: host calls, signal and exception handlers and stack probes overrun below the range silently where the console's guard page would fault. The range is never freed, unmapped or reprotected, so protections the guest set inside it outlive the thread; its top is aligned down to 16 bytes, and a range whose end wraps the address space throws at create. Which setter the console honors and its answer to an unaligned range are not measured - [sceKernelInstallExceptionHandler](../../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler, but a host fault still ends the process: only signals raised with `sceKernelRaiseException` are delivered. Unity's PS5Util module installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Linux sends the host SIGUSR1, which the library takes over, to the target thread, so the handler runs wherever the thread was interrupted, emulator code included. On the console kernel objects are system calls and libthr defers signals while a thread holds its internal locks; here a handler that needs a host lock the interrupted code holds deadlocks, and a thread that the handler keeps waiting keeps its host locks. A thread holding the address-wait lock (`sceKernelSyncOnAddressWait`, `sceKernelSyncOnAddressWake`, `_umtx_op`) records the signal and runs the handler once it releases the lock or while it waits; other emulator locks are not deferred. Host waits resume after the handler. The host calls Linux does not restart end early: sleeps that do not retry return before their time, and the poll of `sceNetEpollWait` and socket calls with a send or receive timeout return `EINTR` to the guest - [sceKernelRaiseException](../../../core/libs/prx/libkernel/System/src/Exception.cpp) on Windows runs the handler after the raise has returned when the target is not in guest code (the executable and the `*.guest.prx` modules): a target in a libkernel wait takes it as an APC at that wait; a target elsewhere in host code, running or blocked, takes it when it returns into guest code, through a debug register 0 execute breakpoint at that return address or a host thread that keeps sampling it. A target in guest code is redirected at once. Unlike the console, the handler's context at such a return holds no vector register beyond what the C runtime context saves; a target that stays in host code waiting on a thread already stopped is never stopped; a target inside NtContinue makes the raise throw after 1 s; a debugger or title using debug register 0 on a raised thread is not modeled