Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make IsResumeGameAfterPluginLoad work with DllInject #1671

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Dalamud.Boot/DalamudStartInfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ void from_json(const nlohmann::json& json, DalamudStartInfo::LoadMethod& value)

}
else if (json.is_string()) {
const auto langstr = unicode::convert<std::string>(json.get<std::string>(), &unicode::lower);
if (langstr == "entrypoint")
const auto loadstr = unicode::convert<std::string>(json.get<std::string>(), &unicode::lower);
if (loadstr == "entrypoint")
value = DalamudStartInfo::LoadMethod::Entrypoint;
else if (langstr == "inject")
else if (loadstr == "inject")
value = DalamudStartInfo::LoadMethod::DllInject;
}
}
Expand Down
14 changes: 10 additions & 4 deletions Dalamud.Boot/dllmain.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<char*>(lpParam))), g_startInfo);
from_json(nlohmann::json::parse(std::string_view(pLoadInfo)), g_startInfo);
} catch (const std::exception& e) {
jsonParseError = e.what();
}
Expand Down Expand Up @@ -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<InitializeParams*>(lpParam);
return InitializeImpl(params->pLoadInfo, params->hMainThread);
}

BOOL APIENTRY DllMain(const HMODULE hModule, const DWORD dwReason, LPVOID lpReserved) {
Expand Down
66 changes: 39 additions & 27 deletions Dalamud.Boot/rewrite_entrypoint.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<char*>(get_mapped_image_base_address(hProcess, pcwzPath));

IMAGE_DOS_HEADER dos_header{};
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -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(&params, 0, MEM_RELEASE);
}
32 changes: 27 additions & 5 deletions Dalamud.Injector/EntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ private static int ProcessInjectCommand(List<string> 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;
Expand Down Expand Up @@ -804,7 +804,7 @@ private static int ProcessLaunchCommand(List<string> 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,
Expand All @@ -822,13 +822,18 @@ private static int ProcessLaunchCommand(List<string> 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;
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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);

// ======================================================

Expand Down
18 changes: 14 additions & 4 deletions Dalamud.Injector/GameStart.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ public static class GameStart
/// <param name="dontFixAcl">Don't actually fix the ACL.</param>
/// <param name="beforeResume">Action to execute before the process is started.</param>
/// <param name="waitForGameWindow">Wait for the game window to be ready before proceeding.</param>
/// <returns>The started process.</returns>
/// <returns>The started process and handle to the started main thread.</returns>
/// <exception cref="Win32Exception">Thrown when a win32 error occurs.</exception>
/// <exception cref="GameStartException">Thrown when the process did not start correctly.</exception>
public static Process LaunchGame(string workingDir, string exePath, string arguments, bool dontFixAcl, Action<Process> beforeResume, bool waitForGameWindow = true)
public static (Process GameProcess, nint MainThreadHandle) LaunchGame(string workingDir, string exePath, string arguments, bool dontFixAcl, Action<Process> beforeResume, bool waitForGameWindow = true)
{
Process process = null;

Expand Down Expand Up @@ -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);
}

/// <summary>
Expand Down
31 changes: 26 additions & 5 deletions Dalamud/Dalamud.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ internal sealed class Dalamud : IServiceType
/// <param name="info">DalamudStartInfo instance.</param>
/// <param name="fs">ReliableFileStorage instance.</param>
/// <param name="configuration">The Dalamud configuration.</param>
/// <param name="mainThreadContinueEvent">Event used to signal the main thread to continue.</param>
public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent)
/// <param name="mainThreadHandle">Handle to the suspended main thread.</param>
public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfiguration configuration, nint mainThreadHandle)
{
this.StartInfo = info;

Expand All @@ -72,7 +72,7 @@ public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfigurati

if (!configuration.IsResumeGameAfterPluginLoad)
{
NativeFunctions.SetEvent(mainThreadContinueEvent);
this.ResumeThread(mainThreadHandle);
ServiceManager.InitializeEarlyLoadableServices()
.ContinueWith(t =>
{
Expand Down Expand Up @@ -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);
}
Expand All @@ -113,7 +113,7 @@ public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfigurati
}
finally
{
NativeFunctions.SetEvent(mainThreadContinueEvent);
this.ResumeThread(mainThreadHandle);
}
});
}
Expand Down Expand Up @@ -243,4 +243,25 @@ private void SetupClientStructsResolver(DirectoryInfo cacheDir)
FFXIVClientStructs.Interop.Resolver.GetInstance.Resolve();
}
}

/// <summary>
/// Resumes a thread by incrementing its suspend count to zero, and then closing its handle.
/// </summary>
/// <param name="threadHandle">Handle to the thread to be resumed.</param>
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);
}

}
Loading
Loading