sharpemu/tests/SharpEmu.Libs.Tests/Loader/GuestRedZoneEnclosingSpanTe...

154 lines
5.3 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Loader;
using Xunit;
namespace SharpEmu.Libs.Tests.Loader;
/// <summary>
/// The forward span search anchors at the faulting instruction, so it gives up
/// whenever the next instruction is a branch target, touches RSP, or changes
/// control flow. These are the three shapes that actually occur around the
/// faulting sites of a red-zone function in a shipped title; the enclosing
/// search has to take its bytes from before the faulting instruction instead,
/// and must never let the RSP shift cover an RSP-relative access.
/// </summary>
public sealed class GuestRedZoneEnclosingSpanTests
{
private const ulong Base = 0x8_0000_0000UL;
[Fact]
public void TakesBytesFromBeforeWhenTheNextInstructionIsABranchTarget()
{
// jne +8 -> makes the instruction after the site a target
// shr r11, 12 -> 4 bytes, no memory operand
// and r10d,[rcx+8] -> 4 bytes, faultable: the site
// mov rax,[rcx] -> branch target, must stay untouched
byte[] code =
[
0x75, 0x08,
0x49, 0xC1, 0xEB, 0x0C,
0x44, 0x23, 0x51, 0x08,
0x48, 0x8B, 0x01,
];
Assert.True(GuestRedZonePatcher.TryBuildEnclosingSpan(
code, Base, Base + 6, out var address, out var length, out var coreStart, out var coreCount));
Assert.Equal(Base + 2, address);
Assert.Equal(8, length);
// The span ends exactly at the branch target, which is never rewritten.
Assert.Equal(Base + 10, address + (ulong)length);
Assert.Equal(1, coreStart);
Assert.Equal(1, coreCount);
}
[Fact]
public void KeepsAnRspRelativeInstructionOutsideTheShiftedCore()
{
// jne +8
// mov rax,[rsp-0x10] -> reads the red zone; must run before the shift
// mov [rax+0x18],ebp -> 3 bytes, faultable: the site
// pop rbx -> branch target and stack dependent
byte[] code =
[
0x75, 0x08,
0x48, 0x8B, 0x44, 0x24, 0xF0,
0x89, 0x68, 0x18,
0x5B,
];
Assert.True(GuestRedZonePatcher.TryBuildEnclosingSpan(
code, Base, Base + 7, out var address, out var length, out var coreStart, out var coreCount));
Assert.Equal(Base + 2, address);
Assert.Equal(8, length);
// coreStart == 1 is what keeps the shift off the [rsp-0x10] load: shifting
// it would move the access 128 bytes and silently read the wrong slot.
Assert.Equal(1, coreStart);
Assert.Equal(1, coreCount);
}
[Fact]
public void BracketsEveryFaultableInstructionOfTheSpan()
{
// mov eax,[rcx+8] -> faultable, absorbed by the span
// test [rcx+8],eax -> faultable: the site
// je +2 -> control flow, cannot be absorbed
byte[] code =
[
0x8B, 0x41, 0x08,
0x85, 0x41, 0x08,
0x74, 0x02,
];
Assert.True(GuestRedZonePatcher.TryBuildEnclosingSpan(
code, Base, Base + 3, out var address, out var length, out var coreStart, out var coreCount));
Assert.Equal(Base, address);
Assert.Equal(6, length);
// A stolen instruction that can fault too has to sit inside the shift,
// otherwise it recreates the very corruption this pass prevents.
Assert.Equal(0, coreStart);
Assert.Equal(2, coreCount);
}
[Fact]
public void RefusesWhenTheFaultingInstructionIsItselfABranchTarget()
{
// jne +6 lands on the faulting instruction, so a jump placed before it
// would be entered in the middle.
byte[] code =
[
0x75, 0x04,
0x49, 0xC1, 0xEB, 0x0C,
0x44, 0x23, 0x51, 0x08,
0x48, 0x8B, 0x01,
];
Assert.False(GuestRedZonePatcher.TryBuildEnclosingSpan(
code, Base, Base + 6, out _, out _, out _, out _));
}
[Fact]
public void RefusesWhenControlFlowSitsBetweenTheStartAndTheSite()
{
// A conditional branch cannot be relocated as part of a span, and the
// bytes before the site are not otherwise sufficient.
byte[] code =
[
0x74, 0x02,
0x44, 0x23, 0x51, 0x08,
0x48, 0x8B, 0x01,
];
Assert.False(GuestRedZonePatcher.TryBuildEnclosingSpan(
code, Base, Base + 2, out _, out _, out _, out _));
}
[Fact]
public void BuildsASpanOverRegisterOnlyShaInstructions()
{
// sha256msg1 xmm1, xmm2 -> 4 bytes, no memory operand: the rewrite site
// sha256msg2 xmm1, xmm2 -> 4 bytes, taken to reach the 5-byte jump
// ret
byte[] code =
[
0x0F, 0x38, 0xCC, 0xCA,
0x0F, 0x38, 0xCD, 0xCA,
0xC3,
];
// A span with no guest memory access has nothing to keep outside the RSP shift, so
// the whole span is the core. Refusing it left SHA instructions unrewritten.
Assert.True(GuestRedZonePatcher.TryBuildForwardSpan(
code, Base, Base, out var length, out var coreStart, out var coreCount));
Assert.Equal(8, length);
Assert.Equal(0, coreStart);
Assert.Equal(2, coreCount);
}
}