[Memory] Pre-reserve the guest address space on POSIX hosts (#905)

* [Memory] Reserve the guest address space again after an image reset

SelfLoader clears virtual memory before loading the eboot, which released the
startup pre-reservation of 0x10_0000_0000..0xFC_0000_0000. Every later large
reservation then had to probe the host address space candidate by candidate.

* [Memory] Pre-reserve the guest address space on POSIX hosts

POSIX had no ReserveFreeAddressRanges and cannot query foreign host mappings,
so TryHoldRangeAtOrAbove stepped through the address space one alignment unit
at a time. On macOS a 512 GiB reservation (all UE titles) took ~196k failed
mmap/munmap pairs before reaching free space, which the stall watchdog reported
as a hang in sceKernelReserveVirtualRange.

Find the free ranges by bisection, and use the first pre-reserved candidate
once a lower candidate fails.
This commit is contained in:
kuba 2026-09-26 02:06:25 +02:00 committed by GitHub
parent f98ff990ec
commit 1759e18b77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 140 additions and 8 deletions

View File

@ -1276,6 +1276,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return true;
}
// POSIX cannot query foreign host mappings. The free ranges were
// reserved at startup, so a failed candidate means the gap is taken.
if (!OperatingSystem.IsWindows() && reservedCandidate != 0)
{
address = reservedCandidate;
return true;
}
start = candidate + GuestMemoryLayout.GuestPage;
if (OperatingSystem.IsWindows() && _hostMemory.Query(candidate, out var info) &&
info.BaseAddress <= candidate && info.BaseAddress < limit &&

View File

@ -30,6 +30,7 @@ public sealed class GuestSpaceOwner : IDisposable
private readonly SortedList<ulong, ulong> _free = new();
private readonly SortedList<ulong, OwnedRange> _mapped = new();
private readonly List<(ulong Address, ulong Size)> _owned = new();
private readonly bool _preReserveGuestAddressSpace;
private bool _disposed;
public GuestSpaceOwner(IHostViewMemory host, ulong backingSize, bool preReserveGuestAddressSpace = false)
@ -39,14 +40,8 @@ public sealed class GuestSpaceOwner : IDisposable
// Create lookup views before concurrent fault handlers can read the range table.
_ = _mapped.Keys;
_ = _mapped.Values;
if (preReserveGuestAddressSpace)
{
foreach (var range in _host.ReserveFreeAddressRanges(UserAddressStart, UserAddressEnd, MinimumPreReservedRange))
{
_owned.Add((range.Address, range.Size));
AddFreeRange(range.Address, range.Size);
}
}
_preReserveGuestAddressSpace = preReserveGuestAddressSpace;
PreReserveGuestAddressSpace();
_views = new SharedBackingViews(host, backingSize);
if (!_views.IsAvailable)
{
@ -495,6 +490,22 @@ public sealed class GuestSpaceOwner : IDisposable
_owned.Clear();
_free.Clear();
_mapped.Clear();
// A new image load needs the guest address space reserved again.
PreReserveGuestAddressSpace();
}
}
private void PreReserveGuestAddressSpace()
{
if (!_preReserveGuestAddressSpace)
{
return;
}
foreach (var range in _host.ReserveFreeAddressRanges(UserAddressStart, UserAddressEnd, MinimumPreReservedRange))
{
_owned.Add((range.Address, range.Size));
AddFreeRange(range.Address, range.Size);
}
}

View File

