diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Amd64Compat.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Amd64Compat.cs
index 213c76b2..01cbd092 100644
--- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Amd64Compat.cs
+++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.Amd64Compat.cs
@@ -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;
}
diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.PosixSignals.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.PosixSignals.cs
index a91b3f19..5d02e97d 100644
--- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.PosixSignals.cs
+++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.PosixSignals.cs
@@ -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;
}
diff --git a/tests/SharpEmu.Libs.Tests/Cpu/Sse4aPosixSignalRecoveryTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/Sse4aPosixSignalRecoveryTests.cs
new file mode 100644
index 00000000..2bbec8dd
--- /dev/null
+++ b/tests/SharpEmu.Libs.Tests/Cpu/Sse4aPosixSignalRecoveryTests.cs
@@ -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;
+
+///
+/// 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.
+///
+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);
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ 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])!;
+ }
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ private static void EnsureBridgeBackend()
+ {
+ if (PosixSignalBackend.GetValue(null) == null)
+ {
+ PosixSignalBackend.SetValue(
+ null,
+ RuntimeHelpers.GetUninitializedObject(typeof(DirectExecutionBackend)));
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ private static nint AllocateProbeVisibleCode(ReadOnlySpan 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((void*)mapping, checked((int)size)));
+ return mapping;
+ }
+
+ private static void FreeProbeVisibleCode(nint mapping)
+ {
+ Assert.True(HostMemory.Free((void*)mapping, 0, HostMemory.MEM_RELEASE));
+ }
+}