mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
cpu: recover SSE4a EXTRQ/INSERTQ faults on Linux (#482)
The fault-time SSE4a fallback was Windows-only because the POSIX signal bridge never carried XMM state: the CONTEXT scratch buffer only held the 17 general-purpose registers, so emulating EXTRQ/INSERTQ there would have computed results from zeroed bytes and discarded the write. Bridge the XMM registers on Linux by copying them between the mcontext's FXSAVE image (kernel sigcontext ABI, libc-independent) and the CONTEXT FltSave slots on capture and write-back, and gate the recovery on that bridge instead of on Windows. Darwin still declines: its XMM area remains unbridged. With this, guest EXTRQ/INSERTQ on Linux hosts without SSE4a (any Intel CPU) resumes with correct register state instead of dying on an unrecovered SIGILL (#328).
This commit is contained in:
@@ -51,12 +51,13 @@ public sealed partial class DirectExecutionBackend
|
||||
// round-trips through the real ucontext, so it works on every supported OS. EXTRQ/
|
||||
// INSERTQ additionally read and write an XMM register: on Windows contextRecord is the
|
||||
// live CONTEXT the OS resumes the thread from, so touching the Xmm0.. slots is visible
|
||||
// to the guest, but on POSIX contextRecord is a CONTEXT-shaped scratch buffer that the
|
||||
// bridge only populates with the 17 general-purpose registers - the XMM region is never
|
||||
// read from or written back to the real mcontext/ucontext. Running this on POSIX would
|
||||
// silently compute a result from stale/zeroed XMM bytes and then discard whatever it
|
||||
// "wrote", so keep it Windows-only, matching Kyty's own scope for the identical fix.
|
||||
return OperatingSystem.IsWindows() && TryRecoverSse4aExtractInsert(contextRecord, rip);
|
||||
// to the guest, and on Linux the bridge copies the mcontext's FXSAVE image into the
|
||||
// Xmm0.. slots and writes them back through sigreturn (_posixXmmContextBridged). On
|
||||
// Darwin the XMM area is still a zeroed scratch buffer - running this there would
|
||||
// silently compute a result from stale bytes and then discard whatever it "wrote", so
|
||||
// the recovery declines until that bridge exists.
|
||||
return (OperatingSystem.IsWindows() || _posixXmmContextBridged) &&
|
||||
TryRecoverSse4aExtractInsert(contextRecord, rip);
|
||||
}
|
||||
|
||||
private unsafe bool TryRecoverMonitorxMwaitx(void* contextRecord, ulong rip)
|
||||
@@ -97,7 +98,8 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
private unsafe bool TryRecoverSse4aExtractInsert(void* contextRecord, ulong rip)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows() || !TryReadFaultingInstruction(rip, out var instruction))
|
||||
if (!OperatingSystem.IsWindows() && !_posixXmmContextBridged ||
|
||||
!TryReadFaultingInstruction(rip, out var instruction))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,19 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
private const int LinuxUcontextGregsOffset = 40;
|
||||
private const int LinuxGregsErrOffset = 19 * 8;
|
||||
|
||||
// The kernel's x86-64 sigcontext places the FXSAVE-image pointer right
|
||||
// after the general registers it hands to the handler: err(152)
|
||||
// trapno(160) oldmask(168) cr2(176) fpstate(184), all relative to
|
||||
// GetPosixRegisterBase. glibc and musl both overlay this kernel layout
|
||||
// verbatim (glibc's mcontext_t.fpregs is the same slot), so the offset
|
||||
// is libc-independent. Inside the FXSAVE image the XMM registers start
|
||||
// at +160 (32-byte header + 8 legacy x87/MMX slots x 16 bytes) - the
|
||||
// same relative position they occupy in the Win64 CONTEXT's FltSave
|
||||
// area (Win64ContextXmm0Offset = 256 + 160).
|
||||
private const int LinuxGregsFpstateOffset = 184;
|
||||
private const int FxsaveXmmOffset = 160;
|
||||
private const int XmmBlockSize = 16 * 16;
|
||||
|
||||
// Byte offsets of the general registers relative to GetPosixRegisterBase,
|
||||
// ordered to match the contiguous Win64 CONTEXT block CTX_RAX..CTX_RIP
|
||||
// (rax, rcx, rdx, rbx, rsp, rbp, rsi, rdi, r8..r15, rip). Verified
|
||||
@@ -71,6 +84,15 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
[ThreadStatic]
|
||||
private static int _posixSignalHandlerDepth;
|
||||
|
||||
// True while the current thread's in-flight POSIX fault carries the real
|
||||
// XMM registers in the CONTEXT scratch buffer and writes to them will
|
||||
// reach the mcontext on resume. Gates recovery paths (SSE4a EXTRQ/
|
||||
// INSERTQ) that would otherwise compute results from a zeroed XMM area
|
||||
// and silently discard what they "wrote". Darwin is not bridged yet, so
|
||||
// the flag stays false there.
|
||||
[ThreadStatic]
|
||||
private static bool _posixXmmContextBridged;
|
||||
|
||||
private void SetupPosixExceptionHandler()
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_POSIX_SIGNALS"), "1", StringComparison.Ordinal))
|
||||
@@ -252,6 +274,26 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
WriteCtxU64(contextRecord, CTX_RAX + i * 8, *(ulong*)(registers + offsets[i]));
|
||||
}
|
||||
|
||||
// Bridge the XMM registers alongside the GPRs where the layout is
|
||||
// known: on Linux the fpstate pointer and FXSAVE image are kernel
|
||||
// ABI, so recovery paths that read or write XMM state (SSE4a
|
||||
// EXTRQ/INSERTQ) see the live registers and their writes reach the
|
||||
// guest through sigreturn.
|
||||
byte* fpstate = null;
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
fpstate = *(byte**)(registers + LinuxGregsFpstateOffset);
|
||||
if (fpstate != null)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
fpstate + FxsaveXmmOffset,
|
||||
contextRecord + Win64ContextXmm0Offset,
|
||||
XmmBlockSize,
|
||||
XmmBlockSize);
|
||||
}
|
||||
}
|
||||
_posixXmmContextBridged = fpstate != null;
|
||||
|
||||
EXCEPTION_RECORD record = default;
|
||||
record.ExceptionAddress = (void*)ReadCtxU64(contextRecord, CTX_RIP);
|
||||
if (signal == PosixSigIll)
|
||||
@@ -317,6 +359,14 @@ public sealed unsafe partial class DirectExecutionBackend
|
||||
{
|
||||
*(ulong*)(registers + offsets[i]) = ReadCtxU64(contextRecord, CTX_RAX + i * 8);
|
||||
}
|
||||
if (fpstate != null)
|
||||
{
|
||||
Buffer.MemoryCopy(
|
||||
contextRecord + Win64ContextXmm0Offset,
|
||||
fpstate + FxsaveXmmOffset,
|
||||
XmmBlockSize,
|
||||
XmmBlockSize);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
// 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;
|
||||
|
||||
/// <summary>
|
||||
/// Coverage for the SSE4a EXTRQ/INSERTQ fault recovery through the POSIX signal bridge on
|
||||
/// Linux. Each test fabricates the exact frame the kernel hands the SIGILL handler - gregs
|
||||
/// whose RIP points at a real EXTRQ/INSERTQ encoding in probe-visible host memory, plus an
|
||||
/// FXSAVE image carrying the XMM registers - and drives the production entry point
|
||||
/// (TryHandlePosixFault) over it. The bridge must capture the XMM state into the CONTEXT
|
||||
/// scratch buffer, the recovery must decode and emulate the instruction, and the write-back
|
||||
/// must land the result in the FXSAVE image and advance RIP, because that is precisely what
|
||||
/// sigreturn restores on a live fault.
|
||||
/// </summary>
|
||||
public sealed unsafe class Sse4aPosixSignalRecoveryTests
|
||||
{
|
||||
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)!;
|
||||
|
||||
[Fact]
|
||||
public void ExtrqSigillRoundTripsXmmThroughTheBridge()
|
||||
{
|
||||
if (!OperatingSystem.IsLinux() ||
|
||||
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() ||
|
||||
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 RecoveryDeclinesWhenNoXmmStateWasBridged()
|
||||
{
|
||||
if (!OperatingSystem.IsLinux() ||
|
||||
RuntimeInformation.ProcessArchitecture != Architecture.X64)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// extrq xmm0, 0x10, 0x08 - valid and recoverable, but without bridged XMM state
|
||||
// (fpstate missing from the frame) the recovery must decline rather than emulate
|
||||
// over the zeroed scratch bytes. Drive the recovery entry directly: earlier tests
|
||||
// on this thread leave the thread-static bridge flag set, so clear it the way a
|
||||
// fpstate-less capture would.
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Linux x86-64 signal frame as TryHandlePosixFault consumes it: a ucontext whose
|
||||
/// mcontext gregs sit at +40 (kernel sigcontext layout) with the fpstate pointer at
|
||||
/// gregs+184 aiming at a 512-byte FXSAVE image.
|
||||
/// </summary>
|
||||
private sealed class FakeSignalFrame
|
||||
{
|
||||
private readonly byte[] _ucontext = new byte[512];
|
||||
private readonly byte[] _fpstate = new byte[512];
|
||||
private readonly bool _wireFpstate;
|
||||
|
||||
public FakeSignalFrame(ulong rip, bool wireFpstate = true)
|
||||
{
|
||||
_wireFpstate = wireFpstate;
|
||||
fixed (byte* ucontext = _ucontext)
|
||||
{
|
||||
*(ulong*)(ucontext + LinuxUcontextGregsOffset + LinuxGregsRipOffset) = rip;
|
||||
}
|
||||
}
|
||||
|
||||
public ulong Rip
|
||||
{
|
||||
get
|
||||
{
|
||||
fixed (byte* ucontext = _ucontext)
|
||||
{
|
||||
return *(ulong*)(ucontext + LinuxUcontextGregsOffset + LinuxGregsRipOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetXmmLow(int fxsaveOffset, ulong value)
|
||||
{
|
||||
fixed (byte* fpstate = _fpstate)
|
||||
{
|
||||
*(ulong*)(fpstate + fxsaveOffset) = value;
|
||||
}
|
||||
}
|
||||
|
||||
public ulong XmmLow(int fxsaveOffset)
|
||||
{
|
||||
fixed (byte* fpstate = _fpstate)
|
||||
{
|
||||
return *(ulong*)(fpstate + fxsaveOffset);
|
||||
}
|
||||
}
|
||||
|
||||
public ulong XmmHigh(int fxsaveOffset)
|
||||
{
|
||||
fixed (byte* fpstate = _fpstate)
|
||||
{
|
||||
return *(ulong*)(fpstate + fxsaveOffset + 8);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Dispatch()
|
||||
{
|
||||
EnsureBridgeBackend();
|
||||
fixed (byte* ucontext = _ucontext)
|
||||
fixed (byte* fpstate = _fpstate)
|
||||
{
|
||||
if (_wireFpstate)
|
||||
{
|
||||
*(byte**)(ucontext + LinuxUcontextGregsOffset + LinuxGregsFpstateOffset) = fpstate;
|
||||
}
|
||||
|
||||
return (bool)TryHandlePosixFault.Invoke(
|
||||
null,
|
||||
[PosixSigIll, (nint)0, (nint)ucontext])!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The instruction bytes must live in memory the fault-time page probe
|
||||
/// (TryReadHostBytes -> VirtualQuery) can see; on POSIX that is HostMemory's shadow
|
||||
/// region table, the same allocator guest code pages come from. A raw libc mmap or a
|
||||
/// pinned managed array would be invisible and the recovery would decline before
|
||||
/// decoding.
|
||||
/// </summary>
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user