277 lines
13 KiB
C#
277 lines
13 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using System.Buffers.Binary;
|
|
using System.Reflection;
|
|
using System.Runtime.InteropServices;
|
|
using Iced.Intel;
|
|
using SharpEmu.Core.Cpu.Native;
|
|
using SharpEmu.HLE;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.Cpu;
|
|
|
|
public sealed class TlsLoadPatchBoundaryTests
|
|
{
|
|
[Theory]
|
|
[InlineData(new byte[] { 0x48, 0xB8, 0x64, 0x48, 0x8B, 0x04, 0x25, 0, 0, 0, 0, 0xC0, 0xC3 })]
|
|
[InlineData(new byte[] { 0x48, 0xB8, 0x64, 0x8B, 0x04, 0x25, 0x28, 0, 0, 0, 0xC3 })]
|
|
[InlineData(new byte[] { 0x48, 0xB8, 0x64, 0xC7, 0x04, 0x25, 0, 0, 0, 0,
|
|
0x48, 0xB8, 1, 2, 3, 4, 5, 6, 7, 8, 0xC3 })]
|
|
public void ScanDoesNotPatchInstructionOperands(byte[] instructions)
|
|
{
|
|
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64) return;
|
|
using var fixture = new PatchFixture();
|
|
fixture.Patch(instructions);
|
|
Assert.Equal(instructions, fixture.Read(instructions.Length));
|
|
}
|
|
|
|
[Fact]
|
|
public void ScanPreservesOperandsAcrossExecutableProtectionBoundary()
|
|
{
|
|
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64) return;
|
|
using var fixture = new PatchFixture();
|
|
byte[] instructions = [0x48, 0xB8, 0x64, 0x48, 0x8B, 0x04, 0x25, 0, 0, 0, 0, 0xC0, 0xC3];
|
|
const int instructionOffset = 0xFFE;
|
|
fixture.Write(instructions, instructionOffset);
|
|
fixture.Protect(0x1000, 0x1000, 0x20);
|
|
fixture.Scan(0, 0x2000);
|
|
Assert.Equal(instructions, fixture.Read(instructions.Length, instructionOffset));
|
|
}
|
|
|
|
[Fact]
|
|
public void ScanPatchesLoadAcrossExecutableProtectionBoundary()
|
|
{
|
|
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64) return;
|
|
using var fixture = new PatchFixture();
|
|
const int instructionOffset = 0xFFC;
|
|
fixture.Write([0x64, 0x48, 0x8B, 0x04, 0x25, 0, 0, 0, 0], instructionOffset);
|
|
fixture.Protect(0x1000, 0x1000, 0x20);
|
|
fixture.Scan(0, 0x2000);
|
|
Assert.Equal(new byte[] { 0x48, 0xE8 }, fixture.Read(2, instructionOffset));
|
|
Assert.Equal(0x40u, fixture.ProtectionAt(instructionOffset));
|
|
Assert.Equal(0x20u, fixture.ProtectionAt(0x1000));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0x01u)]
|
|
[InlineData(0x104u)]
|
|
[InlineData(0x04u)]
|
|
public void ScanDoesNotReadAcrossUnavailableRegion(uint nextPageProtection)
|
|
{
|
|
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64) return;
|
|
using var fixture = new PatchFixture();
|
|
const int instructionOffset = 0xFFC;
|
|
byte[] instructions = [0x64, 0x48, 0x8B, 0x04];
|
|
fixture.Write(instructions, instructionOffset);
|
|
fixture.Protect(0x1000, 0x1000, nextPageProtection);
|
|
fixture.Scan(0, 0x2000);
|
|
Assert.Equal(instructions, fixture.Read(instructions.Length, instructionOffset));
|
|
Assert.Equal(nextPageProtection, fixture.ProtectionAt(0x1000));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(new byte[] { 0x64, 0x48, 0x8B, 0x04, 0x25, 0, 0, 0, 0 })]
|
|
[InlineData(new byte[] { 0x64, 0xC7, 0x04, 0x25, 0, 0, 0, 0, 1, 2, 3, 4 })]
|
|
[InlineData(new byte[] { 0x64, 0x8B, 0x04, 0x25, 0x28, 0, 0, 0 })]
|
|
[InlineData(new byte[] { 0x64, 0x48, 0x8B, 0x04, 0x25, 0x28, 0, 0, 0 })]
|
|
public void ScanPatchesCompleteInstructionAtRangeEnd(byte[] instructions)
|
|
{
|
|
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64) return;
|
|
using var fixture = new PatchFixture();
|
|
fixture.Write(instructions);
|
|
fixture.Scan(0, instructions.Length);
|
|
Assert.NotEqual(instructions, fixture.Read(instructions.Length));
|
|
Assert.Equal(0x90, fixture.Read(1, instructions.Length)[0]);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(new byte[] { 0x64, 0x48, 0x8B, 0x04, 0x25, 0, 0, 0, 0 })]
|
|
[InlineData(new byte[] { 0x64, 0xC7, 0x04, 0x25, 0, 0, 0, 0, 1, 2, 3, 4 })]
|
|
[InlineData(new byte[] { 0x64, 0x8B, 0x04, 0x25, 0x28, 0, 0, 0 })]
|
|
public void ScanLeavesTruncatedInstructionUnchanged(byte[] instructions)
|
|
{
|
|
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64) return;
|
|
using var fixture = new PatchFixture();
|
|
fixture.Write(instructions);
|
|
fixture.Scan(0, instructions.Length - 1);
|
|
Assert.Equal(instructions, fixture.Read(instructions.Length));
|
|
}
|
|
|
|
[Fact]
|
|
public void ScanLeavesSse4aInstructionsUnchanged()
|
|
{
|
|
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64) return;
|
|
using var fixture = new PatchFixture();
|
|
byte[] instructions = [0x66, 0x0F, 0x79, 0xD5, 0x66, 0x41, 0x0F, 0x79, 0xE0, 0xC3];
|
|
fixture.Patch(instructions);
|
|
Assert.Equal(instructions, fixture.Read(instructions.Length));
|
|
}
|
|
|
|
[Fact]
|
|
public void ScanContinuesAfterInvalidEncoding()
|
|
{
|
|
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64) return;
|
|
using var fixture = new PatchFixture();
|
|
byte[] instructions = [0x0F, 0x04, 0xC0, 0x64, 0x48, 0x8B, 0x04, 0x25, 0, 0, 0, 0];
|
|
var decoder = Decoder.Create(64, new ByteArrayCodeReader(instructions));
|
|
var invalid = decoder.Decode();
|
|
Assert.Equal(Code.INVALID, invalid.Code);
|
|
Assert.Equal(3, invalid.Length);
|
|
fixture.Patch(instructions);
|
|
Assert.Equal(new byte[] { 0x0F, 0x04, 0xC0, 0x48, 0xE8 }, fixture.Read(5));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(new byte[] { 0x66, 0x64, 0xC7, 0x04, 0x25, 0, 0, 0, 0, 0x34, 0x12 })]
|
|
[InlineData(new byte[] { 0xF0, 0x64, 0x48, 0x8B, 0x04, 0x25, 0, 0, 0, 0 })]
|
|
[InlineData(new byte[] { 0x64, 0x4A, 0x8B, 0x04, 0x25, 0x28, 0, 0, 0 })]
|
|
public void ScanDoesNotChangeUnsupportedOperandSemantics(byte[] instructions)
|
|
{
|
|
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64) return;
|
|
using var fixture = new PatchFixture();
|
|
fixture.Patch(instructions);
|
|
Assert.Equal(instructions, fixture.Read(instructions.Length));
|
|
}
|
|
|
|
[Fact]
|
|
public void RescanPatchesNewlyExecutableMemoryOnlyOnce()
|
|
{
|
|
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64) return;
|
|
using var fixture = new PatchFixture();
|
|
byte[] instructions = [0x64, 0xC7, 0x04, 0x25, 0, 0, 0, 0, 1, 2, 3, 4];
|
|
fixture.Write(instructions);
|
|
fixture.Protect(0, 0x1000, 0x04);
|
|
fixture.Scan(0, 0x100);
|
|
Assert.Equal(instructions, fixture.Read(instructions.Length));
|
|
fixture.Protect(0, 0x1000, 0x40);
|
|
fixture.RescanExecutable(0, 0x100);
|
|
var patched = fixture.Read(instructions.Length);
|
|
var stubOffset = fixture.StubOffset;
|
|
Assert.Equal(0xE8, patched[0]);
|
|
fixture.RescanExecutable(0, 0x100);
|
|
Assert.Equal(patched, fixture.Read(instructions.Length));
|
|
Assert.Equal(stubOffset, fixture.StubOffset);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(new byte[] { 0xEB, 0x66, 0x66 })]
|
|
[InlineData(new byte[] { 0x75, 0xEB, 0x66, 0x66, 0x66 })]
|
|
[InlineData(new byte[] { 0x4E, 0x8B, 0x34, 0xEB, 0x66, 0x66, 0x66 })]
|
|
[InlineData(new byte[] { 0x90 })]
|
|
public void ScanPreservesBytesBeforeSegmentPrefix(byte[] leadingBytes)
|
|
{
|
|
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64) return;
|
|
using var fixture = new PatchFixture();
|
|
var instructions = leadingBytes.Concat(new byte[]
|
|
{ 0x64, 0x48, 0x8B, 0x04, 0x25, 0, 0, 0, 0 }).ToArray();
|
|
fixture.Patch(instructions);
|
|
var patched = fixture.Read(instructions.Length);
|
|
Assert.Equal(leadingBytes, patched[..leadingBytes.Length]);
|
|
Assert.Equal(0x48, patched[leadingBytes.Length]);
|
|
Assert.Equal(0xE8, patched[leadingBytes.Length + 1]);
|
|
foreach (var options in new[] { DecoderOptions.None, DecoderOptions.AMD })
|
|
{
|
|
var prefixStart = leadingBytes.Length;
|
|
while (prefixStart > 0 && patched[prefixStart - 1] == 0x66) prefixStart--;
|
|
var decoder = Decoder.Create(64, new ByteArrayCodeReader(patched[prefixStart..]), options);
|
|
decoder.IP = (ulong)fixture.Address + (ulong)prefixStart;
|
|
var instruction = decoder.Decode();
|
|
Assert.Equal(Code.Call_rel32_64, instruction.Code);
|
|
Assert.Equal((ulong)fixture.HandlerAddress, instruction.NearBranch64);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public unsafe void NativeLoadPreservesLiveAccumulator()
|
|
{
|
|
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64) return;
|
|
using var fixture = new PatchFixture();
|
|
const ulong accumulator = 0x123456789ABCDEF0;
|
|
byte[] instructions = [0x48, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
0x66, 0x66, 0x64, 0x4C, 0x8B, 0x0C, 0x25, 0, 0, 0, 0,
|
|
0x4C, 0x31, 0xC8, 0xC3];
|
|
BinaryPrimitives.WriteUInt64LittleEndian(instructions.AsSpan(2), accumulator);
|
|
fixture.Patch(instructions);
|
|
var execute = (delegate* unmanaged<ulong>)fixture.Address;
|
|
Assert.Equal(accumulator ^ PatchFixture.ThreadPointer, execute());
|
|
}
|
|
|
|
private sealed unsafe class PatchFixture : IDisposable
|
|
{
|
|
public const ulong ThreadPointer = 0x00007FFE00000000;
|
|
public nint Address { get; } = VirtualAlloc(0, 0x4000, 0x3000, 0x40);
|
|
public nint HandlerAddress => Address + 0x3000;
|
|
public int StubOffset => (int)typeof(DirectExecutionBackend)
|
|
.GetField("_tlsPatchStubOffset", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(_backend)!;
|
|
private readonly DirectExecutionBackend _backend = new(new ModuleManager());
|
|
|
|
public PatchFixture()
|
|
{
|
|
Assert.NotEqual(0, Address);
|
|
new Span<byte>((void*)Address, 0x4000).Fill(0x90);
|
|
var handler = new Span<byte>((void*)HandlerAddress, 11);
|
|
handler[0] = 0x48;
|
|
handler[1] = 0xB8;
|
|
BinaryPrimitives.WriteUInt64LittleEndian(handler[2..], ThreadPointer);
|
|
handler[10] = 0xC3;
|
|
SetField("_tlsHandlerAddress", HandlerAddress);
|
|
SetField("_tlsPatchStubOffset", 0x100);
|
|
}
|
|
|
|
private void SetField(string name, object value) => typeof(DirectExecutionBackend)
|
|
.GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(_backend, value);
|
|
|
|
public void Patch(byte[] instructions)
|
|
{
|
|
Write(instructions);
|
|
Scan(0, 0x100);
|
|
}
|
|
|
|
public void Write(byte[] instructions, int offset = 0) =>
|
|
instructions.CopyTo(new Span<byte>((void*)(Address + offset), instructions.Length));
|
|
|
|
public void Scan(int offset, int length)
|
|
{
|
|
typeof(DirectExecutionBackend).GetMethod("PatchTlsPatternsInRange",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!.Invoke(_backend,
|
|
[(ulong)(Address + offset), (ulong)(Address + offset + length)]);
|
|
}
|
|
|
|
public void RescanExecutable(int offset, int length) => typeof(DirectExecutionBackend)
|
|
.GetMethod("RescanTlsPatternsIfExecutable", BindingFlags.Instance | BindingFlags.NonPublic)!
|
|
.Invoke(_backend, [(ulong)(Address + offset), (ulong)length, 0x40u]);
|
|
|
|
public byte[] Read(int length, int offset = 0) =>
|
|
new ReadOnlySpan<byte>((void*)(Address + offset), length).ToArray();
|
|
|
|
public void Protect(int offset, int length, uint protection) =>
|
|
Assert.True(VirtualProtect(Address + offset, (nuint)length, protection, out _));
|
|
|
|
public uint ProtectionAt(int offset)
|
|
{
|
|
byte* information = stackalloc byte[48];
|
|
Assert.NotEqual((nuint)0, VirtualQuery(Address + offset, information, 48));
|
|
return *(uint*)(information + 36);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
SetField("_tlsHandlerAddress", (nint)0);
|
|
_backend.Dispose();
|
|
Assert.True(VirtualFree(Address, 0, 0x8000));
|
|
}
|
|
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern nint VirtualAlloc(nint address, nuint size, uint allocationType, uint protection);
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool VirtualFree(nint address, nuint size, uint freeType);
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
[return: MarshalAs(UnmanagedType.Bool)]
|
|
private static extern bool VirtualProtect(nint address, nuint size, uint protection, out uint previousProtection);
|
|
[DllImport("kernel32.dll", SetLastError = true)]
|
|
private static extern nuint VirtualQuery(nint address, void* information, nuint length);
|
|
}
|
|
}
|