diff --git a/src/common/hostException.cpp b/src/common/hostException.cpp index ea86fe0ca..ab4e18202 100644 --- a/src/common/hostException.cpp +++ b/src/common/hostException.cpp @@ -9,7 +9,9 @@ #include #include #else +#include #include +#include #include #include // IWYU pragma: keep #include @@ -164,6 +166,46 @@ static void SignalHandler(int sig, siginfo_t* si, void* uctx) { #else +class ThreadSignalStack { +public: + ThreadSignalStack() { + const auto page_size = static_cast(::getpagesize()); + const auto stack_size = + (std::max(64 * 1024, MINSIGSTKSZ) + page_size - 1) & ~(page_size - 1); + if (::posix_memalign(&m_memory, page_size, stack_size) != 0) { + return; + } + + stack_t stack {}; + stack.ss_sp = m_memory; + stack.ss_size = stack_size; + if (::sigaltstack(&stack, &m_previous) != 0) { + std::free(m_memory); + m_memory = nullptr; + } + } + + ~ThreadSignalStack() { + if (m_memory != nullptr && ::sigaltstack(&m_previous, nullptr) == 0) { + std::free(m_memory); + } + } + + [[nodiscard]] bool IsInitialized() const { return m_memory != nullptr; } + + KYTY_CLASS_NO_COPY(ThreadSignalStack) + +private: + void* m_memory = nullptr; + stack_t m_previous {}; +}; + +bool InitializeThreadSignalStack() { + // Keep fault handling off guest stacks, which GPU tracking can make read-only. + thread_local ThreadSignalStack signal_stack; + return signal_stack.IsInitialized(); +} + // x86-64 page-fault error bits. constexpr uint64_t PAGE_FAULT_ERROR_WRITE = 0x02; constexpr uint64_t PAGE_FAULT_ERROR_INSTRUCTION = 0x10; @@ -274,8 +316,7 @@ bool InstallHandler(Handler handler) { struct sigaction action {}; action.sa_sigaction = SignalHandler; sigemptyset(&action.sa_mask); - // Fault resolution needs the normal thread stack. - action.sa_flags = SA_SIGINFO | SA_RESTART; + action.sa_flags = SA_SIGINFO | SA_RESTART | SA_ONSTACK; for (const int signal_number: {SIGSEGV, SIGBUS, SIGILL}) { if (::sigaction(signal_number, &action, nullptr) != 0) { diff --git a/src/common/hostException.h b/src/common/hostException.h index 5540ffa4e..cb4de3858 100644 --- a/src/common/hostException.h +++ b/src/common/hostException.h @@ -39,6 +39,10 @@ using Handler = bool (*)(const ExceptionInfo&); bool InstallHandler(Handler handler); +#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX +bool InitializeThreadSignalStack(); +#endif + } // namespace Common::HostException #endif /* KYTY_COMMON_HOST_EXCEPTION_H_ */ diff --git a/src/kernel/pthread.cpp b/src/kernel/pthread.cpp index d2a7a550b..ad22ca864 100644 --- a/src/kernel/pthread.cpp +++ b/src/kernel/pthread.cpp @@ -4,6 +4,7 @@ #include "common/common.h" #include "common/dateTime.h" #include "common/emulatorConfig.h" +#include "common/hostException.h" #include "common/logging/log.h" #include "common/singleton.h" #include "common/stringUtils.h" @@ -1061,6 +1062,12 @@ void PthreadDeleteStaticObjects(Loader::Program* program) { void PthreadInitSelfForMainThread() { EXIT_IF(g_pthread_self != nullptr); +#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX + if (!Common::HostException::InitializeThreadSignalStack()) { + EXIT("Failed to initialize the thread signal stack\n"); + } +#endif + g_pthread_self = new PthreadPrivate {}; PthreadAttrInit(&g_pthread_self->attr); UpdateCurrentThreadStackAttr(&g_pthread_self->attr); @@ -3382,6 +3389,13 @@ static void CleanupThread(void* arg) { } static void* RunThread(void* arg) { + +#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX + if (!Common::HostException::InitializeThreadSignalStack()) { + EXIT("Failed to initialize the thread signal stack\n"); + } +#endif + auto* thread = static_cast(arg); void* ret = nullptr; diff --git a/tests/MemoryTrackerTests.cpp b/tests/MemoryTrackerTests.cpp index cf319fab3..49ebe2167 100644 --- a/tests/MemoryTrackerTests.cpp +++ b/tests/MemoryTrackerTests.cpp @@ -1,3 +1,4 @@ +#include "common/hostException.h" #include "common/virtualMemory.h" #include "graphics/host_gpu/memoryTracker.h" #include "graphics/host_gpu/rangeSet.h" @@ -21,6 +22,7 @@ #undef min #undef max #else +#include #include #include #include @@ -832,6 +834,68 @@ void TestFatalPaths() { } } +#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX +void *g_fault_stack = nullptr; +constexpr size_t FAULT_STACK_SIZE = 64 * 1024; +volatile sig_atomic_t g_stack_faults = 0; + +bool HandleStackFault(const Common::HostException::ExceptionInfo &info) { + using namespace Common::HostException; + stack_t active_stack{}; + const auto fault_address = reinterpret_cast(g_fault_stack) + + FAULT_STACK_SIZE - sizeof(uintptr_t); + if (info.type != ExceptionType::AccessViolation || + info.access_violation_type != AccessViolationType::Write || + info.access_violation_vaddr != fault_address || + ::sigaltstack(nullptr, &active_stack) != 0 || + (active_stack.ss_flags & SS_ONSTACK) == 0) { + std::_Exit(1); + } + g_stack_faults = 1; + return ::mprotect(g_fault_stack, FAULT_STACK_SIZE, + PROT_READ | PROT_WRITE) == 0; +} + +// A stack write must fault before any signal frame can use the protected stack. +__attribute__((naked)) void WriteProtectedStack(void *) { + asm volatile("mov %rsp, %rax\n" + "mov %rdi, %rsp\n" + "push %rax\n" + "pop %rsp\n" + "ret\n"); +} + +void TestFaultOnProtectedStack() { + const pid_t pid = ::fork(); + Check(pid >= 0, "stack fault fork failed"); + if (pid == 0) { + std::thread worker([] { + Check(Common::HostException::InitializeThreadSignalStack(), + "initialize thread signal stack failed"); + Check(Common::HostException::InstallHandler(HandleStackFault), + "install stack fault handler failed"); + g_fault_stack = ::mmap(nullptr, FAULT_STACK_SIZE, PROT_READ, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + Check(g_fault_stack != MAP_FAILED, "allocate protected stack failed"); + WriteProtectedStack(static_cast(g_fault_stack) + FAULT_STACK_SIZE); + Check(g_stack_faults == 1, "protected stack write did not resume"); + struct sigaction action{}; + Check(::sigaction(SIGSEGV, nullptr, &action) == 0 && + action.sa_handler != SIG_DFL, + "stack fault reset the process handler"); + Check(::munmap(g_fault_stack, FAULT_STACK_SIZE) == 0, + "release protected stack failed"); + }); + worker.join(); + std::_Exit(0); + } + int status = 0; + Check(::waitpid(pid, &status, 0) == pid && WIFEXITED(status) && + WEXITSTATUS(status) == 0, + "fault on a protected stack did not recover"); +} +#endif + } // namespace namespace Libs::LibKernel::Memory { @@ -863,6 +927,9 @@ int main(int argc, char **argv) { TestGpuUnmarkUsesRegionMask(); TestFullRegionGpuUnmarkBatching(); TestFatalPaths(); +#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX + TestFaultOnProtectedStack(); +#endif std::puts("MemoryTrackerTests: all cases passed"); return 0; }