fix(hle): enable GuestImageWriteTracker CPU sync on Windows (#550)

Windows previously hard-disabled the tracker, so CPU-written guest
planes never marked dirty and host textures stayed empty. Arm pages
with VirtualProtect, handle write AVs in VEH, and warm/test on
VirtualAlloc memory so protect cannot poison the CRT heap.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
MarcelMediaDev
2026-07-23 13:37:05 +01:00
committed by GitHub
parent 007bf6fa73
commit 82ab181861
4 changed files with 135 additions and 18 deletions
@@ -55,6 +55,7 @@ public sealed partial class DirectExecutionBackend
}
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
SharpEmu.HLE.GuestImageWriteTracker.WarmUp();
_unhandledFilterDelegate = UnhandledExceptionFilter;
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
@@ -117,6 +118,13 @@ public sealed partial class DirectExecutionBackend
{
return -1;
}
if (exceptionCode == 3221225477u &&
exceptionRecord->NumberParameters >= 2 &&
SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
exceptionRecord->ExceptionInformation[1]))
{
return -1;
}
if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip))
{
return -1;
@@ -69,6 +69,15 @@ public sealed partial class DirectExecutionBackend
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
{
EXCEPTION_RECORD* exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
if (exceptionRecord->ExceptionCode == 3221225477u &&
exceptionRecord->NumberParameters >= 2 &&
SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
exceptionRecord->ExceptionInformation[1]))
{
return -1;
}
return TryRecoverUnresolvedSentinel(exceptionInfo);
}
+75 -15
View File
@@ -1,6 +1,7 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
@@ -80,7 +81,7 @@ public static unsafe class GuestImageWriteTracker
private static RangeSnapshot _rangeSnapshot = RangeSnapshot.Empty;
private static readonly bool _enabled = !OperatingSystem.IsWindows() &&
private static readonly bool _enabled =
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_CPU_SYNC") != "0";
private static readonly (bool Wildcard, ulong[] Addresses) _lifetimeTraceFilter =
ParseAddressList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"));
@@ -95,12 +96,36 @@ public static unsafe class GuestImageWriteTracker
_enabled && _lifetimeTraceEnabled ? GetMonotonicNanoseconds() : 0;
private static long _lifetimeTraceSequence;
private const uint PageReadonly = 0x02;
private const uint PageReadWrite = 0x04;
[DllImport("libc", EntryPoint = "mprotect", SetLastError = true)]
private static extern int Mprotect(nint address, nuint length, int protection);
[DllImport("libc", EntryPoint = "clock_gettime", SetLastError = false)]
private static extern int ClockGetTime(int clockId, Timespec* time);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern int VirtualProtect(
nint lpAddress,
nuint dwSize,
uint flNewProtect,
out uint lpflOldProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint VirtualAlloc(
nint lpAddress,
nuint dwSize,
uint flAllocationType,
uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern int VirtualFree(nint lpAddress, nuint dwSize, uint dwFreeType);
private const uint MemCommit = 0x1000;
private const uint MemReserve = 0x2000;
private const uint MemRelease = 0x8000;
public static bool Enabled => _enabled;
/// <summary>
@@ -115,7 +140,17 @@ public static unsafe class GuestImageWriteTracker
return;
}
var scratch = NativeMemory.AllocZeroed(4096);
// VirtualProtect only belongs on VirtualAlloc/mmap pages. Warming on
// CRT heap memory makes neighbouring heap metadata read-only and
// crashes the process on Windows.
var scratch = OperatingSystem.IsWindows()
? VirtualAlloc(0, 4096, MemCommit | MemReserve, PageReadWrite)
: (nint)NativeMemory.AllocZeroed(4096);
if (scratch == 0)
{
return;
}
try
{
// Warm the timestamp P/Invoke used by the signal-safe scalar
@@ -129,7 +164,14 @@ public static unsafe class GuestImageWriteTracker
}
finally
{
NativeMemory.Free(scratch);
if (OperatingSystem.IsWindows())
{
_ = VirtualFree(scratch, 0, MemRelease);
}
else
{
NativeMemory.Free((void*)scratch);
}
}
}
@@ -445,10 +487,7 @@ public static unsafe class GuestImageWriteTracker
}
if (needsUnprotect &&
Mprotect(
(nint)writableStart,
(nuint)(writableEnd - writableStart),
ProtRead | ProtWrite) != 0)
!TrySetProtection(writableStart, writableEnd - writableStart, writable: true))
{
return false;
}
@@ -497,10 +536,7 @@ public static unsafe class GuestImageWriteTracker
// A new publication/rearm starts a new first-write lifetime.
Volatile.Write(ref range.FirstCpuWriteSeen, 0);
var failed = Mprotect(
(nint)range.Start,
(nuint)(range.End - range.Start),
ProtRead) != 0;
var failed = !TrySetProtection(range.Start, range.End - range.Start, writable: false);
if (failed)
{
Volatile.Write(ref range.Armed, 0);
@@ -520,10 +556,7 @@ public static unsafe class GuestImageWriteTracker
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) == 1;
if (wasArmed)
{
_ = Mprotect(
(nint)range.Start,
(nuint)(range.End - range.Start),
ProtRead | ProtWrite);
_ = TrySetProtection(range.Start, range.End - range.Start, writable: true);
}
if (range.TraceLifetime)
@@ -679,8 +712,35 @@ public static unsafe class GuestImageWriteTracker
$"fault=0x{faultAddress:X16} page=0x{faultPage:X16}");
}
private static bool TrySetProtection(ulong start, ulong length, bool writable)
{
if (length == 0)
{
return true;
}
if (OperatingSystem.IsWindows())
{
return VirtualProtect(
(nint)start,
(nuint)length,
writable ? PageReadWrite : PageReadonly,
out _) != 0;
}
return Mprotect(
(nint)start,
(nuint)length,
writable ? ProtRead | ProtWrite : ProtRead) == 0;
}
private static long GetMonotonicNanoseconds()
{
if (OperatingSystem.IsWindows())
{
return Stopwatch.GetTimestamp() * 1_000_000_000L / Stopwatch.Frequency;
}
Timespec time;
return ClockGetTime(ClockMonotonicRaw, &time) == 0
? unchecked((time.Seconds * 1_000_000_000L) + time.Nanoseconds)
@@ -24,13 +24,53 @@ public sealed unsafe class GuestImageWriteTrackerTests
// spilling onto neighbouring heap pages.
private const nuint TrackedByteCount = 4096;
private const nuint HostPageAlignment = 16384;
private const uint MemCommit = 0x1000;
private const uint MemReserve = 0x2000;
private const uint MemRelease = 0x8000;
private const uint PageReadWrite = 0x04;
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint VirtualAlloc(
nint lpAddress,
nuint dwSize,
uint flAllocationType,
uint flProtect);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern int VirtualFree(nint lpAddress, nuint dwSize, uint dwFreeType);
private static ulong AllocateTrackedPages(out void* allocation)
{
// VirtualProtect (Windows) / mprotect (POSIX) must target
// VirtualAlloc/mmap pages. Protecting CRT heap pages poisons
// neighbouring allocator metadata and crashes the test host.
if (OperatingSystem.IsWindows())
{
var windowsAllocation = VirtualAlloc(
0,
HostPageAlignment,
MemCommit | MemReserve,
PageReadWrite);
Assert.NotEqual(nint.Zero, windowsAllocation);
allocation = (void*)windowsAllocation;
return (ulong)windowsAllocation;
}
allocation = NativeMemory.AlignedAlloc(2 * HostPageAlignment, HostPageAlignment);
return (ulong)allocation;
}
private static void FreeTrackedPages(void* allocation)
{
if (OperatingSystem.IsWindows())
{
_ = VirtualFree((nint)allocation, 0, MemRelease);
return;
}
NativeMemory.Free(allocation);
}
[Fact]
public void GenerationSurvivesDirtyConsume()
{
@@ -58,7 +98,7 @@ public sealed unsafe class GuestImageWriteTrackerTests
finally
{
GuestImageWriteTracker.Untrack(address);
NativeMemory.Free(allocation);
FreeTrackedPages(allocation);
}
}
@@ -89,7 +129,7 @@ public sealed unsafe class GuestImageWriteTrackerTests
finally
{
GuestImageWriteTracker.Untrack(address);
NativeMemory.Free(allocation);
FreeTrackedPages(allocation);
}
}
@@ -118,7 +158,7 @@ public sealed unsafe class GuestImageWriteTrackerTests
finally
{
GuestImageWriteTracker.Untrack(address);
NativeMemory.Free(allocation);
FreeTrackedPages(allocation);
}
}