mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +08:00
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 <omp@local>
This commit is contained in:
@@ -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<byte> 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<InvalidDataException>(() =>
|
||||
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<InvalidDataException>(() =>
|
||||
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<InvalidDataException>(() =>
|
||||
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];
|
||||
|
||||
Reference in New Issue
Block a user