@ -96,6 +96,60 @@ internal sealed unsafe partial class PosixHostViews : IHostViewMemory
return address;
}
// POSIX has no VirtualQuery for foreign host mappings, so find the free
// ranges by reserving halves until each attempt fits or falls below the
// minimum. Occupied host ranges are few, so this needs few attempts.
public IReadOnlyList<HostAddressRange> ReserveFreeAddressRanges(ulong start, ulong end, ulong minimumSize)
{
var reserved = new List<HostAddressRange>();
start = AlignUp(start, Granularity);
end -= end % Granularity;
if (start >= end || minimumSize == 0)
{
return reserved;
}
lock (PosixViewRegions.Gate)
{
ReserveFreeAddressRangesLocked(start, end - start, Math.Max(minimumSize, Granularity), reserved);
}
return reserved;
}
private void ReserveFreeAddressRangesLocked(ulong address, ulong size, ulong minimumSize, List<HostAddressRange> reserved)
{
if (ReserveHoleCore(address, size) == address)
{
PosixViewRegions.Replace(address, size, HostMemory.MEM_RESERVE, HostMemory.PAGE_NOACCESS);
if (reserved.Count != 0 && reserved[^1].Address + reserved[^1].Size == address)
{
reserved[^1] = reserved[^1] with { Size = reserved[^1].Size + size };
}
else
{
reserved.Add(new HostAddressRange(address, size));
}
return;
}
var half = size / 2;
half -= half % Granularity;
if (half < minimumSize)
{
return;
}
ReserveFreeAddressRangesLocked(address, half, minimumSize, reserved);
ReserveFreeAddressRangesLocked(address + half, size - half, minimumSize, reserved);
}
private static ulong AlignUp(ulong value, ulong alignment)
{
var remainder = value % alignment;
return remainder == 0 ? value : value + alignment - remainder;
}
public bool SplitHole(ulong address, ulong size) => size != 0;
public bool JoinHoles(ulong address, ulong size) => size != 0;

View File

@ -422,4 +422,26 @@ public sealed unsafe partial class GuestSpaceOwnerTests
Assert.Equal(baseAddress, raw.ReserveHole(baseAddress, hole));
Assert.True(raw.FreeHole(baseAddress, hole));
}
[Fact]
public void ReleaseAddressRanges_ReservesTheGuestAddressSpaceAgain()
{
if (!Supported)
{
return;
}
const ulong start = 0x10_0000_0000;
const ulong end = 0xFC_0000_0000;
const ulong size = 0x10_0000_0000;
const ulong alignment = 0x20_0000;
using var owner = new GuestSpaceOwner(HostViewMemory.Create(), BackingSize, preReserveGuestAddressSpace: true);
Assert.NotEqual(0UL, owner.FindFreeAddress(start, end, size, alignment));
owner.ReleaseAddressRanges();
var address = owner.FindFreeAddress(start, end, size, alignment);
Assert.NotEqual(0UL, address);
Assert.True(owner.ContainsFreeRange(address, size));
}
}

View File

@ -346,6 +346,43 @@ public sealed unsafe class HostViewMemoryTests
public uint Alignment2;
}
[Fact]
public void ReserveFreeAddressRanges_SkipsOccupiedHostRanges()
{
if (!Supported)
{
return;
}
var views = HostViewMemory.Create();
const ulong span = 0x400_0000;
const ulong minimum = 0x10_0000;
var start = ProbeGuestAddress(views, span);
var blocker = start + span / 4;
var blockerSize = AlignUp(minimum, views.Granularity);
Assert.Equal(blocker, views.ReserveHole(blocker, blockerSize));
var ranges = views.ReserveFreeAddressRanges(start, start + span, minimum);
try
{
Assert.NotEmpty(ranges);
Assert.Equal(start, ranges[0].Address);
Assert.All(ranges, range => Assert.True(
range.Address + range.Size <= blocker || range.Address >= blocker + blockerSize));
var reserved = ranges.Aggregate(0UL, (total, range) => total + range.Size);
Assert.True(reserved >= span - blockerSize - 2 * minimum, $"Only 0x{reserved:X} bytes were reserved.");
}
finally
{
foreach (var range in ranges)
{
Assert.True(views.FreeHole(range.Address, range.Size));
}
Assert.True(views.FreeHole(blocker, blockerSize));
}
}
[DllImport("kernel32.dll")]
private static extern nuint VirtualQuery(void* address, out MemoryBasicInformation info, nuint length);
}