#include "common/common.h" #if KYTY_PLATFORM != KYTY_PLATFORM_LINUX // #error "KYTY_PLATFORM != KYTY_PLATFORM_LINUX" #else #include "common/assert.h" #include "common/virtualMemory.h" #include #include #include #include #include #if defined(__APPLE__) #include #include #endif // IWYU pragma: no_include // IWYU pragma: no_include // IWYU pragma: no_include // IWYU pragma: no_include #if defined(MAP_FIXED_NOREPLACE) && KYTY_PLATFORM == KYTY_PLATFORM_LINUX #define KYTY_FIXED_NOREPLACE #endif namespace Common::VirtualMemory { static pthread_mutex_t g_virtual_mutex {}; static std::map* g_allocs = nullptr; void Init() { pthread_mutexattr_t attr {}; pthread_mutexattr_init(&attr); #if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__) pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_FAST_NP); // glibc-only fast mutex #else pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_NORMAL); #endif pthread_mutex_init(&g_virtual_mutex, &attr); pthread_mutexattr_destroy(&attr); g_allocs = new std::map; } static int get_protection_flag(Mode mode) { int protect = PROT_NONE; switch (mode) { case Mode::Read: protect = PROT_READ; break; case Mode::Write: case Mode::ReadWrite: protect = PROT_READ | PROT_WRITE; break; // NOLINT case Mode::Execute: protect = PROT_EXEC; break; case Mode::ExecuteRead: protect = PROT_EXEC | PROT_READ; break; // NOLINT case Mode::ExecuteWrite: case Mode::ExecuteReadWrite: protect = PROT_EXEC | PROT_WRITE | PROT_READ; break; // NOLINT case Mode::NoAccess: default: protect = PROT_NONE; break; } return protect; } // Keep automatic mappings inside the guest and GPU-addressable low window. #ifdef KYTY_FIXED_NOREPLACE static constexpr uintptr_t LOW_ARENA_LIMIT = 0x000000FC00000000ULL; // libc mspace window ceiling static constexpr uintptr_t LOW_ARENA_FLOOR = 0x000000A000000000ULL; // 640 GiB static constexpr uintptr_t LOW_ARENA_GRAIN = 0x0000000000010000ULL; // 64 KiB static_assert(LOW_ARENA_LIMIT <= 0x0000010000000000ULL, "arena must stay inside the GPU page tracker's 1<<40 window"); static_assert(LOW_ARENA_FLOOR < LOW_ARENA_LIMIT, "arena floor must sit below its ceiling"); static std::atomic g_low_arena_next {LOW_ARENA_LIMIT}; #endif // Caller holds g_virtual_mutex. static void record_alloc(uintptr_t addr, size_t size) { auto next = g_allocs->upper_bound(addr); if (next != g_allocs->begin()) { auto it = std::prev(next); const auto alloc_addr = it->first; const auto alloc_end = alloc_addr + it->second; if (alloc_addr <= addr && addr + size <= alloc_end) { g_allocs->erase(it); if (alloc_addr < addr) { (*g_allocs)[alloc_addr] = addr - alloc_addr; } if (addr + size < alloc_end) { (*g_allocs)[addr + size] = alloc_end - (addr + size); } } } (*g_allocs)[addr] = size; } #ifdef KYTY_FIXED_NOREPLACE static uintptr_t align_up_to(uintptr_t addr, uint64_t alignment) { return (addr + alignment - 1) & ~(alignment - 1); } #endif // Freed arena addresses are not reused while GPU caches remain keyed by address. static void* map_anonymous(uintptr_t addr, size_t size, int protect, int flags) { if (addr != 0) { return mmap(reinterpret_cast(addr), size, protect, flags, -1, 0); // NOLINT } #ifdef KYTY_FIXED_NOREPLACE const auto step = align_up_to(size, LOW_ARENA_GRAIN); for (int attempt = 0; attempt < 256; attempt++) { const auto top = g_low_arena_next.fetch_sub(step, std::memory_order_relaxed); if (top < step || top - step < LOW_ARENA_FLOOR) { break; } const auto hint = (top - step) & ~(LOW_ARENA_GRAIN - 1); void* ptr = mmap(reinterpret_cast(hint), size, protect, flags | MAP_FIXED_NOREPLACE, -1, 0); // NOLINT if (ptr != MAP_FAILED) { return ptr; } } #endif return mmap(nullptr, size, protect, flags, -1, 0); // NOLINT } uint64_t Alloc(uint64_t address, uint64_t size, Mode mode) { EXIT_IF(g_allocs == nullptr); auto addr = static_cast(address); int protect = get_protection_flag(mode); void* ptr = map_anonymous(addr, size, protect, MAP_PRIVATE | MAP_ANON); auto ret_addr = reinterpret_cast(ptr); if (ptr != MAP_FAILED) { pthread_mutex_lock(&g_virtual_mutex); record_alloc(ret_addr, size); pthread_mutex_unlock(&g_virtual_mutex); } return ret_addr; } static uintptr_t align_up(uintptr_t addr, uint64_t alignment) { return (addr + alignment - 1) & ~(alignment - 1); } uint64_t AllocAligned(uint64_t address, uint64_t size, Mode mode, uint64_t alignment) { if (alignment == 0) { return 0; } EXIT_IF(g_allocs == nullptr); auto addr = static_cast(address); int protect = get_protection_flag(mode); void* ptr = map_anonymous(addr, size, protect, MAP_PRIVATE | MAP_ANON); auto ret_addr = reinterpret_cast(ptr); if (ptr != MAP_FAILED && ((ret_addr & (alignment - 1)) != 0)) { munmap(ptr, size); ptr = map_anonymous(addr, size + alignment, protect, MAP_PRIVATE | MAP_ANON | MAP_NORESERVE); ret_addr = reinterpret_cast(ptr); if (ptr != MAP_FAILED) { #if defined(__APPLE__) // Carve the aligned subrange out of the live mapping with MAP_FIXED (in-place // replacement) and trim the slack; never munmap the whole range first, or a // concurrent host mapping (dyld, Rosetta, Metal) could claim the hole and be // destroyed by the MAP_FIXED. Other platforms keep the original path below. auto aligned_addr = align_up(ret_addr, alignment); // NOLINTNEXTLINE void* fixed = mmap(reinterpret_cast(aligned_addr), size, protect, MAP_FIXED | MAP_PRIVATE | MAP_ANON, -1, 0); if (fixed == MAP_FAILED) { munmap(ptr, size + alignment); ret_addr = 0; ptr = MAP_FAILED; } else { if (aligned_addr > ret_addr) { munmap(reinterpret_cast(ret_addr), aligned_addr - ret_addr); } const uintptr_t tail_start = aligned_addr + size; const uintptr_t resv_end = ret_addr + size + alignment; if (resv_end > tail_start) { munmap(reinterpret_cast(tail_start), resv_end - tail_start); } ptr = fixed; ret_addr = aligned_addr; } #else munmap(ptr, size + alignment); auto aligned_addr = align_up(ret_addr, alignment); #ifdef KYTY_FIXED_NOREPLACE // NOLINTNEXTLINE ptr = mmap(reinterpret_cast(aligned_addr), size, protect, MAP_FIXED_NOREPLACE | MAP_PRIVATE | MAP_ANON, -1, 0); #else // NOLINTNEXTLINE ptr = mmap(reinterpret_cast(aligned_addr), size, protect, MAP_FIXED | MAP_PRIVATE | MAP_ANON, -1, 0); #endif ret_addr = reinterpret_cast(ptr); if (ptr != MAP_FAILED && ((ret_addr & (alignment - 1)) != 0)) { munmap(ptr, size); ret_addr = 0; ptr = MAP_FAILED; } #endif } } if (ptr == MAP_FAILED) { return AllocAligned(address, size, mode, alignment << 1u); } pthread_mutex_lock(&g_virtual_mutex); record_alloc(ret_addr, size); pthread_mutex_unlock(&g_virtual_mutex); return ret_addr; } #if defined(__APPLE__) // macOS has no /proc/self/maps; query the Mach VM map directly. mach_vm_region returns // the first mapped region at or above `region_addr`; if it begins before the end of the // requested range, the range overlaps an existing mapping. static bool is_mapped(void* ptr, size_t length) { auto query_addr = reinterpret_cast(ptr); mach_vm_address_t region_addr = query_addr; mach_vm_size_t region_size = 0; vm_region_basic_info_data_64_t info {}; mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64; mach_port_t object_name = MACH_PORT_NULL; kern_return_t kr = mach_vm_region(mach_task_self(), ®ion_addr, ®ion_size, VM_REGION_BASIC_INFO_64, reinterpret_cast(&info), &count, &object_name); if (kr != KERN_SUCCESS) { return false; // no region at or above the address → unmapped } return region_addr < (query_addr + length); } #else static bool is_mapped(void* ptr, size_t length) { FILE* file = fopen("/proc/self/maps", "r"); char line[1024]; bool ret = false; auto addr = reinterpret_cast(ptr); while (feof(file) == 0) { if (fgets(line, 1024, file) == nullptr) { break; } uint64_t start = 0; uint64_t end = 0; // NOLINTNEXTLINE(cert-err34-c) if (sscanf(line, "%" SCNx64 "-%" SCNx64, &start, &end) != 2) { continue; } if (addr >= start && addr + length <= end) { ret = true; break; } } fclose(file); return ret; } #endif bool AllocFixed(uint64_t address, uint64_t size, Mode mode) { EXIT_IF(g_allocs == nullptr); auto addr = static_cast(address); int protect = get_protection_flag(mode); #ifdef KYTY_FIXED_NOREPLACE // NOLINTNEXTLINE void* ptr = mmap(reinterpret_cast(addr), size, protect, MAP_FIXED_NOREPLACE | MAP_PRIVATE | MAP_ANON, -1, 0); #else // NOLINTNEXTLINE void* ptr = (is_mapped(reinterpret_cast(addr), size) ? MAP_FAILED : mmap(reinterpret_cast(addr), size, protect, MAP_FIXED | MAP_PRIVATE | MAP_ANON, -1, 0)); #endif auto ret_addr = reinterpret_cast(ptr); if (ptr != MAP_FAILED && ret_addr != addr) { munmap(ptr, size); ret_addr = 0; ptr = MAP_FAILED; } if (ptr != MAP_FAILED) { pthread_mutex_lock(&g_virtual_mutex); record_alloc(ret_addr, size); pthread_mutex_unlock(&g_virtual_mutex); return true; } return false; } bool Commit(uint64_t address, uint64_t size, Mode mode) { return Protect(address, size, mode); } uint64_t Reserve(uint64_t address, uint64_t size) { return ReserveAligned(address, size, 1); } uint64_t ReserveAligned(uint64_t address, uint64_t size, uint64_t alignment) { if (alignment == 0) { return 0; } EXIT_IF(g_allocs == nullptr); auto addr = static_cast(address); void* ptr = map_anonymous(addr, size, PROT_NONE, MAP_PRIVATE | MAP_ANON | MAP_NORESERVE); auto ret_addr = reinterpret_cast(ptr); if (ptr != MAP_FAILED && ((ret_addr & (alignment - 1)) != 0)) { munmap(ptr, size); ptr = map_anonymous(addr, size + alignment, PROT_NONE, MAP_PRIVATE | MAP_ANON | MAP_NORESERVE); ret_addr = reinterpret_cast(ptr); if (ptr != MAP_FAILED) { #if defined(__APPLE__) // Carve the aligned subrange out of the live reservation with MAP_FIXED (an // in-place replacement), then trim the slack. The range must never be // returned to the OS in between: another thread (dyld, Rosetta, Metal, // malloc) could claim the hole, and the subsequent MAP_FIXED would silently // destroy its mapping. Other platforms keep the original path below. auto aligned_addr = align_up(ret_addr, alignment); // NOLINTNEXTLINE void* fixed = mmap(reinterpret_cast(aligned_addr), size, PROT_NONE, MAP_FIXED | MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0); if (fixed == MAP_FAILED) { munmap(ptr, size + alignment); ret_addr = 0; ptr = MAP_FAILED; } else { if (aligned_addr > ret_addr) { munmap(reinterpret_cast(ret_addr), aligned_addr - ret_addr); } const uintptr_t tail_start = aligned_addr + size; const uintptr_t resv_end = ret_addr + size + alignment; if (resv_end > tail_start) { munmap(reinterpret_cast(tail_start), resv_end - tail_start); } ptr = fixed; ret_addr = aligned_addr; } #else munmap(ptr, size + alignment); auto aligned_addr = align_up(ret_addr, alignment); #ifdef KYTY_FIXED_NOREPLACE // NOLINTNEXTLINE ptr = mmap(reinterpret_cast(aligned_addr), size, PROT_NONE, MAP_FIXED_NOREPLACE | MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0); #else // NOLINTNEXTLINE ptr = mmap(reinterpret_cast(aligned_addr), size, PROT_NONE, MAP_FIXED | MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0); #endif ret_addr = reinterpret_cast(ptr); if (ptr != MAP_FAILED && ((ret_addr & (alignment - 1)) != 0)) { munmap(ptr, size); ret_addr = 0; ptr = MAP_FAILED; } #endif } } if (ptr == MAP_FAILED) { return ReserveAligned(address, size, alignment << 1u); } pthread_mutex_lock(&g_virtual_mutex); record_alloc(ret_addr, size); pthread_mutex_unlock(&g_virtual_mutex); return ret_addr; } bool ReserveFixed(uint64_t address, uint64_t size) { EXIT_IF(g_allocs == nullptr); auto addr = static_cast(address); #ifdef KYTY_FIXED_NOREPLACE // NOLINTNEXTLINE void* ptr = mmap(reinterpret_cast(addr), size, PROT_NONE, MAP_FIXED_NOREPLACE | MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0); #else // NOLINTNEXTLINE void* ptr = (is_mapped(reinterpret_cast(addr), size) ? MAP_FAILED : mmap(reinterpret_cast(addr), size, PROT_NONE, MAP_FIXED | MAP_PRIVATE | MAP_ANON | MAP_NORESERVE, -1, 0)); #endif auto ret_addr = reinterpret_cast(ptr); if (ptr != MAP_FAILED && ret_addr != addr) { munmap(ptr, size); ret_addr = 0; ptr = MAP_FAILED; } if (ptr != MAP_FAILED) { pthread_mutex_lock(&g_virtual_mutex); record_alloc(ret_addr, size); pthread_mutex_unlock(&g_virtual_mutex); return true; } return false; } bool Decommit(uint64_t address, uint64_t size) { // Drop physical pages while preserving the reservation. if (!Protect(address, size, Mode::NoAccess)) { return false; } if (size != 0) { #if defined(__APPLE__) constexpr int RECLAIM_ADVICE = MADV_FREE; #else constexpr int RECLAIM_ADVICE = MADV_DONTNEED; #endif const auto page_size = static_cast(sysconf(_SC_PAGESIZE)); if (page_size != 0) { // Do not discard pages outside the requested range. const auto begin = (static_cast(address) + page_size - 1) & ~(page_size - 1); const auto end = (static_cast(address) + size) & ~(page_size - 1); if (end > begin) { ::madvise(reinterpret_cast(begin), end - begin, RECLAIM_ADVICE); } } } return true; } bool Free(uint64_t address) { EXIT_IF(g_allocs == nullptr); size_t size = 0; auto addr = static_cast(address & ~static_cast(0xfffu)); pthread_mutex_lock(&g_virtual_mutex); if (auto s = g_allocs->find(addr); s != g_allocs->end()) { size = s->second; g_allocs->erase(s); } pthread_mutex_unlock(&g_virtual_mutex); if (size == 0) { return false; } return munmap(reinterpret_cast(addr), size) == 0; } bool FreeRange(uint64_t address, uint64_t size) { EXIT_IF(g_allocs == nullptr); if (size == 0 || (address & 0xfffu) != 0 || (size & 0xfffu) != 0) { return false; } const auto addr = static_cast(address); const auto end = addr + size; if (end < addr) { return false; } pthread_mutex_lock(&g_virtual_mutex); auto next = g_allocs->upper_bound(addr); if (next == g_allocs->begin()) { pthread_mutex_unlock(&g_virtual_mutex); return false; } // A reservation may have been split into several adjacent records. auto first = std::prev(next); const auto alloc_addr = first->first; if (addr < alloc_addr || alloc_addr + first->second <= addr) { pthread_mutex_unlock(&g_virtual_mutex); return false; } auto last = first; uintptr_t cursor = alloc_addr + first->second; while (cursor < end) { auto following = std::next(last); if (following == g_allocs->end() || following->first != cursor) { pthread_mutex_unlock(&g_virtual_mutex); return false; } last = following; cursor = following->first + following->second; } const auto alloc_end = cursor; if (munmap(reinterpret_cast(addr), size) != 0) { pthread_mutex_unlock(&g_virtual_mutex); return false; } g_allocs->erase(first, std::next(last)); if (alloc_addr < addr) { (*g_allocs)[alloc_addr] = addr - alloc_addr; } if (end < alloc_end) { (*g_allocs)[end] = alloc_end - end; } pthread_mutex_unlock(&g_virtual_mutex); return true; } bool Protect(uint64_t address, uint64_t size, Mode mode) { const auto addr = static_cast(address); const auto page_start = addr >> 12u; const auto page_end = (addr + size - 1) >> 12u; return mprotect(reinterpret_cast(page_start << 12u), (page_end - page_start + 1) << 12u, get_protection_flag(mode)) == 0; } bool FlushInstructionCache(uint64_t /*address*/, uint64_t /*size*/) { return true; } } // namespace Common::VirtualMemory #endif