using System; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using MonoMod.Utils; namespace BepInEx.IL2CPP.Allocator { /// /// Based on https://github.com/kubo/funchook /// internal abstract class UnixPageAllocator : PageAllocator { protected abstract IEnumerable<(IntPtr, IntPtr)> MapMemoryAreas(); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool CheckFreeRegionBefore(IntPtr start, IntPtr hint, ref (IntPtr Start, IntPtr End) region) { if (start.ToInt64() < hint.ToInt64()) { var addr = start - PAGE_SIZE; if (hint.ToInt64() - addr.ToInt64() < int.MaxValue) region.Start = addr; } return false; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool CheckFreeRegionAfter(IntPtr end, IntPtr hint, ref (IntPtr Start, IntPtr End) region) { if (hint.ToInt64() < end.ToInt64()) { if (end.ToInt64() - hint.ToInt64() < int.MaxValue) region.End = end; return true; } return false; } private (IntPtr, IntPtr) GetFreeArea(IntPtr hint) { var result = (IntPtr.Zero, IntPtr.Zero); var prevEnd = IntPtr.Zero; foreach (var (start, end) in MapMemoryAreas()) { if ((prevEnd + PAGE_SIZE).ToInt64() <= start.ToInt64()) if (CheckFreeRegionBefore(start, hint, ref result) || CheckFreeRegionAfter(prevEnd, hint, ref result)) return result; prevEnd = end; } if (CheckFreeRegionAfter(prevEnd, hint, ref result)) return result; throw new PageAllocatorException($"Could not find free region near {hint.ToInt64():X8}"); } protected override IntPtr AllocateChunk(IntPtr hint) { /* From https://github.com/kubo/funchook/blob/master/src/funchook_unix.c#L251-L254: * Loop three times just to avoid rare cases such as * unused memory region is used between 'get_free_address()' * and 'mmap()'. */ const int retryCount = 3; for (var attempt = 0; attempt < retryCount; attempt++) { var (start, end) = GetFreeArea(hint); // Try to allocate to end (after original method) first, then try before var addrs = new [] { end, start }; foreach (var addr in addrs) { if (addr == IntPtr.Zero) continue; var result = Unix.mmap(addr, (UIntPtr)PAGE_SIZE, Unix.Protection.PROT_READ | Unix.Protection.PROT_WRITE, Unix.MapFlags.MAP_PRIVATE | Unix.MapFlags.MAP_ANONYMOUS, -1, 0); if (result == addr) return result; if (result == Unix.MAP_FAILED) throw new Win32Exception(Marshal.GetLastWin32Error()); // Yes, this should work on unix too Unix.munmap(result, (UIntPtr)PAGE_SIZE); } } throw new PageAllocatorException("Failed to allocate memory in unused regions"); } private static class Unix { public static readonly IntPtr MAP_FAILED = new IntPtr(-1); [DynDllImport("mmap")] public static mmapDelegate mmap; [DynDllImport("munmap")] public static munmapDelegate munmap; public delegate IntPtr mmapDelegate(IntPtr addr, UIntPtr length, Protection prot, MapFlags flags, int fd, int offset); public delegate int munmapDelegate(IntPtr addr, UIntPtr length); [Flags] public enum MapFlags { MAP_PRIVATE = 0x02, MAP_ANONYMOUS = 0x20 } [Flags] public enum Protection { PROT_READ = 0x1, PROT_WRITE = 0x2 } static Unix() { typeof(Unix).ResolveDynDllImports(new Dictionary> { ["libc"] = new List { "libc.so.6", // Ubuntu glibc "libc", // Linux glibc, "/usr/lib/libSystem.dylib" // OSX POSIX } }); } } } }