mirror of
https://github.com/FEX-Emu/FEX.git
synced 2026-10-14 06:00:41 +02:00
Theoretically, another thread could run after the reprotection but before the invalidation, write some code and jump to it but end up jumping to old code as the invalidation is yet to occur. Fix this by locking the invalidation mutex, invalidating and only then reprotecting.
270 lines
10 KiB
C++
270 lines
10 KiB
C++
// SPDX-License-Identifier: MIT
|
|
|
|
#include <FEXCore/Utils/LogManager.h>
|
|
#include <FEXCore/Utils/TypeDefines.h>
|
|
#include <FEXCore/Utils/SignalScopeGuards.h>
|
|
#include <FEXCore/Core/Context.h>
|
|
#include <FEXCore/Config/Config.h>
|
|
#include <FEXCore/Debug/InternalThreadState.h>
|
|
#include "InvalidationTracker.h"
|
|
#include <windef.h>
|
|
#include <winternl.h>
|
|
|
|
namespace FEX::Windows {
|
|
InvalidationTracker::InvalidationTracker(FEXCore::Context::Context& CTX, const std::unordered_map<DWORD, FEXCore::Core::InternalThreadState*>& Threads)
|
|
: CTX {CTX}
|
|
, Threads {Threads} {
|
|
FEX_CONFIG_OPT(SMCChecks, SMCCHECKS);
|
|
SMCDetectionDisabled = (SMCChecks == FEXCore::Config::CONFIG_SMC_NONE);
|
|
|
|
MEMORY_BASIC_INFORMATION Info;
|
|
uint64_t Address = 0;
|
|
|
|
while (VirtualQuery(reinterpret_cast<LPCVOID>(Address), &Info, sizeof(Info))) {
|
|
uint64_t BaseAddress = reinterpret_cast<uint64_t>(Info.BaseAddress);
|
|
if (Info.State == MEM_COMMIT) {
|
|
HandleMemoryProtectionNotification(BaseAddress, Info.RegionSize, Info.Protect);
|
|
}
|
|
|
|
Address = BaseAddress + Info.RegionSize;
|
|
}
|
|
}
|
|
|
|
void InvalidationTracker::HandleMemoryProtectionNotification(uint64_t Address, uint64_t Size, ULONG Prot) {
|
|
const auto AlignedBase = Address & FEXCore::Utils::FEX_PAGE_MASK;
|
|
const auto AlignedSize = (Address - AlignedBase + Size + FEXCore::Utils::FEX_PAGE_SIZE - 1) & FEXCore::Utils::FEX_PAGE_MASK;
|
|
|
|
const bool NeedsInvalidate = [&]() {
|
|
std::unique_lock Lock(IntervalsLock);
|
|
|
|
FEXCore::IntervalList<uint64_t>::Interval ProtInterval {AlignedBase, AlignedBase + AlignedSize};
|
|
if (Prot & (PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY)) {
|
|
XIntervals.Insert(ProtInterval);
|
|
if (Prot & (PAGE_EXECUTE_WRITECOPY | PAGE_EXECUTE_READWRITE)) {
|
|
LogMan::Msg::DFmt("Add SMC interval: {:X} - {:X}", AlignedBase, AlignedBase + AlignedSize);
|
|
RWXIntervals.Insert(ProtInterval);
|
|
}
|
|
return true;
|
|
} else if (XIntervals.Intersect(ProtInterval)) {
|
|
XIntervals.Remove(ProtInterval);
|
|
RWXIntervals.Remove(ProtInterval);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}();
|
|
|
|
if (NeedsInvalidate) {
|
|
// IntervalsLock cannot be held during invalidation
|
|
InvalidateIntervalInternal(AlignedBase, AlignedSize);
|
|
}
|
|
}
|
|
|
|
void InvalidationTracker::HandleImageMap(std::string_view Name, uint64_t Address) {
|
|
auto* Nt = RtlImageNtHeader(reinterpret_cast<HMODULE>(Address));
|
|
auto* SectionsBegin = IMAGE_FIRST_SECTION(Nt);
|
|
auto* SectionsEnd = SectionsBegin + Nt->FileHeader.NumberOfSections;
|
|
uint64_t LastExecutableSectionEnd = 0;
|
|
|
|
for (auto* Section = SectionsBegin; Section != SectionsEnd; Section++) {
|
|
if (Section->Characteristics & IMAGE_SCN_MEM_EXECUTE) {
|
|
std::unique_lock Lock(IntervalsLock);
|
|
|
|
uint64_t SectionBase = Address + Section->VirtualAddress;
|
|
uint64_t SectionEnd = SectionBase + Section->Misc.VirtualSize;
|
|
XIntervals.Insert({SectionBase, SectionEnd});
|
|
LastExecutableSectionEnd = std::max(LastExecutableSectionEnd, SectionEnd);
|
|
if (Section->Characteristics & IMAGE_SCN_MEM_WRITE) {
|
|
LogMan::Msg::DFmt("Add image SMC interval: {:X} - {:X}", SectionBase, SectionBase + Section->Misc.VirtualSize);
|
|
RWXIntervals.Insert({SectionBase, SectionBase + Section->Misc.VirtualSize});
|
|
}
|
|
}
|
|
}
|
|
|
|
FEX_CONFIG_OPT(MonoHacks, MONOHACKS);
|
|
if (MonoHacks && (Name == "mono-2.0-bdwgc.dll" || Name == "mono.dll")) {
|
|
FEX_CONFIG_OPT(MaxInst, MAXINST);
|
|
FEX_CONFIG_OPT(Multiblock, MULTIBLOCK);
|
|
if (Multiblock && MaxInst() >= 500) {
|
|
// Require these settings to ensure we can safely hook all SMC sites in a single block
|
|
CTX.MarkMonoDetected();
|
|
MonoBackpatcherDetectionPending = true;
|
|
MonoBase = Address;
|
|
MonoEnd = LastExecutableSectionEnd;
|
|
} else {
|
|
LogMan::Msg::IFmt("Not applying mono hacks, Multiblock with MaxInst >= 500 required");
|
|
}
|
|
}
|
|
}
|
|
|
|
InvalidationTracker::InvalidateContainingSectionResult InvalidationTracker::InvalidateContainingSection(uint64_t Address, bool Free) {
|
|
MEMORY_BASIC_INFORMATION Info;
|
|
if (NtQueryVirtualMemory(NtCurrentProcess(), reinterpret_cast<void*>(Address), MemoryBasicInformation, &Info, sizeof(Info), nullptr)) {
|
|
return {Address, 0};
|
|
}
|
|
|
|
const auto SectionBase = reinterpret_cast<uint64_t>(Info.AllocationBase);
|
|
auto SectionSize = reinterpret_cast<uint64_t>(Info.BaseAddress) + Info.RegionSize - SectionBase;
|
|
|
|
while (!NtQueryVirtualMemory(NtCurrentProcess(), reinterpret_cast<void*>(SectionBase + SectionSize), MemoryBasicInformation, &Info,
|
|
sizeof(Info), nullptr) &&
|
|
reinterpret_cast<uint64_t>(Info.AllocationBase) == SectionBase) {
|
|
SectionSize += Info.RegionSize;
|
|
}
|
|
|
|
InvalidateIntervalInternal(SectionBase, SectionSize);
|
|
|
|
if (Free) {
|
|
std::unique_lock Lock(IntervalsLock);
|
|
XIntervals.Remove({SectionBase, SectionBase + SectionSize});
|
|
RWXIntervals.Remove({SectionBase, SectionBase + SectionSize});
|
|
}
|
|
|
|
return {SectionBase, SectionSize};
|
|
}
|
|
|
|
void InvalidationTracker::InvalidateAlignedInterval(uint64_t Address, uint64_t Size, bool Free) {
|
|
if (!Address) {
|
|
// Match the Windows behaviour when passed a NULL base address.
|
|
Size = std::numeric_limits<uint64_t>::max();
|
|
}
|
|
|
|
const auto AlignedBase = Address & FEXCore::Utils::FEX_PAGE_MASK;
|
|
const auto AlignedSize = std::max(Size, (Address - AlignedBase + Size + FEXCore::Utils::FEX_PAGE_SIZE - 1) & FEXCore::Utils::FEX_PAGE_MASK);
|
|
|
|
InvalidateIntervalInternal(AlignedBase, AlignedSize);
|
|
|
|
if (Free) {
|
|
std::unique_lock Lock(IntervalsLock);
|
|
XIntervals.Remove({AlignedBase, AlignedBase + AlignedSize});
|
|
RWXIntervals.Remove({AlignedBase, AlignedBase + AlignedSize});
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
bool InvalidationTracker::HandleRWXAccessViolation(FEXCore::Core::InternalThreadState* Thread, uint64_t HostPc, uint64_t FaultAddress) {
|
|
const bool NeedsInvalidate = [&](uint64_t Address) {
|
|
std::shared_lock Lock(IntervalsLock);
|
|
return RWXIntervals.Query(Address).Enclosed;
|
|
}(FaultAddress);
|
|
|
|
if (NeedsInvalidate) {
|
|
// IntervalsLock cannot be held during invalidation
|
|
{
|
|
std::scoped_lock Lock(CTX.GetCodeInvalidationMutex());
|
|
|
|
InvalidateIntervalInternalLocked(FaultAddress & FEXCore::Utils::FEX_PAGE_MASK, FEXCore::Utils::FEX_PAGE_SIZE);
|
|
|
|
// Invalidate, then unprotect the faulting page with the compilation lock held to ensure that any racing invalidations are not dropped.
|
|
ULONG TmpProt;
|
|
void* TmpAddress = reinterpret_cast<void*>(FaultAddress);
|
|
SIZE_T TmpSize = 1;
|
|
NtProtectVirtualMemory(NtCurrentProcess(), &TmpAddress, &TmpSize, PAGE_EXECUTE_READWRITE, &TmpProt);
|
|
}
|
|
DetectMonoBackpatcherBlock(Thread, HostPc);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
FEXCore::HLE::ExecutableRangeInfo InvalidationTracker::QueryExecutableRange(uint64_t Address) {
|
|
std::shared_lock Lock(IntervalsLock);
|
|
const auto XResult = XIntervals.Query(Address);
|
|
if (!XResult.Enclosed) {
|
|
return {};
|
|
}
|
|
const auto RWXResult = RWXIntervals.Query(Address);
|
|
if (RWXResult.Enclosed) {
|
|
return {RWXResult.Interval.Offset, RWXResult.Interval.End - RWXResult.Interval.Offset, true};
|
|
} else if (RWXResult.Size && RWXResult.Size < XResult.Size) {
|
|
return {XResult.Interval.Offset, RWXResult.Interval.Offset - XResult.Interval.Offset, false};
|
|
}
|
|
return {XResult.Interval.Offset, XResult.Interval.End - XResult.Interval.Offset, false};
|
|
}
|
|
|
|
void InvalidationTracker::DetectMonoBackpatcherBlock(FEXCore::Core::InternalThreadState* Thread, uint64_t HostPc) {
|
|
if (!MonoBackpatcherDetectionPending) {
|
|
return;
|
|
}
|
|
|
|
if (!CTX.IsAddressInCodeBuffer(Thread, HostPc)) {
|
|
return;
|
|
}
|
|
|
|
uint64_t RIP = CTX.RestoreRIPFromHostPC(Thread, HostPc);
|
|
if (!RIP || RIP < MonoBase || RIP >= MonoEnd) {
|
|
return;
|
|
}
|
|
|
|
static constexpr uint8_t XChgOp = 0x87;
|
|
if (*reinterpret_cast<uint8_t*>(RIP) != XChgOp && *reinterpret_cast<uint8_t*>(RIP + 1) != XChgOp) {
|
|
return;
|
|
}
|
|
|
|
uint64_t BlockEntry = CTX.GetGuestBlockEntry(Thread);
|
|
LogMan::Msg::DFmt("Detected mono backpatcher at: {:X}", BlockEntry);
|
|
DisableSMCDetection();
|
|
{
|
|
std::scoped_lock CodeLock(CTX.GetCodeInvalidationMutex());
|
|
CTX.MarkMonoBackpatcherBlock(BlockEntry);
|
|
}
|
|
InvalidateAlignedInterval(BlockEntry, FEXCore::Utils::FEX_PAGE_SIZE, false);
|
|
}
|
|
|
|
void InvalidationTracker::DisableSMCDetection() {
|
|
std::unique_lock Lock(IntervalsLock);
|
|
SMCDetectionDisabled = true;
|
|
uint64_t Address = 0;
|
|
|
|
// Reprotect all RWX intervals as RWX
|
|
FEXCore::IntervalList<uint64_t>::QueryResult Query;
|
|
do {
|
|
Query = RWXIntervals.Query(Address);
|
|
if (Query.Enclosed) {
|
|
void* TmpAddress = reinterpret_cast<void*>(Address);
|
|
SIZE_T TmpSize = static_cast<SIZE_T>(Query.Size);
|
|
ULONG TmpProt;
|
|
NtProtectVirtualMemory(NtCurrentProcess(), &TmpAddress, &TmpSize, PAGE_EXECUTE_READWRITE, &TmpProt);
|
|
}
|
|
Address += Query.Size;
|
|
} while (Query.Size);
|
|
}
|
|
|
|
void InvalidationTracker::InvalidateIntervalInternal(uint64_t Address, uint64_t Size) {
|
|
std::scoped_lock CodeLock(CTX.GetCodeInvalidationMutex());
|
|
InvalidateIntervalInternalLocked(Address, Size);
|
|
}
|
|
|
|
void InvalidationTracker::InvalidateIntervalInternalLocked(uint64_t Address, uint64_t Size) {
|
|
// NOTE: This assumes CodeInvalidationMutex is locked by the caller
|
|
CTX.InvalidateCodeBuffersCodeRange(Address, Size);
|
|
for (auto Thread : Threads) {
|
|
CTX.InvalidateThreadCachedCodeRange(Thread.second, Address, Size);
|
|
}
|
|
}
|
|
|
|
} // namespace FEX::Windows
|