mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-22 19:06:15 +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;
|
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(
|
public bool TryAllocateAtOrAbove(
|
||||||
ulong desiredAddress,
|
ulong desiredAddress,
|
||||||
ulong size,
|
ulong size,
|
||||||
|
|||||||
@@ -15,6 +15,17 @@ public interface IGuestAddressSpace : IGuestMemoryAllocator
|
|||||||
{
|
{
|
||||||
ulong AllocateAt(ulong desiredAddress, ulong size, bool executable = true, bool allowAlternative = true);
|
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 TryAllocateAtOrAbove(ulong desiredAddress, ulong size, bool executable, ulong alignment, out ulong actualAddress);
|
||||||
|
|
||||||
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
|
bool TryProtect(ulong address, ulong size, GuestPageProtection protection);
|
||||||
|
|||||||
@@ -4603,7 +4603,8 @@ public static partial class KernelMemoryCompatExports
|
|||||||
allowSearch: false,
|
allowSearch: false,
|
||||||
allowAllocateAtAlternative: false,
|
allowAllocateAtAlternative: false,
|
||||||
"reserve fixed range",
|
"reserve fixed range",
|
||||||
out _);
|
out _,
|
||||||
|
backPartialOverlap: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool IsGuestRangeBacked(CpuContext ctx, ulong address, ulong length)
|
internal static bool IsGuestRangeBacked(CpuContext ctx, ulong address, ulong length)
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ internal static class KernelVirtualRangeAllocator
|
|||||||
bool allowSearch,
|
bool allowSearch,
|
||||||
bool allowAllocateAtAlternative,
|
bool allowAllocateAtAlternative,
|
||||||
string traceName,
|
string traceName,
|
||||||
out ulong mappedAddress)
|
out ulong mappedAddress,
|
||||||
|
bool backPartialOverlap = false)
|
||||||
{
|
{
|
||||||
mappedAddress = 0;
|
mappedAddress = 0;
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
@@ -42,6 +43,18 @@ internal static class KernelVirtualRangeAllocator
|
|||||||
return true;
|
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);
|
var allocated = addressSpace.AllocateAt(desiredAddress, length, executable, allowAllocateAtAlternative);
|
||||||
if (allocated == 0)
|
if (allocated == 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -121,6 +121,113 @@ public sealed class GuestMemoryAllocatorTests
|
|||||||
Assert.Equal([alignedAddress + 0x1000], host.FreedAddresses);
|
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
|
private sealed class FakeHostMemory : IHostMemory
|
||||||
{
|
{
|
||||||
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
public ulong Allocate(ulong desiredAddress, ulong size, HostPageProtection protection) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user