Prevent invalid SaveData writes from damaging guest memory (#444)

Add an optional write monitor so the team can find future memory damage on each supported desktop system.
This commit is contained in:
StealUrKill
2026-07-19 12:27:17 -05:00
committed by GitHub
parent d7f6e3f578
commit bc51cc2c4d
8 changed files with 437 additions and 21 deletions
@@ -0,0 +1,77 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using Xunit;
namespace SharpEmu.Libs.Tests.HLE;
public sealed class GuestWriteWatchTests
{
[Theory]
[InlineData(0x0000001000000001, "torn")]
[InlineData(0x0000FFFF00000001, "torn")]
[InlineData(0x0000000008000000, "shift")]
[InlineData(0x0000000080015F00, "shift")]
[InlineData(0x0000000007FFFFFF, null)]
[InlineData(0x0000000009000000, null)]
[InlineData(0x000000003F800000, null)]
[InlineData(0x0000000080015F01, null)]
[InlineData(0x0001000000000001, null)]
public void ClassifyBulkValue_RecognizesCorruptionSignatures(ulong value, string? expected)
{
Assert.Equal(expected, GuestWriteWatch.ClassifyBulkValue(value));
}
[Theory]
[InlineData(0, 0)]
[InlineData(1, 7)]
[InlineData(3, 5)]
[InlineData(7, 1)]
[InlineData(8, 0)]
public void FirstAlignedOffset_ReturnsTheNextEightByteBoundary(ulong address, int expected)
{
Assert.Equal(expected, GuestWriteWatch.FirstAlignedOffset(address));
}
[Theory]
[InlineData(0x1000, 8, 0x1000, true)]
[InlineData(0x0FFF, 1, 0x1000, false)]
[InlineData(0x0FFF, 2, 0x1000, true)]
[InlineData(0x1008, 1, 0x1000, false)]
[InlineData(ulong.MaxValue - 3, 4, ulong.MaxValue - 1, true)]
[InlineData(ulong.MaxValue, 1, ulong.MaxValue, true)]
[InlineData(0x1000, 0, 0x1000, false)]
public void Overlaps_HandlesBoundariesWithoutOverflow(
ulong address,
int length,
ulong slot,
bool expected)
{
Assert.Equal(expected, GuestWriteWatch.Overlaps(address, length, slot));
}
[Theory]
[InlineData(0x10000, 0xF2, true)]
[InlineData(0x10001, 0xF2, false)]
[InlineData(0x10000, 0xF1, false)]
public void IsPoolMapping_RequiresTheExpectedSizeAndProtection(
ulong length,
int protection,
bool expected)
{
Assert.Equal(expected, GuestWriteWatch.IsPoolMapping(length, protection));
}
[Theory]
[InlineData(null, 0)]
[InlineData("", 0)]
[InlineData("not-hex", 0)]
[InlineData("80", 0x80)]
[InlineData("0x80", 0x80)]
[InlineData(" 0X801DB3BBB ", 0x801DB3BBB)]
public void Parse_HandlesHexadecimalWatchValues(string? text, ulong expected)
{
Assert.Equal(expected, GuestWriteWatch.Parse(text));
}
}
@@ -31,6 +31,9 @@ public sealed class SaveDataExportsTests : IDisposable
private const ulong SyncParam = Base + 0xC80;
private const ulong SetupParam = Base + 0xD00;
private const ulong SetupResult = Base + 0xD80;
private const ulong TransactionOut = Base + 0xE00;
private const ulong StaleR8 = Base + 0xE08;
private const ulong StaleR9 = Base + 0xE10;
private const int NoEvent = unchecked((int)0x809F0008);
private const int ParameterError = unchecked((int)0x809F0000);
@@ -225,4 +228,64 @@ public sealed class SaveDataExportsTests : IDisposable
unchecked((int)0x809F0008),
SaveDataExports.SaveDataDelete(Reg(rdi: DeleteParam)));
}
[Fact]
public void CreateTransactionResource_WithoutOutPointer_DoesNotProbeStaleRegisters()
{
const uint sentinel = 0xA5A5A5A5;
Assert.True(_ctx.TryWriteUInt32(TransactionOut, sentinel));
Assert.True(_ctx.TryWriteUInt32(StaleR8, sentinel));
Assert.True(_ctx.TryWriteUInt32(StaleR9, sentinel));
var ctx = Reg(rdi: UserId, rdx: 0, rcx: TransactionOut);
ctx[CpuRegister.R8] = StaleR8;
ctx[CpuRegister.R9] = StaleR9;
Assert.Equal(0, SaveDataExports.SaveDataCreateTransactionResource(ctx));
Assert.True(_ctx.TryReadUInt32(TransactionOut, out var rcxValue));
Assert.True(_ctx.TryReadUInt32(StaleR8, out var r8Value));
Assert.True(_ctx.TryReadUInt32(StaleR9, out var r9Value));
Assert.Equal(sentinel, rcxValue);
Assert.Equal(sentinel, r8Value);
Assert.Equal(sentinel, r9Value);
}
[Fact]
public void CreateTransactionResource_WithOutPointerFlag_WritesOnlyRcx()
{
const uint sentinel = 0xA5A5A5A5;
Assert.True(_ctx.TryWriteUInt32(TransactionOut, 0));
Assert.True(_ctx.TryWriteUInt32(StaleR8, sentinel));
Assert.True(_ctx.TryWriteUInt32(StaleR9, sentinel));
var ctx = Reg(rdi: UserId, rdx: 1, rcx: TransactionOut);
ctx[CpuRegister.R8] = StaleR8;
ctx[CpuRegister.R9] = StaleR9;
Assert.Equal(0, SaveDataExports.SaveDataCreateTransactionResource(ctx));
Assert.True(_ctx.TryReadUInt32(TransactionOut, out var resource));
Assert.True(_ctx.TryReadUInt32(StaleR8, out var r8Value));
Assert.True(_ctx.TryReadUInt32(StaleR9, out var r9Value));
Assert.NotEqual(0u, resource);
Assert.Equal(sentinel, r8Value);
Assert.Equal(sentinel, r9Value);
}
[Fact]
public void CreateTransactionResource_WithLegacyOutPointer_WritesOnlyRdx()
{
const uint sentinel = 0xA5A5A5A5;
Assert.True(_ctx.TryWriteUInt32(TransactionOut, 0));
Assert.True(_ctx.TryWriteUInt32(StaleR8, sentinel));
Assert.Equal(
0,
SaveDataExports.SaveDataCreateTransactionResource(
Reg(rdi: UserId, rdx: TransactionOut, rcx: StaleR8)));
Assert.True(_ctx.TryReadUInt32(TransactionOut, out var resource));
Assert.True(_ctx.TryReadUInt32(StaleR8, out var rcxValue));
Assert.NotEqual(0u, resource);
Assert.Equal(sentinel, rcxValue);
}
}