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.
This commit is contained in:
Billy Laws committed 2026-01-05 02:34:00 +00:00
1 parent ec13e5d503
commit 8e9f593c39
4 files changed
+93 -23

No files matched your search

+24 -1
View File
@@ -881,7 +881,30 @@ void BTCpu64NotifyMemoryDirty(void* Address, SIZE_T Size) {
InvalidationTracker->InvalidateAlignedInterval(reinterpret_cast<uint64_t>(Address), static_cast<uint64_t>(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<uint64_t>(Address), static_cast<uint64_t>(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);
+42 -21
View File
@@ -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<void*>(Address);
SIZE_T TmpSize = static_cast<SIZE_T>(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<void*>(Address);
SIZE_T TmpSize = static_cast<SIZE_T>(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
@@ -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<uint64_t> XIntervals;
FEXCore::IntervalList<uint64_t> RWXIntervals;
+19 -1
View File
@@ -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<uint64_t>(Address), static_cast<uint64_t>(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;