mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +08:00
33be88bdf9
A SCE_KERNEL_MAP_FIXED request whose window partially overlaps an existing allocation was failing outright: AllocateAt reserves the whole range in one all-or-nothing VirtualAlloc, which returns 0 on partial overlap. The mapping call then returned NOT_FOUND while leaving the free tail unmapped, so the guest faulted (0xC0000005) writing into it. Add IGuestAddressSpace.TryBackFixedRange, which walks the range via the host Query (VirtualQuery reports contiguous same-state runs) and fills only the free sub-ranges, leaving already-backed pages untouched. This matches the fixed-mapping contract on hardware. Route the fixed reservation path through it via a new backPartialOverlap flag. Co-authored-by: slick-daddy <slick-daddy@users.noreply.github.com>
445 lines
15 KiB
C#
445 lines
15 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.Core.Memory;
|
|
using SharpEmu.Core.Loader;
|
|
using SharpEmu.HLE.Host;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.Memory;
|
|
|
|
public sealed class GuestMemoryAllocatorTests
|
|
{
|
|
[Fact]
|
|
public void FreedRangesAreReusedAndCoalesced()
|
|
{
|
|
using var memory = new PhysicalVirtualMemory(new FakeHostMemory());
|
|
const ulong usableArenaSize = 0x0100_0000 - 0x1000;
|
|
|
|
Assert.True(memory.TryAllocateGuestMemory(0x4000, 0x1000, out var first));
|
|
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var second));
|
|
Assert.True(memory.TryAllocateGuestMemory(usableArenaSize - 0xC000, 0x1000, out var third));
|
|
Assert.False(memory.TryAllocateGuestMemory(1, 1, out _));
|
|
|
|
Assert.True(memory.TryFreeGuestMemory(second));
|
|
Assert.True(memory.TryAllocateGuestMemory(0x8000, 0x1000, out var reused));
|
|
Assert.Equal(second, reused);
|
|
|
|
Assert.True(memory.TryFreeGuestMemory(first));
|
|
Assert.True(memory.TryFreeGuestMemory(reused));
|
|
Assert.True(memory.TryFreeGuestMemory(third));
|
|
Assert.False(memory.TryFreeGuestMemory(third));
|
|
|
|
Assert.True(memory.TryAllocateGuestMemory(usableArenaSize, 0x1000, out var coalesced));
|
|
Assert.Equal(first, coalesced);
|
|
}
|
|
|
|
[Fact]
|
|
public void SegmentProtectionIsAppliedInContiguousRuns()
|
|
{
|
|
const ulong pageSize = 0x1000;
|
|
using var host = new RecordingHostMemory(3 * pageSize);
|
|
using var memory = new PhysicalVirtualMemory(host);
|
|
|
|
memory.Map(host.Address, 3 * pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Read);
|
|
|
|
Assert.Equal(
|
|
[
|
|
(host.Address, 3 * pageSize, HostPageProtection.ReadWrite),
|
|
(host.Address, 3 * pageSize, HostPageProtection.ReadOnly),
|
|
],
|
|
host.ProtectionCalls);
|
|
|
|
host.ProtectionCalls.Clear();
|
|
memory.Map(host.Address + pageSize, pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Write);
|
|
host.ProtectionCalls.Clear();
|
|
|
|
memory.Map(host.Address, 3 * pageSize, 0, ReadOnlySpan<byte>.Empty, ProgramHeaderFlags.Execute);
|
|
|
|
Assert.Equal(
|
|
[
|
|
(host.Address, 3 * pageSize, HostPageProtection.ReadWriteExecute),
|
|
(host.Address, pageSize, HostPageProtection.ReadExecute),
|
|
(host.Address + pageSize, pageSize, HostPageProtection.ReadWriteExecute),
|
|
(host.Address + (2 * pageSize), pageSize, HostPageProtection.ReadExecute),
|
|
],
|
|
host.ProtectionCalls);
|
|
}
|
|
|
|
[Fact]
|
|
public unsafe void GetPointerCommitsLazyPageBeforeReturningIt()
|
|
{
|
|
const ulong address = 0x00005000_0000_0000;
|
|
const ulong pageSize = 0x1000;
|
|
using var host = new LazyHostMemory(address);
|
|
using var memory = new PhysicalVirtualMemory(host);
|
|
memory.AllocateAt(address, (4UL << 30) + pageSize, executable: false, allowAlternative: false);
|
|
host.CommitCalls.Clear();
|
|
|
|
var pointer = memory.GetPointer(address + 0x123);
|
|
|
|
Assert.Equal(address + 0x123, (ulong)pointer);
|
|
Assert.Equal([(address, pageSize, HostPageProtection.ReadWrite)], host.CommitCalls);
|
|
}
|
|
|
|
[Fact]
|
|
public unsafe void GetPointerReturnsNullWhenLazyCommitFails()
|
|
{
|
|
const ulong address = 0x00005000_0000_0000;
|
|
using var host = new LazyHostMemory(address);
|
|
using var memory = new PhysicalVirtualMemory(host);
|
|
memory.AllocateAt(address, (4UL << 30) + 0x1000, executable: false, allowAlternative: false);
|
|
host.CommitCalls.Clear();
|
|
host.CommitSucceeds = false;
|
|
|
|
Assert.Equal(0UL, (ulong)memory.GetPointer(address));
|
|
}
|
|
|
|
[Fact]
|
|
public void AlignedAllocationDoesNotRetainOverallocatedMappingsOutsideMacOS()
|
|
{
|
|
if (OperatingSystem.IsMacOS())
|
|
{
|
|
return;
|
|
}
|
|
|
|
const ulong desiredAddress = 0x00005000_0000_0123;
|
|
const ulong alignment = 0x10000;
|
|
const ulong alignedAddress = 0x00005000_0001_0000;
|
|
const ulong allocationSize = 0x2000;
|
|
using var host = new RelocatingHostMemory(alignedAddress);
|
|
using var memory = new PhysicalVirtualMemory(host);
|
|
|
|
Assert.True(memory.TryAllocateAtOrAbove(desiredAddress, 0x1234, false, alignment, out var actualAddress));
|
|
Assert.Equal(alignedAddress + alignment, actualAddress);
|
|
Assert.Equal(
|
|
[
|
|
(alignedAddress, allocationSize),
|
|
(alignedAddress + alignment, allocationSize),
|
|
],
|
|
host.AllocationCalls);
|
|
Assert.Equal([alignedAddress + 0x1000], host.FreedAddresses);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryBackFixedRangeFillsOnlyTheFreePagesOfAPartiallyOccupiedRange()
|
|
{
|
|
const ulong rangeBase = 0x0000_0020_2F00_0000;
|
|
const ulong rangeSize = 0x40_0000;
|
|
const ulong occupiedSize = 0x4_0000;
|
|
using var host = new PartialOverlapHostMemory(rangeBase, occupiedSize, rangeSize);
|
|
using var memory = new PhysicalVirtualMemory(host);
|
|
|
|
Assert.True(memory.TryBackFixedRange(rangeBase, rangeSize, executable: false));
|
|
|
|
// Only the free tail is allocated; the already-occupied head is untouched.
|
|
Assert.Equal(
|
|
[(rangeBase + occupiedSize, rangeSize - occupiedSize)],
|
|
host.AllocationCalls);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryBackFixedRangeReturnsFalseWhenRangeIsFullyOccupied()
|
|
{
|
|
const ulong rangeBase = 0x0000_0020_2F00_0000;
|
|
const ulong rangeSize = 0x40_0000;
|
|
using var host = new PartialOverlapHostMemory(rangeBase, rangeSize, rangeSize);
|
|
using var memory = new PhysicalVirtualMemory(host);
|
|
|
|
Assert.False(memory.TryBackFixedRange(rangeBase, rangeSize, executable: false));
|
|
Assert.Empty(host.AllocationCalls);
|
|
}
|
|
|
|
private sealed class PartialOverlapHostMemory : IHostMemory, IDisposable
|
|
{
|
|
private readonly ulong _rangeBase;
|
|
private readonly ulong _occupiedEnd;
|
|
private readonly ulong _rangeEnd;
|
|
|
|
public PartialOverlapHostMemory(ulong rangeBase, ulong occupiedSize, ulong rangeSize)
|
|
{
|
|
_rangeBase = rangeBase;
|
|
_occupiedEnd = rangeBase + occupiedSize;
|
|
_rangeEnd = rangeBase + rangeSize;
|
|
}
|
|
|
|
public List<(ulong Address, ulong Size)> AllocationCalls { get; } = [];
|
|
|
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection)
|
|
{
|
|
AllocationCalls.Add((desiredAddress, size));
|
|
return desiredAddress;
|
|
}
|
|
|
|
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
|
|
|
|
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
|
|
|
|
public bool Free(ulong address) => true;
|
|
|
|
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
|
{
|
|
rawOldProtection = 0;
|
|
return true;
|
|
}
|
|
|
|
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
|
{
|
|
rawOldProtection = 0;
|
|
return true;
|
|
}
|
|
|
|
public bool Query(ulong address, out HostRegionInfo info)
|
|
{
|
|
// Report contiguous runs of same-state pages, exactly as VirtualQuery
|
|
// does: the occupied head first, then the free tail.
|
|
if (address < _occupiedEnd)
|
|
{
|
|
info = new HostRegionInfo(
|
|
address,
|
|
_rangeBase,
|
|
_occupiedEnd - address,
|
|
HostRegionState.Committed,
|
|
RawState: 0x1000,
|
|
HostPageProtection.ReadWrite,
|
|
RawProtection: 0x04,
|
|
RawAllocationProtection: 0x04);
|
|
return true;
|
|
}
|
|
|
|
info = new HostRegionInfo(
|
|
address,
|
|
AllocationBase: 0,
|
|
_rangeEnd - address,
|
|
HostRegionState.Free,
|
|
RawState: 0x10000,
|
|
HostPageProtection.NoAccess,
|
|
RawProtection: 0x01,
|
|
RawAllocationProtection: 0);
|
|
return true;
|
|
}
|
|
|
|
public void FlushInstructionCache(ulong address, ulong size)
|
|
{
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class FakeHostMemory : IHostMemory
|
|
{
|
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
|
desiredAddress != 0 ? desiredAddress : 0x00007000_0000_0000;
|
|
|
|
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
|
Allocate(desiredAddress, size, protection);
|
|
|
|
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
|
|
|
|
public bool Free(ulong address) => true;
|
|
|
|
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
|
{
|
|
rawOldProtection = 0;
|
|
return true;
|
|
}
|
|
|
|
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
|
{
|
|
rawOldProtection = 0;
|
|
return true;
|
|
}
|
|
|
|
public bool Query(ulong address, out HostRegionInfo info)
|
|
{
|
|
info = default;
|
|
return false;
|
|
}
|
|
|
|
public void FlushInstructionCache(ulong address, ulong size)
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class RecordingHostMemory : IHostMemory, IDisposable
|
|
{
|
|
private readonly nint _allocation;
|
|
private bool _freed;
|
|
|
|
public RecordingHostMemory(ulong size)
|
|
{
|
|
_allocation = System.Runtime.InteropServices.Marshal.AllocHGlobal(checked((nint)(size + 0xFFF)));
|
|
Address = (unchecked((ulong)_allocation) + 0xFFF) & ~0xFFFUL;
|
|
}
|
|
|
|
public ulong Address { get; }
|
|
|
|
public List<(ulong Address, ulong Size, HostPageProtection Protection)> ProtectionCalls { get; } = [];
|
|
|
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
|
desiredAddress == Address ? Address : 0;
|
|
|
|
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
|
|
|
|
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
|
|
|
|
public bool Free(ulong address)
|
|
{
|
|
if (address != Address || _freed)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
System.Runtime.InteropServices.Marshal.FreeHGlobal(_allocation);
|
|
_freed = true;
|
|
return true;
|
|
}
|
|
|
|
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
|
{
|
|
ProtectionCalls.Add((address, size, protection));
|
|
rawOldProtection = 0;
|
|
return true;
|
|
}
|
|
|
|
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
|
{
|
|
rawOldProtection = 0;
|
|
return true;
|
|
}
|
|
|
|
public bool Query(ulong address, out HostRegionInfo info)
|
|
{
|
|
info = default;
|
|
return false;
|
|
}
|
|
|
|
public void FlushInstructionCache(ulong address, ulong size)
|
|
{
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (!_freed)
|
|
{
|
|
System.Runtime.InteropServices.Marshal.FreeHGlobal(_allocation);
|
|
_freed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private sealed class RelocatingHostMemory(ulong firstAddress) : IHostMemory, IDisposable
|
|
{
|
|
private bool _relocatedFirstAllocation;
|
|
|
|
public List<(ulong Address, ulong Size)> AllocationCalls { get; } = [];
|
|
|
|
public List<ulong> FreedAddresses { get; } = [];
|
|
|
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection)
|
|
{
|
|
AllocationCalls.Add((desiredAddress, size));
|
|
if (!_relocatedFirstAllocation)
|
|
{
|
|
_relocatedFirstAllocation = true;
|
|
return firstAddress + 0x1000;
|
|
}
|
|
|
|
return desiredAddress;
|
|
}
|
|
|
|
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
|
|
|
|
public bool Commit(ulong address, ulong size, HostPageProtection protection) => true;
|
|
|
|
public bool Free(ulong address)
|
|
{
|
|
FreedAddresses.Add(address);
|
|
return true;
|
|
}
|
|
|
|
public bool Protect(ulong address, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
|
{
|
|
rawOldProtection = 0;
|
|
return true;
|
|
}
|
|
|
|
public bool ProtectRaw(ulong address, ulong size, uint rawProtection, out uint rawOldProtection)
|
|
{
|
|
rawOldProtection = 0;
|
|
return true;
|
|
}
|
|
|
|
public bool Query(ulong address, out HostRegionInfo info)
|
|
{
|
|
info = default;
|
|
return false;
|
|
}
|
|
|
|
public void FlushInstructionCache(ulong address, ulong size)
|
|
{
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
|
|
private sealed class LazyHostMemory(ulong address) : IHostMemory, IDisposable
|
|
{
|
|
public bool CommitSucceeds { get; set; } = true;
|
|
|
|
public List<(ulong Address, ulong Size, HostPageProtection Protection)> CommitCalls { get; } = [];
|
|
|
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) => 0;
|
|
|
|
public ulong Reserve(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
|
desiredAddress == address ? address : 0;
|
|
|
|
public bool Commit(ulong commitAddress, ulong size, HostPageProtection protection)
|
|
{
|
|
CommitCalls.Add((commitAddress, size, protection));
|
|
return CommitSucceeds;
|
|
}
|
|
|
|
public bool Free(ulong freeAddress) => freeAddress == address;
|
|
|
|
public bool Protect(ulong protectAddress, ulong size, HostPageProtection protection, out uint rawOldProtection)
|
|
{
|
|
rawOldProtection = 0;
|
|
return true;
|
|
}
|
|
|
|
public bool ProtectRaw(ulong protectAddress, ulong size, uint rawProtection, out uint rawOldProtection)
|
|
{
|
|
rawOldProtection = 0;
|
|
return true;
|
|
}
|
|
|
|
public bool Query(ulong queryAddress, out HostRegionInfo info)
|
|
{
|
|
var pageAddress = queryAddress & ~0xFFFUL;
|
|
info = new HostRegionInfo(
|
|
pageAddress,
|
|
address,
|
|
0x1000,
|
|
HostRegionState.Reserved,
|
|
0,
|
|
HostPageProtection.NoAccess,
|
|
0,
|
|
0);
|
|
return true;
|
|
}
|
|
|
|
public void FlushInstructionCache(ulong flushAddress, ulong size)
|
|
{
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
}
|
|
}
|
|
}
|