feat(mosstank): add VTank-style automation PoC

This commit is contained in:
Erik 2026-08-27 18:57:21 +02:00
parent f6fe0f2a4f
commit 4e6e9bc9d9
212 changed files with 49462 additions and 416 deletions

View file

@ -151,6 +151,13 @@ internal static class GraphicalWindowBackendConfigurator
_ => throw new ArgumentOutOfRangeException(
nameof(requested)),
});
// #451: InitHint proves the packaged glfw3.dll is loaded but runs
// before glfwInit creates any window or begins polling. This is
// the one safe point to narrow GLFW's GetActiveWindow import so a
// temporarily joined Win32 input queue cannot hand it another
// acdream process's private GLFWwindow pointer.
if (platform.OperatingSystem == GraphicalHostOperatingSystem.Windows)
Win32GlfwActiveWindowGuard.Install();
_glfw = glfw;
_configuredProtocol = requested;
}

View file

@ -0,0 +1,282 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace AcDream.App.Platform;
/// <summary>
/// Prevents GLFW's Win32 modifier-key repair pass from accepting a window
/// owned by another process.
/// </summary>
/// <remarks>
/// <para>
/// GLFW 3.4's <c>_glfwPollEventsWin32</c> calls <c>GetActiveWindow</c>, then
/// reads that HWND's process-global <c>GLFW</c> property and dereferences the
/// result as a local <c>_GLFWwindow*</c>. Normally <c>GetActiveWindow</c> can
/// only return a window from this thread's input queue. Windows automation,
/// accessibility software, and some multi-box window managers temporarily
/// join input queues, however, allowing it to return another acdream process's
/// window. Every GLFW process uses the same property name, so <c>GetPropW</c>
/// then succeeds but returns a pointer meaningful only in the other process.
/// The next modifier-key read is an access violation (#451).
/// </para>
/// <para>
/// Patch only GLFW's import-address-table entry for <c>GetActiveWindow</c>.
/// The replacement returns the real active HWND when it belongs to this
/// process and zero otherwise. Zero is GLFW's existing, intentional
/// "nothing to repair" path. No process-global Win32 hook is installed and
/// no other module's User32 calls are changed.
/// </para>
/// </remarks>
internal static unsafe class Win32GlfwActiveWindowGuard
{
private const string GlfwModuleName = "glfw3.dll";
private const string User32ModuleName = "USER32.dll";
private const string GetActiveWindowImport = "GetActiveWindow";
private const uint PageReadWrite = 0x04;
private const ushort DosSignature = 0x5A4D;
private const uint PeSignature = 0x00004550;
private const ushort Pe32Magic = 0x010B;
private const ushort Pe32PlusMagic = 0x020B;
private const int ImportDescriptorSize = 20;
private static readonly uint CurrentProcessId =
checked((uint)Environment.ProcessId);
private static int _installState;
internal static bool IsInstalled => Volatile.Read(ref _installState) == 1;
internal static void Install()
{
if (!OperatingSystem.IsWindows()
|| Interlocked.CompareExchange(ref _installState, 2, 0) != 0)
{
return;
}
try
{
nint module = GetModuleHandleW(GlfwModuleName);
if (module == 0
|| !TryFindImportSlot(
module,
User32ModuleName,
GetActiveWindowImport,
out nint slot))
{
Volatile.Write(ref _installState, -1);
Console.Error.WriteLine(
"windowing: could not install the GLFW foreign-active-window guard");
return;
}
nint replacement = (nint)(delegate* unmanaged[Stdcall]<nint>)
&GetCurrentProcessActiveWindow;
if (!VirtualProtect(
slot,
checked((nuint)IntPtr.Size),
PageReadWrite,
out uint oldProtection))
{
Volatile.Write(ref _installState, -1);
Console.Error.WriteLine(
"windowing: GLFW active-window import was not writable");
return;
}
try
{
*(nint*)slot = replacement;
}
finally
{
_ = VirtualProtect(
slot,
checked((nuint)IntPtr.Size),
oldProtection,
out _);
}
Volatile.Write(ref _installState, 1);
Console.WriteLine(
"windowing: GLFW foreign-active-window guard installed (#451).");
}
catch (Exception failure)
{
Volatile.Write(ref _installState, -1);
Console.Error.WriteLine(
$"windowing: GLFW active-window guard failed: {failure.Message}");
}
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
private static nint GetCurrentProcessActiveWindow()
{
nint window = GetActiveWindow();
if (window == 0)
return 0;
_ = GetWindowThreadProcessId(window, out uint ownerProcessId);
return AcceptWindow(window, ownerProcessId, CurrentProcessId);
}
internal static nint AcceptWindow(
nint window,
uint ownerProcessId,
uint currentProcessId) =>
window != 0
&& ownerProcessId != 0
&& ownerProcessId == currentProcessId
? window
: 0;
private static bool TryFindImportSlot(
nint module,
string importedModule,
string importedFunction,
out nint slot)
{
slot = 0;
byte* image = (byte*)module;
if (*(ushort*)image != DosSignature)
return false;
int peOffset = *(int*)(image + 0x3C);
if (peOffset <= 0 || *(uint*)(image + peOffset) != PeSignature)
return false;
byte* optionalHeader = image + peOffset + 24;
ushort magic = *(ushort*)optionalHeader;
int dataDirectoryOffset;
int thunkSize;
ulong ordinalFlag;
if (magic == Pe32PlusMagic)
{
dataDirectoryOffset = 112;
thunkSize = 8;
ordinalFlag = 0x8000000000000000UL;
}
else if (magic == Pe32Magic)
{
dataDirectoryOffset = 96;
thunkSize = 4;
ordinalFlag = 0x80000000UL;
}
else
{
return false;
}
uint sizeOfImage = *(uint*)(optionalHeader + 56);
uint importRva = *(uint*)(optionalHeader + dataDirectoryOffset + 8);
uint importSize = *(uint*)(optionalHeader + dataDirectoryOffset + 12);
if (!Contains(sizeOfImage, importRva, ImportDescriptorSize))
return false;
int descriptorLimit = importSize >= ImportDescriptorSize
? checked((int)(importSize / ImportDescriptorSize))
: checked((int)((sizeOfImage - importRva) / ImportDescriptorSize));
for (int descriptorIndex = 0;
descriptorIndex < descriptorLimit;
descriptorIndex++)
{
byte* descriptor = image
+ importRva
+ descriptorIndex * ImportDescriptorSize;
uint originalFirstThunk = *(uint*)descriptor;
uint nameRva = *(uint*)(descriptor + 12);
uint firstThunk = *(uint*)(descriptor + 16);
if (originalFirstThunk == 0 && nameRva == 0 && firstThunk == 0)
break;
if (!MatchesAsciiZ(image, sizeOfImage, nameRva, importedModule, true))
continue;
if (originalFirstThunk == 0
|| !Contains(sizeOfImage, originalFirstThunk, thunkSize)
|| !Contains(sizeOfImage, firstThunk, thunkSize))
{
return false;
}
int thunkLimit = checked((int)Math.Min(
(sizeOfImage - originalFirstThunk) / (uint)thunkSize,
(sizeOfImage - firstThunk) / (uint)thunkSize));
for (int thunkIndex = 0; thunkIndex < thunkLimit; thunkIndex++)
{
ulong nameThunk = thunkSize == 8
? *(ulong*)(image + originalFirstThunk + thunkIndex * thunkSize)
: *(uint*)(image + originalFirstThunk + thunkIndex * thunkSize);
if (nameThunk == 0)
break;
if ((nameThunk & ordinalFlag) != 0)
continue;
uint importByNameRva = checked((uint)nameThunk);
if (!Contains(sizeOfImage, importByNameRva, 3)
|| !MatchesAsciiZ(
image,
sizeOfImage,
importByNameRva + 2,
importedFunction,
false))
{
continue;
}
slot = (nint)(image + firstThunk + thunkIndex * thunkSize);
return true;
}
return false;
}
return false;
}
private static bool Contains(uint imageSize, uint offset, int length) =>
length >= 0
&& offset < imageSize
&& (ulong)offset + (uint)length <= imageSize;
private static bool MatchesAsciiZ(
byte* image,
uint imageSize,
uint offset,
string expected,
bool ignoreCase)
{
if (!Contains(imageSize, offset, expected.Length + 1))
return false;
for (int i = 0; i < expected.Length; i++)
{
char actual = (char)image[offset + (uint)i];
char wanted = expected[i];
if (ignoreCase)
{
actual = char.ToUpperInvariant(actual);
wanted = char.ToUpperInvariant(wanted);
}
if (actual != wanted)
return false;
}
return image[offset + (uint)expected.Length] == 0;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern nint GetModuleHandleW(string moduleName);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualProtect(
nint address,
nuint size,
uint newProtection,
out uint oldProtection);
[DllImport("user32.dll")]
private static extern nint GetActiveWindow();
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(
nint window,
out uint processId);
}