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)); + } +}