diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs index ca25bb1c..b76069ee 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Exceptions.cs @@ -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; diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs index e6797ee6..c6516079 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Imports.cs @@ -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); } diff --git a/src/SharpEmu.HLE/GuestImageWriteTracker.cs b/src/SharpEmu.HLE/GuestImageWriteTracker.cs index c749f7a3..c72b3263 100644 --- a/src/SharpEmu.HLE/GuestImageWriteTracker.cs +++ b/src/SharpEmu.HLE/GuestImageWriteTracker.cs @@ -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; /// @@ -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) diff --git a/tests/SharpEmu.Libs.Tests/Memory/GuestImageWriteTrackerTests.cs b/tests/SharpEmu.Libs.Tests/Memory/GuestImageWriteTrackerTests.cs index ad564297..9a7d92be 100644 --- a/tests/SharpEmu.Libs.Tests/Memory/GuestImageWriteTrackerTests.cs +++ b/tests/SharpEmu.Libs.Tests/Memory/GuestImageWriteTrackerTests.cs @@ -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); } }