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
@@ -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) =>