From 6f1075771f4a5fcf0af150336a0c818033f0dda1 Mon Sep 17 00:00:00 2001 From: kuba Date: Wed, 30 Sep 2026 02:45:09 +0200 Subject: [PATCH] [Cpu] Keep fiber switches from writing stack bounds into the macOS TSD The guest context-transfer stub that fiber switches go through keeps the Windows TEB stack bounds in sync by storing the target stack top and bottom to gs:[8] and gs:[16]. Unlike the guest-entry and exception stubs, it did so on every host. On macOS gs addresses the pthread TSD, where those slots hold errno's address and another key: a fiber switch wrote its stack bounds over them, and the next failed libc call on the thread (a timed-out condition wait) crashed reading errno through the pointer. Demon's Souls died this way within 10-50 s of boot; a hardware watchpoint on the thread's errno slot stopped in this stub. The fiber bounds sync now lives next to the other TEB-bound helpers and, like them, emits nothing off Windows. --- .../DirectExecutionBackend.StackBounds.cs | 30 +++++++++++++++++++ .../Cpu/Native/DirectExecutionBackend.cs | 7 +---- .../Cpu/FiberStackBoundsTests.cs | 30 +++++++++++++++++++ 3 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Cpu/FiberStackBoundsTests.cs diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.StackBounds.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.StackBounds.cs index 7138c88..b6ec6e9 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.StackBounds.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.StackBounds.cs @@ -33,6 +33,36 @@ public sealed partial class DirectExecutionBackend EmitUInt32(code, ref offset, field); } + // Fiber switches replace the guest stack without passing through a guest-entry stub, so the + // context-transfer stub keeps the Windows TEB bounds in sync from the target state in R11 + // (stack top at +160, bottom at +168). Only Windows keeps stack bounds at gs:[8]/gs:[16]: on + // macOS gs addresses the pthread TSD, where those slots hold errno's address and another key. + // Writing a fiber's bounds there nulled errno, and the next failed libc call on the thread + // crashed reading it (Demon's Souls, about 10 s into boot). + private static unsafe void EmitFiberStackBounds(byte* code, ref int offset) + { + if (!OperatingSystem.IsWindows()) return; + ReadOnlySpan loadTop = [0x4D, 0x8B, 0x93, 160, 0, 0, 0]; // mov r10, [r11+160] + foreach (var value in loadTop) EmitByte(code, ref offset, value); + EmitStackBound(code, ref offset, 10, 8, store: true); + ReadOnlySpan loadBottom = [0x4D, 0x8B, 0x93, 168, 0, 0, 0]; // mov r10, [r11+168] + foreach (var value in loadBottom) EmitByte(code, ref offset, value); + EmitStackBound(code, ref offset, 10, 16, store: true); + } + + // Test hook: the bytes the fiber stack-bounds sync emits on this host. + internal static unsafe byte[] EmitFiberStackBoundsForTest() + { + var buffer = new byte[64]; + var offset = 0; + fixed (byte* code = buffer) + { + EmitFiberStackBounds(code, ref offset); + } + + return buffer[..offset]; + } + // R10 points to the entry state. R11 is scratch; RAX remains unchanged. private static unsafe void EmitHostStackBounds(byte* code, ref int offset, bool save) { diff --git a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs index 3af1142..9c154a0 100644 --- a/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs +++ b/src/SharpEmu.Core/Cpu/Native/DirectExecutionBackend.cs @@ -2261,12 +2261,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I } Emit(0x49); Emit(0x89); Emit(0xC3); // mov r11, rax - // Fiber switches replace the guest stack without passing through a - // guest-entry stub, so keep the Windows TEB bounds in sync here. - EmitLoadFromR11Disp32(10, 160); // r10 = target stack top - EmitStackBound(code, ref offset, 10, 8, store: true); - EmitLoadFromR11Disp32(10, 168); // r10 = target stack bottom - EmitStackBound(code, ref offset, 10, 16, store: true); + EmitFiberStackBounds(code, ref offset); // A new >=3.50 fiber receives the SDK-defined MXCSR verbatim. A // resumed fiber follows _sceFiberLongJmp: preserve status bits 0-5 // while restoring the saved control bits. diff --git a/tests/SharpEmu.Libs.Tests/Cpu/FiberStackBoundsTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/FiberStackBoundsTests.cs new file mode 100644 index 0000000..214233d --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Cpu/FiberStackBoundsTests.cs @@ -0,0 +1,30 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Core.Cpu.Native; +using Xunit; + +namespace SharpEmu.Libs.Tests.Cpu; + +public sealed class FiberStackBoundsTests +{ + [Fact] + public void FiberSwitchesTouchGsOnlyWhereItHoldsTheTebStackBounds() + { + var code = DirectExecutionBackend.EmitFiberStackBoundsForTest(); + + if (!OperatingSystem.IsWindows()) + { + // On macOS gs addresses the pthread TSD: gs:[8] is errno's address. A fiber switch + // must not store its stack bounds there. + Assert.Empty(code); + return; + } + + // Windows: both TEB bounds (gs:[8] StackBase, gs:[16] StackLimit) are stored from r10. + byte[] storeBase = [0x65, 0x4C, 0x89, 0x14, 0x25, 8, 0, 0, 0]; + byte[] storeLimit = [0x65, 0x4C, 0x89, 0x14, 0x25, 16, 0, 0, 0]; + Assert.True(code.AsSpan().IndexOf(storeBase) >= 0); + Assert.True(code.AsSpan().IndexOf(storeLimit) >= 0); + } +}