From f9b352a093e64759f17f499fff3e3d046f2c38a1 Mon Sep 17 00:00:00 2001 From: Ryan Houdek Date: Sat, 1 Jul 2023 17:41:15 -0700 Subject: [PATCH] Linux: Fixes hangs due to mutexes locked while fork happens. When a fork occurs FEX needs to be incredibly careful as any thread (that isn't forking) that holds a lock will vanish when the fork occurs. At this point if the newly forked process tries to use these mutexes then the process hangs indefinitely. The three major mutexes that need to be held during a fork: - Code Invalidation mutex - This is the highest priority and causes us to hang frequently. - This is highly likely to occur when one thread is loading shared libraries and another thread is forking. - Happens frequently with Wine and steam. - VMA tracking mutex - This one happens when one thread is allocating memory while a fork occurs. - This closely relates to the code invalidation mutex, just happens at the syscall layer instead of the FEXCore layer. - Happens as frequently as the code invalidation mutex. - Allocation mutex - This mutex is used for FEX's 64-bit Allocator, this happens when FEX is allocating memory on one thread and a fork occurs. - Fairly infrequent because jemalloc doesn't allocate VMA regions that often. While this likely doesn't hit all of the FEX mutexes, this hits the ones that are burning fires and are happening frequently. - FEXCore: Adds forkable mutex/locks Necessary since we have a few locations in FEX that need to be locked before and after a fork. When a fork occurs the locks must be locked prior to the fork. Then afterwards they either need to unlock or be set to default initialization state. - Parent - Does an unlock - Child - Sets the lock to default initialization state - This is because it pthreads does TID based ownership checking on unique locks and refcount based waiting for shared locks. - No way to "unlock" after fork in this case other than default initializing. --- .../Source/Interface/Context/Context.h | 11 +- .../FEXCore/Source/Interface/Core/Core.cpp | 27 ++++- External/FEXCore/Source/Utils/Allocator.cpp | 12 ++ External/FEXCore/Source/Utils/Allocator.h | 10 ++ .../Source/Utils/Allocator/64BitAllocator.cpp | 21 +++- .../Source/Utils/Allocator/HostAllocator.h | 3 + .../FEXCore/include/FEXCore/Core/Context.h | 3 +- .../FEXCore/Utils/DeferredSignalMutex.h | 113 ++++++++++++++++++ .../FEXHeaderUtils/ScopedSignalMask.h | 15 ++- .../FEXLoader/LinuxSyscalls/Syscalls.cpp | 43 +++++-- .../Tools/FEXLoader/LinuxSyscalls/Syscalls.h | 6 +- .../LinuxSyscalls/Syscalls/Thread.cpp | 36 ++++-- .../LinuxSyscalls/SyscallsSMCTracking.cpp | 22 ++-- 13 files changed, 275 insertions(+), 47 deletions(-) create mode 100644 External/FEXCore/Source/Utils/Allocator.h diff --git a/External/FEXCore/Source/Interface/Context/Context.h b/External/FEXCore/Source/Interface/Context/Context.h index 9fad8ab27..586a2e092 100644 --- a/External/FEXCore/Source/Interface/Context/Context.h +++ b/External/FEXCore/Source/Interface/Context/Context.h @@ -152,7 +152,9 @@ namespace FEXCore::Context { * @param Thread The internal FEX thread state object */ void DestroyThread(FEXCore::Core::InternalThreadState *Thread) override; - void CleanupAfterFork(FEXCore::Core::InternalThreadState *Thread) override; + + void LockBeforeFork(FEXCore::Core::InternalThreadState *Thread) override; + void UnlockAfterFork(FEXCore::Core::InternalThreadState *Thread, bool Child) override; void SetSignalDelegator(FEXCore::SignalDelegator *SignalDelegation) override; void SetSyscallHandler(FEXCore::HLE::SyscallHandler *Handler) override; @@ -252,7 +254,7 @@ namespace FEXCore::Context { Event PauseWait; bool Running{}; - std::shared_mutex CodeInvalidationMutex; + FEXCore::ForkableSharedMutex CodeInvalidationMutex; FEXCore::CPUIDEmu CPUID; FEXCore::HLE::SyscallHandler *SyscallHandler{}; @@ -289,7 +291,7 @@ namespace FEXCore::Context { template static uint64_t ThreadExitFunctionLink(FEXCore::Core::CpuStateFrame *Frame, uint64_t *record) { auto Thread = Frame->Thread; - ScopedDeferredSignalWithSharedLock lk(static_cast(Thread->CTX)->CodeInvalidationMutex, Thread); + ScopedDeferredSignalWithForkableSharedLock lk(static_cast(Thread->CTX)->CodeInvalidationMutex, Thread); return Fn(Frame, record); } @@ -300,8 +302,7 @@ namespace FEXCore::Context { auto Thread = Frame->Thread; LogMan::Throw::AFmt(Thread->ThreadManager.GetTID() == FHU::Syscalls::gettid(), "Must be called from owning thread {}, not {}", Thread->ThreadManager.GetTID(), FHU::Syscalls::gettid()); - - ScopedDeferredSignalWithUniqueLock lk(static_cast(Thread->CTX)->CodeInvalidationMutex, Thread); + ScopedDeferredSignalWithForkableUniqueLock lk(static_cast(Thread->CTX)->CodeInvalidationMutex, Thread); ThreadRemoveCodeEntry(Thread, GuestRIP); } diff --git a/External/FEXCore/Source/Interface/Core/Core.cpp b/External/FEXCore/Source/Interface/Core/Core.cpp index b5e53f816..2d1b1f022 100644 --- a/External/FEXCore/Source/Interface/Core/Core.cpp +++ b/External/FEXCore/Source/Interface/Core/Core.cpp @@ -8,6 +8,7 @@ $end_info$ */ #include +#include "FEXCore/Utils/DeferredSignalMutex.h" #include "Interface/Context/Context.h" #include "Interface/Core/LookupCache.h" #include "Interface/Core/Core.h" @@ -24,6 +25,7 @@ $end_info$ #include "Interface/IR/Passes/RegisterAllocationPass.h" #include "Interface/IR/Passes.h" #include "Interface/IR/PassManager.h" +#include "Utils/Allocator.h" #include "Utils/Allocator/HostAllocator.h" #include @@ -663,7 +665,17 @@ namespace FEXCore::Context { delete Thread; } - void ContextImpl::CleanupAfterFork(FEXCore::Core::InternalThreadState *LiveThread) { + void ContextImpl::UnlockAfterFork(FEXCore::Core::InternalThreadState *LiveThread, bool Child) { + Allocator::UnlockAfterFork(LiveThread, Child); + + if (Child) { + CodeInvalidationMutex.StealAndDropActiveLocks(); + } + else { + CodeInvalidationMutex.unlock(); + return; + } + // This function is called after fork // We need to cleanup some of the thread data that is dead for (auto &DeadThread : Threads) { @@ -702,6 +714,11 @@ namespace FEXCore::Context { FEXCore::Threads::Thread::CleanupAfterFork(); } + void ContextImpl::LockBeforeFork(FEXCore::Core::InternalThreadState *Thread) { + CodeInvalidationMutex.lock(); + Allocator::LockBeforeFork(Thread); + } + void ContextImpl::AddBlockMapping(FEXCore::Core::InternalThreadState *Thread, uint64_t Address, void *Ptr) { Thread->LookupCache->AddBlockMapping(Address, Ptr); } @@ -1048,7 +1065,7 @@ namespace FEXCore::Context { auto Thread = Frame->Thread; // Invalidate might take a unique lock on this, to guarantee that during invalidation no code gets compiled - std::shared_lock lk(CodeInvalidationMutex); + ScopedDeferredSignalWithForkableSharedLock lk(CodeInvalidationMutex, Thread); // Is the code in the cache? // The backends only check L1 and L2, not L3 @@ -1228,7 +1245,7 @@ namespace FEXCore::Context { // Potential deferred since Thread might not be valid. // Thread object isn't valid very early in frontend's initialization. // To be more optimal the frontend should provide this code with a valid Thread object earlier. - ScopedPotentialDeferredSignalWithUniqueLock CodeInvalidationLock(CodeInvalidationMutex, Thread); + ScopedPotentialDeferredSignalWithForkableUniqueLock lk(CodeInvalidationMutex, Thread); InvalidateGuestCodeRangeInternal(this, Start, Length); } @@ -1237,7 +1254,7 @@ namespace FEXCore::Context { // Potential deferred since Thread might not be valid. // Thread object isn't valid very early in frontend's initialization. // To be more optimal the frontend should provide this code with a valid Thread object earlier. - ScopedPotentialDeferredSignalWithUniqueLock CodeInvalidationLock(CodeInvalidationMutex, Thread); + ScopedPotentialDeferredSignalWithForkableUniqueLock lk(CodeInvalidationMutex, Thread); InvalidateGuestCodeRangeInternal(this, Start, Length); CallAfter(Start, Length); @@ -1265,7 +1282,7 @@ namespace FEXCore::Context { } void ContextImpl::ThreadAddBlockLink(FEXCore::Core::InternalThreadState *Thread, uint64_t GuestDestination, uintptr_t HostLink, const std::function &delinker) { - std::shared_lock lk(static_cast(Thread->CTX)->CodeInvalidationMutex); + ScopedDeferredSignalWithForkableSharedLock lk(static_cast(Thread->CTX)->CodeInvalidationMutex, Thread); Thread->LookupCache->AddBlockLink(GuestDestination, HostLink, delinker); } diff --git a/External/FEXCore/Source/Utils/Allocator.cpp b/External/FEXCore/Source/Utils/Allocator.cpp index ff96a124f..3458b570c 100644 --- a/External/FEXCore/Source/Utils/Allocator.cpp +++ b/External/FEXCore/Source/Utils/Allocator.cpp @@ -340,5 +340,17 @@ namespace FEXCore::Allocator { ::munmap(Region.Ptr, Region.Size); } } + + void LockBeforeFork(FEXCore::Core::InternalThreadState *Thread) { + if (Alloc64) { + Alloc64->LockBeforeFork(Thread); + } + } + + void UnlockAfterFork(FEXCore::Core::InternalThreadState *Thread, bool Child) { + if (Alloc64) { + Alloc64->UnlockAfterFork(Thread, Child); + } + } } #endif diff --git a/External/FEXCore/Source/Utils/Allocator.h b/External/FEXCore/Source/Utils/Allocator.h new file mode 100644 index 000000000..70d9f3fcb --- /dev/null +++ b/External/FEXCore/Source/Utils/Allocator.h @@ -0,0 +1,10 @@ +#pragma once + +namespace FEXCore::Core { +struct InternalThreadState; +} + +namespace FEXCore::Allocator { + void LockBeforeFork(FEXCore::Core::InternalThreadState *Thread); + void UnlockAfterFork(FEXCore::Core::InternalThreadState *Thread, bool Child); +} diff --git a/External/FEXCore/Source/Utils/Allocator/64BitAllocator.cpp b/External/FEXCore/Source/Utils/Allocator/64BitAllocator.cpp index 50e7117dd..aaa104848 100644 --- a/External/FEXCore/Source/Utils/Allocator/64BitAllocator.cpp +++ b/External/FEXCore/Source/Utils/Allocator/64BitAllocator.cpp @@ -49,6 +49,19 @@ namespace Alloc::OSAllocator { void *Mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset) override; int Munmap(void *addr, size_t length) override; + void LockBeforeFork(FEXCore::Core::InternalThreadState *Thread) override { + AllocationMutex.lock(); + } + + void UnlockAfterFork(FEXCore::Core::InternalThreadState *Thread, bool Child) override { + if (Child) { + AllocationMutex.StealAndDropActiveLocks(); + } + else { + AllocationMutex.unlock(); + } + } + private: // Upper bound is the maximum virtual address space of the host processor uintptr_t UPPER_BOUND = (1ULL << 57); @@ -139,7 +152,7 @@ namespace Alloc::OSAllocator { LiveRegionListType *LiveRegions{}; Alloc::ForwardOnlyIntrusiveArenaAllocator *ObjectAlloc{}; - std::mutex AllocationMutex{}; + FEXCore::ForkableUniqueMutex AllocationMutex; void DetermineVASize(); LiveVMARegion *MakeRegionActive(ReservedRegionListType::iterator ReservedIterator, uint64_t UsedSize) { @@ -258,7 +271,7 @@ void *OSAllocator_64Bit::Mmap(void *addr, size_t length, int prot, int flags, in size_t NumberOfPages = length / FHU::FEX_PAGE_SIZE; // This needs a mutex to be thread safe - FEXCore::ScopedPotentialDeferredSignalWithMutex lk(AllocationMutex, TLSThread); + FEXCore::ScopedPotentialDeferredSignalWithForkableMutex lk(AllocationMutex, TLSThread); uint64_t AllocatedOffset{}; LiveVMARegion *LiveRegion{}; @@ -446,7 +459,7 @@ int OSAllocator_64Bit::Munmap(void *addr, size_t length) { } // This needs a mutex to be thread safe - FEXCore::ScopedPotentialDeferredSignalWithMutex lk(AllocationMutex, TLSThread); + FEXCore::ScopedPotentialDeferredSignalWithForkableMutex lk(AllocationMutex, TLSThread); length = FEXCore::AlignUp(length, FHU::FEX_PAGE_SIZE); @@ -571,7 +584,7 @@ OSAllocator_64Bit::OSAllocator_64Bit() { OSAllocator_64Bit::~OSAllocator_64Bit() { // This needs a mutex to be thread safe - FEXCore::ScopedPotentialDeferredSignalWithMutex lk(AllocationMutex, TLSThread); + FEXCore::ScopedPotentialDeferredSignalWithForkableMutex lk(AllocationMutex, TLSThread); // Walk the pages and deallocate // First walk the live regions diff --git a/External/FEXCore/Source/Utils/Allocator/HostAllocator.h b/External/FEXCore/Source/Utils/Allocator/HostAllocator.h index 6678c1d99..42653857c 100644 --- a/External/FEXCore/Source/Utils/Allocator/HostAllocator.h +++ b/External/FEXCore/Source/Utils/Allocator/HostAllocator.h @@ -22,6 +22,9 @@ namespace Alloc { virtual void *Mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset) { return nullptr; } virtual int Munmap(void *addr, size_t length) { return -1; } + + virtual void LockBeforeFork(FEXCore::Core::InternalThreadState *Thread) {} + virtual void UnlockAfterFork(FEXCore::Core::InternalThreadState *Thread, bool Child) {} }; class GlobalAllocator { diff --git a/External/FEXCore/include/FEXCore/Core/Context.h b/External/FEXCore/include/FEXCore/Core/Context.h index 80dc56ef4..af2b2a15e 100644 --- a/External/FEXCore/include/FEXCore/Core/Context.h +++ b/External/FEXCore/include/FEXCore/Core/Context.h @@ -243,7 +243,8 @@ namespace FEXCore::Context { FEX_DEFAULT_VISIBILITY virtual void RunThread(FEXCore::Core::InternalThreadState *Thread) = 0; FEX_DEFAULT_VISIBILITY virtual void StopThread(FEXCore::Core::InternalThreadState *Thread) = 0; FEX_DEFAULT_VISIBILITY virtual void DestroyThread(FEXCore::Core::InternalThreadState *Thread) = 0; - FEX_DEFAULT_VISIBILITY virtual void CleanupAfterFork(FEXCore::Core::InternalThreadState *Thread) = 0; + FEX_DEFAULT_VISIBILITY virtual void LockBeforeFork(FEXCore::Core::InternalThreadState *Thread) {} + FEX_DEFAULT_VISIBILITY virtual void UnlockAfterFork(FEXCore::Core::InternalThreadState *Thread, bool Child) {} FEX_DEFAULT_VISIBILITY virtual void SetSignalDelegator(FEXCore::SignalDelegator *SignalDelegation) = 0; FEX_DEFAULT_VISIBILITY virtual void SetSyscallHandler(FEXCore::HLE::SyscallHandler *Handler) = 0; diff --git a/External/FEXCore/include/FEXCore/Utils/DeferredSignalMutex.h b/External/FEXCore/include/FEXCore/Utils/DeferredSignalMutex.h index 7fb2c6fbf..e84fb90b5 100644 --- a/External/FEXCore/include/FEXCore/Utils/DeferredSignalMutex.h +++ b/External/FEXCore/include/FEXCore/Utils/DeferredSignalMutex.h @@ -11,6 +11,91 @@ #include namespace FEXCore { +#ifndef _WIN32 + // Replacement for std::mutexes to deal with unlocking issues in the face of Linux fork() semantics. + // + // A fork() only clones the parent's calling thread. Other threads are silently dropped, which permanently leaves any mutexes owned by them locked. + // To address this issue, ForkableUniqueMutex and ForkableSharedMutex provide a way to forcefully remove any dangling locks and reset the mutexes to their default state. + class ForkableUniqueMutex final { + public: + ForkableUniqueMutex() + : Mutex (PTHREAD_MUTEX_INITIALIZER) { + } + + // Move-only type + ForkableUniqueMutex(const ForkableUniqueMutex&) = delete; + ForkableUniqueMutex& operator=(const ForkableUniqueMutex&) = delete; + ForkableUniqueMutex(ForkableUniqueMutex &&rhs) = default; + ForkableUniqueMutex& operator=(ForkableUniqueMutex &&) = default; + + void lock() { + [[maybe_unused]] const auto Result = pthread_mutex_lock(&Mutex); + LOGMAN_THROW_A_FMT(Result == 0, "{} failed to lock with {}", __func__, Result); + } + void unlock() { + [[maybe_unused]] const auto Result = pthread_mutex_unlock(&Mutex); + LOGMAN_THROW_A_FMT(Result == 0, "{} failed to unlock with {}", __func__, Result); + } + // Initialize the internal pthread object to its default initializer state. + // Should only ever be used in the child process when a Linux fork() has occured. + void StealAndDropActiveLocks() { + Mutex = PTHREAD_MUTEX_INITIALIZER; + } + private: + pthread_mutex_t Mutex; + }; + + class ForkableSharedMutex final { + public: + ForkableSharedMutex() + : Mutex (PTHREAD_RWLOCK_INITIALIZER) { + } + + // Move-only type + ForkableSharedMutex(const ForkableSharedMutex&) = delete; + ForkableSharedMutex& operator=(const ForkableSharedMutex&) = delete; + ForkableSharedMutex(ForkableSharedMutex &&rhs) = default; + ForkableSharedMutex& operator=(ForkableSharedMutex &&) = default; + + void lock() { + [[maybe_unused]] const auto Result = pthread_rwlock_wrlock(&Mutex); + LOGMAN_THROW_A_FMT(Result == 0, "{} failed to lock with {}", __func__, Result); + } + void unlock() { + [[maybe_unused]] const auto Result = pthread_rwlock_unlock(&Mutex); + LOGMAN_THROW_A_FMT(Result == 0, "{} failed to unlock with {}", __func__, Result); + } + void lock_shared() { + [[maybe_unused]] const auto Result = pthread_rwlock_rdlock(&Mutex); + LOGMAN_THROW_A_FMT(Result == 0, "{} failed to lock with {}", __func__, Result); + } + + void unlock_shared() { + unlock(); + } + + bool try_lock() { + const auto Result = pthread_rwlock_trywrlock(&Mutex); + return Result == 0; + } + + bool try_lock_shared() { + const auto Result = pthread_rwlock_tryrdlock(&Mutex); + return Result == 0; + } + // Initialize the internal pthread object to its default initializer state. + // Should only ever be used in the child process when a Linux fork() has occured. + void StealAndDropActiveLocks() { + Mutex = PTHREAD_RWLOCK_INITIALIZER; + } + private: + pthread_rwlock_t Mutex; + }; +#else + // Windows doesn't support forking, so these can be standard mutexes. + using ForkableUniqueMutex = std::mutex; + using ForkableSharedMutex = std::shared_mutex; +#endif template class ScopedDeferredSignalWithMutexBase final { @@ -66,6 +151,20 @@ namespace FEXCore { using ScopedDeferredSignalWithSharedLock = ScopedDeferredSignalWithMutexBase; using ScopedDeferredSignalWithUniqueLock = ScopedDeferredSignalWithMutexBase; + // Forkable variant + using ScopedDeferredSignalWithForkableMutex = ScopedDeferredSignalWithMutexBase< + FEXCore::ForkableUniqueMutex, + &FEXCore::ForkableUniqueMutex::lock, + &FEXCore::ForkableUniqueMutex::unlock>; + using ScopedDeferredSignalWithForkableSharedLock = ScopedDeferredSignalWithMutexBase< + FEXCore::ForkableSharedMutex, + &FEXCore::ForkableSharedMutex::lock_shared, + &FEXCore::ForkableSharedMutex::unlock_shared>; + using ScopedDeferredSignalWithForkableUniqueLock = ScopedDeferredSignalWithMutexBase< + FEXCore::ForkableSharedMutex, + &FEXCore::ForkableSharedMutex::lock, + &FEXCore::ForkableSharedMutex::unlock>; + template class ScopedPotentialDeferredSignalWithMutexBase final { public: @@ -131,4 +230,18 @@ namespace FEXCore { using ScopedPotentialDeferredSignalWithMutex = ScopedPotentialDeferredSignalWithMutexBase; using ScopedPotentialDeferredSignalWithSharedLock = ScopedPotentialDeferredSignalWithMutexBase; using ScopedPotentialDeferredSignalWithUniqueLock = ScopedPotentialDeferredSignalWithMutexBase; + + // Forkable variant + using ScopedPotentialDeferredSignalWithForkableMutex = ScopedPotentialDeferredSignalWithMutexBase< + FEXCore::ForkableUniqueMutex, + &FEXCore::ForkableUniqueMutex::lock, + &FEXCore::ForkableUniqueMutex::unlock>; + using ScopedPotentialDeferredSignalWithForkableSharedLock = ScopedPotentialDeferredSignalWithMutexBase< + FEXCore::ForkableSharedMutex, + &FEXCore::ForkableSharedMutex::lock_shared, + &FEXCore::ForkableSharedMutex::unlock_shared>; + using ScopedPotentialDeferredSignalWithForkableUniqueLock = ScopedPotentialDeferredSignalWithMutexBase< + FEXCore::ForkableSharedMutex, + &FEXCore::ForkableSharedMutex::lock, + &FEXCore::ForkableSharedMutex::unlock>; } diff --git a/FEXHeaderUtils/FEXHeaderUtils/ScopedSignalMask.h b/FEXHeaderUtils/FEXHeaderUtils/ScopedSignalMask.h index cb393ad94..e7e97572a 100644 --- a/FEXHeaderUtils/FEXHeaderUtils/ScopedSignalMask.h +++ b/FEXHeaderUtils/FEXHeaderUtils/ScopedSignalMask.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include @@ -106,4 +106,17 @@ namespace FHU { using ScopedSignalMaskWithMutex = ScopedSignalMaskWithMutexBase; using ScopedSignalMaskWithSharedLock = ScopedSignalMaskWithMutexBase; using ScopedSignalMaskWithUniqueLock = ScopedSignalMaskWithMutexBase; + + using ScopedSignalMaskWithForkableMutex = ScopedSignalMaskWithMutexBase< + FEXCore::ForkableUniqueMutex, + &FEXCore::ForkableUniqueMutex::lock, + &FEXCore::ForkableUniqueMutex::unlock>; + using ScopedSignalMaskWithForkableSharedLock = ScopedSignalMaskWithMutexBase< + FEXCore::ForkableSharedMutex, + &FEXCore::ForkableSharedMutex::lock_shared, + &FEXCore::ForkableSharedMutex::unlock_shared>; + using ScopedSignalMaskWithForkableUniqueLock = ScopedSignalMaskWithMutexBase< + FEXCore::ForkableSharedMutex, + &FEXCore::ForkableSharedMutex::lock, + &FEXCore::ForkableSharedMutex::unlock>; } diff --git a/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.cpp b/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.cpp index 4ed167f9f..9f7568ba2 100644 --- a/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.cpp +++ b/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.cpp @@ -580,12 +580,33 @@ uint64_t CloneHandler(FEXCore::Core::CpuStateFrame *Frame, FEX::HLE::clone3_args if (!AnyFlagsSet(flags, CLONE_THREAD)) { // Has an unsupported flag // Fall to a handler that can handle this case + auto Thread = Frame->Thread; + + args->SignalMask = ~0ULL; + ::syscall(SYS_rt_sigprocmask, SIG_SETMASK, &args->SignalMask, &args->SignalMask, sizeof(args->SignalMask)); + Thread->CTX->LockBeforeFork(Frame->Thread); + + FEX::HLE::_SyscallHandler->LockBeforeFork(); + + uint64_t Result{}; if (args->Type == TYPE_CLONE2) { - return Clone2Handler(Frame, args); + Result = Clone2Handler(Frame, args); } else { - return Clone3Handler(Frame, args); + Result = Clone3Handler(Frame, args); } + + if (Result != 0) { + // Parent + // Unlock the mutexes on both sides of the fork + FEX::HLE::_SyscallHandler->UnlockAfterFork(false); + + // Clear all the other threads that are being tracked + Thread->CTX->UnlockAfterFork(Frame->Thread, false); + + ::syscall(SYS_rt_sigprocmask, SIG_SETMASK, &args->SignalMask, nullptr, sizeof(args->SignalMask)); + } + return Result; } else { LogMan::Msg::IFmt("Unsupported flag with CLONE_THREAD. This breaks TLS, falling down classic thread path"); @@ -634,6 +655,7 @@ uint64_t CloneHandler(FEXCore::Core::CpuStateFrame *Frame, FEX::HLE::clone3_args // Normally a thread cleans itself up on exit. But because we need to join, we are now responsible Thread->CTX->DestroyThread(NewThread); } + SYSCALL_ERRNO(); } }; @@ -808,17 +830,16 @@ uint64_t UnimplementedSyscallSafe(FEXCore::Core::CpuStateFrame *Frame, uint64_t } void SyscallHandler::LockBeforeFork() { - // XXX shared_mutex has issues with locking and forks - // VMATracking.Mutex.lock(); - - // Add other mutexes here + VMATracking.Mutex.lock(); } -void SyscallHandler::UnlockAfterFork() { - // Add other mutexes here - - // XXX shared_mutex has issues with locking and forks - // VMATracking.Mutex.unlock(); +void SyscallHandler::UnlockAfterFork(bool Child) { + if (Child) { + VMATracking.Mutex.StealAndDropActiveLocks(); + } + else { + VMATracking.Mutex.unlock(); + } } static bool isHEX(char c) { diff --git a/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.h b/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.h index 0b43e7e95..ea6b333ee 100644 --- a/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.h +++ b/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls.h @@ -15,6 +15,7 @@ $end_info$ #include #include #include +#include #include #include #include @@ -220,7 +221,7 @@ public: ///// FORK tracking ///// void LockBeforeFork(); - void UnlockAfterFork(); + void UnlockAfterFork(bool Child); SourcecodeResolver *GetSourcecodeResolver() override { return this; } @@ -327,7 +328,7 @@ private: struct VMATracking { using VMAEntry = SyscallHandler::VMAEntry; // Held while reading/writing this struct - std::shared_mutex Mutex; + FEXCore::ForkableSharedMutex Mutex; // Memory ranges indexed by page aligned starting address fextl::map VMAs; @@ -430,6 +431,7 @@ enum TypeOfClone { struct clone3_args { TypeOfClone Type; + uint64_t SignalMask; kernel_clone3_args args; }; diff --git a/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls/Thread.cpp b/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls/Thread.cpp index 7d39ea7b4..19d70c7b7 100644 --- a/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls/Thread.cpp +++ b/Source/Tools/FEXLoader/LinuxSyscalls/Syscalls/Thread.cpp @@ -140,7 +140,13 @@ namespace FEX::HLE { // If we don't have CLONE_THREAD then we are effectively a fork // Clear all the other threads that are being tracked // Frame->Thread is /ONLY/ safe to access when CLONE_THREAD flag is not set - CTX->CleanupAfterFork(Frame->Thread); + // Unlock the mutexes on both sides of the fork + FEX::HLE::_SyscallHandler->UnlockAfterFork(true); + + // Clear all the other threads that are being tracked + Thread->CTX->UnlockAfterFork(Frame->Thread, true); + + ::syscall(SYS_rt_sigprocmask, SIG_SETMASK, &CloneArgs->SignalMask, nullptr, sizeof(CloneArgs->SignalMask)); Thread->CurrentFrame->State.gregs[FEXCore::X86State::REG_RAX] = 0; Thread->CurrentFrame->State.gregs[FEXCore::X86State::REG_RBX] = 0; @@ -187,6 +193,11 @@ namespace FEX::HLE { uint64_t ForkGuest(FEXCore::Core::InternalThreadState *Thread, FEXCore::Core::CpuStateFrame *Frame, uint32_t flags, void *stack, size_t StackSize, pid_t *parent_tid, pid_t *child_tid, void *tls) { // Just before we fork, we lock all syscall mutexes so that both processes will end up with a locked mutex + + uint64_t Mask{~0ULL}; + ::syscall(SYS_rt_sigprocmask, SIG_SETMASK, &Mask, &Mask, sizeof(Mask)); + Thread->CTX->LockBeforeFork(Frame->Thread); + FEX::HLE::_SyscallHandler->LockBeforeFork(); const bool IsVFork = flags & CLONE_VFORK; @@ -215,11 +226,17 @@ namespace FEX::HLE { else { Result = fork(); } + const bool IsChild = Result == 0; - // Unlock the mutexes on both sides of the fork - FEX::HLE::_SyscallHandler->UnlockAfterFork(); + if (IsChild) { + // Unlock the mutexes on both sides of the fork + FEX::HLE::_SyscallHandler->UnlockAfterFork(IsChild); + + // Clear all the other threads that are being tracked + Thread->CTX->UnlockAfterFork(Frame->Thread, IsChild); + + ::syscall(SYS_rt_sigprocmask, SIG_SETMASK, &Mask, nullptr, sizeof(Mask)); - if (Result == 0) { // Child // update the internal TID Thread->ThreadManager.TID = FHU::Syscalls::gettid(); @@ -227,9 +244,6 @@ namespace FEX::HLE { FEX::HLE::_SyscallHandler->FM.UpdatePID(Thread->ThreadManager.PID); Thread->ThreadManager.clear_child_tid = nullptr; - // Clear all the other threads that are being tracked - Thread->CTX->CleanupAfterFork(Frame->Thread); - // only a single thread running so no need to remove anything from the thread array // Handle child setup now @@ -275,6 +289,14 @@ namespace FEX::HLE { } } + // Unlock the mutexes on both sides of the fork + FEX::HLE::_SyscallHandler->UnlockAfterFork(IsChild); + + // Clear all the other threads that are being tracked + Thread->CTX->UnlockAfterFork(Frame->Thread, IsChild); + + ::syscall(SYS_rt_sigprocmask, SIG_SETMASK, &Mask, nullptr, sizeof(Mask)); + // VFork needs the parent to wait for the child to exit. if (IsVFork) { // Wait for the read end of the pipe to close. diff --git a/Source/Tools/FEXLoader/LinuxSyscalls/SyscallsSMCTracking.cpp b/Source/Tools/FEXLoader/LinuxSyscalls/SyscallsSMCTracking.cpp index d8c8b3489..e029269c7 100644 --- a/Source/Tools/FEXLoader/LinuxSyscalls/SyscallsSMCTracking.cpp +++ b/Source/Tools/FEXLoader/LinuxSyscalls/SyscallsSMCTracking.cpp @@ -54,7 +54,7 @@ bool SyscallHandler::HandleSegfault(FEXCore::Core::InternalThreadState *Thread, { // Can't use the deferred signal lock in the SIGSEGV handler. - FHU::ScopedSignalMaskWithSharedLock lk(_SyscallHandler->VMATracking.Mutex); + FHU::ScopedSignalMaskWithForkableSharedLock lk(_SyscallHandler->VMATracking.Mutex); auto VMATracking = &_SyscallHandler->VMATracking; @@ -111,7 +111,7 @@ void SyscallHandler::MarkGuestExecutableRange(FEXCore::Core::InternalThreadState return; } - FEXCore::ScopedDeferredSignalWithSharedLock lk(VMATracking.Mutex, Thread); + FEXCore::ScopedDeferredSignalWithForkableSharedLock lk(VMATracking.Mutex, Thread); // Find the first mapping at or after the range ends, or ::end(). // Top points to the address after the end of the range @@ -166,7 +166,7 @@ void SyscallHandler::MarkGuestExecutableRange(FEXCore::Core::InternalThreadState // Used for AOT FEXCore::HLE::AOTIRCacheEntryLookupResult SyscallHandler::LookupAOTIRCacheEntry(FEXCore::Core::InternalThreadState *Thread, uint64_t GuestAddr) { - FEXCore::ScopedDeferredSignalWithSharedLock lk(VMATracking.Mutex, Thread); + FEXCore::ScopedDeferredSignalWithForkableSharedLock lk(VMATracking.Mutex, Thread); // Get the first mapping after GuestAddr, or end // GuestAddr is inclusive @@ -194,7 +194,7 @@ void SyscallHandler::TrackMmap(FEXCore::Core::InternalThreadState *Thread, uintp // NOTE: Frontend calls this with a nullptr Thread during initialization, but // providing this code with a valid Thread object earlier would allow // us to be more optimal by using ScopedDeferredSignalWithUniqueLock instead - FEXCore::ScopedPotentialDeferredSignalWithUniqueLock lk(VMATracking.Mutex, Thread); + FEXCore::ScopedPotentialDeferredSignalWithForkableUniqueLock lk(VMATracking.Mutex, Thread); static uint64_t AnonSharedId = 1; @@ -233,6 +233,7 @@ void SyscallHandler::TrackMmap(FEXCore::Core::InternalThreadState *Thread, uintp } if (SMCChecks != FEXCore::Config::CONFIG_SMC_NONE) { + // VMATracking.Mutex can't be held while executing this, otherwise it hangs if the JIT is in the process of looking up code in the AOT JIT. CTX->InvalidateGuestCodeRange(Thread, (uintptr_t)Base, Size); } } @@ -244,7 +245,7 @@ void SyscallHandler::TrackMunmap(FEXCore::Core::InternalThreadState *Thread, uin // Frontend calls this with nullptr Thread during initialization. // This is why `ScopedPotentialDeferredSignalWithUniqueLock` is used here. // To be more optimal the frontend should provide this code with a valid Thread object earlier. - FEXCore::ScopedPotentialDeferredSignalWithUniqueLock lk(VMATracking.Mutex, Thread); + FEXCore::ScopedPotentialDeferredSignalWithForkableUniqueLock lk(VMATracking.Mutex, Thread); VMATracking.ClearUnsafe(CTX, Base, Size); } @@ -258,7 +259,7 @@ void SyscallHandler::TrackMprotect(FEXCore::Core::InternalThreadState *Thread, u Size = FEXCore::AlignUp(Size, FHU::FEX_PAGE_SIZE); { - FEXCore::ScopedDeferredSignalWithUniqueLock lk(VMATracking.Mutex, Thread); + FEXCore::ScopedDeferredSignalWithForkableUniqueLock lk(VMATracking.Mutex, Thread); VMATracking.ChangeUnsafe(Base, Size, VMAProt::fromProt(Prot)); } @@ -273,7 +274,7 @@ void SyscallHandler::TrackMremap(FEXCore::Core::InternalThreadState *Thread, uin NewSize = FEXCore::AlignUp(NewSize, FHU::FEX_PAGE_SIZE); { - FEXCore::ScopedDeferredSignalWithUniqueLock lk(VMATracking.Mutex, Thread); + FEXCore::ScopedDeferredSignalWithForkableUniqueLock lk(VMATracking.Mutex, Thread); const auto OldVMA = VMATracking.LookupVMAUnsafe(OldAddress); @@ -331,7 +332,7 @@ void SyscallHandler::TrackShmat(FEXCore::Core::InternalThreadState *Thread, int uint64_t Length = stat.shm_segsz; { - FEXCore::ScopedDeferredSignalWithUniqueLock lk(VMATracking.Mutex, Thread); + FEXCore::ScopedDeferredSignalWithForkableUniqueLock lk(VMATracking.Mutex, Thread); // TODO MRID mrid{SpecialDev::SHM, static_cast(shmid)}; @@ -353,7 +354,7 @@ void SyscallHandler::TrackShmat(FEXCore::Core::InternalThreadState *Thread, int void SyscallHandler::TrackShmdt(FEXCore::Core::InternalThreadState *Thread, uintptr_t Base) { uintptr_t Length = 0; { - FEXCore::ScopedDeferredSignalWithUniqueLock lk(VMATracking.Mutex, Thread); + FEXCore::ScopedDeferredSignalWithForkableUniqueLock lk(VMATracking.Mutex, Thread); Length = VMATracking.ClearShmUnsafe(CTX, Base); } @@ -367,8 +368,7 @@ void SyscallHandler::TrackShmdt(FEXCore::Core::InternalThreadState *Thread, uint void SyscallHandler::TrackMadvise(FEXCore::Core::InternalThreadState *Thread, uintptr_t Base, uintptr_t Size, int advice) { Size = FEXCore::AlignUp(Size, FHU::FEX_PAGE_SIZE); { - FEXCore::ScopedDeferredSignalWithUniqueLock lk(VMATracking.Mutex, Thread); - + FEXCore::ScopedDeferredSignalWithForkableUniqueLock lk(VMATracking.Mutex, Thread); // TODO } }