// SPDX-License-Identifier: MIT #include #include #include #include #include #include #include "InvalidationTracker.h" #include #include namespace FEX::Windows { InvalidationTracker::InvalidationTracker(FEXCore::Context::Context& CTX, const std::unordered_map& 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(Address), &Info, sizeof(Info))) { uint64_t BaseAddress = reinterpret_cast(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::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 std::scoped_lock Lock(CTX.GetCodeInvalidationMutex()); FEXCore::Context::InvalidatedEntryAccumulator Accumulator; for (auto Thread : Threads) { CTX.InvalidateGuestCodeRange(Thread.second, Accumulator, AlignedBase, AlignedSize); } } } void InvalidationTracker::HandleImageMap(std::string_view Name, uint64_t Address) { auto* Nt = RtlImageNtHeader(reinterpret_cast(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(Address), MemoryBasicInformation, &Info, sizeof(Info), nullptr)) { return {Address, 0}; } const auto SectionBase = reinterpret_cast(Info.AllocationBase); auto SectionSize = reinterpret_cast(Info.BaseAddress) + Info.RegionSize - SectionBase; while (!NtQueryVirtualMemory(NtCurrentProcess(), reinterpret_cast(SectionBase + SectionSize), MemoryBasicInformation, &Info, sizeof(Info), nullptr) && reinterpret_cast(Info.AllocationBase) == SectionBase) { SectionSize += Info.RegionSize; } { std::scoped_lock Lock(CTX.GetCodeInvalidationMutex()); FEXCore::Context::InvalidatedEntryAccumulator Accumulator; for (auto Thread : Threads) { CTX.InvalidateGuestCodeRange(Thread.second, Accumulator, 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::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); { std::scoped_lock Lock(CTX.GetCodeInvalidationMutex()); FEXCore::Context::InvalidatedEntryAccumulator Accumulator; for (auto Thread : Threads) { CTX.InvalidateGuestCodeRange(Thread.second, Accumulator, 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(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); } bool InvalidationTracker::HandleRWXAccessViolation(FEXCore::Core::InternalThreadState* Thread, uint64_t HostPc, uint64_t FaultAddress) { const bool NeedsInvalidate = [&](uint64_t Address) { std::shared_lock Lock(IntervalsLock); const bool Enclosed = RWXIntervals.Query(Address).Enclosed; // Invalidate just the single faulting page if (!Enclosed) { return false; } ULONG TmpProt; void* TmpAddress = reinterpret_cast(Address); SIZE_T TmpSize = 1; NtProtectVirtualMemory(NtCurrentProcess(), &TmpAddress, &TmpSize, PAGE_EXECUTE_READWRITE, &TmpProt); return true; }(FaultAddress); if (NeedsInvalidate) { { // IntervalsLock cannot be held during invalidation std::scoped_lock Lock(CTX.GetCodeInvalidationMutex()); FEXCore::Context::InvalidatedEntryAccumulator Accumulator; for (auto Thread : Threads) { CTX.InvalidateGuestCodeRange(Thread.second, Accumulator, FaultAddress & FEXCore::Utils::FEX_PAGE_MASK, FEXCore::Utils::FEX_PAGE_SIZE); } } 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(RIP) != XChgOp && *reinterpret_cast(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::QueryResult Query; do { Query = RWXIntervals.Query(Address); if (Query.Enclosed) { void* TmpAddress = reinterpret_cast(Address); SIZE_T TmpSize = static_cast(Query.Size); ULONG TmpProt; NtProtectVirtualMemory(NtCurrentProcess(), &TmpAddress, &TmpSize, PAGE_EXECUTE_READWRITE, &TmpProt); } Address += Query.Size; } while (Query.Size); } } // namespace FEX::Windows