mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 10:56:20 +08:00
memory: back the free pages of a partially-overlapping fixed mapping (#458)
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>
This commit is contained in:
@@ -425,6 +425,87 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return actualAddress;
|
||||
}
|
||||
|
||||
public bool TryBackFixedRange(ulong address, ulong size, bool executable)
|
||||
{
|
||||
if (size == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var start = AlignDown(address, PageSize);
|
||||
var end = AlignUp(address + size, PageSize);
|
||||
if (end <= start)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var hostProtection = executable ? HostPageProtection.ReadWriteExecute : HostPageProtection.ReadWrite;
|
||||
|
||||
// Walk the range page-run by page-run. VirtualQuery reports the largest run
|
||||
// of same-state pages from the queried address, so a single query advances
|
||||
// us over whole free or occupied stretches. Only free stretches get backed;
|
||||
// stretches already reserved or committed by another allocation are left as
|
||||
// they are, which is exactly what a fixed mapping does on hardware.
|
||||
var cursor = start;
|
||||
var backedAny = false;
|
||||
while (cursor < end)
|
||||
{
|
||||
if (!_hostMemory.Query(cursor, out var info))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var queriedEnd = info.RegionSize > ulong.MaxValue - info.BaseAddress
|
||||
? ulong.MaxValue
|
||||
: info.BaseAddress + info.RegionSize;
|
||||
var runEnd = Math.Min(end, queriedEnd);
|
||||
if (runEnd <= cursor)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info.State == HostRegionState.Free)
|
||||
{
|
||||
var runSize = runEnd - cursor;
|
||||
var allocated = _hostMemory.Allocate(cursor, runSize, hostProtection);
|
||||
if (allocated != cursor)
|
||||
{
|
||||
if (allocated != 0)
|
||||
{
|
||||
_hostMemory.Free(allocated);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
InsertRegionSorted(new MemoryRegion
|
||||
{
|
||||
VirtualAddress = cursor,
|
||||
Size = runSize,
|
||||
IsExecutable = executable,
|
||||
IsReservedOnly = false,
|
||||
Protection = protection
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
TraceVmem($"Backed fixed range gap: 0x{cursor:X16} - 0x{runEnd:X16} ({runSize} bytes)");
|
||||
backedAny = true;
|
||||
}
|
||||
|
||||
cursor = runEnd;
|
||||
}
|
||||
|
||||
return backedAny;
|
||||
}
|
||||
|
||||
public bool TryAllocateAtOrAbove(
|
||||
ulong desiredAddress,
|
||||
ulong size,
|
||||
|
||||
@@ -15,6 +15,17 @@ public interface IGuestAddressSpace : IGuestMemoryAllocator
|
||||
{
|
||||
ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true);
|
||||
|
||||
/// <summary>
|
||||
/// Backs an entire fixed-address range, matching the guest's
|
||||
/// <c>SCE_KERNEL_MAP_FIXED</c> contract. Unlike <see cref="AllocateAt"/>, which
|
||||
/// reserves the range in one all-or-nothing host call, this walks the range and
|
||||
/// fills only the sub-ranges that are not already backed. That keeps a fixed
|
||||
/// mapping whole when part of the requested window is already occupied — the
|
||||
/// partial-overlap case where the single-call reservation fails outright and
|
||||
/// leaves the remainder unmapped for the guest to fault into.
|
||||
/// </summary>
|
||||
bool TryBackFixedRange(ulong address, ulong size, bool executable);
|
||||
|
||||
bool TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
|
||||
|
||||
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
|
||||
|
||||
@@ -4603,7 +4603,8 @@ public static partial class KernelMemoryCompatExports
|
||||
allowSearch: false,
|
||||
allowAllocateAtAlternative: false,
|
||||
"reserve fixed range",
|
||||
out _);
|
||||
out _,
|
||||
backPartialOverlap: true);
|
||||
}
|
||||
|
||||
internal static bool IsGuestRangeBacked(CpuContext ctx, ulong address, ulong length)
|
||||
|
||||
@@ -18,7 +18,8 @@ internal static class KernelVirtualRangeAllocator
|
||||
bool allowSearch,
|
||||
bool allowAllocateAtAlternative,
|
||||
string traceName,
|
||||
out ulong mappedAddress)
|
||||
out ulong mappedAddress,
|
||||
bool backPartialOverlap = false)
|
||||
{
|
||||
mappedAddress = 0;
|
||||
if (length == 0)
|
||||
@@ -42,6 +43,18 @@ internal static class KernelVirtualRangeAllocator
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fixed mappings must cover the whole requested window even when part of
|
||||
// it is already backed by another allocation. The single-call AllocateAt
|
||||
// below is all-or-nothing and fails outright on partial overlap, leaving
|
||||
// the untouched pages unmapped for the guest to fault into. Fill the free
|
||||
// pages directly instead.
|
||||
if (backPartialOverlap &&
|
||||
addressSpace.TryBackFixedRange(desiredAddress, length, executable))
|
||||
{
|
||||
mappedAddress = desiredAddress;
|
||||
return true;
|
||||
}
|
||||
|
||||
var allocated = addressSpace.AllocateAt(desiredAddress, length, executable, allowAllocateAtAlternative);
|
||||
if (allocated == 0)
|
||||
{
|
||||
|
||||
@@ -121,6 +121,113 @@ public sealed class GuestMemoryAllocatorTests
|
||||
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) =>
|
||||
|
||||
Reference in New Issue
Block a user