mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 10:56:20 +08:00
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:
@@ -0,0 +1,57 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Guest write watch
|
||||||
|
|
||||||
|
`GuestWriteWatch` is an optional diagnostic tool. It helps you find managed
|
||||||
|
code and HLE code that damage guest memory. The tool starts only if you set one
|
||||||
|
or more `SHARPEMU_WATCH_*` environment variables.
|
||||||
|
|
||||||
|
The tool monitors writes through the SharpEmu managed virtual-memory APIs. It
|
||||||
|
does not monitor stores that native guest code makes directly. Use a platform
|
||||||
|
debugger or a hardware watchpoint to monitor these stores.
|
||||||
|
|
||||||
|
## Watch modes
|
||||||
|
|
||||||
|
- `SHARPEMU_WATCH_WRITE=0x<address>` logs a write that overlaps the eight-byte
|
||||||
|
block at the specified guest address.
|
||||||
|
- `SHARPEMU_WATCH_POOL_HEADER=1` monitors the pointer at offset `0x40`. It
|
||||||
|
monitors the first 64 direct mappings that have a size of 64 KiB and
|
||||||
|
protection value `0xF2`.
|
||||||
|
- `SHARPEMU_WATCH_VALUE_PATTERN=1` logs an eight-byte write if its lower 32 bits
|
||||||
|
are `1`. The upper 32 bits must look like a small guest-pointer prefix.
|
||||||
|
- `SHARPEMU_WATCH_VALUE1=1` logs short writes of value `1` in the high guest
|
||||||
|
memory range. The tool logs a maximum of 128 entries for each process.
|
||||||
|
- `SHARPEMU_WATCH_BULK_TORN=1` scans aligned 64-bit words in bulk writes. It
|
||||||
|
finds damaged pointer patterns and byte-shifted pointer patterns. The tool
|
||||||
|
logs a maximum of 64 entries for each process.
|
||||||
|
- `SHARPEMU_WATCH_BULK_DEST_HI=0x<high-dword>` scans only writes that have the
|
||||||
|
specified upper 32 bits in the destination address.
|
||||||
|
|
||||||
|
For each match, the tool logs the destination address, the data pattern, and the
|
||||||
|
managed call stack. The log uses the `watch_write` or `watch_bulk_torn` warning
|
||||||
|
tag.
|
||||||
|
|
||||||
|
Use these variables together to scan bulk writes in the
|
||||||
|
`0x00000080xxxxxxxx` region.
|
||||||
|
|
||||||
|
macOS and Linux:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
SHARPEMU_WATCH_BULK_TORN=1 \
|
||||||
|
SHARPEMU_WATCH_BULK_DEST_HI=0x80 \
|
||||||
|
SharpEmu /path/to/eboot.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:SHARPEMU_WATCH_BULK_TORN = "1"
|
||||||
|
$env:SHARPEMU_WATCH_BULK_DEST_HI = "0x80"
|
||||||
|
& .\SharpEmu.exe C:\path\to\game\eboot.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
To reduce unnecessary log entries, use an exact `SHARPEMU_WATCH_WRITE`
|
||||||
|
address from a crash dump.
|
||||||
@@ -919,6 +919,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotifyGuestWriteWatch(virtualAddress, source);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -944,6 +945,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void NotifyGuestWriteWatch(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
|
{
|
||||||
|
if (GuestWriteWatch.Armed)
|
||||||
|
{
|
||||||
|
GuestWriteWatch.Check(virtualAddress, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
||||||
{
|
{
|
||||||
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
||||||
@@ -1016,6 +1025,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotifyGuestWriteWatch(virtualAddress, source);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1040,6 +1050,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotifyGuestWriteWatch(virtualAddress, source);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Memory;
|
namespace SharpEmu.Core.Memory;
|
||||||
|
|
||||||
@@ -93,8 +94,14 @@ public sealed class VirtualMemory : IVirtualMemory
|
|||||||
}
|
}
|
||||||
|
|
||||||
CopyToRegions(virtualAddress, source, regionIndex);
|
CopyToRegions(virtualAddress, source, regionIndex);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (GuestWriteWatch.Armed)
|
||||||
|
{
|
||||||
|
GuestWriteWatch.Check(virtualAddress, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryValidateRange(
|
private bool TryValidateRange(
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
// This tool monitors guest-memory writes only when a watch mode is active.
|
||||||
|
public static class GuestWriteWatch
|
||||||
|
{
|
||||||
|
private const ulong WatchBytes = 8;
|
||||||
|
private const int MaxBulkReports = 64;
|
||||||
|
|
||||||
|
private static readonly ulong WatchBase = Parse(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_WATCH_WRITE"));
|
||||||
|
|
||||||
|
private static readonly bool WatchPoolHeaders = IsEnabled("SHARPEMU_WATCH_POOL_HEADER");
|
||||||
|
|
||||||
|
private static readonly ulong[] PoolSlots = new ulong[64];
|
||||||
|
private static int _poolSlotCount;
|
||||||
|
|
||||||
|
private static readonly bool WatchValuePattern = IsEnabled("SHARPEMU_WATCH_VALUE_PATTERN");
|
||||||
|
|
||||||
|
private static readonly bool WatchValue1 = IsEnabled("SHARPEMU_WATCH_VALUE1");
|
||||||
|
|
||||||
|
private const ulong DirectBandLow = 0x100_0000_0000;
|
||||||
|
private const ulong DirectBandHigh = 0x1000_0000_0000;
|
||||||
|
private static int _value1Reports;
|
||||||
|
|
||||||
|
private static readonly bool WatchBulkTorn = IsEnabled("SHARPEMU_WATCH_BULK_TORN");
|
||||||
|
|
||||||
|
private static readonly ulong BulkDestHigh = Parse(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_WATCH_BULK_DEST_HI"));
|
||||||
|
private static int _bulkTornReports;
|
||||||
|
private static int _bulkShiftReports;
|
||||||
|
|
||||||
|
public static bool Armed =>
|
||||||
|
WatchBase != 0 || WatchPoolHeaders || WatchValuePattern || WatchValue1 || WatchBulkTorn;
|
||||||
|
|
||||||
|
public static void OnDirectMapping(ulong mappedAddress, ulong length, int protection)
|
||||||
|
{
|
||||||
|
if (!WatchPoolHeaders || !IsPoolMapping(length, protection))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = Interlocked.Increment(ref _poolSlotCount) - 1;
|
||||||
|
if (index < PoolSlots.Length)
|
||||||
|
{
|
||||||
|
Volatile.Write(ref PoolSlots[index], mappedAddress + 0x40);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] watch_write armed on pool header slot 0x{mappedAddress + 0x40:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Check(ulong address, ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
if (WatchBulkTorn &&
|
||||||
|
data.Length >= 8 &&
|
||||||
|
(BulkDestHigh != 0
|
||||||
|
? (address >> 32) == BulkDestHigh
|
||||||
|
: address >= DirectBandLow && address < DirectBandHigh))
|
||||||
|
{
|
||||||
|
for (var offset = FirstAlignedOffset(address); offset + 8 <= data.Length; offset += 8)
|
||||||
|
{
|
||||||
|
var qword = BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(offset, 8));
|
||||||
|
var kind = ClassifyBulkValue(qword);
|
||||||
|
if (kind is not null && ReserveBulkReport(kind))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] watch_bulk_torn HIT ({kind}) " +
|
||||||
|
$"dest=0x{address + (ulong)offset:X16} (base=0x{address:X16}+0x{offset:X}) " +
|
||||||
|
$"len={data.Length} qword=0x{qword:X16}{Environment.NewLine}{Environment.StackTrace}");
|
||||||
|
Console.Error.Flush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WatchValue1 &&
|
||||||
|
address >= DirectBandLow && address < DirectBandHigh &&
|
||||||
|
data.Length is >= 1 and <= 8 &&
|
||||||
|
LittleEndianValue(data) == 1 &&
|
||||||
|
Interlocked.Increment(ref _value1Reports) <= 128)
|
||||||
|
{
|
||||||
|
Report(address, data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WatchValuePattern && data.Length == 8)
|
||||||
|
{
|
||||||
|
var value = BinaryPrimitives.ReadUInt64LittleEndian(data);
|
||||||
|
if ((value & 0xFFFFFFFF) == 1 && value >> 32 is > 0 and <= 0xFFFF)
|
||||||
|
{
|
||||||
|
Report(address, data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WatchBase != 0 && Overlaps(address, data.Length, WatchBase))
|
||||||
|
{
|
||||||
|
Report(address, data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var slots = Math.Min(Volatile.Read(ref _poolSlotCount), PoolSlots.Length);
|
||||||
|
for (var i = 0; i < slots; i++)
|
||||||
|
{
|
||||||
|
var slot = Volatile.Read(ref PoolSlots[i]);
|
||||||
|
if (slot != 0 && Overlaps(address, data.Length, slot))
|
||||||
|
{
|
||||||
|
Report(address, data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string? ClassifyBulkValue(ulong qword)
|
||||||
|
{
|
||||||
|
var low32 = qword & 0xFFFFFFFF;
|
||||||
|
var high32 = qword >> 32;
|
||||||
|
if (low32 == 1 && high32 is > 0 and <= 0xFFFF)
|
||||||
|
{
|
||||||
|
return "torn";
|
||||||
|
}
|
||||||
|
|
||||||
|
var prefix = low32 & 0xFF00_0000;
|
||||||
|
var hasShiftedPointerPrefix = prefix is 0x0800_0000 or 0x8000_0000;
|
||||||
|
return high32 == 0 && hasShiftedPointerPrefix && (low32 & 0xFF) == 0
|
||||||
|
? "shift"
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static int FirstAlignedOffset(ulong address) =>
|
||||||
|
(int)((8 - (address & 7)) & 7);
|
||||||
|
|
||||||
|
internal static bool IsPoolMapping(ulong length, int protection) =>
|
||||||
|
length == 0x10000 && protection == 0xF2;
|
||||||
|
|
||||||
|
internal static bool Overlaps(ulong address, int length, ulong slot)
|
||||||
|
{
|
||||||
|
if (length <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var writeLength = (ulong)length - 1;
|
||||||
|
var writeEnd = address > ulong.MaxValue - writeLength
|
||||||
|
? ulong.MaxValue
|
||||||
|
: address + writeLength;
|
||||||
|
var slotEnd = slot > ulong.MaxValue - (WatchBytes - 1)
|
||||||
|
? ulong.MaxValue
|
||||||
|
: slot + WatchBytes - 1;
|
||||||
|
return address <= slotEnd && slot <= writeEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong LittleEndianValue(ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
ulong value = 0;
|
||||||
|
for (var i = 0; i < data.Length; i++)
|
||||||
|
{
|
||||||
|
value |= (ulong)data[i] << (i * 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Report(ulong address, ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] watch_write HIT addr=0x{address:X16} len={data.Length} " +
|
||||||
|
$"first_qword=0x{LittleEndianValue(data):X16}{Environment.NewLine}{Environment.StackTrace}");
|
||||||
|
Console.Error.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsEnabled(string name) =>
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable(name), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static bool ReserveBulkReport(string kind) =>
|
||||||
|
kind == "torn"
|
||||||
|
? Interlocked.Increment(ref _bulkTornReports) <= MaxBulkReports
|
||||||
|
: Interlocked.Increment(ref _bulkShiftReports) <= MaxBulkReports;
|
||||||
|
|
||||||
|
internal static ulong Parse(string? text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
text = text.Trim();
|
||||||
|
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
text = text[2..];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ulong.TryParse(text, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value)
|
||||||
|
? value
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2984,6 +2984,7 @@ public static partial class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GuestWriteWatch.OnDirectMapping(mappedAddress, length, protection);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -792,28 +792,15 @@ public static class SaveDataExports
|
|||||||
|
|
||||||
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
||||||
|
|
||||||
// The resource-out pointer's argument slot varies by SDK revision: some
|
// A small RDX value is a flag, and RCX contains the output address.
|
||||||
// callers pass it in rdx, others in rcx (a 4-arg form where rdx holds a
|
// A larger RDX value is the output address for the older ABI.
|
||||||
// count/flag). Void Terrarium passes rdx=0x1 (not a pointer) and the
|
|
||||||
// real out-pointer in rcx. Probe the plausible candidates and write the
|
|
||||||
// handle to the first writable one instead of faulting on a bad rdx.
|
|
||||||
// This is a stub-level create (matches shadPS4's return-OK semantics);
|
|
||||||
// never return MEMORY_FAULT for it, or the guest treats savedata init as
|
|
||||||
// failed and never advances.
|
|
||||||
var resourceAddress = 0UL;
|
var resourceAddress = 0UL;
|
||||||
foreach (var candidate in new[]
|
var selectedAddress = SelectTransactionResourceAddress(
|
||||||
{
|
ctx[CpuRegister.Rdx],
|
||||||
ctx[CpuRegister.Rdx],
|
ctx[CpuRegister.Rcx]);
|
||||||
ctx[CpuRegister.Rcx],
|
if (selectedAddress != 0 && TryWriteUInt32(ctx, selectedAddress, id))
|
||||||
ctx[CpuRegister.R8],
|
|
||||||
ctx[CpuRegister.R9],
|
|
||||||
})
|
|
||||||
{
|
{
|
||||||
if (candidate != 0 && TryWriteUInt32(ctx, candidate, id))
|
resourceAddress = selectedAddress;
|
||||||
{
|
|
||||||
resourceAddress = candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSaveData(
|
TraceSaveData(
|
||||||
@@ -822,6 +809,16 @@ public static class SaveDataExports
|
|||||||
return SetReturn(ctx, 0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static ulong SelectTransactionResourceAddress(ulong rdx, ulong rcx)
|
||||||
|
{
|
||||||
|
if (rdx == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rdx <= ushort.MaxValue ? rcx : rdx;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "lJUQuaKqoKY",
|
Nid = "lJUQuaKqoKY",
|
||||||
ExportName = "sceSaveDataDeleteTransactionResource",
|
ExportName = "sceSaveDataDeleteTransactionResource",
|
||||||
|
|||||||
@@ -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 SyncParam = Base + 0xC80;
|
||||||
private const ulong SetupParam = Base + 0xD00;
|
private const ulong SetupParam = Base + 0xD00;
|
||||||
private const ulong SetupResult = Base + 0xD80;
|
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 NoEvent = unchecked((int)0x809F0008);
|
||||||
private const int ParameterError = unchecked((int)0x809F0000);
|
private const int ParameterError = unchecked((int)0x809F0000);
|
||||||
@@ -225,4 +228,64 @@ public sealed class SaveDataExportsTests : IDisposable
|
|||||||
unchecked((int)0x809F0008),
|
unchecked((int)0x809F0008),
|
||||||
SaveDataExports.SaveDataDelete(Reg(rdi: DeleteParam)));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user