From 8e9f593c393b05ab26ef08d152a801808a08e571 Mon Sep 17 00:00:00 2001 From: Billy Laws Date: Mon, 5 Jan 2026 02:30:36 +0000 Subject: [PATCH] Windows: Handle NtReadFile calls to RX protected RWX memory Handling this safely requires blocking all compilation for the duration of the read, as fault-based tracking doesn't work when wine's unix side itself is the one performing the write into the RWX region (we cannot catch unix faults). Fixes a startup crash in Persona 5. --- Source/Windows/ARM64EC/Module.cpp | 25 +++++++- Source/Windows/Common/InvalidationTracker.cpp | 63 ++++++++++++------- Source/Windows/Common/InvalidationTracker.h | 8 +++ Source/Windows/WOW64/Module.cpp | 20 +++++- 4 files changed, 93 insertions(+), 23 deletions(-) diff --git a/Source/Windows/ARM64EC/Module.cpp b/Source/Windows/ARM64EC/Module.cpp index ab07af93f..9015a55c7 100644 --- a/Source/Windows/ARM64EC/Module.cpp +++ b/Source/Windows/ARM64EC/Module.cpp @@ -881,7 +881,30 @@ void BTCpu64NotifyMemoryDirty(void* Address, SIZE_T Size) { InvalidationTracker->InvalidateAlignedInterval(reinterpret_cast(Address), static_cast(Size), false); } -void BTCpu64NotifyReadFile(HANDLE Handle, void* Address, SIZE_T Size, BOOL After, NTSTATUS Status) {} +void BTCpu64NotifyReadFile(HANDLE Handle, void* Address, SIZE_T Size, BOOL After, NTSTATUS Status) { + auto* ThreadState = GetCPUArea().ThreadState(); + if (!InvalidationTracker || !ThreadState) { + return; + } + + auto& InLockedRWXRead = GetFrontendThreadData(ThreadState)->InLockedRWXRead; + if (!After) { + ThreadCreationMutex.lock(); + CTX->GetCodeInvalidationMutex().lock(); + if (InvalidationTracker->BeginUntrackedWriteLocked(reinterpret_cast(Address), static_cast(Size))) { + InLockedRWXRead = true; + } else { + CTX->GetCodeInvalidationMutex().unlock(); + ThreadCreationMutex.unlock(); + } + } else { + if (InLockedRWXRead) { + InLockedRWXRead = false; + CTX->GetCodeInvalidationMutex().unlock(); + ThreadCreationMutex.unlock(); + } + } +} NTSTATUS ThreadInit() { std::scoped_lock Lock(ThreadCreationMutex); diff --git a/Source/Windows/Common/InvalidationTracker.cpp b/Source/Windows/Common/InvalidationTracker.cpp index 77c980177..8f888b761 100644 --- a/Source/Windows/Common/InvalidationTracker.cpp +++ b/Source/Windows/Common/InvalidationTracker.cpp @@ -142,27 +142,7 @@ void InvalidationTracker::InvalidateAlignedInterval(uint64_t Address, uint64_t S } void InvalidationTracker::ReprotectRWXIntervals(uint64_t Address, uint64_t Size) { - const auto End = Address + Size; - std::shared_lock Lock(IntervalsLock); - - if (SMCDetectionDisabled) { - return; - } - - do { - const auto Query = RWXIntervals.Query(Address); - if (Query.Enclosed) { - void* TmpAddress = reinterpret_cast(Address); - SIZE_T TmpSize = static_cast(std::min(End, Address + Query.Size) - Address); - ULONG TmpProt; - NtProtectVirtualMemory(NtCurrentProcess(), &TmpAddress, &TmpSize, PAGE_EXECUTE_READ, &TmpProt); - } else if (!Query.Size) { - // No more regions past `Address` in the interval list - break; - } - - Address += Query.Size; - } while (Address < End); + ProtectRWXIntervalsInternal(Address, Size, false); } bool InvalidationTracker::HandleRWXAccessViolation(FEXCore::Core::InternalThreadState* Thread, uint64_t HostPc, uint64_t FaultAddress) { @@ -190,6 +170,10 @@ bool InvalidationTracker::HandleRWXAccessViolation(FEXCore::Core::InternalThread return false; } +bool InvalidationTracker::BeginUntrackedWriteLocked(uint64_t Address, uint64_t Size) { + return ProtectRWXIntervalsInternal(Address, Size, true); +} + FEXCore::HLE::ExecutableRangeInfo InvalidationTracker::QueryExecutableRange(uint64_t Address) { std::shared_lock Lock(IntervalsLock); const auto XResult = XIntervals.Query(Address); @@ -266,4 +250,41 @@ void InvalidationTracker::InvalidateIntervalInternalLocked(uint64_t Address, uin } } +bool InvalidationTracker::ProtectRWXIntervalsInternal(uint64_t Address, uint64_t Size, bool ForWriteLocked) { + const auto End = Address + Size; + std::shared_lock Lock(IntervalsLock); + + if (SMCDetectionDisabled) { + return false; + } + + bool HitRWXInterval = false; + do { + const auto Query = RWXIntervals.Query(Address); + if (Query.Enclosed) { + if (!HitRWXInterval) { + if (ForWriteLocked) { + // If we are protecting as writable, then the entire range must be invalidated before any protections are + // applied and the invalidation mutex must be locked throughout. + // Do this lazily only when an RWX region is actually hit. + // NOTE: This assumes CodeInvalidationMutex is locked by the caller + InvalidateIntervalInternalLocked(Address, Size); + } + HitRWXInterval = true; + } + void* TmpAddress = reinterpret_cast(Address); + SIZE_T TmpSize = static_cast(std::min(End, Address + Query.Size) - Address); + ULONG TmpProt; + NtProtectVirtualMemory(NtCurrentProcess(), &TmpAddress, &TmpSize, ForWriteLocked ? PAGE_EXECUTE_READWRITE : PAGE_EXECUTE_READ, &TmpProt); + } else if (!Query.Size) { + // No more regions past `Address` in the interval list + break; + } + + Address += Query.Size; + } while (Address < End); + + return HitRWXInterval; +} + } // namespace FEX::Windows diff --git a/Source/Windows/Common/InvalidationTracker.h b/Source/Windows/Common/InvalidationTracker.h index 92e49d1a2..73b08b915 100644 --- a/Source/Windows/Common/InvalidationTracker.h +++ b/Source/Windows/Common/InvalidationTracker.h @@ -33,6 +33,11 @@ public: void InvalidateAlignedInterval(uint64_t Address, uint64_t Size, bool Free); void ReprotectRWXIntervals(uint64_t Address, uint64_t Size); bool HandleRWXAccessViolation(FEXCore::Core::InternalThreadState* Thread, uint64_t HostPC, uint64_t FaultAddress); + + // Unprotects any RWX intervals in the input interval and invalidates code + // NOTE: CodeInvalidationMutex must be locked when calling this, and if true is returned, kept locked until the write ends. + bool BeginUntrackedWriteLocked(uint64_t Address, uint64_t Size); + FEXCore::HLE::ExecutableRangeInfo QueryExecutableRange(uint64_t Address); private: @@ -42,6 +47,9 @@ private: // NOTE: This assumed CodeInvalidationMutex is locked by the caller void InvalidateIntervalInternalLocked(uint64_t Address, uint64_t Size); + // NOTE: If ForWriteLocked is true then this assumes CodeInvalidationMutex is locked by the caller, + // and any code in the range will be invalidated before protection as RWX, otherwise protects as RX if false. + bool ProtectRWXIntervalsInternal(uint64_t Address, uint64_t Size, bool ForWriteLocked); FEXCore::IntervalList XIntervals; FEXCore::IntervalList RWXIntervals; diff --git a/Source/Windows/WOW64/Module.cpp b/Source/Windows/WOW64/Module.cpp index 39800fa51..ea18a93cb 100644 --- a/Source/Windows/WOW64/Module.cpp +++ b/Source/Windows/WOW64/Module.cpp @@ -1004,7 +1004,25 @@ void BTCpuNotifyUnmapViewOfSection(void* Address, BOOL After, ULONG Status) { } } -void BTCpuNotifyReadFile(HANDLE Handle, void* Address, SIZE_T Size, BOOL After, NTSTATUS Status) {} +void BTCpuNotifyReadFile(HANDLE Handle, void* Address, SIZE_T Size, BOOL After, NTSTATUS Status) { + auto& InLockedRWXRead = GetFrontendThreadData(GetTLS().ThreadState())->InLockedRWXRead; + if (!After) { + ThreadCreationMutex.lock(); + CTX->GetCodeInvalidationMutex().lock(); + if (InvalidationTracker->BeginUntrackedWriteLocked(reinterpret_cast(Address), static_cast(Size))) { + InLockedRWXRead = true; + } else { + CTX->GetCodeInvalidationMutex().unlock(); + ThreadCreationMutex.unlock(); + } + } else { + if (InLockedRWXRead) { + InLockedRWXRead = false; + CTX->GetCodeInvalidationMutex().unlock(); + ThreadCreationMutex.unlock(); + } + } +} BOOLEAN WINAPI BTCpuIsProcessorFeaturePresent(UINT Feature) { return CPUFeatures->IsFeaturePresent(Feature) ? TRUE : FALSE;