sharpemu/tests/SharpEmu.Libs.Tests/Cpu/Sse4aPosixSignalRecoveryTes...

392 lines
15 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SharpEmu.Core.Cpu.Emulation;
using SharpEmu.Core.Cpu.Native;
using SharpEmu.HLE;
using Xunit;
namespace SharpEmu.Libs.Tests.Cpu;
// Test instruction recovery with Linux and macOS signal frames.
// Confirm that recovery reads and updates the vector register values.
[Collection("PosixSignalRecovery")]
public sealed unsafe class Sse4aPosixSignalRecoveryTests : IDisposable
{
private readonly object? _previousBackend = PosixSignalBackend.GetValue(null);
public void Dispose() => PosixSignalBackend.SetValue(null, _previousBackend);
private const int PosixSigIll = 4;
private const int LinuxUcontextGregsOffset = 40;
private const int LinuxGregsRipOffset = 16 * 8;
private const int LinuxGregsFpstateOffset = 184;
private const int FxsaveXmm0Offset = 160;
private const int FxsaveXmm1Offset = 176;
private static readonly MethodInfo TryHandlePosixFault = typeof(DirectExecutionBackend).GetMethod(
"TryHandlePosixFault",
BindingFlags.Static | BindingFlags.NonPublic)!;
private static readonly FieldInfo PosixSignalBackend = typeof(DirectExecutionBackend).GetField(
"_posixSignalBackend",
BindingFlags.Static | BindingFlags.NonPublic)!;
private static readonly FieldInfo EmulatedCounter = typeof(DirectExecutionBackend).GetField(
"_sse4aInstructionsEmulated",
BindingFlags.Static | BindingFlags.NonPublic)!;
private static readonly FieldInfo XmmBridgedFlag = typeof(DirectExecutionBackend).GetField(
"_posixXmmContextBridged",
BindingFlags.Static | BindingFlags.NonPublic)!;
private static readonly MethodInfo TryRecoverAmdCompat = typeof(DirectExecutionBackend).GetMethod(
"TryRecoverAmdCompatInstruction",
BindingFlags.Instance | BindingFlags.NonPublic)!;
[Theory]
[InlineData(184UL, false)]
[InlineData(607UL, false)]
[InlineData(608UL, true)]
[InlineData(712UL, true)]
[InlineData(1032UL, true)]
public void MacOsSignalRequiresCompleteVectorRegisters(ulong contextSize, bool expected)
{
if (!OperatingSystem.IsMacOS() || RuntimeInformation.ProcessArchitecture != Architecture.X64)
return;
var getVectorRegisterAddress = typeof(DirectExecutionBackend).GetMethod(
"GetSignalVectorRegisterAddress", BindingFlags.Static | BindingFlags.NonPublic)!;
byte* userContext = stackalloc byte[64];
byte* machineContext = stackalloc byte[1032];
*(ulong*)(userContext + 40) = contextSize;
var result = Pointer.Unbox(getVectorRegisterAddress.Invoke(null,
[(nint)userContext, Pointer.Box(machineContext, typeof(byte*))])!);
Assert.Equal(expected ? (nint)(machineContext + 352) : 0, (nint)result);
}
[Fact]
public void ExtrqSigillRoundTripsXmmThroughTheBridge()
{
if ((!OperatingSystem.IsLinux() && !OperatingSystem.IsMacOS()) ||
RuntimeInformation.ProcessArchitecture != Architecture.X64)
{
return;
}
// extrq xmm0, 0x10, 0x08
var code = AllocateProbeVisibleCode([0x66, 0x0F, 0x78, 0xC0, 0x10, 0x08]);
try
{
const ulong value = 0x1234_5678_9ABC_DEF0UL;
var frame = new FakeSignalFrame((ulong)code);
frame.SetXmmLow(FxsaveXmm0Offset, value);
var emulatedBefore = (long)EmulatedCounter.GetValue(null)!;
Assert.True(frame.Dispatch());
Assert.Equal(
Sse4aBitFieldEmulator.ExtractBitField(value, length: 0x10, index: 0x08),
frame.XmmLow(FxsaveXmm0Offset));
Assert.Equal(0UL, frame.XmmHigh(FxsaveXmm0Offset));
Assert.Equal((ulong)code + 6, frame.Rip);
Assert.True((long)EmulatedCounter.GetValue(null)! > emulatedBefore);
}
finally
{
FreeProbeVisibleCode(code);
}
}
[Fact]
public void InsertqSigillReadsSourceXmmThroughTheBridge()
{
if ((!OperatingSystem.IsLinux() && !OperatingSystem.IsMacOS()) ||
RuntimeInformation.ProcessArchitecture != Architecture.X64)
{
return;
}
// insertq xmm0, xmm1, 0x10, 0x08
var code = AllocateProbeVisibleCode([0xF2, 0x0F, 0x78, 0xC1, 0x10, 0x08]);
try
{
const ulong destination = 0x1111_2222_3333_4444UL;
const ulong source = 0xAAAA_BBBB_CCCC_DDDDUL;
var frame = new FakeSignalFrame((ulong)code);
frame.SetXmmLow(FxsaveXmm0Offset, destination);
frame.SetXmmLow(FxsaveXmm1Offset, source);
Assert.True(frame.Dispatch());
Assert.Equal(
Sse4aBitFieldEmulator.InsertBitField(destination, source, length: 0x10, index: 0x08),
frame.XmmLow(FxsaveXmm0Offset));
Assert.Equal((ulong)code + 6, frame.Rip);
}
finally
{
FreeProbeVisibleCode(code);
}
}
[Fact]
public void ExtractSignalPreservesLivePointerAndOtherVectorRegisters()
{
if ((!OperatingSystem.IsLinux() && !OperatingSystem.IsMacOS()) ||
RuntimeInformation.ProcessArchitecture != Architecture.X64)
return;
var code = AllocateProbeVisibleCode([0x66, 0x0F, 0x78, 0xC1, 0x28, 0x00]);
try
{
var frame = new FakeSignalFrame((ulong)code) { Accumulator = 0x1B86FD090 };
for (int registerIndex = 0; registerIndex < 16; registerIndex++)
{
frame.SetXmmLow(FxsaveXmm0Offset + registerIndex * 16, (ulong)registerIndex);
frame.SetXmmLow(FxsaveXmm0Offset + registerIndex * 16 + 8, ~(ulong)registerIndex);
}
frame.SetXmmLow(FxsaveXmm1Offset, 0xAABB_FF93_00FF_9300);
frame.FillMacOsExtendedVectorState(0xA5);
Assert.True(frame.Dispatch());
Assert.Equal(0x1B86FD090UL, frame.Accumulator);
Assert.Equal(0x0000_0093_00FF_9300UL, frame.XmmLow(FxsaveXmm1Offset));
Assert.Equal(0UL, frame.XmmHigh(FxsaveXmm1Offset));
Assert.Equal((ulong)code + 6, frame.Rip);
for (int registerIndex = 0; registerIndex < 16; registerIndex++)
{
if (registerIndex == 1) continue;
Assert.Equal((ulong)registerIndex, frame.XmmLow(FxsaveXmm0Offset + registerIndex * 16));
Assert.Equal(~(ulong)registerIndex, frame.XmmHigh(FxsaveXmm0Offset + registerIndex * 16));
}
frame.AssertMacOsExtendedVectorStateEquals(0xA5);
}
finally
{
FreeProbeVisibleCode(code);
}
}
[Fact]
public void InsertInstructionPreservesOtherVectorRegisters()
{
if ((!OperatingSystem.IsLinux() && !OperatingSystem.IsMacOS()) ||
RuntimeInformation.ProcessArchitecture != Architecture.X64)
return;
// Use the same register for both INSERTQ operands.
// Keep all other vector register values unchanged.
var code = AllocateProbeVisibleCode([0xF2, 0x0F, 0x78, 0xC0, 0x08, 0x08]);
try
{
var frame = new FakeSignalFrame((ulong)code);
frame.SetXmmLow(FxsaveXmm0Offset, 0x10);
frame.SetXmmLow(FxsaveXmm0Offset + 8, ulong.MaxValue);
for (int registerIndex = 1; registerIndex < 16; registerIndex++)
{
frame.SetXmmLow(FxsaveXmm0Offset + registerIndex * 16, (ulong)registerIndex);
frame.SetXmmLow(FxsaveXmm0Offset + registerIndex * 16 + 8, ~(ulong)registerIndex);
}
frame.FillMacOsExtendedVectorState(0xA5);
Assert.True(frame.Dispatch());
Assert.Equal(0x1010UL, frame.XmmLow(FxsaveXmm0Offset));
Assert.Equal(0UL, frame.XmmHigh(FxsaveXmm0Offset));
Assert.Equal((ulong)code + 6, frame.Rip);
for (int registerIndex = 1; registerIndex < 16; registerIndex++)
{
Assert.Equal((ulong)registerIndex, frame.XmmLow(FxsaveXmm0Offset + registerIndex * 16));
Assert.Equal(~(ulong)registerIndex, frame.XmmHigh(FxsaveXmm0Offset + registerIndex * 16));
}
frame.AssertMacOsExtendedVectorStateEquals(0xA5);
}
finally
{
FreeProbeVisibleCode(code);
}
}
[Fact]
public void RecoveryDeclinesWhenNoXmmStateWasBridged()
{
if ((!OperatingSystem.IsLinux() && !OperatingSystem.IsMacOS()) ||
RuntimeInformation.ProcessArchitecture != Architecture.X64)
{
return;
}
// Clear the register state flag to represent a signal frame without vector registers.
// Recovery must reject this frame.
var code = AllocateProbeVisibleCode([0x66, 0x0F, 0x78, 0xC0, 0x10, 0x08]);
try
{
XmmBridgedFlag.SetValue(null, false);
var backend = RuntimeHelpers.GetUninitializedObject(typeof(DirectExecutionBackend));
var contextRecord = stackalloc byte[0x4D0];
var recovered = (bool)TryRecoverAmdCompat.Invoke(
backend,
[Pointer.Box(contextRecord, typeof(void*)), (ulong)code])!;
Assert.False(recovered);
}
finally
{
FreeProbeVisibleCode(code);
}
}
// Build the signal frame that the host system gives to the signal handler.
// Use the register offsets for Linux or macOS.
private sealed class FakeSignalFrame
{
private readonly byte[] _userContext = new byte[512];
private readonly byte[] _floatingPointState = new byte[512];
private readonly byte[] _machineContext = new byte[1032];
private readonly bool _includeFloatingPointState;
public FakeSignalFrame(ulong instructionPointer, bool includeFloatingPointState = true)
{
_includeFloatingPointState = includeFloatingPointState;
fixed (byte* userContext = _userContext)
fixed (byte* machineContext = _machineContext)
{
if (OperatingSystem.IsMacOS())
*(ulong*)(machineContext + 144) = instructionPointer;
else
*(ulong*)(userContext + LinuxUcontextGregsOffset + LinuxGregsRipOffset) = instructionPointer;
}
}
public ulong Rip
{
get
{
fixed (byte* userContext = _userContext)
fixed (byte* machineContext = _machineContext)
{
return OperatingSystem.IsMacOS()
? *(ulong*)(machineContext + 144)
: *(ulong*)(userContext + LinuxUcontextGregsOffset + LinuxGregsRipOffset);
}
}
}
public ulong Accumulator
{
get
{
fixed (byte* state = OperatingSystem.IsMacOS() ? _machineContext : _userContext)
return *(ulong*)(state + (OperatingSystem.IsMacOS() ? 16 : LinuxUcontextGregsOffset + 13 * 8));
}
set
{
fixed (byte* state = OperatingSystem.IsMacOS() ? _machineContext : _userContext)
*(ulong*)(state + (OperatingSystem.IsMacOS() ? 16 : LinuxUcontextGregsOffset + 13 * 8)) = value;
}
}
public void SetXmmLow(int fxsaveOffset, ulong value)
{
fixed (byte* state = OperatingSystem.IsMacOS() ? _machineContext : _floatingPointState)
{
var offset = OperatingSystem.IsMacOS() ? 352 + fxsaveOffset - FxsaveXmm0Offset : fxsaveOffset;
*(ulong*)(state + offset) = value;
}
}
public ulong XmmLow(int fxsaveOffset)
{
fixed (byte* state = OperatingSystem.IsMacOS() ? _machineContext : _floatingPointState)
{
var offset = OperatingSystem.IsMacOS() ? 352 + fxsaveOffset - FxsaveXmm0Offset : fxsaveOffset;
return *(ulong*)(state + offset);
}
}
public ulong XmmHigh(int fxsaveOffset)
{
return XmmLow(fxsaveOffset + 8);
}
public void FillMacOsExtendedVectorState(byte value) => _machineContext.AsSpan(772, 256).Fill(value);
public void AssertMacOsExtendedVectorStateEquals(byte value)
{
foreach (var actual in _machineContext.AsSpan(772, 256))
Assert.Equal(value, actual);
}
public bool Dispatch()
{
EnsureBridgeBackend();
fixed (byte* userContext = _userContext)
fixed (byte* floatingPointState = _floatingPointState)
fixed (byte* machineContext = _machineContext)
{
if (OperatingSystem.IsMacOS())
{
*(byte**)(userContext + 48) = machineContext;
*(ulong*)(userContext + 40) = _includeFloatingPointState ? (ulong)_machineContext.Length : 184;
}
else if (_includeFloatingPointState)
{
*(byte**)(userContext + LinuxUcontextGregsOffset + LinuxGregsFpstateOffset) = floatingPointState;
}
return (bool)TryHandlePosixFault.Invoke(
null,
[PosixSigIll, (nint)0, (nint)userContext])!;
}
}
}
/// <summary>
/// TryHandlePosixFault only runs the recovery chain when a backend instance is
/// registered. The tests do not need any of the constructor's state (and must not run
/// it: it installs process-wide signal handlers), so register an uninitialized
/// instance - the SIGILL recovery path only touches static state.
/// </summary>
private static void EnsureBridgeBackend()
{
if (PosixSignalBackend.GetValue(null) == null)
{
PosixSignalBackend.SetValue(
null,
RuntimeHelpers.GetUninitializedObject(typeof(DirectExecutionBackend)));
}
}
// Use the host allocator so Linux memory probes can read the instruction bytes.
// macOS reads these bytes through Mach.
private static nint AllocateProbeVisibleCode(ReadOnlySpan<byte> instructions)
{
var size = checked((nuint)Environment.SystemPageSize);
var mapping = (nint)HostMemory.Alloc(
null,
size,
HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE,
HostMemory.PAGE_READWRITE);
Assert.NotEqual((nint)0, mapping);
instructions.CopyTo(new Span<byte>((void*)mapping, checked((int)size)));
return mapping;
}
private static void FreeProbeVisibleCode(nint mapping)
{
Assert.True(HostMemory.Free((void*)mapping, 0, HostMemory.MEM_RELEASE));
}
}
[CollectionDefinition("PosixSignalRecovery", DisableParallelization = true)]
public sealed class PosixSignalRecoveryCollection;