diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt index b8c52b3db390..4f5c01e6a655 100644 --- a/Source/Core/Common/CMakeLists.txt +++ b/Source/Core/Common/CMakeLists.txt @@ -96,7 +96,7 @@ if(USE_EGL) endif() if(WIN32) - set(SRCS ${SRCS} GL/GLInterface/WGL.cpp) + set(SRCS ${SRCS} GL/GLInterface/WGL.cpp CompatPatches.cpp) elseif(APPLE) set(SRCS ${SRCS} GL/GLInterface/AGL.mm) elseif(HAIKU) @@ -125,4 +125,6 @@ if(UNIX) if(HAIKU) target_link_libraries(traversal_server network) endif() +elseif(WIN32) + target_link_libraries(common PRIVATE "-INCLUDE:enableCompatPatches") endif() diff --git a/Source/Core/Common/Common.vcxproj b/Source/Core/Common/Common.vcxproj index ed8a24bbc194..4c8d2f1b610f 100644 --- a/Source/Core/Common/Common.vcxproj +++ b/Source/Core/Common/Common.vcxproj @@ -125,6 +125,7 @@ + @@ -163,6 +164,7 @@ + @@ -179,6 +181,7 @@ + @@ -197,7 +200,6 @@ - @@ -231,4 +233,4 @@ - + \ No newline at end of file diff --git a/Source/Core/Common/Common.vcxproj.filters b/Source/Core/Common/Common.vcxproj.filters index 7729f21960d6..10aa2c9d365e 100644 --- a/Source/Core/Common/Common.vcxproj.filters +++ b/Source/Core/Common/Common.vcxproj.filters @@ -253,6 +253,7 @@ GL\GLExtensions + @@ -317,10 +318,11 @@ GL\GLInterface - + + @@ -328,4 +330,4 @@ - + \ No newline at end of file diff --git a/Source/Core/Common/CompatPatches.cpp b/Source/Core/Common/CompatPatches.cpp new file mode 100644 index 000000000000..f835ac2279a0 --- /dev/null +++ b/Source/Core/Common/CompatPatches.cpp @@ -0,0 +1,274 @@ +// Copyright 2008 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/LdrWatcher.h" +#include "Common/StringUtil.h" + +typedef NTSTATUS(NTAPI* PRTL_HEAP_COMMIT_ROUTINE)(IN PVOID Base, IN OUT PVOID* CommitAddress, + IN OUT PSIZE_T CommitSize); + +typedef struct _RTL_HEAP_PARAMETERS +{ + ULONG Length; + SIZE_T SegmentReserve; + SIZE_T SegmentCommit; + SIZE_T DeCommitFreeBlockThreshold; + SIZE_T DeCommitTotalFreeThreshold; + SIZE_T MaximumAllocationSize; + SIZE_T VirtualMemoryThreshold; + SIZE_T InitialCommit; + SIZE_T InitialReserve; + PRTL_HEAP_COMMIT_ROUTINE CommitRoutine; + SIZE_T Reserved[2]; +} RTL_HEAP_PARAMETERS, *PRTL_HEAP_PARAMETERS; + +typedef PVOID (*RtlCreateHeap_t)(_In_ ULONG Flags, _In_opt_ PVOID HeapBase, + _In_opt_ SIZE_T ReserveSize, _In_opt_ SIZE_T CommitSize, + _In_opt_ PVOID Lock, _In_opt_ PRTL_HEAP_PARAMETERS Parameters); + +static HANDLE WINAPI HeapCreateLow4GB(_In_ DWORD flOptions, _In_ SIZE_T dwInitialSize, + _In_ SIZE_T dwMaximumSize) +{ + auto ntdll = GetModuleHandleW(L"ntdll"); + if (!ntdll) + return nullptr; + auto RtlCreateHeap = reinterpret_cast(GetProcAddress(ntdll, "RtlCreateHeap")); + if (!RtlCreateHeap) + return nullptr; + // These values are arbitrary; just change them if problems are encountered later. + uintptr_t target_addr = 0x00200000; + size_t max_heap_size = 0x01000000; + uintptr_t highest_addr = (1ull << 32) - max_heap_size; + void* low_heap = nullptr; + for (; !low_heap && target_addr <= highest_addr; target_addr += 0x1000) + low_heap = VirtualAlloc((void*)target_addr, max_heap_size, MEM_RESERVE, PAGE_READWRITE); + if (!low_heap) + return nullptr; + return RtlCreateHeap(0, low_heap, 0, 0, nullptr, nullptr); +} + +static bool ModifyProtectedRegion(void* address, size_t size, std::function func) +{ + DWORD old_protect; + if (!VirtualProtect(address, size, PAGE_READWRITE, &old_protect)) + return false; + func(); + if (!VirtualProtect(address, size, old_protect, &old_protect)) + return false; + return true; +} + +// Does not do input sanitization - assumes well-behaved input since Ldr has already parsed it. +class ImportPatcher +{ +public: + ImportPatcher(uintptr_t module_base) : base(module_base) + { + auto mz = reinterpret_cast(base); + auto pe = reinterpret_cast(base + mz->e_lfanew); + directories = pe->OptionalHeader.DataDirectory; + } + template + T GetRva(uint32_t rva) + { + return reinterpret_cast(base + rva); + } + bool PatchIAT(const char* module_name, const char* function_name, void* value) + { + auto import_dir = &directories[IMAGE_DIRECTORY_ENTRY_IMPORT]; + for (auto import_desc = GetRva(import_dir->VirtualAddress); + import_desc->OriginalFirstThunk; import_desc++) + { + auto module = GetRva(import_desc->Name); + auto names = GetRva(import_desc->OriginalFirstThunk); + auto thunks = GetRva(import_desc->FirstThunk); + if (!stricmp(module, module_name)) + { + for (auto name = names; name->u1.Function; name++) + { + if (!IMAGE_SNAP_BY_ORDINAL(name->u1.Ordinal)) + { + auto import = GetRva(name->u1.AddressOfData); + if (!strcmp(import->Name, function_name)) + { + auto index = name - names; + return ModifyProtectedRegion(&thunks[index], sizeof(thunks[index]), [=] { + thunks[index].u1.Function = + reinterpret_cast(value); + }); + } + } + } + // Function not found + return false; + } + } + // Module not found + return false; + } + +private: + uintptr_t base; + PIMAGE_DATA_DIRECTORY directories; +}; + +struct UcrtPatchInfo +{ + u32 checksum; + u32 rva; + u32 length; +}; + +bool ApplyUcrtPatch(const wchar_t* name, const UcrtPatchInfo& patch) +{ + auto module = GetModuleHandleW(name); + if (!module) + return false; + auto pe = (PIMAGE_NT_HEADERS)((uintptr_t)module + ((PIMAGE_DOS_HEADER)module)->e_lfanew); + if (pe->OptionalHeader.CheckSum != patch.checksum) + return false; + void* patch_addr = (void*)((uintptr_t)module + patch.rva); + size_t patch_size = patch.length; + ModifyProtectedRegion(patch_addr, patch_size, [=] { memset(patch_addr, 0x90, patch_size); }); + FlushInstructionCache(GetCurrentProcess(), patch_addr, patch_size); + return true; +} + +#pragma comment(lib, "version.lib") + +struct Version +{ + u16 major; + u16 minor; + u16 build; + u16 qfe; + Version& operator=(u64&& rhs) + { + major = static_cast(rhs >> 48); + minor = static_cast(rhs >> 32); + build = static_cast(rhs >> 16); + qfe = static_cast(rhs); + return *this; + } +}; + +static bool GetModulePath(const wchar_t* name, std::wstring* path) +{ + auto module = GetModuleHandleW(name); + if (module == nullptr) + return false; + DWORD path_len = MAX_PATH; +retry: + path->resize(path_len); + path_len = GetModuleFileNameW(module, const_cast(path->data()), + static_cast(path->size())); + if (!path_len) + return false; + auto error = GetLastError(); + if (error == ERROR_SUCCESS) + return true; + if (error == ERROR_INSUFFICIENT_BUFFER) + goto retry; + return false; +} + +static bool GetModuleVersion(const wchar_t* name, Version* version) +{ + std::wstring path; + if (!GetModulePath(name, &path)) + return false; + DWORD handle; + DWORD data_len = GetFileVersionInfoSizeW(path.c_str(), &handle); + if (!data_len) + return false; + std::vector block(data_len); + if (!GetFileVersionInfoW(path.c_str(), handle, data_len, block.data())) + return false; + void* buf; + UINT buf_len; + if (!VerQueryValueW(block.data(), LR"(\)", &buf, &buf_len)) + return false; + auto info = static_cast(buf); + *version = (static_cast(info->dwFileVersionMS) << 32) | info->dwFileVersionLS; + return true; +} + +void CompatPatchesInstall(LdrWatcher* watcher) +{ + watcher->Install({{L"EZFRD64.dll", L"811EZFRD64.DLL"}, + [](const LdrDllLoadEvent& event) { + // *EZFRD64 is incldued in software packages for cheapo third-party gamepads + // (and gamepad adapters). The module cannot handle its heap being above 4GB, + // which tends to happen very often on modern Windows. + // NOTE: The patch will always be applied, but it will only actually avoid the + // crash if applied before module initialization (i.e. called on the Ldr + // callout path). + auto patcher = ImportPatcher(event.base_address); + patcher.PatchIAT("kernel32.dll", "HeapCreate", HeapCreateLow4GB); + }}); + watcher->Install({{L"ucrtbase.dll"}, + [](const LdrDllLoadEvent& event) { + // ucrtbase implements caching between fseek/fread, old versions have a bug + // such that some reads return incorrect data. This causes noticable bugs + // in dolphin since we use these APIs for reading game images. + Version version; + if (!GetModuleVersion(event.name.c_str(), &version)) + return; + const u16 fixed_build = 10548; + if (version.build >= fixed_build) + return; + const UcrtPatchInfo patches[] = { + // 10.0.10240.16384 (th1.150709-1700) + {0xF61ED, 0x6AE7B, 5}, + // 10.0.10240.16390 (th1_st1.150714-1601) + {0xF5ED9, 0x6AE7B, 5}, + // 10.0.10137.0 (th1.150602-2238) + {0xF8B5E, 0x63ED6, 2}, + }; + for (const auto& patch : patches) + { + if (ApplyUcrtPatch(event.name.c_str(), patch)) + return; + } + // If we reach here, the version is buggy (afaik) and patching failed + auto msg = StringFromFormat( + "You are running %S version %d.%d.%d.%d.\n" + "An important fix affecting Dolphin was introduced in build %d.\n" + "You can use Dolphin, but there will be known bugs.\n" + "Please update this file by installing the latest Universal C Runtime.\n", + event.name.c_str(), version.major, version.minor, version.build, + version.qfe, fixed_build); + // Use MessageBox for maximal user annoyance + MessageBoxA(nullptr, msg.c_str(), "WARNING: BUGGY UCRT VERSION", + MB_ICONEXCLAMATION); + }}); +} + +int __cdecl EnableCompatPatches() +{ + static LdrWatcher watcher; + CompatPatchesInstall(&watcher); + return 0; +} + +// Create a segment which is recognized by the linker to be part of the CRT +// initialization. XI* = C startup, XC* = C++ startup. "A" placement is reserved +// for system use. C startup is before C++. +// Use last C++ slot in hopes that makes using C++ from this code safe. +#pragma section(".CRT$XCZ", read) + +// Place a symbol in the special segment, make it have C linkage so that +// referencing it doesn't require ugly decorated names. +// Use /include:enableCompatPatches linker flag to enable this. +extern "C" { +__declspec(allocate(".CRT$XCZ")) decltype(&EnableCompatPatches) + enableCompatPatches = EnableCompatPatches; +}; diff --git a/Source/Core/Common/LdrWatcher.cpp b/Source/Core/Common/LdrWatcher.cpp new file mode 100644 index 000000000000..8fe1ee5f72cc --- /dev/null +++ b/Source/Core/Common/LdrWatcher.cpp @@ -0,0 +1,178 @@ +// Copyright 2008 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include +#include +#include +#include + +#include "Common/LdrWatcher.h" + +typedef struct _LDR_DLL_LOADED_NOTIFICATION_DATA +{ + ULONG Flags; // Reserved. + PCUNICODE_STRING FullDllName; // The full path name of the DLL module. + PCUNICODE_STRING BaseDllName; // The base file name of the DLL module. + PVOID DllBase; // A pointer to the base address for the DLL in memory. + ULONG SizeOfImage; // The size of the DLL image, in bytes. +} LDR_DLL_LOADED_NOTIFICATION_DATA, *PLDR_DLL_LOADED_NOTIFICATION_DATA; + +typedef struct _LDR_DLL_UNLOADED_NOTIFICATION_DATA +{ + ULONG Flags; // Reserved. + PCUNICODE_STRING FullDllName; // The full path name of the DLL module. + PCUNICODE_STRING BaseDllName; // The base file name of the DLL module. + PVOID DllBase; // A pointer to the base address for the DLL in memory. + ULONG SizeOfImage; // The size of the DLL image, in bytes. +} LDR_DLL_UNLOADED_NOTIFICATION_DATA, *PLDR_DLL_UNLOADED_NOTIFICATION_DATA; + +typedef union _LDR_DLL_NOTIFICATION_DATA +{ + LDR_DLL_LOADED_NOTIFICATION_DATA Loaded; + LDR_DLL_UNLOADED_NOTIFICATION_DATA Unloaded; +} LDR_DLL_NOTIFICATION_DATA, *PLDR_DLL_NOTIFICATION_DATA; +typedef const LDR_DLL_NOTIFICATION_DATA* PCLDR_DLL_NOTIFICATION_DATA; + +#define LDR_DLL_NOTIFICATION_REASON_LOADED (1) +#define LDR_DLL_NOTIFICATION_REASON_UNLOADED (2) + +typedef VOID NTAPI LDR_DLL_NOTIFICATION_FUNCTION(_In_ ULONG NotificationReason, + _In_ PCLDR_DLL_NOTIFICATION_DATA NotificationData, + _In_opt_ PVOID Context); +typedef LDR_DLL_NOTIFICATION_FUNCTION* PLDR_DLL_NOTIFICATION_FUNCTION; + +typedef NTSTATUS(NTAPI* LdrRegisterDllNotification_t)( + _In_ ULONG Flags, _In_ PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction, + _In_opt_ PVOID Context, _Out_ PVOID* Cookie); + +typedef NTSTATUS(NTAPI* LdrUnregisterDllNotification_t)(_In_ PVOID Cookie); + +static void LdrObserverRun(const LdrObserver& observer, PCUNICODE_STRING module_name, + uintptr_t base_address) +{ + for (auto& needle : observer.module_names) + { + // Like RtlCompareUnicodeString, but saves dynamically resolving it. + // NOTE: Does not compare null terminator. + auto compare_length = module_name->Length / sizeof(wchar_t); + if (!_wcsnicmp(needle.c_str(), module_name->Buffer, compare_length)) + observer.action({needle, base_address}); + } +} + +static VOID DllNotificationCallback(ULONG NotificationReason, + PCLDR_DLL_NOTIFICATION_DATA NotificationData, PVOID Context) +{ + if (NotificationReason != LDR_DLL_NOTIFICATION_REASON_LOADED) + return; + auto& data = NotificationData->Loaded; + auto observer = static_cast(Context); + LdrObserverRun(*observer, data.BaseDllName, reinterpret_cast(data.DllBase)); +} + +// This only works on Vista+. On lower platforms, it will be a no-op. +class LdrDllNotifier +{ +public: + static LdrDllNotifier& GetInstance() + { + static LdrDllNotifier notifier; + return notifier; + }; + void Install(LdrObserver* observer); + void Uninstall(LdrObserver* observer); + +private: + LdrDllNotifier(); + bool Init(); + LdrRegisterDllNotification_t LdrRegisterDllNotification{}; + LdrUnregisterDllNotification_t LdrUnregisterDllNotification{}; + bool initialized{}; +}; + +LdrDllNotifier::LdrDllNotifier() +{ + initialized = Init(); +} + +bool LdrDllNotifier::Init() +{ + auto ntdll = GetModuleHandleW(L"ntdll"); + if (!ntdll) + return false; + LdrRegisterDllNotification = reinterpret_cast( + GetProcAddress(ntdll, "LdrRegisterDllNotification")); + if (!LdrRegisterDllNotification) + return false; + LdrUnregisterDllNotification = reinterpret_cast( + GetProcAddress(ntdll, "LdrUnregisterDllNotification")); + if (!LdrUnregisterDllNotification) + return false; + return true; +} + +void LdrDllNotifier::Install(LdrObserver* observer) +{ + if (!initialized) + return; + void* cookie{}; + if (!NT_SUCCESS(LdrRegisterDllNotification(0, DllNotificationCallback, + static_cast(observer), &cookie))) + cookie = {}; + observer->cookie = cookie; + return; +} + +void LdrDllNotifier::Uninstall(LdrObserver* observer) +{ + if (!initialized) + return; + LdrUnregisterDllNotification(observer->cookie); + observer->cookie = {}; + return; +} + +LdrWatcher::~LdrWatcher() +{ + UninstallAll(); +} + +// Needed for RtlInitUnicodeString +#pragma comment(lib, "ntdll") + +bool LdrWatcher::InjectCurrentModules(const LdrObserver& observer) +{ + // Use TlHelp32 instead of psapi functions to reduce dolphin's dependency on psapi + // (revisit this when Win7 support is dropped). + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, GetCurrentProcessId()); + if (snapshot == INVALID_HANDLE_VALUE) + return false; + MODULEENTRY32 entry; + entry.dwSize = sizeof(entry); + for (BOOL rv = Module32First(snapshot, &entry); rv == TRUE; rv = Module32Next(snapshot, &entry)) + { + UNICODE_STRING module_name; + RtlInitUnicodeString(&module_name, entry.szModule); + LdrObserverRun(observer, &module_name, reinterpret_cast(entry.modBaseAddr)); + } + CloseHandle(snapshot); + return true; +} + +void LdrWatcher::Install(const LdrObserver& observer) +{ + observers.emplace_back(observer); + auto& new_observer = observers.back(); + // Register for notifications before looking at the list of current modules. + // This ensures none are missed, but there is a tiny chance some will be seen twice. + LdrDllNotifier::GetInstance().Install(&new_observer); + InjectCurrentModules(new_observer); +} + +void LdrWatcher::UninstallAll() +{ + for (auto& observer : observers) + LdrDllNotifier::GetInstance().Uninstall(&observer); + observers.clear(); +} diff --git a/Source/Core/Common/LdrWatcher.h b/Source/Core/Common/LdrWatcher.h new file mode 100644 index 000000000000..7ec1fdf7c1ae --- /dev/null +++ b/Source/Core/Common/LdrWatcher.h @@ -0,0 +1,38 @@ +// Copyright 2008 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include +#include + +struct LdrDllLoadEvent +{ + const std::wstring& name; + uintptr_t base_address; +}; + +class LdrObserver +{ +public: + std::vector module_names; + // NOTE: This may be called from a Ldr callout. While most things are probably fine, try to + // keep things as simple as possible, and just queue real work onto something else. + std::function action; + void* cookie{}; +}; + +class LdrWatcher +{ +public: + void Install(const LdrObserver& observer); + ~LdrWatcher(); + +private: + bool InjectCurrentModules(const LdrObserver& observer); + void UninstallAll(); + std::list observers; +}; diff --git a/Source/Core/Common/ucrtFreadWorkaround.cpp b/Source/Core/Common/ucrtFreadWorkaround.cpp deleted file mode 100644 index 80ad855fec5a..000000000000 --- a/Source/Core/Common/ucrtFreadWorkaround.cpp +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2014 Dolphin Emulator Project -// Licensed under GPLv2+ -// Refer to the license.txt file included. - -#if defined(_WIN32) - -#include -#include "CommonTypes.h" - -struct PatchInfo -{ - const wchar_t* module_name; - u32 checksum; - u32 rva; - u32 length; -} static const s_patches[] = { - // 10.0.10240.16384 (th1.150709-1700) - {L"ucrtbase.dll", 0xF61ED, 0x6AE7B, 5}, - // 10.0.10240.16390 (th1_st1.150714-1601) - {L"ucrtbase.dll", 0xF5ED9, 0x6AE7B, 5}, - // 10.0.10137.0 (th1.150602-2238) - {L"ucrtbase.dll", 0xF8B5E, 0x63ED6, 2}, - // 10.0.10150.0 (th1.150616-1659) - {L"ucrtbased.dll", 0x1C1915, 0x91905, 5}, -}; - -bool ApplyPatch(const PatchInfo& patch) -{ - auto module = GetModuleHandleW(patch.module_name); - if (module == nullptr) - { - return false; - } - - auto ucrtbase_pe = (PIMAGE_NT_HEADERS)((uintptr_t)module + ((PIMAGE_DOS_HEADER)module)->e_lfanew); - if (ucrtbase_pe->OptionalHeader.CheckSum != patch.checksum) - { - return false; - } - - void* patch_addr = (void*)((uintptr_t)module + patch.rva); - size_t patch_size = patch.length; - - DWORD old_protect; - if (!VirtualProtect(patch_addr, patch_size, PAGE_EXECUTE_READWRITE, &old_protect)) - { - return false; - } - - memset(patch_addr, 0x90, patch_size); - - VirtualProtect(patch_addr, patch_size, old_protect, &old_protect); - - FlushInstructionCache(GetCurrentProcess(), patch_addr, patch_size); - - return true; -} - -int __cdecl EnableucrtFreadWorkaround() -{ - // This patches ucrtbase such that fseek will always - // synchronize the file object's internal buffer. - - bool applied_at_least_one = false; - for (const auto& patch : s_patches) - { - if (ApplyPatch(patch)) - { - applied_at_least_one = true; - } - } - - /* For forward compat, do not fail if patches don't apply (e.g. version mismatch) - if (!applied_at_least_one) { - std::abort(); - } - //*/ - - return 0; -} - -// Create a segment which is recognized by the linker to be part of the CRT -// initialization. XI* = C startup, XC* = C++ startup. "A" placement is reserved -// for system use. Thus, the earliest we can get is XIB (C startup is before -// C++). -#pragma section(".CRT$XIB", read) - -// Place a symbol in the special segment, make it have C linkage so that -// referencing it doesn't require ugly decorated names. -// Use /include:EnableucrtFreadWorkaround linker flag to enable this. -extern "C" { -__declspec(allocate(".CRT$XIB")) decltype(&EnableucrtFreadWorkaround) - ucrtFreadWorkaround = EnableucrtFreadWorkaround; -}; - -#endif diff --git a/Source/VSProps/Base.props b/Source/VSProps/Base.props index 335d2537a3b6..b7d04a660209 100644 --- a/Source/VSProps/Base.props +++ b/Source/VSProps/Base.props @@ -157,8 +157,8 @@ - - ucrtFreadWorkaround + + enableCompatPatches /NODEFAULTLIB:libcmt %(AdditionalOptions)