From 6dda6589d0afc8fa37fb7c2a825c48b39cc0e916 Mon Sep 17 00:00:00 2001 From: kostyaff Date: Sat, 18 Jul 2026 20:19:55 +0300 Subject: [PATCH] test: add Kernel/Loader unit tests (22 tests) (#373) - SelfLoader: reject unknown magic, truncated headers; parse PS5 SELF embedded ELF - KernelMemory: MapNamedFlexibleMemory/mprotect/munmap argument validation - KernelEventQueue: create/delete/add/trigger/wait lifecycle Co-authored-by: OMP --- .../KernelEventQueueCompatExportsTests.cs | 180 ++++++++++++++++++ .../Kernel/KernelMemoryCompatExportsTests.cs | 145 ++++++++++++++ .../Loader/SelfLoaderTests.cs | 79 ++++++++ 3 files changed, 404 insertions(+) create mode 100644 tests/SharpEmu.Libs.Tests/Kernel/KernelEventQueueCompatExportsTests.cs diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelEventQueueCompatExportsTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelEventQueueCompatExportsTests.cs new file mode 100644 index 00000000..90ed32a3 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelEventQueueCompatExportsTests.cs @@ -0,0 +1,180 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.Kernel; +using Xunit; + +namespace SharpEmu.Libs.Tests.Kernel; + +public sealed class KernelEventQueueCompatExportsTests +{ + private const ulong MemoryBase = 0x1_0000_0000; + private const int MemorySize = 0x4000; + + [Fact] + public void CreateEqueue_WritesNonZeroHandleAndSucceeds() + { + var (context, outAddress) = NewContextWithOutSlot(); + context[CpuRegister.Rdi] = outAddress; + + var result = KernelEventQueueCompatExports.KernelCreateEqueue(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result); + Assert.True(context.TryReadUInt64(outAddress, out var handle)); + Assert.NotEqual(0UL, handle); + Assert.True(KernelEventQueueCompatExports.IsValidEqueue(handle)); + } + + [Fact] + public void CreateEqueue_NullOutAddressReturnsInvalidArgument() + { + var (context, _) = NewContextWithOutSlot(); + context[CpuRegister.Rdi] = 0; + + var result = KernelEventQueueCompatExports.KernelCreateEqueue(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, result); + } + + [Fact] + public void DeleteEqueue_RemovesQueueFromRegistry() + { + var (context, outAddress) = NewContextWithOutSlot(); + context[CpuRegister.Rdi] = outAddress; + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, KernelEventQueueCompatExports.KernelCreateEqueue(context)); + Assert.True(context.TryReadUInt64(outAddress, out var handle)); + + context[CpuRegister.Rdi] = handle; + var result = KernelEventQueueCompatExports.KernelDeleteEqueue(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result); + Assert.False(KernelEventQueueCompatExports.IsValidEqueue(handle)); + } + + [Fact] + public void AddUserEvent_OnUnknownQueueReturnsNotFound() + { + var (context, _) = NewContextWithOutSlot(); + const ulong unknownHandle = 0xDEAD_BEEF; + context[CpuRegister.Rdi] = unknownHandle; + context[CpuRegister.Rsi] = 42; + + var result = KernelEventQueueCompatExports.KernelAddUserEvent(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, result); + } + + [Fact] + public void AddUserEvent_OnValidQueueSucceeds() + { + var handle = CreateEqueue(); + var (context, _) = NewContextWithOutSlot(); + context[CpuRegister.Rdi] = handle; + context[CpuRegister.Rsi] = 0x1234; + + var result = KernelEventQueueCompatExports.KernelAddUserEvent(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result); + } + + [Fact] + public void TriggerUserEvent_OnUnknownQueueReturnsNotFound() + { + var (context, _) = NewContextWithOutSlot(); + context[CpuRegister.Rdi] = 0xDEAD_BEEF; + context[CpuRegister.Rsi] = 1; + context[CpuRegister.Rdx] = 0; + + var result = KernelEventQueueCompatExports.KernelTriggerUserEvent(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, result); + } + + [Fact] + public void TriggerUserEvent_OnUnregisteredEventReturnsNotFound() + { + var handle = CreateEqueue(); + var (context, _) = NewContextWithOutSlot(); + context[CpuRegister.Rdi] = handle; + context[CpuRegister.Rsi] = 0xABCD; // never registered + context[CpuRegister.Rdx] = 0; + + var result = KernelEventQueueCompatExports.KernelTriggerUserEvent(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, result); + } + + // Full lifecycle: create -> register user event -> trigger -> wait delivers + // the queued event with the registered ident/filter and the trigger data. + // DequeueEvents runs before the blocking path, so a pre-triggered queue + // returns immediately without touching the guest thread scheduler. + [Fact] + public void CreateAddTriggerWait_DeliversTriggeredUserEvent() + { + const ulong eventIdent = 0x4242; + const ulong triggerData = 0x55AA_55AA; + var handle = CreateEqueue(); + + // Register the user event on the queue. + var (addCtx, _) = NewContextWithOutSlot(); + addCtx[CpuRegister.Rdi] = handle; + addCtx[CpuRegister.Rsi] = eventIdent; + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelAddUserEvent(addCtx)); + + // Trigger it with a distinct data payload. + var (triggerCtx, _) = NewContextWithOutSlot(); + triggerCtx[CpuRegister.Rdi] = handle; + triggerCtx[CpuRegister.Rsi] = eventIdent; + triggerCtx[CpuRegister.Rdx] = triggerData; + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelTriggerUserEvent(triggerCtx)); + + // Wait should deliver the single pending event without blocking. + var memory = new FakeCpuMemory(MemoryBase, MemorySize); + var waitContext = new CpuContext(memory, Generation.Gen5); + const ulong eventsAddress = MemoryBase + 0x100; + const ulong outCountAddress = MemoryBase + 0x300; + waitContext[CpuRegister.Rdi] = handle; + waitContext[CpuRegister.Rsi] = eventsAddress; + waitContext[CpuRegister.Rdx] = 1; // capacity + waitContext[CpuRegister.Rcx] = outCountAddress; + waitContext[CpuRegister.R8] = 0; // no timeout -> would block, but event is pending + + var result = KernelEventQueueCompatExports.KernelWaitEqueue(waitContext); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result); + Assert.True(waitContext.TryReadUInt32(outCountAddress, out var delivered)); + Assert.Equal(1u, delivered); + + // KernelEvent layout (0x20): ident(0x00) filter(0x08) flags(0x0A) + // fflags(0x0C) data(0x10) userdata(0x18). + Span evt = stackalloc byte[0x20]; + Assert.True(memory.TryRead(eventsAddress, evt)); + Assert.Equal(eventIdent, BinaryPrimitives.ReadUInt64LittleEndian(evt[0x00..])); + Assert.Equal(KernelEventQueueCompatExports.KernelEventFilterUser, + BinaryPrimitives.ReadInt16LittleEndian(evt[0x08..])); + Assert.Equal(triggerData, BinaryPrimitives.ReadUInt64LittleEndian(evt[0x10..])); + } + + private static ulong CreateEqueue() + { + var memory = new FakeCpuMemory(MemoryBase, MemorySize); + var context = new CpuContext(memory, Generation.Gen5); + const ulong outAddress = MemoryBase + 0x10; + context[CpuRegister.Rdi] = outAddress; + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, + KernelEventQueueCompatExports.KernelCreateEqueue(context)); + Assert.True(context.TryReadUInt64(outAddress, out var handle)); + return handle; + } + + private static (CpuContext Context, ulong OutAddress) NewContextWithOutSlot() + { + var memory = new FakeCpuMemory(MemoryBase, MemorySize); + var context = new CpuContext(memory, Generation.Gen5); + return (context, MemoryBase + 0x10); + } +} diff --git a/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs b/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs index d2d7510b..c7a50581 100644 --- a/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTests.cs @@ -164,4 +164,149 @@ public sealed class KernelMemoryCompatExportsTests Assert.Equal(0, KernelMemoryCompatExports.KernelReleaseDirectMemory(context)); } + + [Fact] + public void MapNamedFlexibleMemory_NullInOutPointerReturnsInvalidArgument() + { + var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = 0; + context[CpuRegister.Rsi] = 0x1000; + context[CpuRegister.Rdx] = 0x03; // CPU read|write + context[CpuRegister.Rcx] = 0; + + var result = KernelMemoryCompatExports.KernelMapNamedFlexibleMemory(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, result); + } + + [Fact] + public void MapNamedFlexibleMemory_ZeroLengthReturnsInvalidArgument() + { + const ulong memoryBase = 0x1_0000_0000; + const ulong inOutAddress = memoryBase + 0x100; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + memory.TryWrite(inOutAddress, BitConverter.GetBytes(0UL)); + context[CpuRegister.Rdi] = inOutAddress; + context[CpuRegister.Rsi] = 0; + context[CpuRegister.Rdx] = 0x03; + context[CpuRegister.Rcx] = 0; + + var result = KernelMemoryCompatExports.KernelMapNamedFlexibleMemory(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, result); + } + + [Fact] + public void MapNamedFlexibleMemory_UnreadableInOutPointerReturnsMemoryFault() + { + // The in-out pointer points outside the FakeCpuMemory backing store, so + // the first TryReadUInt64 must fail before any reservation is attempted. + const ulong memoryBase = 0x1_0000_0000; + const ulong unreachableInOut = memoryBase + 0x10_0000; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = unreachableInOut; + context[CpuRegister.Rsi] = 0x1000; + context[CpuRegister.Rdx] = 0x03; + context[CpuRegister.Rcx] = 0; + + var result = KernelMemoryCompatExports.KernelMapNamedFlexibleMemory(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, result); + } + + [Fact] + public void Mprotect_ZeroAddressReturnsInvalidArgument() + { + var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = 0; + context[CpuRegister.Rsi] = 0x4000; + context[CpuRegister.Rdx] = 0x03; + + var result = KernelMemoryCompatExports.KernelMprotect(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, result); + } + + [Fact] + public void Mprotect_ZeroLengthReturnsInvalidArgument() + { + const ulong memoryBase = 0x1_0000_0000; + var memory = new FakeCpuMemory(memoryBase, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = memoryBase; + context[CpuRegister.Rsi] = 0; + context[CpuRegister.Rdx] = 0x03; + + var result = KernelMemoryCompatExports.KernelMprotect(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, result); + } + + [Fact] + public void Mprotect_UnmappedRangeReturnsNotFound() + { + // A plausible guest address that FakeCpuMemory does not back and that + // has no host reservation. TryProtectHostRange calls VirtualProtect, + // which fails on an unmapped range, yielding NOT_FOUND rather than + // mutating protection or throwing. + const ulong unmappedAddress = 0x2_0000_0000; + var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = unmappedAddress; + context[CpuRegister.Rsi] = 0x4000; + context[CpuRegister.Rdx] = 0x03; + + var result = KernelMemoryCompatExports.KernelMprotect(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, result); + } + + [Fact] + public void Munmap_ZeroAddressReturnsInvalidArgument() + { + var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = 0; + context[CpuRegister.Rsi] = 0x4000; + + var result = KernelMemoryCompatExports.KernelMunmap(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, result); + } + + [Fact] + public void Munmap_OverflowRangeReturnsInvalidArgument() + { + // address + length would overflow; KernelMunmap guards this explicitly + // before touching any region accounting. + var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = ulong.MaxValue - 0x10; + context[CpuRegister.Rsi] = 0x20; + + var result = KernelMemoryCompatExports.KernelMunmap(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, result); + } + + [Fact] + public void Munmap_UnmappedRangeReturnsNotFound() + { + // No flexible region is registered at this address and FakeCpuMemory + // does not back it, so both physicallyBacked and removedRegions are + // empty and the export reports NOT_FOUND. + const ulong unmappedAddress = 0x2_0000_0000; + var memory = new FakeCpuMemory(0x1_0000_0000, 0x1000); + var context = new CpuContext(memory, Generation.Gen5); + context[CpuRegister.Rdi] = unmappedAddress; + context[CpuRegister.Rsi] = 0x4000; + + var result = KernelMemoryCompatExports.KernelMunmap(context); + + Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, result); + } } diff --git a/tests/SharpEmu.Libs.Tests/Loader/SelfLoaderTests.cs b/tests/SharpEmu.Libs.Tests/Loader/SelfLoaderTests.cs index e3d95806..dec178b7 100644 --- a/tests/SharpEmu.Libs.Tests/Loader/SelfLoaderTests.cs +++ b/tests/SharpEmu.Libs.Tests/Loader/SelfLoaderTests.cs @@ -48,6 +48,85 @@ public sealed class SelfLoaderTests new SelfLoader().Load(imageData, new VirtualMemory())); } + [Theory] + [InlineData(0xDEADBEEF)] + [InlineData(0x7F454C47)] // bare ELF magic read big-endian as a "SELF" candidate is not a SELF + public void Load_RejectsUnrecognizedLeadingMagic(uint magic) + { + var imageData = new byte[SelfHeaderSize + ElfHeaderSize]; + BinaryPrimitives.WriteUInt32BigEndian(imageData, magic); + + Assert.Throws(() => + new SelfLoader().Load(imageData, new VirtualMemory())); + } + + [Fact] + public void Load_RejectsImageSmallerThanElfHeader() + { + // A few bytes short of ElfHeaderSize (0x40); ParseLayout guards this + // before any magic dispatch, so the error is deterministic for both + // SELF and ELF inputs. + var imageData = new byte[ElfHeaderSize - 1]; + + Assert.Throws(() => + new SelfLoader().Load(imageData, new VirtualMemory())); + } + + [Fact] + public void Load_RejectsTruncatedSelfHeader() + { + // SELF magic is present and recognized, but the image ends before the + // SELF header + embedded ELF header can be read. ParseLayout computes + // elfOffset = SelfHeaderSize + segments*SelfSegmentSize and then + // EnsureRange must fail. + var imageData = new byte[SelfHeaderSize + ElfHeaderSize]; + BinaryPrimitives.WriteUInt32BigEndian(imageData, Ps5SelfMagic); + imageData[0x05] = 0x01; + imageData[0x06] = 0x01; + imageData[0x07] = 0x12; + var truncated = imageData.AsSpan(0, SelfHeaderSize + 0x10).ToArray(); + + Assert.Throws(() => + new SelfLoader().Load(truncated, new VirtualMemory())); + } + + [Fact] + public void Load_ParsesEmbeddedElfHeaderFromSelfContainer() + { + var imageData = CreateSelfImage(Ps5SelfMagic, 0x10, 0x1000_0101, 0x32); + + var image = new SelfLoader().Load(imageData, new VirtualMemory()); + + Assert.True(image.IsSelf); + // The ELF header parsed out of the SELF container must be a valid x86-64 + // ELF64 little-endian image with the PS5 ABI marker that drives Gen5 + // selection in SharpEmuRuntime. + Assert.True(image.ElfHeader.HasElfMagic); + Assert.True(image.ElfHeader.Is64Bit); + Assert.True(image.ElfHeader.IsLittleEndian); + Assert.Equal(2, image.ElfHeader.AbiVersion); + Assert.Equal(62, image.ElfHeader.Machine); + } + + [Fact] + public void Load_AcceptsBareDecryptedElf() + { + // A decrypted eboot that has already been stripped of its SELF wrapper + // is accepted directly; IsSelf must be false and the ELF header is read + // from offset 0. + var imageData = new byte[ElfHeaderSize]; + WriteMinimalElfHeader(imageData); + + var image = new SelfLoader().Load(imageData, new VirtualMemory()); + + Assert.False(image.IsSelf); + Assert.True(image.ElfHeader.HasElfMagic); + Assert.True(image.ElfHeader.Is64Bit); + Assert.Equal(62, image.ElfHeader.Machine); + Assert.Empty(image.ProgramHeaders); + Assert.Empty(image.MappedRegions); + } + private static byte[] CreateSelfImage(uint magic, byte version, uint keyType, ushort flags) { var imageData = new byte[SelfHeaderSize + ElfHeaderSize];