using System.ComponentModel; using System.Diagnostics; using System.Reflection.PortableExecutable; using System.Runtime.InteropServices; using System.Text; using XFEEliteDangerous.Client; namespace XFEEliteDangerous.Bootstrapper; /// Loads the owned NativeAOT module into the suspended child created by this launcher. internal sealed class OfflineNativeModule(nint process, nint module, IReadOnlyDictionary exports) { internal Task ReadStatusAsync() => CallAsync(process, module + (int)exports["XfeOfflineStatus"]); internal Task TransfersAsync() => CallAsync(process, module + (int)exports["XfeOfflineTransfers"]); internal static async Task LoadAsync(nint process, string library, byte[] configuration) { var exports = ReadExports(library); var loader = NativeLibrary.GetExport(NativeLibrary.Load("ntdll.dll"), "LdrLoadDll"); var local = new byte[32]; Marshal.Copy(loader, local, 0, 32); if (!local.AsSpan().SequenceEqual(Read(process, loader, 32))) throw new InvalidDataException("The child system-loader mapping differs."); var path = Encoding.Unicode.GetBytes(library + '\0'); var argument = Allocate(process, new byte[32 + path.Length]); var input = Allocate(process, configuration); try { var buffer = new byte[32 + path.Length]; BitConverter.TryWriteBytes(buffer, checked((ushort)(path.Length - 2))); BitConverter.TryWriteBytes(buffer.AsSpan(2), checked((ushort)path.Length)); BitConverter.TryWriteBytes(buffer.AsSpan(8), (long)(argument + 32)); path.CopyTo(buffer, 32); Write(process, argument, buffer); var result = await CallAsync(process, loader, 0, 0, argument, argument + 16); if (unchecked((int)result) < 0) throw new InvalidOperationException("Owned LdrLoadDll failed: " + unchecked((uint)result).ToString("X8")); EnsureAlive(process,"LdrLoadDll output"); var loaded = (nint)BitConverter.ToInt64(Read(process, argument + 16, 8)); if (loaded == 0) throw new InvalidDataException("Owned native module was not loaded."); result = await CallAsync(process, loaded + (int)exports["XfeOfflineInitialize"], input); if (result != 0) throw new InvalidOperationException("Offline runtime initialization failed; inspect this launch's initialization-error.txt."); return new(process, loaded, exports); } finally { Free(process, input); Free(process, argument); } } internal static Dictionary ReadExports(string path) { using var file = File.OpenRead(path); using var pe = new PEReader(file); if (pe.PEHeaders.CoffHeader.Machine != System.Reflection.PortableExecutable.Machine.Amd64) throw new InvalidDataException("A native x64 module is required."); var directory = pe.PEHeaders.PEHeader!.ExportTableDirectory; var header = pe.GetSectionData(directory.RelativeVirtualAddress).GetContent(0,40).ToArray(); var functionsCount = BitConverter.ToInt32(header,20); var namesCount = BitConverter.ToInt32(header,24); if (functionsCount is <1 or >4096 || namesCount is <1 or >4096) throw new InvalidDataException("Invalid export count."); var functions = pe.GetSectionData(BitConverter.ToInt32(header,28)).GetContent(0,functionsCount*4).ToArray(); var names = pe.GetSectionData(BitConverter.ToInt32(header,32)).GetContent(0,namesCount*4).ToArray(); var ordinals = pe.GetSectionData(BitConverter.ToInt32(header,36)).GetContent(0,namesCount*2).ToArray(); var result = new Dictionary(StringComparer.Ordinal); for (var index=0;index=functionsCount) throw new InvalidDataException("Invalid export ordinal."); result.Add(Encoding.ASCII.GetString(bytes[..end]),BitConverter.ToUInt32(functions,ordinal*4)); } foreach(var name in new[]{"XfeOfflineInitialize","XfeOfflineStatus","XfeOfflineTransfers"}) if(!result.TryGetValue(name,out var rva)||rva==0||(rva>=directory.RelativeVirtualAddress&&rva CallAsync(nint process,nint function,nint arg1=0,nint arg2=0,nint arg3=0,nint arg4=0) { var allocation=VirtualAllocEx(process,0,8192,0x3000,0x04);if(allocation==0)throw new Win32Exception(); nint thread=0;var completed=false; try { var code=new List{0x48,0x83,0xEC,0x28,0x48,0xB9};code.AddRange(BitConverter.GetBytes((long)arg1)); code.AddRange([0x48,0xBA]);code.AddRange(BitConverter.GetBytes((long)arg2)); code.AddRange([0x49,0xB8]);code.AddRange(BitConverter.GetBytes((long)arg3)); code.AddRange([0x49,0xB9]);code.AddRange(BitConverter.GetBytes((long)arg4)); code.AddRange([0x48,0xB8]);code.AddRange(BitConverter.GetBytes((long)function));code.AddRange([0xFF,0xD0,0x49,0xBB]); // A zero-filled return cell is not proof that the callback returned. // The completion marker is written only after its actual return. code.AddRange(BitConverter.GetBytes((long)(allocation+4096)));code.AddRange([0x49,0x89,0x03,0x41,0xC7,0x43,0x08,0x58,0x46,0x45,0x31,0x31,0xC0,0x48,0x83,0xC4,0x28,0xC3]); Write(process,allocation,code.ToArray()); if(!Native.VirtualProtectEx(process,allocation,4096,0x20,out _)||!Native.FlushInstructionCache(process,allocation,(nuint)code.Count))throw new Win32Exception(); thread=CreateRemoteThread(process,0,0,allocation,0,0,out _);if(thread==0)throw new Win32Exception(); var started=Stopwatch.GetTimestamp(); while(true) { var wait=Native.WaitForSingleObject(thread,50);if(wait==0){completed=true;break;} if(wait!=258)throw new Win32Exception(); if(Stopwatch.GetElapsedTime(started)>TimeSpan.FromSeconds(25))throw new TimeoutException("Owned native initialization timed out."); await Task.Delay(10).ConfigureAwait(false); } if(!GetExitCodeThread(thread,out var exit))throw new Win32Exception(); EnsureAlive(process,"native callback",exit); if(exit!=0)throw new InvalidOperationException("Owned native callback thread failed: 0x"+exit.ToString("X8")); var output=Read(process,allocation+4096,12); if(BitConverter.ToUInt32(output,8)!=0x31454658)throw new InvalidOperationException("Owned native callback thread exited without its completion marker; return value is unavailable."); var result=BitConverter.ToUInt64(output); OfflineDiagnostics.Current?.Write("debug","bootstrapper.loader","callback.returned","Native callback completed with a verified marker.", new Dictionary{["threadExit"]="0x"+exit.ToString("X8"),["elapsedMs"]=Stopwatch.GetElapsedTime(started).TotalMilliseconds.ToString("F1",System.Globalization.CultureInfo.InvariantCulture)}); return result; } finally{if(thread!=0)Native.CloseHandle(thread);if(completed||thread==0)Free(process,allocation);} } private static nint Allocate(nint process,byte[] bytes) {var pointer=VirtualAllocEx(process,0,(nuint)bytes.Length,0x3000,0x04);if(pointer==0)throw new Win32Exception();try{Write(process,pointer,bytes);return pointer;}catch{Free(process,pointer);throw;}} private static void Write(nint process,nint pointer,byte[] bytes) {if(!Native.WriteProcessMemory(process,pointer,bytes,(nuint)bytes.Length,out var written)||written!=(nuint)bytes.Length)throw new Win32Exception();} private static byte[] Read(nint process,nint pointer,int count) { var bytes=new byte[count]; if(!Native.ReadProcessMemory(process,pointer,bytes,(nuint)count,out var read)||read!=(nuint)count) { var code=Marshal.GetLastWin32Error();var exit=Native.GetExitCodeProcess(process,out var status)?"0x"+status.ToString("X8"):"unavailable"; throw new InvalidOperationException($"Owned child memory read failed: Win32={code}, bytes={read}/{count}, processExit={exit}.",new Win32Exception(code)); } return bytes; } private static void Free(nint process,nint pointer){if(pointer!=0)VirtualFreeEx(process,pointer,0,0x8000);} private static void EnsureAlive(nint process,string stage,uint? threadExit=null) { if(!Native.GetExitCodeProcess(process,out var exit))throw new Win32Exception(); var wait=Native.WaitForSingleObject(process,0); if(wait==0) throw new InvalidOperationException($"Owned game exited during {stage}: processExit=0x{exit:X8}, threadExit={(threadExit is {} value?"0x"+value.ToString("X8"):"unavailable")}. No further child-memory read was attempted."); if(wait!=258)throw new Win32Exception(); } internal static async Task RunCallbackChecksAsync() { // Exercise the same remote-thread transport in this diagnostic host; // neither a game nor a desktop is opened by these regression checks. var process=Native.GetCurrentProcess(); var code=VirtualAllocEx(process,0,4096,0x3000,0x04);if(code==0)throw new Win32Exception(); try { const ulong expected=0xFEDCBA98C0000005; var body=new List{0x48,0xB8};body.AddRange(BitConverter.GetBytes(expected));body.Add(0xC3); Write(process,code,body.ToArray()); if(!Native.VirtualProtectEx(process,code,4096,0x20,out _)||!Native.FlushInstructionCache(process,code,(nuint)body.Count))throw new Win32Exception(); if(await CallAsync(process,code)!=expected)throw new InvalidOperationException("Native callback regression: full 64-bit return value changed."); var exitThread=NativeLibrary.GetExport(NativeLibrary.Load("kernel32.dll"),"ExitThread"); foreach(var status in new[]{0,42}) { var rejected=false; try{await CallAsync(process,exitThread,status);} catch(InvalidOperationException error)when(status==0?error.Message.Contains("completion marker",StringComparison.Ordinal):error.Message.Contains("thread failed: 0x0000002A",StringComparison.Ordinal)){rejected=true;} if(!rejected)throw new InvalidOperationException("Native callback regression: an early thread exit was accepted."); } } finally{Free(process,code);} var childInfo=new ProcessStartInfo(Environment.ProcessPath??throw new InvalidOperationException("Diagnostic host path is unavailable.")) {UseShellExecute=false,CreateNoWindow=true,WindowStyle=ProcessWindowStyle.Hidden,RedirectStandardOutput=true}; childInfo.ArgumentList.Add("--loader-check-child"); using var child=Process.Start(childInfo)??throw new IOException("Diagnostic child did not start."); try { using var deadline=new CancellationTokenSource(TimeSpan.FromSeconds(10)); if(await child.StandardOutput.ReadLineAsync(deadline.Token)!="loader-check-child-ready")throw new IOException("Diagnostic child was not ready."); var exitProcess=NativeLibrary.GetExport(NativeLibrary.Load("kernel32.dll"),"ExitProcess"); var rejected=false; try{await CallAsync(child.Handle,exitProcess,299);} catch(InvalidOperationException error)when(error.Message.Contains("processExit=0x0000012B",StringComparison.Ordinal) &&error.Message.Contains("No further child-memory read",StringComparison.Ordinal)){rejected=true;} if(!rejected)throw new InvalidOperationException("Native callback regression: an exited child was read as a successful callback."); } finally{if(!child.HasExited){child.Kill(entireProcessTree:true);await child.WaitForExitAsync();}} } [DllImport("kernel32",SetLastError=true)]private static extern nint VirtualAllocEx(nint process,nint address,nuint size,uint type,uint protect); [DllImport("kernel32",SetLastError=true)][return:MarshalAs(UnmanagedType.Bool)]private static extern bool VirtualFreeEx(nint process,nint address,nuint size,uint type); [DllImport("kernel32",SetLastError=true)]private static extern nint CreateRemoteThread(nint process,nint attributes,nuint size,nint function,nint argument,uint flags,out uint thread); [DllImport("kernel32",SetLastError=true)][return:MarshalAs(UnmanagedType.Bool)]private static extern bool GetExitCodeThread(nint thread,out uint code); }