mirror of
https://github.com/FEX-Emu/FEX.git
synced 2026-10-06 10:00:16 +02:00
431 lines
14 KiB
C++
431 lines
14 KiB
C++
// SPDX-License-Identifier: MIT
|
|
#include "LinuxSyscalls/Utils/Threads.h"
|
|
#include "LinuxSyscalls/Syscalls.h"
|
|
|
|
#include <FEXCore/Core/Context.h>
|
|
#include <FEXCore/Utils/Allocator.h>
|
|
#include <FEXCore/Utils/LongJump.h>
|
|
#include <FEXCore/Utils/Threads.h>
|
|
#include <FEXHeaderUtils/Syscalls.h>
|
|
#include <sys/resource.h>
|
|
|
|
namespace FEX::LinuxEmulation::Threads {
|
|
void* StackTracker::AllocateStackObject() {
|
|
std::lock_guard lk {DeadStackPoolMutex};
|
|
// Keep the first item in the stack pool
|
|
void* Ptr {};
|
|
|
|
for (auto it = DeadStackPool.begin(); it != DeadStackPool.end();) {
|
|
auto Ready = std::atomic_ref<bool>(it->ReadyToBeReaped);
|
|
bool ReadyToBeReaped = Ready.load();
|
|
if (Ptr == nullptr && ReadyToBeReaped) {
|
|
Ptr = it->Ptr;
|
|
it = DeadStackPool.erase(it);
|
|
continue;
|
|
}
|
|
|
|
if (ReadyToBeReaped) {
|
|
FEXCore::Allocator::munmap(it->Ptr, it->Size);
|
|
it = DeadStackPool.erase(it);
|
|
continue;
|
|
}
|
|
|
|
++it;
|
|
}
|
|
|
|
if (Ptr == nullptr) {
|
|
Ptr = FEXCore::Allocator::mmap(nullptr, FEX::LinuxEmulation::Threads::STACK_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
|
FEXCore::Allocator::VirtualName("FEXMem_Misc", reinterpret_cast<void*>(Ptr), FEX::LinuxEmulation::Threads::STACK_SIZE);
|
|
}
|
|
|
|
return Ptr;
|
|
}
|
|
|
|
bool* StackTracker::AddStackToDeadPool(void* Ptr) {
|
|
std::lock_guard lk {DeadStackPoolMutex};
|
|
auto& it = DeadStackPool.emplace_back(DeadStackPoolItem {Ptr, FEX::LinuxEmulation::Threads::STACK_SIZE, false});
|
|
return &it.ReadyToBeReaped;
|
|
}
|
|
|
|
void StackTracker::AddStackToLivePool(void* Ptr) {
|
|
std::lock_guard lk {LiveStackPoolMutex};
|
|
LiveStackPool.emplace_back(StackPoolItem {Ptr, FEX::LinuxEmulation::Threads::STACK_SIZE});
|
|
}
|
|
|
|
void StackTracker::RemoveStackFromLivePool(void* Ptr) {
|
|
std::lock_guard lk {LiveStackPoolMutex};
|
|
for (auto it = LiveStackPool.begin(); it != LiveStackPool.end(); ++it) {
|
|
if (it->Ptr == Ptr) {
|
|
LiveStackPool.erase(it);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void StackTracker::CleanupAfterFork_PThread() {
|
|
// We don't need to pull the mutex here
|
|
// After a fork we are the only thread running
|
|
// Just need to make sure not to delete our own stack
|
|
uintptr_t StackLocation = reinterpret_cast<uintptr_t>(alloca(0));
|
|
|
|
auto ClearStackPool = [StackLocation](auto& StackPool) {
|
|
for (auto it = StackPool.begin(); it != StackPool.end();) {
|
|
auto& Item = *it;
|
|
uintptr_t ItemStack = reinterpret_cast<uintptr_t>(Item.Ptr);
|
|
if (ItemStack <= StackLocation && (ItemStack + Item.Size) > StackLocation) {
|
|
// This is our stack item, skip it
|
|
++it;
|
|
} else {
|
|
// Untracked stack. Clean it up
|
|
FEXCore::Allocator::munmap(Item.Ptr, Item.Size);
|
|
it = StackPool.erase(it);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Clear both dead stacks and live stacks
|
|
ClearStackPool(DeadStackPool);
|
|
ClearStackPool(LiveStackPool);
|
|
|
|
LogMan::Throw::AFmt((DeadStackPool.size() + LiveStackPool.size()) <= 1, "After fork we should only have zero or one tracked stacks!");
|
|
}
|
|
|
|
void StackTracker::Shutdown() {
|
|
std::lock_guard lk {DeadStackPoolMutex};
|
|
std::lock_guard lk2 {LiveStackPoolMutex};
|
|
// Erase all the dead stack pools
|
|
for (auto& Item : DeadStackPool) {
|
|
FEXCore::Allocator::munmap(Item.Ptr, Item.Size);
|
|
}
|
|
|
|
// Now clean up any that are considered to still be live
|
|
// We are in shutdown phase, everything in the process is dead
|
|
for (auto& Item : LiveStackPool) {
|
|
FEXCore::Allocator::munmap(Item.Ptr, Item.Size);
|
|
}
|
|
|
|
DeadStackPool.clear();
|
|
LiveStackPool.clear();
|
|
}
|
|
|
|
void StackTracker::DeallocateStackObjectImmediately(void* Ptr) {
|
|
if (Ptr) {
|
|
RemoveStackFromLivePool(Ptr);
|
|
auto ReadyToBeReaped = AddStackToDeadPool(Ptr);
|
|
*ReadyToBeReaped = true;
|
|
}
|
|
}
|
|
|
|
[[noreturn]]
|
|
void StackTracker::DeallocateStackObjectAndExit(void* Ptr, int Status) {
|
|
if (Ptr) {
|
|
RemoveStackFromLivePool(Ptr);
|
|
auto ReadyToBeReaped = AddStackToDeadPool(Ptr);
|
|
*ReadyToBeReaped = true;
|
|
}
|
|
|
|
#ifdef ARCHITECTURE_arm64
|
|
__asm volatile("mov x8, %[SyscallNum];"
|
|
"mov w0, %w[Result];"
|
|
"svc #0;" ::[SyscallNum] "i"(SYSCALL_DEF(exit)),
|
|
[Result] "r"(Status)
|
|
: "memory", "x0", "x8");
|
|
#else
|
|
__asm volatile("mov %[Result], %%edi;"
|
|
"syscall;" ::"a"(SYSCALL_DEF(exit)),
|
|
[Result] "r"(Status)
|
|
: "memory", "rdi");
|
|
#endif
|
|
FEX_UNREACHABLE;
|
|
}
|
|
|
|
#ifdef ARCHITECTURE_arm64
|
|
__attribute__((naked)) static void StackPivotAndCall(void* Arg, FEXCore::Threads::ThreadFunc Func, uint64_t StackPivot) {
|
|
// x0: Arg
|
|
// x1: Function to call
|
|
// x2: StackPivot
|
|
__asm volatile(R"(
|
|
// Stack pivot.
|
|
mov x3, sp;
|
|
mov sp, x2;
|
|
|
|
// Store stack storage location on to current stack
|
|
stp x3, lr, [sp, -16]!;
|
|
|
|
// x0 already has argument to pass.
|
|
blr x1
|
|
|
|
// Reload stack storage location
|
|
ldp x2, lr, [sp], 16;
|
|
|
|
// Stack pivot back
|
|
mov sp, x2;
|
|
|
|
ret;
|
|
)" ::
|
|
: "memory");
|
|
}
|
|
#else
|
|
__attribute__((naked)) static void StackPivotAndCall(void* Arg, FEXCore::Threads::ThreadFunc Func, uint64_t StackPivot) {
|
|
// rdi: Arg
|
|
// rsi: Function to call
|
|
// rdx: StackPivot
|
|
__asm volatile(R"(
|
|
// Copy original stack in to RSP.
|
|
movq %%rsp, %%rcx;
|
|
|
|
// Store original stack on new stack
|
|
pushq %%rcx;
|
|
|
|
// Store stack pivot on new stack.
|
|
pushq %%rdx;
|
|
|
|
// rdi already contains function argument.
|
|
callq *%%rsi;
|
|
|
|
// Restore original stack
|
|
popq %%rsp;
|
|
|
|
ret;
|
|
|
|
)" ::
|
|
: "memory");
|
|
}
|
|
#endif
|
|
namespace PThreads {
|
|
void* InitializeThread(void* Ptr);
|
|
|
|
class PThread final : public FEXCore::Threads::Thread {
|
|
public:
|
|
PThread(StackTracker* STracker, FEXCore::Threads::ThreadFunc Func, void* Arg, FEXCore::Threads::Flags Flags, const char* ThreadName)
|
|
: STracker {STracker}
|
|
, UserFunc {Func}
|
|
, UserArg {Arg}
|
|
, Flags {Flags} {
|
|
pthread_attr_t Attr {};
|
|
Stack = STracker->AllocateStackObject();
|
|
// pthreads allocates its dtv region behind our back and there is nothing we can do about it.
|
|
FEXCore::Allocator::YesIKnowImNotSupposedToUseTheGlibcAllocator glibc;
|
|
STracker->AddStackToLivePool(Stack);
|
|
pthread_attr_init(&Attr);
|
|
// Allocate a minimum size stack through pthreads, then stack pivot to FEX's allocated stack.
|
|
// This is required due to a race condition with pthread's DTV/TLS regions when a stack is reused before pthreads deletes that thread's
|
|
// DTV/TLS regions.
|
|
// This can be seen as a crash when running Steam fairly easily, but is very confusing when debugging.
|
|
// The cause of this race condition is from glibc associating a DTV/TLS region with a stack region until the kernel clears the
|
|
// `set_tid_address` address construct. If the stack is reused before the address is set to zero, then glibc won't initialize the new thread's
|
|
// DTV/TLS region, resulting in TLS usage crashing.
|
|
pthread_attr_setstacksize(&Attr, PTHREAD_STACK_MIN);
|
|
uint64_t OldMask;
|
|
bool BlockingSignals = Flags.Internal;
|
|
if (BlockingSignals) {
|
|
OldMask = HLE::ThreadManager::SetSignalMask(~0ULL);
|
|
}
|
|
pthread_create(&Thread, &Attr, InitializeThread, this);
|
|
if (BlockingSignals) {
|
|
HLE::ThreadManager::SetSignalMask(OldMask);
|
|
}
|
|
pthread_attr_destroy(&Attr);
|
|
if (ThreadName) {
|
|
pthread_setname_np(Thread, ThreadName);
|
|
}
|
|
}
|
|
|
|
bool joinable() override {
|
|
pthread_attr_t Attr {};
|
|
if (pthread_getattr_np(Thread, &Attr) == 0) {
|
|
int AttachState {};
|
|
if (pthread_attr_getdetachstate(&Attr, &AttachState) == 0) {
|
|
if (AttachState == PTHREAD_CREATE_JOINABLE) {
|
|
pthread_attr_destroy(&Attr);
|
|
return true;
|
|
}
|
|
}
|
|
pthread_attr_destroy(&Attr);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool join(void** ret) override {
|
|
return pthread_join(Thread, ret) == 0;
|
|
}
|
|
|
|
bool detach() override {
|
|
return pthread_detach(Thread) == 0;
|
|
}
|
|
|
|
bool IsSelf() override {
|
|
auto self = pthread_self();
|
|
return self == Thread;
|
|
}
|
|
|
|
FEXCore::Threads::ThreadFunc GetUserFunc() const {
|
|
return UserFunc;
|
|
}
|
|
|
|
void* GetUserArg() const {
|
|
return UserArg;
|
|
}
|
|
|
|
void* GetPivotStack() const {
|
|
return Stack;
|
|
}
|
|
|
|
FEXCore::Threads::Flags GetFlags() const {
|
|
return Flags;
|
|
}
|
|
|
|
StackTracker* GetStackTracker() const {
|
|
return STracker;
|
|
}
|
|
|
|
void SetupLongJump(FEXCore::UncheckedLongJump::JumpBuf* exit_resolver) {
|
|
_exit_resolver = exit_resolver;
|
|
}
|
|
|
|
[[noreturn]]
|
|
void LongJumpExit(FEX::HLE::ThreadStateObject* ThreadObject, uint32_t Status) {
|
|
this->Status = Status;
|
|
this->ThreadObject = ThreadObject;
|
|
FEXCore::UncheckedLongJump::LongJump(*_exit_resolver, 1);
|
|
FEX_UNREACHABLE;
|
|
}
|
|
|
|
uint32_t GetStatus() const {
|
|
return Status;
|
|
}
|
|
|
|
FEX::HLE::ThreadStateObject* GetThreadObject() const {
|
|
return ThreadObject;
|
|
}
|
|
|
|
private:
|
|
pthread_t Thread;
|
|
StackTracker* STracker;
|
|
FEXCore::Threads::ThreadFunc UserFunc;
|
|
void* UserArg;
|
|
void* Stack {};
|
|
FEXCore::Threads::Flags Flags {};
|
|
|
|
// Use FEXCore's UncheckedLongJump to avoid fortification checks.
|
|
// This avoids a false positive since glibc does not understand stack pivots.
|
|
FEXCore::UncheckedLongJump::JumpBuf* _exit_resolver {};
|
|
FEX::HLE::ThreadStateObject* ThreadObject {};
|
|
uint32_t Status {};
|
|
};
|
|
|
|
void* InitializeThread(void* Ptr) {
|
|
void* StackBase {};
|
|
StackTracker* STracker {};
|
|
PThread* Thread {reinterpret_cast<PThread*>(Ptr)};
|
|
StackBase = Thread->GetPivotStack();
|
|
STracker = Thread->GetStackTracker();
|
|
FEXCore::UncheckedLongJump::JumpBuf exit_resolver {};
|
|
|
|
bool LongJumpExit {};
|
|
|
|
if (Thread->GetFlags().LowPriority) {
|
|
setpriority(PRIO_PROCESS, FHU::Syscalls::gettid(), 19);
|
|
}
|
|
|
|
if (FEXCore::UncheckedLongJump::SetJump(exit_resolver) == 0) {
|
|
Thread->SetupLongJump(&exit_resolver);
|
|
// Run the user function.
|
|
// `Thread` object is dead after this function returns.
|
|
StackPivotAndCall(Thread->GetUserArg(), Thread->GetUserFunc(),
|
|
reinterpret_cast<uint64_t>(StackBase) + FEX::LinuxEmulation::Threads::STACK_SIZE);
|
|
} else {
|
|
LongJumpExit = true;
|
|
}
|
|
|
|
const auto Status = Thread->GetStatus();
|
|
auto ThreadObject = Thread->GetThreadObject();
|
|
// TLS/DTV teardown is something FEX can't control. Disable glibc checking when we leave a pthread.
|
|
FEXCore::Allocator::YesIKnowImNotSupposedToUseTheGlibcAllocator::HardDisable();
|
|
|
|
// Detach to ensure thread teardown occurs.
|
|
Thread->detach();
|
|
|
|
if (LongJumpExit) {
|
|
// We have ownership of the thread object. Make sure to clean it up to prevent memory leaks.
|
|
FEX::HLE::_SyscallHandler->TM.DestroyThread(ThreadObject, true);
|
|
if (Status == 0) {
|
|
// If status is zero then we can safely deallocate this thread's pivot stack (Which is no longer used).
|
|
STracker->DeallocateStackObjectImmediately(StackBase);
|
|
StackBase = nullptr;
|
|
}
|
|
}
|
|
|
|
if (!LongJumpExit || Status != 0) {
|
|
// If we didn't have a long jump exit (So not a pthread thread) OR status wasn't zero then we need to terminate locally.
|
|
// There is an api limitation in glibc/pthreads where a function's return value is ignored and not passed to SYS_exit.
|
|
// In or to match error condition thread exits, we must call SYS_exit ourselves in this case.
|
|
//
|
|
// This is a memory leak if this is a pthread based thread! We can't work around this.
|
|
// - Leaks 128KB PTHREAD_STACK_MIN
|
|
// - Leaks some glibc internal dtv tracking data.
|
|
STracker->DeallocateStackObjectAndExit(StackBase, Status);
|
|
FEX_UNREACHABLE;
|
|
}
|
|
|
|
// Give control back to pthreads.
|
|
// This is required so glibc puts this thread's stack back in the stack cache, preventing a memory leak.
|
|
// We can't use pthread_exit since that requires libgcc_s.so unwinder support which might not be available.
|
|
// We are /expecting/ pthreads to return this status to the _exit syscall.
|
|
return (void*)(uint64_t)Status;
|
|
}
|
|
|
|
static StackTracker* STracker {};
|
|
|
|
static fextl::unique_ptr<FEXCore::Threads::Thread>
|
|
CreateThread_PThread(FEXCore::Threads::ThreadFunc Func, void* Arg, FEXCore::Threads::Flags Flags, const char* ThreadName) {
|
|
return fextl::make_unique<PThread>(STracker, Func, Arg, Flags, ThreadName);
|
|
}
|
|
|
|
static void CleanupAfterFork_PThread() {
|
|
STracker->CleanupAfterFork_PThread();
|
|
}
|
|
|
|
}; // namespace PThreads
|
|
|
|
fextl::unique_ptr<StackTracker> SetupThreadHandlers() {
|
|
FEXCore::Threads::Pointers Ptrs = {
|
|
.CreateThread = PThreads::CreateThread_PThread,
|
|
.CleanupAfterFork = PThreads::CleanupAfterFork_PThread,
|
|
};
|
|
|
|
FEXCore::Threads::Thread::SetInternalPointers(Ptrs);
|
|
|
|
PThreads::STracker = new StackTracker();
|
|
return fextl::unique_ptr<StackTracker>(PThreads::STracker);
|
|
}
|
|
|
|
void* AllocateStackObject() {
|
|
return PThreads::STracker->AllocateStackObject();
|
|
}
|
|
|
|
[[noreturn]]
|
|
void DeallocateStackObjectAndExit(void* Ptr, int Status) {
|
|
PThreads::STracker->DeallocateStackObjectAndExit(Ptr, Status);
|
|
FEX_UNREACHABLE;
|
|
}
|
|
|
|
[[noreturn]]
|
|
void LongjumpDeallocateAndExit(FEX::HLE::ThreadStateObject* ThreadObject, int Status) {
|
|
auto ThreadObject_P = reinterpret_cast<PThreads::PThread*>(ThreadObject->ExecutionThread.get());
|
|
ThreadObject_P->LongJumpExit(ThreadObject, Status);
|
|
FEX_UNREACHABLE;
|
|
}
|
|
|
|
void* GetStackBase(FEXCore::Threads::Thread* ThreadObject) {
|
|
auto ThreadObject_P = reinterpret_cast<PThreads::PThread*>(ThreadObject);
|
|
return ThreadObject_P->GetPivotStack();
|
|
}
|
|
|
|
void Shutdown(fextl::unique_ptr<StackTracker> STracker) {
|
|
STracker->Shutdown();
|
|
STracker.reset();
|
|
PThreads::STracker = nullptr;
|
|
}
|
|
} // namespace FEX::LinuxEmulation::Threads
|