mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-30 22:49:53 +08:00
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:
@@ -55,6 +55,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
|
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
||||||
|
SharpEmu.HLE.GuestImageWriteTracker.WarmUp();
|
||||||
|
|
||||||
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
||||||
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
||||||
@@ -117,6 +118,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
if (exceptionCode == 3221225477u &&
|
||||||
|
exceptionRecord->NumberParameters >= 2 &&
|
||||||
|
SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
|
||||||
|
exceptionRecord->ExceptionInformation[1]))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip))
|
if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -69,6 +69,15 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
|
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);
|
return TryRecoverUnresolvedSentinel(exceptionInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
|
|
||||||
private static RangeSnapshot _rangeSnapshot = RangeSnapshot.Empty;
|
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";
|
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_CPU_SYNC") != "0";
|
||||||
private static readonly (bool Wildcard, ulong[] Addresses) _lifetimeTraceFilter =
|
private static readonly (bool Wildcard, ulong[] Addresses) _lifetimeTraceFilter =
|
||||||
ParseAddressList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"));
|
ParseAddressList(Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGE_ADDRS"));
|
||||||
@@ -95,12 +96,36 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
_enabled && _lifetimeTraceEnabled ? GetMonotonicNanoseconds() : 0;
|
_enabled && _lifetimeTraceEnabled ? GetMonotonicNanoseconds() : 0;
|
||||||
private static long _lifetimeTraceSequence;
|
private static long _lifetimeTraceSequence;
|
||||||
|
|
||||||
|
private const uint PageReadonly = 0x02;
|
||||||
|
private const uint PageReadWrite = 0x04;
|
||||||
|
|
||||||
[DllImport("libc", EntryPoint = "mprotect", SetLastError = true)]
|
[DllImport("libc", EntryPoint = "mprotect", SetLastError = true)]
|
||||||
private static extern int Mprotect(nint address, nuint length, int protection);
|
private static extern int Mprotect(nint address, nuint length, int protection);
|
||||||
|
|
||||||
[DllImport("libc", EntryPoint = "clock_gettime", SetLastError = false)]
|
[DllImport("libc", EntryPoint = "clock_gettime", SetLastError = false)]
|
||||||
private static extern int ClockGetTime(int clockId, Timespec* time);
|
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;
|
public static bool Enabled => _enabled;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -115,7 +140,17 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
return;
|
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
|
try
|
||||||
{
|
{
|
||||||
// Warm the timestamp P/Invoke used by the signal-safe scalar
|
// Warm the timestamp P/Invoke used by the signal-safe scalar
|
||||||
@@ -129,7 +164,14 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
}
|
}
|
||||||
finally
|
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 &&
|
if (needsUnprotect &&
|
||||||
Mprotect(
|
!TrySetProtection(writableStart, writableEnd - writableStart, writable: true))
|
||||||
(nint)writableStart,
|
|
||||||
(nuint)(writableEnd - writableStart),
|
|
||||||
ProtRead | ProtWrite) != 0)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -497,10 +536,7 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
|
|
||||||
// A new publication/rearm starts a new first-write lifetime.
|
// A new publication/rearm starts a new first-write lifetime.
|
||||||
Volatile.Write(ref range.FirstCpuWriteSeen, 0);
|
Volatile.Write(ref range.FirstCpuWriteSeen, 0);
|
||||||
var failed = Mprotect(
|
var failed = !TrySetProtection(range.Start, range.End - range.Start, writable: false);
|
||||||
(nint)range.Start,
|
|
||||||
(nuint)(range.End - range.Start),
|
|
||||||
ProtRead) != 0;
|
|
||||||
if (failed)
|
if (failed)
|
||||||
{
|
{
|
||||||
Volatile.Write(ref range.Armed, 0);
|
Volatile.Write(ref range.Armed, 0);
|
||||||
@@ -520,10 +556,7 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) == 1;
|
var wasArmed = Interlocked.Exchange(ref range.Armed, 0) == 1;
|
||||||
if (wasArmed)
|
if (wasArmed)
|
||||||
{
|
{
|
||||||
_ = Mprotect(
|
_ = TrySetProtection(range.Start, range.End - range.Start, writable: true);
|
||||||
(nint)range.Start,
|
|
||||||
(nuint)(range.End - range.Start),
|
|
||||||
ProtRead | ProtWrite);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range.TraceLifetime)
|
if (range.TraceLifetime)
|
||||||
@@ -679,8 +712,35 @@ public static unsafe class GuestImageWriteTracker
|
|||||||
$"fault=0x{faultAddress:X16} page=0x{faultPage:X16}");
|
$"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()
|
private static long GetMonotonicNanoseconds()
|
||||||
{
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return Stopwatch.GetTimestamp() * 1_000_000_000L / Stopwatch.Frequency;
|
||||||
|
}
|
||||||
|
|
||||||
Timespec time;
|
Timespec time;
|
||||||
return ClockGetTime(ClockMonotonicRaw, &time) == 0
|
return ClockGetTime(ClockMonotonicRaw, &time) == 0
|
||||||
? unchecked((time.Seconds * 1_000_000_000L) + time.Nanoseconds)
|
? unchecked((time.Seconds * 1_000_000_000L) + time.Nanoseconds)
|
||||||
|
|||||||
@@ -24,13 +24,53 @@ public sealed unsafe class GuestImageWriteTrackerTests
|
|||||||
// spilling onto neighbouring heap pages.
|
// spilling onto neighbouring heap pages.
|
||||||
private const nuint TrackedByteCount = 4096;
|
private const nuint TrackedByteCount = 4096;
|
||||||
private const nuint HostPageAlignment = 16384;
|
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)
|
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);
|
allocation = NativeMemory.AlignedAlloc(2 * HostPageAlignment, HostPageAlignment);
|
||||||
return (ulong)allocation;
|
return (ulong)allocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void FreeTrackedPages(void* allocation)
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
_ = VirtualFree((nint)allocation, 0, MemRelease);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
NativeMemory.Free(allocation);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GenerationSurvivesDirtyConsume()
|
public void GenerationSurvivesDirtyConsume()
|
||||||
{
|
{
|
||||||
@@ -58,7 +98,7 @@ public sealed unsafe class GuestImageWriteTrackerTests
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
GuestImageWriteTracker.Untrack(address);
|
GuestImageWriteTracker.Untrack(address);
|
||||||
NativeMemory.Free(allocation);
|
FreeTrackedPages(allocation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +129,7 @@ public sealed unsafe class GuestImageWriteTrackerTests
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
GuestImageWriteTracker.Untrack(address);
|
GuestImageWriteTracker.Untrack(address);
|
||||||
NativeMemory.Free(allocation);
|
FreeTrackedPages(allocation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +158,7 @@ public sealed unsafe class GuestImageWriteTrackerTests
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
GuestImageWriteTracker.Untrack(address);
|
GuestImageWriteTracker.Untrack(address);
|
||||||
NativeMemory.Free(allocation);
|
FreeTrackedPages(allocation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user