From 7caf430aa9bdb1de2bac57f5d4634016933465f5 Mon Sep 17 00:00:00 2001 From: Atson Melo <104263082+AtsonMelo@users.noreply.github.com> Date: Mon, 10 Aug 2026 22:52:49 -0300 Subject: [PATCH] test(cpu): cover Gen5 native return smoke path (#818) --- .../Cpu/Gen5NativeReturnSmokeTests.cs | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 tests/SharpEmu.Libs.Tests/Cpu/Gen5NativeReturnSmokeTests.cs diff --git a/tests/SharpEmu.Libs.Tests/Cpu/Gen5NativeReturnSmokeTests.cs b/tests/SharpEmu.Libs.Tests/Cpu/Gen5NativeReturnSmokeTests.cs new file mode 100644 index 00000000..baf9755e --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Cpu/Gen5NativeReturnSmokeTests.cs @@ -0,0 +1,192 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using System.Diagnostics; +using System.Runtime.InteropServices; +using SharpEmu.Core.Cpu; +using SharpEmu.Core.Cpu.Native; +using SharpEmu.Core.Loader; +using SharpEmu.Core.Memory; +using SharpEmu.HLE; +using Xunit; + +namespace SharpEmu.Libs.Tests.Cpu; + +public sealed class Gen5NativeReturnSmokeTests +{ + private const string WorkerEnvironmentVariable = "SHARPEMU_NATIVE_RETURN_SMOKE_WORKER"; + private static readonly TimeSpan WorkerTimeout = TimeSpan.FromSeconds(30); + + [Fact] + public async Task SyntheticGen5Entry_ReturnsToHost() + { + if (!IsSupportedHost) + { + return; + } + + if (string.Equals( + Environment.GetEnvironmentVariable(WorkerEnvironmentVariable), + "1", + StringComparison.Ordinal)) + { + ExecuteSyntheticGuest(); + return; + } + + var result = await RunIsolatedWorker(); + + Assert.True( + result.Completed, + $"native return worker did not exit within {WorkerTimeout.TotalSeconds:F0} seconds\n{result.Output}"); + Assert.True( + result.ExitCode == 0, + $"native return worker exited with code {result.ExitCode}\n{result.Output}"); + } + + private static bool IsSupportedHost => + RuntimeInformation.ProcessArchitecture == Architecture.X64 && + (OperatingSystem.IsWindows() || OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()); + + private static void ExecuteSyntheticGuest() + { + using var memory = new PhysicalVirtualMemory(); + var image = new SelfLoader().Load(BuildSyntheticElf(), memory); + Assert.Equal((byte)2, image.ElfHeader.AbiVersion); + Assert.Equal(0x0000_0008_0000_1000UL, image.EntryPoint); + + var moduleManager = new ModuleManager(); + moduleManager.Freeze(); + + var backend = new DirectExecutionBackend(moduleManager); + using var dispatcher = new CpuDispatcher(memory, moduleManager, backend); + var result = dispatcher.DispatchEntry( + image.EntryPoint, + Generation.Gen5, + image.ImportStubs, + image.RuntimeSymbols, + "synthetic-native-return", + new CpuExecutionOptions + { + CpuEngine = CpuExecutionEngine.NativeOnly, + EnableDisasmDiagnostics = false, + StrictDynlibResolution = true, + ImportTraceLimit = 0, + DebugHook = null + }); + + Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, result); + Assert.Equal(OrbisGen2Result.ORBIS_GEN2_OK, dispatcher.LastSessionSummary.Result); + Assert.Equal(CpuExitReason.ReturnedToHost, dispatcher.LastSessionSummary.Reason); + Assert.Equal(0, dispatcher.LastSessionSummary.ImportsHit); + Assert.Equal(0, dispatcher.LastSessionSummary.UniqueNidsHit); + Assert.Null(dispatcher.LastTrapInfo); + Assert.Null(dispatcher.LastMemoryFaultInfo); + Assert.Null(dispatcher.LastNotImplementedInfo); + } + + private static async Task RunIsolatedWorker() + { + var startInfo = new ProcessStartInfo + { + FileName = ResolveDotnetHost(), + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + startInfo.ArgumentList.Add("test"); + startInfo.ArgumentList.Add(typeof(Gen5NativeReturnSmokeTests).Assembly.Location); + startInfo.ArgumentList.Add("--filter"); + startInfo.ArgumentList.Add( + $"FullyQualifiedName={typeof(Gen5NativeReturnSmokeTests).FullName}.{nameof(SyntheticGen5Entry_ReturnsToHost)}"); + startInfo.Environment[WorkerEnvironmentVariable] = "1"; + startInfo.Environment["SHARPEMU_SENTINEL_PROBE"] = null; + + using var process = Process.Start(startInfo) ?? + throw new InvalidOperationException("Could not start the isolated native return worker."); + var stdout = process.StandardOutput.ReadToEndAsync(); + var stderr = process.StandardError.ReadToEndAsync(); + + try + { + await process.WaitForExitAsync().WaitAsync(WorkerTimeout); + } + catch (TimeoutException) + { + process.Kill(entireProcessTree: true); + await process.WaitForExitAsync(); + return new WorkerResult(false, process.ExitCode, await ReadOutput(stdout, stderr)); + } + + return new WorkerResult(true, process.ExitCode, await ReadOutput(stdout, stderr)); + } + + private static string ResolveDotnetHost() + { + var configuredHost = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); + if (!string.IsNullOrWhiteSpace(configuredHost)) + { + return configuredHost; + } + + var processPath = Environment.ProcessPath; + if (processPath is not null && + string.Equals( + Path.GetFileNameWithoutExtension(processPath), + "dotnet", + StringComparison.OrdinalIgnoreCase)) + { + return processPath; + } + + return "dotnet"; + } + + private static async Task ReadOutput(Task stdout, Task stderr) => + await stdout + await stderr; + + private static byte[] BuildSyntheticElf() + { + const int elfHeaderSize = 0x40; + const int programHeaderSize = 0x38; + const int fileOffset = 0x1000; + const ulong entryPoint = 0x1000; + ReadOnlySpan payload = [0x31, 0xC0, 0xC3]; // xor eax, eax; ret + var image = new byte[fileOffset + payload.Length]; + + image[0] = 0x7F; + image[1] = (byte)'E'; + image[2] = (byte)'L'; + image[3] = (byte)'F'; + image[4] = 2; + image[5] = 1; + image[6] = 1; + image[7] = 9; + image[8] = 2; + BinaryPrimitives.WriteUInt16LittleEndian(image.AsSpan(0x10), 3); + BinaryPrimitives.WriteUInt16LittleEndian(image.AsSpan(0x12), 0x3E); + BinaryPrimitives.WriteUInt32LittleEndian(image.AsSpan(0x14), 1); + BinaryPrimitives.WriteUInt64LittleEndian(image.AsSpan(0x18), entryPoint); + BinaryPrimitives.WriteUInt64LittleEndian(image.AsSpan(0x20), elfHeaderSize); + BinaryPrimitives.WriteUInt16LittleEndian(image.AsSpan(0x34), elfHeaderSize); + BinaryPrimitives.WriteUInt16LittleEndian(image.AsSpan(0x36), programHeaderSize); + BinaryPrimitives.WriteUInt16LittleEndian(image.AsSpan(0x38), 1); + + var programHeader = image.AsSpan(elfHeaderSize, programHeaderSize); + BinaryPrimitives.WriteUInt32LittleEndian(programHeader, 1); + BinaryPrimitives.WriteUInt32LittleEndian(programHeader[0x04..], 5); + BinaryPrimitives.WriteUInt64LittleEndian(programHeader[0x08..], fileOffset); + BinaryPrimitives.WriteUInt64LittleEndian(programHeader[0x10..], entryPoint); + BinaryPrimitives.WriteUInt64LittleEndian(programHeader[0x18..], entryPoint); + BinaryPrimitives.WriteUInt64LittleEndian(programHeader[0x20..], (ulong)payload.Length); + BinaryPrimitives.WriteUInt64LittleEndian(programHeader[0x28..], (ulong)payload.Length); + BinaryPrimitives.WriteUInt64LittleEndian(programHeader[0x30..], 0x1000); + payload.CopyTo(image.AsSpan(fileOffset)); + + return image; + } + + private sealed record WorkerResult(bool Completed, int ExitCode, string Output); +}