From 608ea6ade9907d5104b92fa74878ef7cff55474b Mon Sep 17 00:00:00 2001 From: marzent Date: Sat, 17 Feb 2024 19:57:31 +0100 Subject: [PATCH] make IsResumeGameAfterPluginLoad work with DllInject --- Dalamud.Boot/DalamudStartInfo.cpp | 6 +-- Dalamud.Boot/dllmain.cpp | 14 ++++-- Dalamud.Boot/rewrite_entrypoint.cpp | 66 +++++++++++++++++------------ Dalamud.Injector/EntryPoint.cs | 32 +++++++++++--- Dalamud.Injector/GameStart.cs | 18 ++++++-- Dalamud/Dalamud.cs | 31 +++++++++++--- Dalamud/EntryPoint.cs | 41 +++++++++++++++--- Dalamud/NativeFunctions.cs | 34 +++++++++++++++ 8 files changed, 187 insertions(+), 55 deletions(-) diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index d20265bf86..8e1c51fcb2 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -74,10 +74,10 @@ void from_json(const nlohmann::json& json, DalamudStartInfo::LoadMethod& value) } else if (json.is_string()) { - const auto langstr = unicode::convert(json.get(), &unicode::lower); - if (langstr == "entrypoint") + const auto loadstr = unicode::convert(json.get(), &unicode::lower); + if (loadstr == "entrypoint") value = DalamudStartInfo::LoadMethod::Entrypoint; - else if (langstr == "inject") + else if (loadstr == "inject") value = DalamudStartInfo::LoadMethod::DllInject; } } diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index e6aa9c4ac5..5de5f1c6a1 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -9,12 +9,12 @@ HMODULE g_hModule; HINSTANCE g_hGameInstance = GetModuleHandleW(nullptr); -HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { +HRESULT WINAPI InitializeImpl(char* pLoadInfo, HANDLE hMainThread) { g_startInfo.from_envvars(); std::string jsonParseError; try { - from_json(nlohmann::json::parse(std::string_view(static_cast(lpParam))), g_startInfo); + from_json(nlohmann::json::parse(std::string_view(pLoadInfo)), g_startInfo); } catch (const std::exception& e) { jsonParseError = e.what(); } @@ -153,14 +153,20 @@ HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { // utils::wait_for_game_window(); logging::I("Initializing Dalamud..."); - entrypoint_fn(lpParam, hMainThreadContinue); + entrypoint_fn(pLoadInfo, hMainThread); logging::I("Done!"); return S_OK; } +struct InitializeParams { + char* pLoadInfo; + HANDLE hMainThread; +}; + extern "C" DWORD WINAPI Initialize(LPVOID lpParam) { - return InitializeImpl(lpParam, CreateEvent(nullptr, TRUE, FALSE, nullptr)); + InitializeParams* params = static_cast(lpParam); + return InitializeImpl(params->pLoadInfo, params->hMainThread); } BOOL APIENTRY DllMain(const HMODULE hModule, const DWORD dwReason, LPVOID lpReserved) { diff --git a/Dalamud.Boot/rewrite_entrypoint.cpp b/Dalamud.Boot/rewrite_entrypoint.cpp index 3a1672af7f..b8a8b43a27 100644 --- a/Dalamud.Boot/rewrite_entrypoint.cpp +++ b/Dalamud.Boot/rewrite_entrypoint.cpp @@ -3,7 +3,7 @@ #include "logging.h" #include "utils.h" -HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue); +HRESULT WINAPI InitializeImpl(char* pLoadInfo, HANDLE hMainThread); struct RewrittenEntryPointParameters { char* pEntrypoint; @@ -226,21 +226,13 @@ void* get_mapped_image_base_address(HANDLE hProcess, const std::filesystem::path throw std::runtime_error("corresponding base address not found"); } -/// @brief Rewrite target process' entry point so that this DLL can be loaded and executed first. +/// @brief Get the target process' entry point. /// @param hProcess Process handle. /// @param pcwzPath Path to target process. -/// @param pcwzLoadInfo JSON string to be passed to Initialize. -/// @return null if successful; memory containing wide string allocated via GlobalAlloc if unsuccessful -/// -/// When the process has just been started up via CreateProcess (CREATE_SUSPENDED), GetModuleFileName and alikes result in an error. -/// Instead, we have to enumerate through all the files mapped into target process' virtual address space and find the base address -/// of memory region corresponding to the path given. +/// @return address to entry point; null if unsuccessful. /// -extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath, const wchar_t* pcwzLoadInfo) { - std::wstring last_operation; - SetLastError(ERROR_SUCCESS); +extern "C" char* WINAPI GetRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath) { try { - last_operation = L"get_mapped_image_base_address"; const auto base_address = static_cast(get_mapped_image_base_address(hProcess, pcwzPath)); IMAGE_DOS_HEADER dos_header{}; @@ -249,14 +241,37 @@ extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_ IMAGE_NT_HEADERS64 nt_header64{}; }; - last_operation = L"read_process_memory_or_throw(base_address)"; read_process_memory_or_throw(hProcess, base_address, dos_header); - - last_operation = L"read_process_memory_or_throw(base_address + dos_header.e_lfanew)"; read_process_memory_or_throw(hProcess, base_address + dos_header.e_lfanew, nt_header64); const auto entrypoint = base_address + (nt_header32.OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC ? nt_header32.OptionalHeader.AddressOfEntryPoint : nt_header64.OptionalHeader.AddressOfEntryPoint); + return entrypoint; + } + catch (const std::exception& e) { + logging::E("Failed to retrieve entry point for 0x{:X}: {}", hProcess, e.what()); + return nullptr; + } +} + +/// @brief Rewrite target process' entry point so that this DLL can be loaded and executed first. +/// @param hProcess Process handle. +/// @param pcwzPath Path to target process. +/// @param pcwzLoadInfo JSON string to be passed to Initialize. +/// @return null if successful; memory containing wide string allocated via GlobalAlloc if unsuccessful +/// +/// When the process has just been started up via CreateProcess (CREATE_SUSPENDED), GetModuleFileName and alikes result in an error. +/// Instead, we have to enumerate through all the files mapped into target process' virtual address space and find the base address +/// of memory region corresponding to the path given. +/// +extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath, const wchar_t* pcwzLoadInfo) { + std::wstring last_operation; + SetLastError(ERROR_SUCCESS); + try { + last_operation = L"GetRemoteEntryPointW"; + const auto entrypoint = GetRemoteEntryPointW(hProcess, pcwzPath); + if (!entrypoint) + throw std::runtime_error("GetRemoteEntryPointW"); last_operation = L"get_path_from_local_module(g_hModule)"; auto local_module_path = get_path_from_local_module(g_hModule); @@ -336,7 +351,7 @@ extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_ /// @brief Entry point function "called" instead of game's original main entry point. /// @param params Parameters set up from RewriteRemoteEntryPoint. extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointParameters & params) { - HANDLE hMainThreadContinue = nullptr; + HANDLE hMainThread = nullptr; auto hr = S_OK; std::wstring last_operation; std::wstring exc_msg; @@ -352,13 +367,13 @@ extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointPara write_process_memory_or_throw(GetCurrentProcess(), params.pEntrypoint, pOriginalEntryPointBytes, params.entrypointLength); FlushInstructionCache(GetCurrentProcess(), params.pEntrypoint, params.entrypointLength); - hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr); - last_operation = L"hMainThreadContinue = CreateEventW"; - if (!hMainThreadContinue) - throw std::runtime_error("CreateEventW"); + last_operation = L"duplicate main thread handle"; + DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &hMainThread, 0, FALSE, DUPLICATE_SAME_ACCESS); + if (!hMainThread) + throw std::runtime_error("DuplicateHandle"); last_operation = L"InitializeImpl"; - hr = InitializeImpl(pLoadInfo, hMainThreadContinue); + hr = InitializeImpl(pLoadInfo, hMainThread); } catch (const std::exception& e) { if (hr == S_OK) { const auto err = GetLastError(); @@ -385,14 +400,11 @@ extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointPara desc.GetBSTR()).c_str(), L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO) ExitProcess(-1); - if (hMainThreadContinue) { - CloseHandle(hMainThreadContinue); - hMainThreadContinue = nullptr; + if (hMainThread) { + CloseHandle(hMainThread); + hMainThread = nullptr; } } - if (hMainThreadContinue) - WaitForSingleObject(hMainThreadContinue, INFINITE); - VirtualFree(¶ms, 0, MEM_RELEASE); } diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index c784ec1d1c..3fe5f1b75b 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -555,7 +555,7 @@ private static int ProcessInjectCommand(List args, DalamudStartInfo dala } foreach (var process in processes) - Inject(process, AdjustStartInfo(dalamudStartInfo, process.MainModule.FileName), tryFixAcl); + Inject(process, AdjustStartInfo(dalamudStartInfo, process.MainModule.FileName), nint.Zero, tryFixAcl); Log.CloseAndFlush(); return 0; @@ -804,7 +804,7 @@ private static int ProcessLaunchCommand(List args, DalamudStartInfo dala gameArgumentString = string.Join(" ", gameArguments.Select(x => EncodeParameterArgument(x))); } - var process = GameStart.LaunchGame( + var (process, mainThreadHandle) = GameStart.LaunchGame( Path.GetDirectoryName(gamePath), gamePath, gameArgumentString, @@ -822,13 +822,18 @@ private static int ProcessLaunchCommand(List args, DalamudStartInfo dala }, waitForGameWindow); + if (withoutDalamud || dalamudStartInfo.LoadMethod == LoadMethod.Entrypoint) + { + CloseHandle(mainThreadHandle); + } + Log.Verbose("Game process started with PID {0}", process.Id); if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.DllInject) { var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); - Inject(process, startInfo, false); + Inject(process, startInfo, mainThreadHandle, false); } var processHandleForOwner = IntPtr.Zero; @@ -908,7 +913,14 @@ private static DalamudStartInfo AdjustStartInfo(DalamudStartInfo startInfo, stri }; } - private static void Inject(Process process, DalamudStartInfo startInfo, bool tryFixAcl = false) + [StructLayout(LayoutKind.Sequential)] + private struct InitializeParams + { + public nuint LoadInfo; + public nint MainThread; + } + + private static void Inject(Process process, DalamudStartInfo startInfo, nint mainThreadHandle, bool tryFixAcl = false) { if (tryFixAcl) { @@ -944,8 +956,18 @@ private static void Inject(Process process, DalamudStartInfo startInfo, bool try throw new Exception("Unable to allocate start info JSON"); } + using var initParamsBuffer = new MemoryBufferHelper(process).CreatePrivateMemoryBuffer(Marshal.SizeOf(typeof(InitializeParams)) + 0x8); + + var initParams = new InitializeParams + { + LoadInfo = startInfoAddress, + MainThread = mainThreadHandle, + }; + + var initParamsAddress = initParamsBuffer.Add(ref initParams); + injector.GetFunctionAddress(bootModule, "Initialize", out var initAddress); - injector.CallRemoteFunction(initAddress, startInfoAddress, out var exitCode); + injector.CallRemoteFunction(initAddress, initParamsAddress, out var exitCode); // ====================================================== diff --git a/Dalamud.Injector/GameStart.cs b/Dalamud.Injector/GameStart.cs index e340489786..8709bc4489 100644 --- a/Dalamud.Injector/GameStart.cs +++ b/Dalamud.Injector/GameStart.cs @@ -25,10 +25,10 @@ public static class GameStart /// Don't actually fix the ACL. /// Action to execute before the process is started. /// Wait for the game window to be ready before proceeding. - /// The started process. + /// The started process and handle to the started main thread. /// Thrown when a win32 error occurs. /// Thrown when the process did not start correctly. - public static Process LaunchGame(string workingDir, string exePath, string arguments, bool dontFixAcl, Action beforeResume, bool waitForGameWindow = true) + public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string workingDir, string exePath, string arguments, bool dontFixAcl, Action beforeResume, bool waitForGameWindow = true) { Process process = null; @@ -172,10 +172,20 @@ public static Process LaunchGame(string workingDir, string exePath, string argum { if (psecDesc != IntPtr.Zero) Marshal.FreeHGlobal(psecDesc); - PInvoke.CloseHandle(lpProcessInformation.hThread); } - return process; + NativeFunctions.DuplicateHandle( + PInvoke.GetCurrentProcess(), + lpProcessInformation.hThread, + lpProcessInformation.hProcess, + out var mainThreadHandle, + 0, + false, + NativeFunctions.DuplicateOptions.SameAccess); + + PInvoke.CloseHandle(lpProcessInformation.hThread); + + return (process, mainThreadHandle); } /// diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 8c858ce7cb..c075ab1531 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -44,8 +44,8 @@ internal sealed class Dalamud : IServiceType /// DalamudStartInfo instance. /// ReliableFileStorage instance. /// The Dalamud configuration. - /// Event used to signal the main thread to continue. - public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent) + /// Handle to the suspended main thread. + public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfiguration configuration, nint mainThreadHandle) { this.StartInfo = info; @@ -72,7 +72,7 @@ public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfigurati if (!configuration.IsResumeGameAfterPluginLoad) { - NativeFunctions.SetEvent(mainThreadContinueEvent); + this.ResumeThread(mainThreadHandle); ServiceManager.InitializeEarlyLoadableServices() .ContinueWith(t => { @@ -102,7 +102,7 @@ public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfigurati if (faultedTasks.Any()) throw new AggregateException(faultedTasks); - NativeFunctions.SetEvent(mainThreadContinueEvent); + this.ResumeThread(mainThreadHandle); await Task.WhenAll(tasks); } @@ -113,7 +113,7 @@ public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfigurati } finally { - NativeFunctions.SetEvent(mainThreadContinueEvent); + this.ResumeThread(mainThreadHandle); } }); } @@ -243,4 +243,25 @@ private void SetupClientStructsResolver(DirectoryInfo cacheDir) FFXIVClientStructs.Interop.Resolver.GetInstance.Resolve(); } } + + /// + /// Resumes a thread by incrementing its suspend count to zero, and then closing its handle. + /// + /// Handle to the thread to be resumed. + private void ResumeThread(nint threadHandle) + { + int previousSuspendCount; + + while ((previousSuspendCount = (int)NativeFunctions.ResumeThread(threadHandle)) > 1) + { + if (previousSuspendCount == -1) + { + Log.Error("Failed to resume main thread"); + return; + } + } + + NativeFunctions.CloseHandle(threadHandle); + } + } diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index d0f9e8845d..c477cd5400 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -50,8 +50,8 @@ public sealed class EntryPoint /// Initialize Dalamud. /// /// Pointer to a serialized data. - /// Event used to signal the main thread to continue. - public static void Initialize(IntPtr infoPtr, IntPtr mainThreadContinueEvent) + /// Handle to the (suspended) main thread. + public static void Initialize(IntPtr infoPtr, IntPtr mainThreadHandle) { var infoStr = Marshal.PtrToStringUTF8(infoPtr)!; var info = JsonConvert.DeserializeObject(infoStr)!; @@ -59,7 +59,12 @@ public static void Initialize(IntPtr infoPtr, IntPtr mainThreadContinueEvent) if ((info.BootWaitMessageBox & 4) != 0) MessageBoxW(IntPtr.Zero, "Press OK to continue (BeforeDalamudConstruct)", "Dalamud Boot", MessageBoxType.Ok); - new Thread(() => RunThread(info, mainThreadContinueEvent)).Start(); + var suspendSignal = new ManualResetEvent(false); + suspendSignal.Reset(); + + new Thread(() => RunThread(info, mainThreadHandle, suspendSignal)).Start(); + + suspendSignal.WaitOne(); } /// @@ -130,8 +135,9 @@ internal static void InitLogging(string baseDirectory, bool logConsole, bool log /// Initialize all Dalamud subsystems and start running on the main thread. /// /// The containing information needed to initialize Dalamud. - /// Event used to signal the main thread to continue. - private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEvent) + /// Handle to the (suspended) main thread. + /// Signal to notifiy the initiliazing thread once the main thread has been suspended. + private static void RunThread(DalamudStartInfo info, IntPtr mainThreadHandle, ManualResetEvent suspendSignal) { // Setup logger InitLogging(info.LogPath!, info.BootShowConsole, true, info.LogName); @@ -161,6 +167,25 @@ private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEv Thread.Sleep(info.DelayInitializeMs); } + var currentSuspendCount = (int)NativeFunctions.SuspendThread(mainThreadHandle) + 1; + Log.Verbose("Current main thread suspend count {0}", currentSuspendCount); + suspendSignal.Set(); + + switch (info.LoadMethod) + { + case LoadMethod.Entrypoint: + if (currentSuspendCount != 1) + Log.Warning("Unexpected suspend count {0} for main thread with Entrypoint", currentSuspendCount); + break; + case LoadMethod.DllInject: + if (currentSuspendCount != 1) + Log.Warning("Unexpected suspend count {0} for main thread with DllInject", currentSuspendCount); + break; + default: + Log.Warning("Unknown LoadMethod {0}", info.LoadMethod); + break; + } + Log.Information(new string('-', 80)); Log.Information("Initializing a session.."); @@ -175,7 +200,7 @@ private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEv if (!Util.IsWine()) InitSymbolHandler(info); - var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent); + var dalamud = new Dalamud(info, fs, configuration, mainThreadHandle); Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]", Util.GetGitHash(), Util.GetGitHashClientStructs(), FFXIVClientStructs.Interop.Resolver.Version); dalamud.WaitForUnload(); @@ -192,7 +217,9 @@ private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEv } catch (Exception ex) { - Log.Fatal(ex, "Unhandled exception on main thread."); + suspendSignal.Set(); + NativeFunctions.ResumeThread(mainThreadHandle); + Log.Fatal(ex, "Unhandled exception on Dalamuds initialization thread."); } finally { diff --git a/Dalamud/NativeFunctions.cs b/Dalamud/NativeFunctions.cs index 92dfe5dd7a..0443d5576f 100644 --- a/Dalamud/NativeFunctions.cs +++ b/Dalamud/NativeFunctions.cs @@ -1917,6 +1917,40 @@ public static extern bool WriteProcessMemory( /// The thread ID. [DllImport("kernel32.dll")] public static extern uint GetCurrentThreadId(); + + /// + /// Suspends a thread. + /// + /// Handle to the thread to be suspended. + /// If the function succeeds, the return value is the thread's previous suspend count. + /// If the function fails, the return value is (DWORD) -1. + [DllImport("kernel32.dll", SetLastError = true)] + public static extern uint SuspendThread(IntPtr hThread); + + /// + /// Resumes a thread that was suspended. + /// + /// Handle to the thread to be resumed. + /// If the function succeeds, the return value is the thread's previous suspend count. + /// If the function fails, the return value is (DWORD) -1. + [DllImport("kernel32.dll", SetLastError = true)] + public static extern uint ResumeThread(IntPtr hThread); + + /// + /// Closes an open object handle. + /// + /// + /// A valid handle to an open object. + /// + /// + /// If the function succeeds, the return value is nonzero. If the function fails, the return value is zero. To get extended error + /// information, call GetLastError. If the application is running under a debugger, the function will throw an exception if it receives + /// either a handle value that is not valid or a pseudo-handle value. This can happen if you close a handle twice, or if you call + /// CloseHandle on a handle returned by the FindFirstFile function instead of calling the FindClose function. + /// + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CloseHandle(IntPtr hObject); } ///