kernel: use per-thread signal stacks for guest stack faults

This commit is contained in:
nmzik
2026-09-06 09:28:19 +04:00
parent 6ae5cb7b64
commit 211242452b
4 changed files with 128 additions and 2 deletions
+43 -2
View File
@@ -9,7 +9,9 @@
#include <csignal>
#include <sys/ucontext.h>
#else
#include <algorithm>
#include <csignal>
#include <cstdlib>
#include <initializer_list>
#include <ucontext.h> // IWYU pragma: keep
#include <unistd.h>
@@ -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<size_t>(::getpagesize());
const auto stack_size =
(std::max<size_t>(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) {
+4
View File
@@ -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_ */
+14
View File
@@ -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<Pthread>(arg);
void* ret = nullptr;
+67
View File
@@ -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 <csignal>
#include <map>
#include <sys/mman.h>
#include <sys/wait.h>
@@ -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<uintptr_t>(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<char *>(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;
}