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:
Slick Daddy
2026-07-20 09:08:29 +03:00
committed by GitHub
parent 184e24fbb6
commit 33be88bdf9
5 changed files with 215 additions and 2 deletions
@@ -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,
+11
View File
@@ -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) =>