sharpemu/tests/SharpEmu.Libs.Tests/Kernel/KernelMemoryCompatExportsTe...

859 lines
35 KiB
C#

// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Core.Memory;
using SharpEmu.HLE.Host;
using SharpEmu.HLE.GpuMemory;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.Tests.Memory.GpuMemory;
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;
[Theory]
[InlineData("/dev/urandom")]
[InlineData("/dev/random")]
public void EntropyDevice_ReadsFreshBytesAndCloses(string path)
{
var memory = new FakeCpuMemory(GuestMemoryBase, 0x2000);
var ctx = new CpuContext(memory, Generation.Gen5);
Assert.True(memory.TryWrite(GuestMemoryBase, Encoding.UTF8.GetBytes(path + "\0")));
ctx[CpuRegister.Rdi] = GuestMemoryBase;
ctx[CpuRegister.Rsi] = 0;
Assert.Equal(0, KernelMemoryCompatExports.PosixOpen(ctx));
var fd = ctx[CpuRegister.Rax];
Assert.True(fd > 2);
try
{
ctx[CpuRegister.Rdi] = fd;
ctx[CpuRegister.Rsi] = GuestMemoryBase + 0x200;
Assert.Equal(0, KernelMemoryCompatExports.PosixFstat(ctx));
var stat = new byte[120];
Assert.True(memory.TryRead(GuestMemoryBase + 0x200, stat));
Assert.Equal(0x2000, BitConverter.ToUInt16(stat, 8) & 0xF000);
Assert.Equal(0L, BitConverter.ToInt64(stat, 72));
var first = new byte[64];
var second = new byte[64];
ctx[CpuRegister.Rdi] = fd;
ctx[CpuRegister.Rsi] = GuestMemoryBase + 0x100;
ctx[CpuRegister.Rdx] = 64;
Assert.Equal(0, KernelMemoryCompatExports.PosixRead(ctx));
Assert.Equal(64UL, ctx[CpuRegister.Rax]);
Assert.True(memory.TryRead(GuestMemoryBase + 0x100, first));
Assert.Equal(0, KernelMemoryCompatExports.PosixRead(ctx));
Assert.True(memory.TryRead(GuestMemoryBase + 0x100, second));
Assert.False(first.SequenceEqual(second));
Assert.Contains(first, value => value != 0);
ctx[CpuRegister.Rsi] = 0;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT,
KernelMemoryCompatExports.KernelReadUnderscore(ctx));
}
finally
{
ctx[CpuRegister.Rdi] = fd;
Assert.Equal(0, KernelMemoryCompatExports.PosixClose(ctx));
}
ctx[CpuRegister.Rsi] = GuestMemoryBase + 0x100;
ctx[CpuRegister.Rdx] = 64;
Assert.Equal(-1, KernelMemoryCompatExports.PosixRead(ctx));
Assert.Equal(-1, KernelMemoryCompatExports.PosixFstat(ctx));
Assert.Equal(-1, KernelMemoryCompatExports.PosixClose(ctx));
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(0x20000)]
public void EntropyDevice_RejectsUnsupportedAccess(int flags)
{
var memory = new FakeCpuMemory(GuestMemoryBase, 0x1000);
var ctx = new CpuContext(memory, Generation.Gen5);
Assert.True(memory.TryWrite(GuestMemoryBase, Encoding.UTF8.GetBytes("/dev/urandom\0")));
ctx[CpuRegister.Rdi] = GuestMemoryBase;
ctx[CpuRegister.Rsi] = unchecked((ulong)flags);
Assert.Equal(-1, KernelMemoryCompatExports.PosixOpen(ctx));
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void UppercaseStringConversion_ConsumesWideArgumentBetweenNarrowStrings(bool useVaList)
{
var memory = new FakeCpuMemory(GuestMemoryBase, 0x2000);
var context = new CpuContext(memory, Generation.Gen5);
memory.WriteCString(GuestMemoryBase + 0x100, "assets");
Assert.True(memory.TryWrite(GuestMemoryBase + 0x200, Encoding.Unicode.GetBytes("armor\0")));
memory.WriteCString(GuestMemoryBase + 0x300, "body");
const string format = "%s/%S_%s.bin";
string rendered;
if (useVaList)
{
var arguments = GuestMemoryBase + 0x400;
var savedRegisters = GuestMemoryBase + 0x500;
Assert.True(context.TryWriteUInt64(arguments, 48UL << 32));
Assert.True(context.TryWriteUInt64(arguments + 8, GuestMemoryBase + 0x600));
Assert.True(context.TryWriteUInt64(arguments + 16, savedRegisters));
Assert.True(context.TryWriteUInt64(savedRegisters, GuestMemoryBase + 0x100));
Assert.True(context.TryWriteUInt64(savedRegisters + 8, GuestMemoryBase + 0x200));
Assert.True(context.TryWriteUInt64(savedRegisters + 16, GuestMemoryBase + 0x300));
Assert.True(KernelMemoryCompatExports.TryFormatStringFromVaList(context, format, arguments, out rendered));
}
else
{
context[CpuRegister.Rcx] = GuestMemoryBase + 0x100;
context[CpuRegister.R8] = GuestMemoryBase + 0x200;
context[CpuRegister.R9] = GuestMemoryBase + 0x300;
rendered = KernelMemoryCompatExports.FormatStringFromVarArgs(context, format, 3);
}
Assert.Equal("assets/armor_body.bin", rendered);
}
[Theory]
[InlineData("%8.3S", " arm")]
[InlineData("%-8.3S", "arm ")]
public void UppercaseStringConversion_UsesExistingWideStringFormatting(string format, string expected)
{
var memory = new FakeCpuMemory(GuestMemoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
Assert.True(memory.TryWrite(GuestMemoryBase + 0x100, Encoding.Unicode.GetBytes("armor\0")));
context[CpuRegister.Rcx] = GuestMemoryBase + 0x100;
Assert.Equal(expected, KernelMemoryCompatExports.FormatStringFromVarArgs(context, format, 3));
}
[Theory]
[InlineData("a/b/c.xml", '/', 1, 3)]
[InlineData("a/b/c.xml", '.', 5, 5)]
[InlineData("a/b/c.xml", 'z', -1, -1)]
[InlineData("a/b/c.xml", '\0', 9, 9)]
[InlineData("", 'a', -1, -1)]
public void StrchrStrrchr_MatchNativeLibc(string text, char needle, int firstIndex, int lastIndex)
{
const ulong address = GuestMemoryBase + 0x100;
var memory = new FakeCpuMemory(GuestMemoryBase, 0x1000);
memory.WriteCString(address, text);
Assert.Equal(ExpectedMatch(address, firstIndex), ScanString(memory, address, needle, last: false));
Assert.Equal(ExpectedMatch(address, lastIndex), ScanString(memory, address, needle, last: true));
}
[Fact]
public void StrchrStrrchr_ScanAcrossPagesAndPastChunkSize()
{
// Starts just before a page boundary and runs well past one 4 KiB chunk.
const ulong address = GuestMemoryBase + 0xFF0;
var text = "<" + new string('x', 6000) + ">" + new string('y', 100) + ">";
var memory = new FakeCpuMemory(GuestMemoryBase, 0x3000);
memory.WriteCString(address, text);
Assert.Equal(address + 6001, ScanString(memory, address, '>', last: false));
Assert.Equal(address + (ulong)text.Length - 1, ScanString(memory, address, '>', last: true));
Assert.Equal(address + (ulong)text.Length, ScanString(memory, address, '\0', last: false));
}
[Fact]
public void StrchrStrrchr_StringEndingAtEndOfMappedMemory()
{
// The first chunk would read past the mapping, so the byte-wise fallback
// must still find the terminator that lies inside it.
var memory = new FakeCpuMemory(GuestMemoryBase, 0x1000);
const ulong address = GuestMemoryBase + 0x1000 - 6;
memory.WriteCString(address, "a/b/c");
Assert.Equal(address + 1, ScanString(memory, address, '/', last: false));
Assert.Equal(address + 3, ScanString(memory, address, '/', last: true));
}
[Fact]
public void StrchrStrrchr_UnterminatedStringAtEndOfMemoryFaults()
{
var memory = new FakeCpuMemory(GuestMemoryBase, 0x1000);
const ulong address = GuestMemoryBase + 0x1000 - 4;
Assert.True(memory.TryWrite(address, "abcd"u8));
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = address;
context[CpuRegister.Rsi] = 'z';
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, KernelMemoryCompatExports.Strchr(context));
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, KernelMemoryCompatExports.Strrchr(context));
}
[Fact]
public void StrchrStrrchr_ScanMappedMemoryInPlace()
{
using var memory = new PhysicalVirtualMemory();
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var baseAddress));
// Crosses a page boundary and runs past one 4 KiB page.
var address = baseAddress + 0xFF0;
var text = "<" + new string('x', 6000) + ">" + new string('y', 100) + ">";
Assert.True(memory.TryWrite(address, Encoding.ASCII.GetBytes(text + "\0")));
Assert.True(memory.TryScanCString(address, (byte)'>', findLast: false, 1_048_576, out var first));
Assert.Equal(address + 6001, first);
Assert.True(memory.TryScanCString(address, (byte)'>', findLast: true, 1_048_576, out var last));
Assert.Equal(address + (ulong)text.Length - 1, last);
Assert.True(memory.TryScanCString(address, 0, findLast: true, 1_048_576, out var terminator));
Assert.Equal(address + (ulong)text.Length, terminator);
Assert.True(memory.TryScanCString(address, (byte)'z', findLast: false, 1_048_576, out var missing));
Assert.Equal(0UL, missing);
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = address;
context[CpuRegister.Rsi] = '>';
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, KernelMemoryCompatExports.Strrchr(context));
Assert.Equal(address + (ulong)text.Length - 1, context[CpuRegister.Rax]);
}
[Fact]
public void StrchrStrrchr_UnmappedMemoryIsNotScannedInPlace()
{
using var memory = new PhysicalVirtualMemory();
Assert.False(memory.TryScanCString(0x7_0000_0000, (byte)'a', findLast: false, 1_048_576, out _));
}
private static ulong ExpectedMatch(ulong address, int index) =>
index < 0 ? 0 : address + (ulong)index;
private static ulong ScanString(FakeCpuMemory memory, ulong address, char needle, bool last)
{
var context = new CpuContext(memory, Generation.Gen5);
context[CpuRegister.Rdi] = address;
context[CpuRegister.Rsi] = needle;
var result = last
? KernelMemoryCompatExports.Strrchr(context)
: KernelMemoryCompatExports.Strchr(context);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_OK, result);
return context[CpuRegister.Rax];
}
[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 UnderscoreOpen_MissingFileUsesPosixFailureAbi()
{
var memory = new FakeCpuMemory(GuestMemoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5) { FsBase = GuestMemoryBase + 0x800 };
memory.WriteCString(GuestMemoryBase + 0x100, "/__sharpemu_test_missing__/resource_level_high.bin");
context[CpuRegister.Rdi] = GuestMemoryBase + 0x100;
Assert.Equal(-1, KernelMemoryCompatExports.KernelOpenUnderscore(context));
Assert.Equal(ulong.MaxValue, context[CpuRegister.Rax]);
Assert.True(context.TryReadUInt32(context.FsBase + 0x40, out var errno));
Assert.Equal(2u, errno);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, KernelExports.KernelOpen(context));
}
[Theory]
[InlineData("/dev/random")]
[InlineData("/dev/urandom")]
public void PosixRandomDevice_OpenReadFstatAndClose(string path)
{
var memory = new FakeCpuMemory(GuestMemoryBase, 0x1000);
var context = new CpuContext(memory, Generation.Gen5);
memory.WriteCString(GuestMemoryBase + 0x100, path);
context[CpuRegister.Rdi] = GuestMemoryBase + 0x100;
Assert.Equal(0, KernelMemoryCompatExports.KernelOpenUnderscore(context));
var fd = context[CpuRegister.Rax];
Assert.True(fd >= 3);
try
{
context[CpuRegister.Rdi] = fd;
context[CpuRegister.Rsi] = GuestMemoryBase + 0x200;
context[CpuRegister.Rdx] = 32;
Assert.Equal(0, KernelMemoryCompatExports.PosixRead(context));
Assert.Equal(32UL, context[CpuRegister.Rax]);
var randomBytes = new byte[32];
Assert.True(memory.TryRead(GuestMemoryBase + 0x200, randomBytes));
Assert.Contains(randomBytes, value => value != 0);
context[CpuRegister.Rsi] = GuestMemoryBase + 0x400;
Assert.Equal(0, KernelMemoryCompatExports.PosixFstat(context));
}
finally
{
context[CpuRegister.Rdi] = fd;
Assert.Equal(0, KernelMemoryCompatExports.PosixClose(context));
}
context[CpuRegister.Rsi] = GuestMemoryBase + 0x200;
context[CpuRegister.Rdx] = 1;
Assert.Equal(-1, KernelMemoryCompatExports.PosixRead(context));
}
[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 KernelMkdir_GuestRootReturnsAlreadyExists()
{
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, "/");
context[CpuRegister.Rdi] = pathAddress;
var result = KernelMemoryCompatExports.KernelMkdir(context);
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS, result);
}
[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);
}
}
[Fact]
public void AvailableDirectMemorySize_FullRangeClearsOutputsAndReportsNoSpace()
{
const ulong allocationStart = 0;
const ulong allocationLength = 0x0040_0000;
var context = new CpuContext(new FakeCpuMemory(GuestMemoryBase, 0x1000), Generation.Gen5);
try
{
AllocateDirectMemory(context, allocationStart, allocationLength);
Assert.True(context.TryWriteUInt64(SpanStartOutAddress, 0xDEAD_BEEF));
Assert.True(context.TryWriteUInt64(SpanSizeOutAddress, 0xDEAD_BEEF));
context[CpuRegister.Rdi] = allocationStart;
context[CpuRegister.Rsi] = allocationStart + allocationLength;
context[CpuRegister.Rdx] = 0;
context[CpuRegister.Rcx] = SpanStartOutAddress;
context[CpuRegister.R8] = SpanSizeOutAddress;
Assert.Equal(unchecked((int)0x8002000C), KernelMemoryCompatExports.KernelAvailableDirectMemorySize(context));
Assert.True(context.TryReadUInt64(SpanStartOutAddress, out var spanStart));
Assert.True(context.TryReadUInt64(SpanSizeOutAddress, out var spanSize));
Assert.Equal(0UL, spanStart);
Assert.Equal(0UL, spanSize);
}
finally
{
ReleaseDirectMemory(context, allocationStart, allocationLength);
}
}
[Fact]
public void AvailableDirectMemorySize_EmptySearchRangeReportsNoSpace()
{
var context = new CpuContext(new FakeCpuMemory(GuestMemoryBase, 0x1000), Generation.Gen5);
context[CpuRegister.Rdi] = 0x0010_0000;
context[CpuRegister.Rsi] = 0x0010_0000;
context[CpuRegister.Rdx] = 0;
context[CpuRegister.Rcx] = SpanStartOutAddress;
context[CpuRegister.R8] = SpanSizeOutAddress;
Assert.Equal(unchecked((int)0x8002000C), KernelMemoryCompatExports.KernelAvailableDirectMemorySize(context));
}
private static void AllocateDirectMemory(CpuContext context, ulong start, ulong length, ulong outputAddress = AllocationOutAddress)
{
context[CpuRegister.Rdi] = start;
context[CpuRegister.Rsi] = start + length;
context[CpuRegister.Rdx] = length;
context[CpuRegister.Rcx] = 0x4000;
context[CpuRegister.R8] = 0;
context[CpuRegister.R9] = outputAddress;
Assert.Equal(0, KernelMemoryCompatExports.KernelAllocateDirectMemory(context));
Assert.True(context.TryReadUInt64(outputAddress, 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(unchecked((int)0x80020016), 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(unchecked((int)0x80020016), 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] = 0x4000;
context[CpuRegister.Rdx] = 0x03;
context[CpuRegister.Rcx] = 0;
var result = KernelMemoryCompatExports.KernelMapNamedFlexibleMemory(context);
Assert.Equal(unchecked((int)0x8002000E), result);
}
[Fact]
public void VirtualQuery_PreservesReservationPastFixedCommitAtSameBase()
{
const ulong reservedLength = 0x1_0000;
const ulong committedLength = 0x4000;
using var test = new BackedKernelMemory();
var context = test.Context;
var infoAddress = test.Output + 0x100;
var memoryBase = test.Reserve(reservedLength);
test.Allocate(0, committedLength);
Assert.Equal(memoryBase, test.Map(0, committedLength, memoryBase));
context[CpuRegister.Rdi] = memoryBase + 0x8000;
context[CpuRegister.Rsi] = 0;
context[CpuRegister.Rdx] = infoAddress;
context[CpuRegister.Rcx] = 0x48;
Assert.Equal(0, KernelMemoryCompatExports.KernelVirtualQuery(context));
Assert.True(context.TryReadUInt64(infoAddress, out var regionStart));
Assert.True(context.TryReadUInt64(infoAddress + 8, out var regionEnd));
Assert.Equal(memoryBase + committedLength, regionStart);
Assert.Equal(memoryBase + reservedLength, regionEnd);
}
[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(unchecked((int)0x80020016), 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(unchecked((int)0x80020016), result);
}
[Fact]
public void Mprotect_UnmappedRangeReturnsNotFound()
{
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(unchecked((int)0x80020002), result);
}
[Fact]
public void Munmap_ZeroAddressReturnsAccessDenied()
{
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(unchecked((int)0x8002000D), 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(unchecked((int)0x80020016), result);
}
[Fact]
public void Munmap_UnmappedRangeReturnsAccessDenied()
{
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(unchecked((int)0x8002000D), result);
}
[Fact]
public void MapDirectMemory_RegistersAndMunmapUnregistersTheGpuSpan()
{
const ulong directStart = 0x0300_0000;
const ulong length = 0x0001_0000;
const ulong requestedAddress = 0x2_0000_0000;
using var memory = new PhysicalVirtualMemory(viewHost: HostViewMemory.Create(), backingBytes: 128UL * 1024 * 1024);
var outputAddress = memory.AllocateAt(0, 0x4000, false);
var context = new CpuContext(memory, Generation.Gen5);
var gpuMemory = new GuestGpuMemory(new RecordingAddressSpace());
GuestGpuMemoryHook.Attach(gpuMemory);
var mappedAddress = 0UL;
try
{
AllocateDirectMemory(context, directStart, length, outputAddress);
Assert.True(context.TryWriteUInt64(outputAddress, requestedAddress));
context[CpuRegister.Rdi] = outputAddress;
context[CpuRegister.Rsi] = length;
context[CpuRegister.Rdx] = 0x33; // CPU read|write, GPU read|write
context[CpuRegister.Rcx] = 0;
context[CpuRegister.R8] = directStart;
context[CpuRegister.R9] = 0;
Assert.Equal(0, KernelMemoryCompatExports.KernelMapDirectMemory(context));
Assert.True(context.TryReadUInt64(outputAddress, out mappedAddress));
Assert.True(gpuMemory.Covers(mappedAddress, length));
context[CpuRegister.Rdi] = mappedAddress;
context[CpuRegister.Rsi] = length;
Assert.Equal(0, KernelMemoryCompatExports.KernelMunmap(context));
Assert.False(gpuMemory.Covers(mappedAddress, length));
mappedAddress = 0;
}
finally
{
if (mappedAddress != 0)
{
context[CpuRegister.Rdi] = mappedAddress;
context[CpuRegister.Rsi] = length;
_ = KernelMemoryCompatExports.KernelMunmap(context);
}
ReleaseDirectMemory(context, directStart, length);
GuestGpuMemoryHook.Attach(null);
gpuMemory.Dispose();
KernelMemoryCompatExports.ResetBackingMappings(memory);
}
}
[Fact]
public void ReleaseDirectMemory_FailedReleaseKeepsTheMappedGpuSpan()
{
const ulong directStart = 0x0400_0000;
const ulong length = 0x0001_0000;
const ulong unallocatedStart = 0x0500_0000;
const ulong requestedAddress = 0x2_1000_0000;
using var memory = new PhysicalVirtualMemory(viewHost: HostViewMemory.Create(), backingBytes: 128UL * 1024 * 1024);
var outputAddress = memory.AllocateAt(0, 0x4000, false);
var context = new CpuContext(memory, Generation.Gen5);
var gpuMemory = new GuestGpuMemory(new RecordingAddressSpace());
GuestGpuMemoryHook.Attach(gpuMemory);
var mappedAddress = 0UL;
try
{
AllocateDirectMemory(context, directStart, length, outputAddress);
Assert.True(context.TryWriteUInt64(outputAddress, requestedAddress));
context[CpuRegister.Rdi] = outputAddress;
context[CpuRegister.Rsi] = length;
context[CpuRegister.Rdx] = 0x33; // CPU read|write, GPU read|write
context[CpuRegister.Rcx] = 0;
context[CpuRegister.R8] = directStart;
context[CpuRegister.R9] = 0;
Assert.Equal(0, KernelMemoryCompatExports.KernelMapDirectMemory(context));
Assert.True(context.TryReadUInt64(outputAddress, out mappedAddress));
Assert.True(gpuMemory.Covers(mappedAddress, length));
context[CpuRegister.Rdi] = unallocatedStart;
context[CpuRegister.Rsi] = length;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, KernelMemoryCompatExports.KernelCheckedReleaseDirectMemory(context));
Assert.True(gpuMemory.Covers(mappedAddress, length));
// Overlaps the second half of the allocation and runs past its end.
context[CpuRegister.Rdi] = directStart + length / 2;
context[CpuRegister.Rsi] = length;
Assert.Equal((int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND, KernelMemoryCompatExports.KernelCheckedReleaseDirectMemory(context));
Assert.True(gpuMemory.Covers(mappedAddress, length));
context[CpuRegister.Rdi] = directStart + length / 2;
context[CpuRegister.Rsi] = length;
Assert.Equal(0, KernelMemoryCompatExports.KernelReleaseDirectMemory(context));
Assert.True(gpuMemory.Covers(mappedAddress, length));
ReleaseDirectMemory(context, directStart, length);
Assert.False(gpuMemory.Covers(mappedAddress, length));
}
finally
{
if (mappedAddress != 0)
{
context[CpuRegister.Rdi] = mappedAddress;
context[CpuRegister.Rsi] = length;
_ = KernelMemoryCompatExports.KernelMunmap(context);
}
GuestGpuMemoryHook.Attach(null);
gpuMemory.Dispose();
KernelMemoryCompatExports.ResetBackingMappings(memory);
}
}
}