mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 12:48:39 +08:00
bb3318a503
* kernel: return -1/errno from POSIX open and fstat on failure The POSIX-named open (wuCroIGjt2g) and fstat (mqQMh1zPPT8) exports routed straight to the raw sceKernel* implementations, which report failure via the 0x8002xxxx OrbisGen2Result sentinel in the return value. libc callers follow the POSIX ABI and expect -1 with errno set, so they stored the sentinel as a valid fd. Unity's IL2CPP file layer did exactly this while probing the absent /app0/Media/il2cpp.usym: open returned NOT_FOUND (0x80020002), the guest kept the sentinel as an fd, passed it back into fstat, and eventually dereferenced a null pointer (vmovups xmm0,[rdi], rdi=0) deep in a native .prx, crashing with 0xC0000005. Wrap both entry points to translate a failed raw result into -1/errno, mirroring the existing PosixStat/PosixLseek convention. Add a shared PosixFailure helper (fstat maps a bad handle to EBADF; path calls default to ENOENT) and route it through PosixStat too. Covered by two regression tests reproducing the missing-file and misused-sentinel-fd cases. * kernel: return -1/errno from POSIX close, read and write on failure Same defect class as open/fstat: the POSIX-named close (bY-PO6JhzhQ), read (AqBioC2vF3I) and write (FN4gaPmuFV8) exports forwarded the raw sceKernel* core result, leaking the 0x8002xxxx sentinel to libc callers that expect -1/errno on a bad fd. close in particular is on the crashing Unity path, invoked on the sentinel the guest mistook for an fd. Wrap all three through PosixFailure with EBADF as the fd-not-found errno. Add regression tests for each, and correct the socket test that had locked in the old raw-sentinel contract for a double close. --------- Co-authored-by: slick-daddy <slick-daddy@users.noreply.github.com>
397 lines
15 KiB
C#
397 lines
15 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.HLE;
|
|
using SharpEmu.Libs.Kernel;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.Kernel;
|
|
|
|
[CollectionDefinition(KernelMemoryCompatStateCollection.Name, DisableParallelization = true)]
|
|
public sealed class KernelMemoryCompatStateCollection
|
|
{
|
|
public const string Name = "KernelMemoryCompatState";
|
|
}
|
|
|
|
[Collection(KernelMemoryCompatStateCollection.Name)]
|
|
public sealed class KernelMemoryCompatExportsTests
|
|
{
|
|
private const ulong GuestMemoryBase = 0x1_0000_0000;
|
|
private const ulong AllocationOutAddress = GuestMemoryBase + 0x100;
|
|
private const ulong SpanStartOutAddress = GuestMemoryBase + 0x108;
|
|
private const ulong SpanSizeOutAddress = GuestMemoryBase + 0x110;
|
|
|
|
[Fact]
|
|
public void PosixStat_MissingFileReturnsMinusOne()
|
|
{
|
|
const ulong memoryBase = 0x1_0000_0000;
|
|
const ulong pathAddress = memoryBase + 0x100;
|
|
const ulong statAddress = memoryBase + 0x400;
|
|
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
memory.WriteCString(pathAddress, "/__sharpemu_test_missing__/shader.cache");
|
|
context[CpuRegister.Rdi] = pathAddress;
|
|
context[CpuRegister.Rsi] = statAddress;
|
|
|
|
var result = KernelMemoryCompatExports.PosixStat(context);
|
|
|
|
Assert.Equal(-1, result);
|
|
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
|
}
|
|
|
|
[Fact]
|
|
public void PosixOpen_MissingFileReturnsMinusOne()
|
|
{
|
|
const ulong memoryBase = 0x1_0000_0000;
|
|
const ulong pathAddress = memoryBase + 0x100;
|
|
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
memory.WriteCString(pathAddress, "/__sharpemu_test_missing__/il2cpp.usym");
|
|
context[CpuRegister.Rdi] = pathAddress;
|
|
context[CpuRegister.Rsi] = 0; // O_RDONLY
|
|
|
|
var result = KernelMemoryCompatExports.PosixOpen(context);
|
|
|
|
// A libc open() failure must be -1, not the raw 0x8002xxxx sentinel the
|
|
// guest would otherwise store as a valid fd and later dereference.
|
|
Assert.Equal(-1, result);
|
|
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
|
}
|
|
|
|
[Fact]
|
|
public void PosixFstat_BadDescriptorReturnsMinusOne()
|
|
{
|
|
const ulong memoryBase = 0x1_0000_0000;
|
|
const ulong statAddress = memoryBase + 0x400;
|
|
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
context[CpuRegister.Rdi] = 0x80020002; // the not-found sentinel misused as an fd
|
|
context[CpuRegister.Rsi] = statAddress;
|
|
|
|
var result = KernelMemoryCompatExports.PosixFstat(context);
|
|
|
|
Assert.Equal(-1, result);
|
|
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
|
}
|
|
|
|
[Fact]
|
|
public void PosixClose_BadDescriptorReturnsMinusOne()
|
|
{
|
|
const ulong memoryBase = 0x1_0000_0000;
|
|
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd
|
|
|
|
var result = KernelMemoryCompatExports.PosixClose(context);
|
|
|
|
Assert.Equal(-1, result);
|
|
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
|
}
|
|
|
|
[Fact]
|
|
public void PosixRead_BadDescriptorReturnsMinusOne()
|
|
{
|
|
const ulong memoryBase = 0x1_0000_0000;
|
|
const ulong bufferAddress = memoryBase + 0x200;
|
|
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd
|
|
context[CpuRegister.Rsi] = bufferAddress;
|
|
context[CpuRegister.Rdx] = 0x40;
|
|
|
|
var result = KernelMemoryCompatExports.PosixRead(context);
|
|
|
|
Assert.Equal(-1, result);
|
|
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
|
}
|
|
|
|
[Fact]
|
|
public void PosixWrite_BadDescriptorReturnsMinusOne()
|
|
{
|
|
const ulong memoryBase = 0x1_0000_0000;
|
|
const ulong bufferAddress = memoryBase + 0x200;
|
|
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
memory.WriteCString(bufferAddress, "payload");
|
|
context[CpuRegister.Rdi] = 0x80020002; // never-opened / sentinel fd
|
|
context[CpuRegister.Rsi] = bufferAddress;
|
|
context[CpuRegister.Rdx] = 0x7;
|
|
|
|
var result = KernelMemoryCompatExports.PosixWrite(context);
|
|
|
|
Assert.Equal(-1, result);
|
|
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
|
|
}
|
|
|
|
[Fact]
|
|
public void Sprintf_ReadsVariadicDoubleFromXmmRegister()
|
|
{
|
|
const ulong memoryBase = 0x1_0000_0000;
|
|
const ulong destinationAddress = memoryBase + 0x100;
|
|
const ulong formatAddress = memoryBase + 0x200;
|
|
var memory = new FakeCpuMemory(memoryBase, 0x1000);
|
|
var context = new CpuContext(memory, Generation.Gen5);
|
|
memory.WriteCString(formatAddress, "%.4f");
|
|
context[CpuRegister.Rdi] = destinationAddress;
|
|
context[CpuRegister.Rsi] = formatAddress;
|
|
context.SetXmmRegister(
|
|
0,
|
|
unchecked((ulong)BitConverter.DoubleToInt64Bits(0.5576)),
|
|
0);
|
|
|
|
var previousCulture = CultureInfo.CurrentCulture;
|
|
try
|
|
{
|
|
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("es-ES");
|
|
|
|
var result = KernelMemoryCompatExports.Sprintf(context);
|
|
|
|
Assert.Equal(0, result);
|
|
Assert.Equal(6UL, context[CpuRegister.Rax]);
|
|
Span<byte> output = stackalloc byte[7];
|
|
Assert.True(memory.TryRead(destinationAddress, output));
|
|
Assert.Equal("0.5576\0", Encoding.UTF8.GetString(output));
|
|
}
|
|
finally
|
|
{
|
|
CultureInfo.CurrentCulture = previousCulture;
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AvailableDirectMemorySize_FragmentedRangeReturnsLargestAlignedSpan()
|
|
{
|
|
const ulong firstAllocationStart = 0x0020_0000;
|
|
const ulong firstAllocationLength = 0x0020_0000;
|
|
const ulong secondAllocationStart = 0x00C0_0000;
|
|
const ulong secondAllocationLength = 0x0040_0000;
|
|
var context = new CpuContext(new FakeCpuMemory(GuestMemoryBase, 0x1000), Generation.Gen5);
|
|
|
|
try
|
|
{
|
|
AllocateDirectMemory(context, firstAllocationStart, firstAllocationLength);
|
|
AllocateDirectMemory(context, secondAllocationStart, secondAllocationLength);
|
|
|
|
QueryAvailableDirectMemory(context, 0, 0x0100_0000, 0x4000);
|
|
|
|
Assert.True(context.TryReadUInt64(SpanStartOutAddress, out var spanStart));
|
|
Assert.True(context.TryReadUInt64(SpanSizeOutAddress, out var spanSize));
|
|
Assert.Equal(0x0040_0000UL, spanStart);
|
|
Assert.Equal(0x0080_0000UL, spanSize);
|
|
}
|
|
finally
|
|
{
|
|
ReleaseDirectMemory(context, firstAllocationStart, firstAllocationLength);
|
|
ReleaseDirectMemory(context, secondAllocationStart, secondAllocationLength);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AvailableDirectMemorySize_AppliesAlignmentBeforeComparingSpans()
|
|
{
|
|
const ulong allocationStart = 0x0070_0000;
|
|
const ulong allocationLength = 0x0010_0000;
|
|
var context = new CpuContext(new FakeCpuMemory(GuestMemoryBase, 0x1000), Generation.Gen5);
|
|
|
|
try
|
|
{
|
|
AllocateDirectMemory(context, allocationStart, allocationLength);
|
|
|
|
QueryAvailableDirectMemory(context, 0x0010_0000, 0x00C0_0000, 0x0040_0000);
|
|
|
|
Assert.True(context.TryReadUInt64(SpanStartOutAddress, out var spanStart));
|
|
Assert.True(context.TryReadUInt64(SpanSizeOutAddress, out var spanSize));
|
|
Assert.Equal(0x0080_0000UL, spanStart);
|
|
Assert.Equal(0x0040_0000UL, spanSize);
|
|
}
|
|
finally
|
|
{
|
|
ReleaseDirectMemory(context, allocationStart, allocationLength);
|
|
}
|
|
}
|
|
|
|
private static void AllocateDirectMemory(CpuContext context, ulong start, ulong length)
|
|
{
|
|
context[CpuRegister.Rdi] = start;
|
|
context[CpuRegister.Rsi] = start + length;
|
|
context[CpuRegister.Rdx] = length;
|
|
context[CpuRegister.Rcx] = 0x4000;
|
|
context[CpuRegister.R8] = 0;
|
|
context[CpuRegister.R9] = AllocationOutAddress;
|
|
|
|
Assert.Equal(0, KernelMemoryCompatExports.KernelAllocateDirectMemory(context));
|
|
Assert.True(context.TryReadUInt64(AllocationOutAddress, out var allocatedAddress));
|
|
Assert.Equal(start, allocatedAddress);
|
|
}
|
|
|
|
private static void QueryAvailableDirectMemory(
|
|
CpuContext context,
|
|
ulong searchStart,
|
|
ulong searchEnd,
|
|
ulong alignment)
|
|
{
|
|
context[CpuRegister.Rdi] = searchStart;
|
|
context[CpuRegister.Rsi] = searchEnd;
|
|
context[CpuRegister.Rdx] = alignment;
|
|
context[CpuRegister.Rcx] = SpanStartOutAddress;
|
|
context[CpuRegister.R8] = SpanSizeOutAddress;
|
|
|
|
Assert.Equal(0, KernelMemoryCompatExports.KernelAvailableDirectMemorySize(context));
|
|
}
|
|
|
|
private static void ReleaseDirectMemory(CpuContext context, ulong start, ulong length)
|
|
{
|
|
context[CpuRegister.Rdi] = start;
|
|
context[CpuRegister.Rsi] = length;
|
|
|
|
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);
|
|
}
|
|
}
|