// Internal memory address-space helpers included by memory.cpp. // Kept in the same translation unit because they depend on memory.cpp-local helpers. static constexpr uint64_t HOST_SYSTEM_MANAGED_MIN = 0x0000040000ull; static constexpr uint64_t HOST_SYSTEM_MANAGED_MAX = 0x07ffffbfffull; static constexpr uint64_t HOST_SYSTEM_RESERVED_MIN = HOST_SYSTEM_MANAGED_MAX + 1u; #if defined(__APPLE__) && (defined(__x86_64__) || defined(_M_X64)) static constexpr uint64_t HOST_SYSTEM_RESERVED_MAX = 0x0fbfffffffull; static constexpr uint64_t HOST_USER_MIN = 0x7000000000ull; #else static constexpr uint64_t HOST_SYSTEM_RESERVED_MAX = 0x0fffffffffull; static constexpr uint64_t HOST_USER_MIN = 0x1000000000ull; #endif static constexpr uint64_t HOST_USER_MAX = 0xfbffffffffull; static bool GuestFreeRangeContains(uint64_t base, uint64_t range_size, uint64_t candidate, uint64_t size) { if (UINT64_MAX - base < range_size || candidate < base) { return false; } const auto range_end = base + range_size; return candidate <= range_end && size <= range_end - candidate; } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS static bool CoalescePlaceholders(uint64_t vaddr, uint64_t size) { return size != 0 && VirtualFree(reinterpret_cast(vaddr), size, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS) != 0; } #endif class GuestBackingStore { public: enum class FailureReason { None, BackingUnavailable, PhysicalRangeOutOfBounds, FixedMapViewFailed, PlaceholderMapFailed, ProtectFailed, WrongHostAddress, AddressRangeUnavailable, }; explicit GuestBackingStore(uint64_t size): m_size(size) { #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS m_handle = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, static_cast(m_size >> 32u), static_cast(m_size & 0xffffffffu), nullptr); if (m_handle == nullptr) { LOGF_COLOR(Log::Color::Red, "\t direct-memory backing: CreateFileMapping failed: 0x%08" PRIx32 "\n", static_cast(GetLastError())); return; } m_backing_base = static_cast( MapViewOfFile(m_handle, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, m_size)); if (m_backing_base == nullptr) { LOGF_COLOR(Log::Color::Red, "\t direct-memory backing: writable alias failed: 0x%08" PRIx32 "\n", static_cast(GetLastError())); CloseHandle(m_handle); m_handle = nullptr; } #elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX #if defined(__APPLE__) { char shm_name[64]; snprintf(shm_name, sizeof(shm_name), "/kyty-dm-%d-%llx", static_cast(getpid()), static_cast(reinterpret_cast(this))); m_fd = shm_open(shm_name, O_RDWR | O_CREAT | O_EXCL, 0600); if (m_fd >= 0) { shm_unlink(shm_name); } } #else m_fd = static_cast(syscall(SYS_memfd_create, "KytyDirectMemory", 0)); #endif if (m_fd < 0) { LOGF_COLOR(Log::Color::Red, "\t direct-memory backing: shared fd creation failed: %d\n", errno); return; } if (ftruncate(m_fd, static_cast(m_size)) != 0) { LOGF_COLOR(Log::Color::Red, "\t direct-memory backing: ftruncate failed: %d\n", errno); close(m_fd); m_fd = -1; return; } auto* backing = mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0); if (backing == MAP_FAILED) { LOGF_COLOR(Log::Color::Red, "\t direct-memory backing: writable alias failed: %d\n", errno); close(m_fd); m_fd = -1; return; } m_backing_base = static_cast(backing); #endif } ~GuestBackingStore() { #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS for (const auto& [_, entry]: m_maps) { UnmapViewPreservePlaceholder(reinterpret_cast(entry.vaddr), entry.size); } m_maps.clear(); if (m_backing_base != nullptr) { UnmapViewOfFile(m_backing_base); } if (m_handle != nullptr) { CloseHandle(m_handle); } #elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX for (const auto& [_, entry]: m_maps) { EXIT_IF( !UnmapViewPreservePlaceholder(reinterpret_cast(entry.vaddr), entry.size)); } m_maps.clear(); if (m_backing_base != nullptr) { munmap(m_backing_base, m_size); } if (m_fd >= 0) { close(m_fd); } #endif } KYTY_CLASS_NO_COPY(GuestBackingStore); static const char* GetFailureReasonName(FailureReason reason) { switch (reason) { case FailureReason::None: return "n/a"; case FailureReason::BackingUnavailable: return "backing-unavailable"; case FailureReason::PhysicalRangeOutOfBounds: return "physical-range-out-of-bounds"; case FailureReason::FixedMapViewFailed: return "fixed-map-view-failed"; case FailureReason::PlaceholderMapFailed: return "placeholder-map-failed"; case FailureReason::ProtectFailed: return "protect-failed"; case FailureReason::WrongHostAddress: return "wrong-host-address"; case FailureReason::AddressRangeUnavailable: return "address-range-unavailable"; default: return "unknown"; } } [[nodiscard]] bool IsAvailable() const { #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS return m_handle != nullptr && m_backing_base != nullptr; #elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX return m_fd >= 0 && m_backing_base != nullptr; #else return false; #endif } [[nodiscard]] uint64_t GetBackingBase() const { return reinterpret_cast(m_backing_base); } [[nodiscard]] uint64_t GetBackingSize() const { return m_size; } bool ZeroRange(uint64_t backing_offset, uint64_t size) { if (!IsAvailable() || !RangeOk(backing_offset, size)) { return false; } std::memset(m_backing_base + backing_offset, 0, size); return true; } bool TryWriteBacking(uint64_t vaddr, const void* data, uint64_t size) { if (data == nullptr) { return false; } const auto* source = static_cast(data); return TryTransferBacking(vaddr, size, [source](uint8_t* backing, uint64_t data_offset, size_t bytes) { std::memcpy(backing, source + data_offset, bytes); }); } bool TryReadBacking(uint64_t vaddr, void* data, uint64_t size) { if (data == nullptr) { return false; } auto* destination = static_cast(data); return TryTransferBacking( vaddr, size, [destination](uint8_t* backing, uint64_t data_offset, size_t bytes) { std::memcpy(destination + data_offset, backing, bytes); }); } bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size) { if (!IsAvailable() || data == nullptr || size == 0 || UINT64_MAX - vaddr < size) { return false; } std::lock_guard lock(m_mutex); const auto end = vaddr + size; auto first = m_maps.lower_bound(vaddr); if (first != m_maps.begin()) { auto previous = std::prev(first); if (previous->second.vaddr + previous->second.size > vaddr) { first = previous; } } for (auto mapping = first; mapping != m_maps.end() && mapping->second.vaddr < end; ++mapping) { const auto& entry = mapping->second; const auto start = std::max(vaddr, entry.vaddr); const auto bytes = std::min(end, entry.vaddr + entry.size) - start; if (!RangeOk(entry.backing_offset + start - entry.vaddr, bytes)) { return false; } } auto* destination = static_cast(data); std::memset(destination, 0, size); for (auto mapping = first; mapping != m_maps.end() && mapping->second.vaddr < end; ++mapping) { const auto& entry = mapping->second; const auto start = std::max(vaddr, entry.vaddr); const auto bytes = std::min(end, entry.vaddr + entry.size) - start; const auto offset = entry.backing_offset + start - entry.vaddr; std::memcpy(destination + start - vaddr, m_backing_base + offset, bytes); } return true; } bool MapExistingPlaceholderFixed(uint64_t vaddr, uint64_t size, uint64_t backing_offset, VirtualMemory::Mode mode, FailureReason* failure_reason = nullptr) { if (!IsAvailable() || !RangeOk(backing_offset, size)) { SetFailure(failure_reason, !IsAvailable() ? FailureReason::BackingUnavailable : FailureReason::PhysicalRangeOutOfBounds); return false; } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS void* mapped = ReplacePlaceholderView(vaddr, size, backing_offset, mode, failure_reason); if (mapped == nullptr || reinterpret_cast(mapped) != vaddr) { if (mapped != nullptr) { UnmapViewPreservePlaceholder(mapped, size); SetFailure(failure_reason, FailureReason::WrongHostAddress); } return false; } { std::lock_guard lock(m_mutex); EXIT_IF(!m_maps.emplace(vaddr, MapEntry {vaddr, size, backing_offset, mode}).second); } SetFailure(failure_reason, FailureReason::None); return true; #else void* mapped = MapView(vaddr, size, backing_offset, mode, failure_reason); if (mapped == nullptr || reinterpret_cast(mapped) != vaddr) { return false; } { std::lock_guard lock(m_mutex); EXIT_IF(!m_maps.emplace(vaddr, MapEntry {vaddr, size, backing_offset, mode}).second); } SetFailure(failure_reason, FailureReason::None); return true; #endif } bool Unmap(uint64_t vaddr, uint64_t size, bool* placeholder_preserved = nullptr) { if (size == 0 || UINT64_MAX - vaddr < size) { return false; } if (placeholder_preserved != nullptr) { *placeholder_preserved = false; } const auto end = vaddr + size; std::vector targets; { std::lock_guard lock(m_mutex); auto current = vaddr; while (current < end) { MapEntry entry {}; if (!FindContainingUnlocked(current, 1, &entry)) { return false; } const auto part_size = std::min(end, entry.vaddr + entry.size) - current; targets.push_back( {current, part_size, entry.backing_offset + current - entry.vaddr, entry.mode}); current += part_size; } } std::vector removed; removed.reserve(targets.size()); for (const auto& entry: targets) { bool part_placeholder_preserved = false; if (!UnmapOne(entry.vaddr, entry.size, &part_placeholder_preserved)) { for (auto it = removed.rbegin(); it != removed.rend(); ++it) { EXIT_IF(!MapExistingPlaceholderFixed(it->vaddr, it->size, it->backing_offset, it->mode)); } if (placeholder_preserved != nullptr) { *placeholder_preserved = false; } return false; } EXIT_IF(!part_placeholder_preserved); removed.push_back(entry); } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS // Each unmapped file view becomes a separate Windows placeholder. The address-space owner // records this contiguous guest span as one free range, so normalize fragmented backing // views into one placeholder before publishing it there. if (removed.size() > 1 && !CoalescePlaceholders(vaddr, size)) { for (auto it = removed.rbegin(); it != removed.rend(); ++it) { EXIT_IF(!MapExistingPlaceholderFixed(it->vaddr, it->size, it->backing_offset, it->mode)); } return false; } #endif if (placeholder_preserved != nullptr) { *placeholder_preserved = true; } return true; } bool UnmapOne(uint64_t vaddr, uint64_t size, bool* placeholder_preserved = nullptr) { #if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS) if (g_test_backing_store_unmaps_before_failure == 0) { g_test_backing_store_unmaps_before_failure = UINT32_MAX; return false; } if (g_test_backing_store_unmaps_before_failure != UINT32_MAX) { g_test_backing_store_unmaps_before_failure--; } #endif MapEntry old {}; { std::lock_guard lock(m_mutex); auto next = m_maps.upper_bound(vaddr); if (next != m_maps.begin()) { auto entry = std::prev(next); if (vaddr + size <= entry->second.vaddr + entry->second.size) { old = entry->second; m_maps.erase(entry); } } } if (old.size == 0) { return false; } const bool exact_unmap = (vaddr == old.vaddr && size == old.size); if (exact_unmap) { const bool unmapped = UnmapViewPreservePlaceholder(reinterpret_cast(old.vaddr), old.size); if (!unmapped) { std::lock_guard lock(m_mutex); m_maps.emplace(old.vaddr, old); return false; } if (placeholder_preserved != nullptr) { *placeholder_preserved = true; } return true; } const uint64_t left_size = vaddr - old.vaddr; const uint64_t right_addr = vaddr + size; const uint64_t old_end = old.vaddr + old.size; const uint64_t right_size = old_end - right_addr; #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS if (UnmapViewPreservePlaceholder(reinterpret_cast(old.vaddr), old.size)) { bool ok = true; if (left_size != 0 && left_size != old.size) { ok = SplitPlaceholder(old.vaddr, left_size) && ok; } if (right_size != 0) { ok = SplitPlaceholder(vaddr, size) && ok; } if (left_size != 0) { ok = MapExistingPlaceholderFixed(old.vaddr, left_size, old.backing_offset, old.mode) && ok; } if (right_size != 0) { ok = MapExistingPlaceholderFixed(right_addr, right_size, old.backing_offset + (right_addr - old.vaddr), old.mode) && ok; } if (ok) { if (placeholder_preserved != nullptr) { *placeholder_preserved = true; } return true; } if (left_size != 0 && Contains(old.vaddr, left_size)) { EXIT_IF(!UnmapOne(old.vaddr, left_size)); } if (right_size != 0 && Contains(right_addr, right_size)) { EXIT_IF(!UnmapOne(right_addr, right_size)); } EXIT_IF(VirtualFree(reinterpret_cast(old.vaddr), old.size, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS) == 0); EXIT_IF( !MapExistingPlaceholderFixed(old.vaddr, old.size, old.backing_offset, old.mode)); return false; } std::lock_guard lock(m_mutex); m_maps.emplace(old.vaddr, old); return false; #else if (!UnmapViewPreservePlaceholder(reinterpret_cast(old.vaddr), old.size)) { std::lock_guard lock(m_mutex); m_maps.emplace(old.vaddr, old); return false; } bool ok = true; if (left_size != 0) { ok = MapExistingPlaceholderFixed(old.vaddr, left_size, old.backing_offset, old.mode) && ok; } if (right_size != 0) { ok = MapExistingPlaceholderFixed(right_addr, right_size, old.backing_offset + (right_addr - old.vaddr), old.mode) && ok; } if (!ok) { if (left_size != 0 && Contains(old.vaddr, left_size)) { EXIT_IF(!UnmapOne(old.vaddr, left_size)); } if (right_size != 0 && Contains(right_addr, right_size)) { EXIT_IF(!UnmapOne(right_addr, right_size)); } EXIT_IF( !MapExistingPlaceholderFixed(old.vaddr, old.size, old.backing_offset, old.mode)); return false; } if (placeholder_preserved != nullptr) { *placeholder_preserved = true; } return ok; #endif } bool Contains(uint64_t vaddr, uint64_t size) { if (size == 0 || UINT64_MAX - vaddr < size) { return false; } std::lock_guard lock(m_mutex); const auto end = vaddr + size; auto current = vaddr; while (current < end) { MapEntry entry {}; if (!FindContainingUnlocked(current, 1, &entry)) { return false; } const auto entry_end = entry.vaddr + entry.size; if (entry_end <= current) { return false; } current = std::min(end, entry_end); } return true; } private: template bool TryTransferBacking(uint64_t vaddr, uint64_t size, Transfer transfer) { if (!IsAvailable() || size == 0 || UINT64_MAX - vaddr < size) { return false; } std::lock_guard lock(m_mutex); const auto end = vaddr + size; auto current = vaddr; while (current < end) { MapEntry entry {}; if (!FindContainingUnlocked(current, 1, &entry)) { return false; } const auto bytes = std::min(end, entry.vaddr + entry.size) - current; if (!RangeOk(entry.backing_offset + current - entry.vaddr, bytes)) { return false; } current += bytes; } current = vaddr; while (current < end) { MapEntry entry {}; EXIT_IF(!FindContainingUnlocked(current, 1, &entry)); const auto bytes = std::min(end, entry.vaddr + entry.size) - current; const auto offset = entry.backing_offset + current - entry.vaddr; transfer(m_backing_base + offset, current - vaddr, static_cast(bytes)); current += bytes; } return true; } struct MapEntry { uint64_t vaddr = 0; uint64_t size = 0; uint64_t backing_offset = 0; VirtualMemory::Mode mode = VirtualMemory::Mode::NoAccess; }; bool FindContainingUnlocked(uint64_t vaddr, uint64_t size, MapEntry* out) const { EXIT_IF(out == nullptr); if (size == 0 || UINT64_MAX - vaddr < size) { return false; } auto next = m_maps.upper_bound(vaddr); if (next == m_maps.begin()) { return false; } const auto& entry = std::prev(next)->second; if (vaddr + size <= entry.vaddr + entry.size) { *out = entry; return true; } return false; } [[nodiscard]] bool RangeOk(uint64_t backing_offset, uint64_t size) const { return size != 0 && backing_offset < m_size && size <= m_size - backing_offset; } static void SetFailure(FailureReason* out, FailureReason reason) { if (out != nullptr) { *out = reason; } } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS static DWORD GetProtect(VirtualMemory::Mode mode) { switch (mode) { case VirtualMemory::Mode::Read: return PAGE_READONLY; case VirtualMemory::Mode::Write: case VirtualMemory::Mode::ReadWrite: return PAGE_READWRITE; case VirtualMemory::Mode::Execute: return PAGE_EXECUTE; case VirtualMemory::Mode::ExecuteRead: return PAGE_EXECUTE_READ; case VirtualMemory::Mode::ExecuteWrite: case VirtualMemory::Mode::ExecuteReadWrite: return PAGE_EXECUTE_READWRITE; case VirtualMemory::Mode::NoAccess: default: return PAGE_NOACCESS; } } static bool ApplyProtection(void* ptr, uint64_t size, VirtualMemory::Mode mode) { DWORD old_protect = 0; return VirtualProtect(ptr, size, GetProtect(mode), &old_protect) != 0; } static bool SplitPlaceholder(uint64_t vaddr, uint64_t size) { if (size == 0) { return true; } return VirtualFree(reinterpret_cast(vaddr), size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER) != 0; } void* ReplacePlaceholderView(uint64_t vaddr, uint64_t size, uint64_t backing_offset, VirtualMemory::Mode mode, FailureReason* failure_reason) { const DWORD map_protect = mode == VirtualMemory::Mode::NoAccess ? PAGE_READWRITE : GetProtect(mode); void* ptr = MapViewOfFile3(m_handle, GetCurrentProcess(), reinterpret_cast(vaddr), backing_offset, size, MEM_REPLACE_PLACEHOLDER, map_protect, nullptr, 0); if (ptr == nullptr || reinterpret_cast(ptr) != vaddr) { if (ptr != nullptr) { UnmapViewOfFile2(GetCurrentProcess(), ptr, MEM_PRESERVE_PLACEHOLDER); } SetFailure(failure_reason, FailureReason::PlaceholderMapFailed); return nullptr; } if (!ApplyProtection(ptr, size, mode)) { UnmapViewOfFile2(GetCurrentProcess(), ptr, MEM_PRESERVE_PLACEHOLDER); SetFailure(failure_reason, FailureReason::ProtectFailed); return nullptr; } return ptr; } static bool UnmapViewPreservePlaceholder(void* vaddr, uint64_t /*size*/) { return UnmapViewOfFile2(GetCurrentProcess(), vaddr, MEM_PRESERVE_PLACEHOLDER) != 0; } HANDLE m_handle = nullptr; #elif KYTY_PLATFORM == KYTY_PLATFORM_LINUX [[nodiscard]] static uint64_t GetMappableTestSize() { return 0x4000; } static int GetProtect(VirtualMemory::Mode mode) { switch (mode) { case VirtualMemory::Mode::Read: return PROT_READ; case VirtualMemory::Mode::Write: case VirtualMemory::Mode::ReadWrite: return PROT_READ | PROT_WRITE; case VirtualMemory::Mode::Execute: return PROT_EXEC; case VirtualMemory::Mode::ExecuteRead: return PROT_EXEC | PROT_READ; case VirtualMemory::Mode::ExecuteWrite: case VirtualMemory::Mode::ExecuteReadWrite: return PROT_EXEC | PROT_READ | PROT_WRITE; case VirtualMemory::Mode::NoAccess: default: return PROT_NONE; } } void* MapView(uint64_t vaddr, uint64_t size, uint64_t backing_offset, VirtualMemory::Mode mode, FailureReason* failure_reason) { void* ptr = mmap(reinterpret_cast(vaddr), size, GetProtect(mode), MAP_SHARED | MAP_FIXED, m_fd, static_cast(backing_offset)); if (ptr == MAP_FAILED) { SetFailure(failure_reason, FailureReason::FixedMapViewFailed); return nullptr; } return ptr; } // Replace the file view atomically instead of exposing an unmapped hole in the // process-lifetime guest reservation. static bool UnmapViewPreservePlaceholder(void* vaddr, uint64_t size) { void* ptr = mmap(vaddr, size, PROT_NONE, MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0); return ptr != MAP_FAILED && ptr == vaddr; } int m_fd = -1; #endif uint8_t* m_backing_base = nullptr; uint64_t m_size = 0; std::mutex m_mutex; std::map m_maps; }; class GuestAddressSpace { public: explicit GuestAddressSpace(uint64_t backing_size) { #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS SYSTEM_INFO info {}; GetSystemInfo(&info); m_granularity = info.dwAllocationGranularity; #endif ReserveGuestRegions(); m_backing = new GuestBackingStore(backing_size); EXIT_IF(!m_backing->IsAvailable()); } ~GuestAddressSpace() { delete m_backing; m_backing = nullptr; #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS for (const auto& [start, size]: m_owned) { const auto end = start + size; auto current = start; while (current < end) { MEMORY_BASIC_INFORMATION info {}; EXIT_IF(VirtualQuery(reinterpret_cast(current), &info, sizeof(info)) == 0); const auto region_start = reinterpret_cast(info.BaseAddress); const auto region_end = region_start + static_cast(info.RegionSize); if (info.State == MEM_COMMIT) { EXIT_IF(VirtualFree(reinterpret_cast(region_start), info.RegionSize, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER) == 0); continue; } if (info.State == MEM_RESERVE) { EXIT_IF(VirtualFree(info.AllocationBase, 0, MEM_RELEASE) == 0); } current = region_end; } } #else for (const auto& [start, size]: m_owned) { munmap(reinterpret_cast(start), size); } #endif } KYTY_CLASS_NO_COPY(GuestAddressSpace); [[nodiscard]] uint64_t GetGranularity() const { return m_granularity; } [[nodiscard]] uint64_t GetBackingBase() const { return m_backing->GetBackingBase(); } [[nodiscard]] uint64_t GetBackingSize() const { return m_backing->GetBackingSize(); } bool ZeroBacking(uint64_t backing_offset, uint64_t size) { return m_backing->ZeroRange(backing_offset, size); } [[nodiscard]] bool Owns(uint64_t vaddr, uint64_t size) { if (size == 0 || UINT64_MAX - vaddr < size) { return false; } std::lock_guard lock(m_mutex); return std::any_of(m_owned.begin(), m_owned.end(), [vaddr, size](const auto& range) { return vaddr >= range.first && vaddr + size <= range.first + range.second; }); } [[nodiscard]] bool OverlapsOwned(uint64_t vaddr, uint64_t size) { if (size == 0 || UINT64_MAX - vaddr < size) { return false; } std::lock_guard lock(m_mutex); return std::any_of(m_owned.begin(), m_owned.end(), [vaddr, size](const auto& range) { return VirtualRangesOverlap(vaddr, size, range.first, range.second); }); } bool TryWriteBacking(uint64_t vaddr, const void* data, uint64_t size) { return m_backing->TryWriteBacking(vaddr, data, size); } bool TryReadBacking(uint64_t vaddr, void* data, uint64_t size) { return m_backing->TryReadBacking(vaddr, data, size); } bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size) { return m_backing->TryReadSparseBacking(vaddr, data, size); } bool BackingContains(uint64_t vaddr, uint64_t size) { return m_backing->Contains(vaddr, size); } bool Protect(uint64_t vaddr, uint64_t size, VirtualMemory::Mode mode) { if (size == 0 || UINT64_MAX - vaddr < size) { return false; } std::lock_guard lock(m_mutex); return ProtectMappedUnlocked(vaddr, size, mode); } bool ProtectTransient(uint64_t vaddr, uint64_t size, VirtualMemory::Mode mode) { if (size == 0 || UINT64_MAX - vaddr < size) { return false; } std::lock_guard lock(m_mutex); return ProtectMappedUnlocked(vaddr, size, mode); } bool MapBacking(uint64_t vaddr, uint64_t size, uint64_t backing_offset, VirtualMemory::Mode mode, GuestBackingStore::FailureReason* failure_reason = nullptr) { std::lock_guard lock(m_mutex); if (!ConsumeUnlocked(vaddr, size)) { if (failure_reason != nullptr) { *failure_reason = GuestBackingStore::FailureReason::AddressRangeUnavailable; } return false; } const auto host_mode = VirtualMemory::IsExecute(mode) ? VirtualMemory::Mode::ExecuteReadWrite : VirtualMemory::Mode::ReadWrite; if (m_backing->MapExistingPlaceholderFixed(vaddr, size, backing_offset, host_mode, failure_reason)) { EXIT_IF(!AddMappedUnlocked(vaddr, size, MappingType::Backing)); return true; } AddFreeUnlocked(vaddr, size); return false; } bool UnmapBacking(uint64_t vaddr, uint64_t size) { std::lock_guard lock(m_mutex); if (!ContainsMappedUnlocked(vaddr, size, MappingType::Backing)) { return false; } bool placeholder_preserved = false; if (!m_backing->Unmap(vaddr, size, &placeholder_preserved)) { return false; } if (!placeholder_preserved) { return false; } EXIT_IF(!RemoveMappedUnlocked(vaddr, size, MappingType::Backing)); AddFreeUnlocked(vaddr, size); return true; } uint64_t ReserveAligned(uint64_t search_addr, uint64_t size, uint64_t alignment) { if (size == 0) { return 0; } alignment = std::max(alignment != 0 ? alignment : PageSize(), PageSize()); std::lock_guard lock(m_mutex); if (search_addr == 0) { auto addr = FindFreeAlignedUnlocked(HOST_SYSTEM_MANAGED_MIN, HOST_SYSTEM_MANAGED_MAX + 1u, size, alignment); return addr != 0 ? addr : FindFreeAlignedUnlocked(HOST_USER_MIN, HOST_USER_MAX + 1u, size, alignment); } return FindFreeAlignedUnlocked(search_addr, HOST_USER_MAX + 1u, size, alignment); } uint64_t FindFreeAligned(uint64_t search_start, uint64_t search_end, uint64_t size, uint64_t alignment) { if (size == 0 || alignment == 0) { return 0; } alignment = std::max(alignment, PageSize()); std::lock_guard lock(m_mutex); return FindFreeAlignedUnlocked(search_start, search_end, size, alignment); } bool ReserveFixed(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || !IsPageAligned(vaddr) || !IsPageAligned(size)) { return false; } std::lock_guard lock(m_mutex); return FindContaining(vaddr, size) != m_free.end(); } #if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS) bool TestContainsFree(uint64_t vaddr, uint64_t size) { std::lock_guard lock(m_mutex); return FindContaining(vaddr, size) != m_free.end(); } #endif bool ReleaseFree(uint64_t vaddr, uint64_t size) { std::lock_guard lock(m_mutex); return FindContaining(vaddr, size) != m_free.end(); } bool Commit(uint64_t vaddr, uint64_t size, VirtualMemory::Mode mode) { std::lock_guard lock(m_mutex); if (!ConsumeUnlocked(vaddr, size)) { return false; } const auto host_mode = VirtualMemory::IsExecute(mode) ? VirtualMemory::Mode::ExecuteReadWrite : VirtualMemory::Mode::ReadWrite; #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS auto* ptr = VirtualAlloc2(GetCurrentProcess(), reinterpret_cast(vaddr), size, MEM_RESERVE | MEM_COMMIT | MEM_REPLACE_PLACEHOLDER, GetProtect(host_mode), nullptr, 0); if (ptr == nullptr || reinterpret_cast(ptr) != vaddr) { if (ptr != nullptr) { VirtualFree(ptr, 0, MEM_RELEASE); } AddFreeUnlocked(vaddr, size); return false; } EXIT_IF(!AddMappedUnlocked(vaddr, size, MappingType::Private)); return true; #else if (VirtualMemory::Protect(vaddr, size, host_mode)) { EXIT_IF(!AddMappedUnlocked(vaddr, size, MappingType::Private)); return true; } AddFreeUnlocked(vaddr, size); return false; #endif } bool ReleaseCommitted(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0 || !IsPageAligned(vaddr) || !IsPageAligned(size)) { return false; } std::lock_guard lock(m_mutex); if (!ContainsMappedUnlocked(vaddr, size, MappingType::Private)) { return false; } const auto end = vaddr + size; auto current = vaddr; std::vector> regions; while (current < end) { const auto it = FindMappedContainingUnlocked(current); const auto part_end = std::min(end, it->second.vaddr + it->second.size); regions.emplace_back(current, part_end - current); current = part_end; } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS for (const auto& [region_vaddr, region_size]: regions) { EXIT_IF(VirtualFree(reinterpret_cast(region_vaddr), region_size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER) == 0); } // ReleaseCommitted also publishes the aggregate span as one logical free range. EXIT_IF(regions.size() > 1 && !CoalescePlaceholders(vaddr, size)); #else for (const auto& [region_vaddr, region_size]: regions) { RestorePlaceholder(region_vaddr, region_size); } #endif EXIT_IF(!RemoveMappedUnlocked(vaddr, size, MappingType::Private)); AddFreeUnlocked(vaddr, size); return true; } bool SelfTest() { const auto addr = ReserveAligned(0, PageSize(), PageSize()); if (addr == 0) { LOGF_COLOR(Log::Color::Red, "\t guest address-space self-test: 16 KiB range unavailable\n"); return false; } if (!Commit(addr, PageSize(), VirtualMemory::Mode::ReadWrite)) { LOGF_COLOR(Log::Color::Red, "\t guest address-space self-test: commit failed\n"); return false; } auto* ptr = reinterpret_cast(addr); *ptr = 0x4b59545950484f4cull; // "KYTYPHOL" if (!ReleaseCommitted(addr, PageSize())) { LOGF_COLOR(Log::Color::Red, "\t guest address-space self-test: release-committed failed\n"); return false; } LOGF_COLOR(Log::Color::Green, "\t guest address-space self-test: ok\n"); return true; } private: enum class MappingType { Backing, Private }; struct MappedRegion { uint64_t vaddr = 0; uint64_t size = 0; MappingType type = MappingType::Private; }; static constexpr uint64_t PageSize() { return 0x4000; } static uint64_t AlignUp(uint64_t value, uint64_t alignment) { if (alignment == 0) { return 0; } const auto remainder = value % alignment; const auto increment = (remainder != 0 ? alignment - remainder : 0); return increment <= UINT64_MAX - value ? value + increment : 0; } static uint64_t AlignDown(uint64_t value, uint64_t alignment) { return value & ~(alignment - 1u); } [[nodiscard]] static bool IsPageAligned(uint64_t value) { return (value & (PageSize() - 1u)) == 0; } [[nodiscard]] bool IsGranularityAligned(uint64_t value) const { return (value & (m_granularity - 1u)) == 0; } bool ProtectMappedUnlocked(uint64_t vaddr, uint64_t size, VirtualMemory::Mode mode) { const auto end = vaddr + size; auto it = m_mapped.upper_bound(vaddr); if (it != m_mapped.begin()) { --it; } for (; it != m_mapped.end() && it->second.vaddr < end; ++it) { const auto& region = it->second; const auto range_start = std::max(vaddr, region.vaddr); const auto range_end = std::min(end, region.vaddr + region.size); if (range_start < range_end && !VirtualMemory::Protect(range_start, range_end - range_start, mode)) { return false; } } return true; } bool ConsumeUnlocked(uint64_t vaddr, uint64_t size) { auto it = FindContaining(vaddr, size); if (it == m_free.end() || !SplitUnlocked(it, vaddr, size)) { return false; } it = m_free.find(vaddr); if (it == m_free.end() || it->second != size) { return false; } m_free.erase(it); return true; } std::map::iterator FindMappedContainingUnlocked(uint64_t vaddr) { auto next = m_mapped.upper_bound(vaddr); if (next == m_mapped.begin()) { return m_mapped.end(); } auto it = std::prev(next); return vaddr < it->second.vaddr + it->second.size ? it : m_mapped.end(); } bool ContainsMappedUnlocked(uint64_t vaddr, uint64_t size, MappingType type) { if (size == 0 || UINT64_MAX - vaddr < size) { return false; } const auto end = vaddr + size; auto current = vaddr; while (current < end) { const auto it = FindMappedContainingUnlocked(current); if (it == m_mapped.end() || it->second.type != type) { return false; } current = std::min(end, it->second.vaddr + it->second.size); } return true; } bool AddMappedUnlocked(uint64_t vaddr, uint64_t size, MappingType type) { if (size == 0 || UINT64_MAX - vaddr < size) { return false; } const auto end = vaddr + size; auto next = m_mapped.lower_bound(vaddr); if (next != m_mapped.end() && next->second.vaddr < end) { return false; } if (next != m_mapped.begin()) { const auto& previous = std::prev(next)->second; if (previous.vaddr + previous.size > vaddr) { return false; } } return m_mapped.emplace(vaddr, MappedRegion {vaddr, size, type}).second; } bool RemoveMappedUnlocked(uint64_t vaddr, uint64_t size, MappingType type) { if (!ContainsMappedUnlocked(vaddr, size, type)) { return false; } const auto end = vaddr + size; auto current = vaddr; while (current < end) { auto it = FindMappedContainingUnlocked(current); const auto original = it->second; const auto part_end = std::min(end, original.vaddr + original.size); m_mapped.erase(it); if (original.vaddr < current) { const auto left_size = current - original.vaddr; EXIT_IF(!AddMappedUnlocked(original.vaddr, left_size, original.type)); } if (part_end < original.vaddr + original.size) { const auto right_size = original.vaddr + original.size - part_end; EXIT_IF(!AddMappedUnlocked(part_end, right_size, original.type)); } current = part_end; } return true; } #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS static DWORD GetProtect(VirtualMemory::Mode mode) { switch (mode) { case VirtualMemory::Mode::Read: return PAGE_READONLY; case VirtualMemory::Mode::Write: case VirtualMemory::Mode::ReadWrite: return PAGE_READWRITE; case VirtualMemory::Mode::Execute: return PAGE_EXECUTE; case VirtualMemory::Mode::ExecuteRead: return PAGE_EXECUTE_READ; case VirtualMemory::Mode::ExecuteWrite: case VirtualMemory::Mode::ExecuteReadWrite: return PAGE_EXECUTE_READWRITE; case VirtualMemory::Mode::NoAccess: default: return PAGE_NOACCESS; } } #endif void ReserveGuestRegions() { #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS constexpr uint64_t MinimumRegionSize = 0x1000000; const auto process = GetCurrentProcess(); const auto guest_end = HOST_USER_MAX + 1u; auto next_addr = HOST_SYSTEM_MANAGED_MIN; while (next_addr < guest_end) { MEMORY_BASIC_INFORMATION info {}; if (VirtualQuery(reinterpret_cast(next_addr), &info, sizeof(info)) == 0) { EXIT("failed to inspect guest address space at 0x%016" PRIx64 "\n", next_addr); } const auto region_base = reinterpret_cast(info.BaseAddress); const auto region_end = std::min(guest_end, region_base + static_cast(info.RegionSize)); const auto reserve_start = AlignUp(std::max(region_base, HOST_SYSTEM_MANAGED_MIN), m_granularity); const auto reserve_end = AlignDown(region_end, m_granularity); if (info.State == MEM_FREE && reserve_end > reserve_start && reserve_end - reserve_start > MinimumRegionSize) { auto* ptr = VirtualAlloc2( process, reinterpret_cast(reserve_start), reserve_end - reserve_start, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0); if (reinterpret_cast(ptr) != reserve_start) { EXIT("failed to reserve guest address space at 0x%016" PRIx64 ", size 0x%016" PRIx64 "\n", reserve_start, reserve_end - reserve_start); } AddFreeUnlocked(reserve_start, reserve_end - reserve_start); m_owned.emplace_back(reserve_start, reserve_end - reserve_start); } if (region_end <= next_addr) { EXIT("guest address-space scan did not advance at 0x%016" PRIx64 "\n", next_addr); } next_addr = region_end; } #else const std::array, 3> regions {{ {HOST_SYSTEM_MANAGED_MIN, HOST_SYSTEM_MANAGED_MAX + 1u}, {HOST_SYSTEM_RESERVED_MIN, HOST_SYSTEM_RESERVED_MAX + 1u}, {HOST_USER_MIN, HOST_USER_MAX + 1u}, }}; #if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__) std::vector> occupied; FILE* maps = fopen("/proc/self/maps", "r"); EXIT_IF(maps == nullptr); char line[512]; while (fgets(line, sizeof(line), maps) != nullptr) { unsigned long long mapping_start = 0; unsigned long long mapping_end = 0; if (sscanf(line, "%llx-%llx", &mapping_start, &mapping_end) == 2) { occupied.emplace_back(static_cast(mapping_start), static_cast(mapping_end)); } } fclose(maps); auto reserve_range = [this](uint64_t start, uint64_t end) { start = AlignUp(start, PageSize()); end = AlignDown(end, PageSize()); if (start == 0 || end <= start) { return; } const auto size = end - start; int flags = MAP_PRIVATE | MAP_ANON | MAP_NORESERVE; #if defined(KYTY_LINKED_GUEST_ADDRESS_SPACE) flags |= MAP_FIXED; #elif defined(MAP_FIXED_NOREPLACE) flags |= MAP_FIXED_NOREPLACE; #endif void* ptr = mmap(reinterpret_cast(start), size, PROT_NONE, flags, -1, 0); if (ptr == MAP_FAILED || reinterpret_cast(ptr) != start) { if (ptr != MAP_FAILED) { munmap(ptr, size); } return; } AddFreeUnlocked(start, size); m_owned.emplace_back(start, size); }; for (const auto& [region_start, region_end]: regions) { auto current = region_start; for (const auto& [mapping_start, mapping_end]: occupied) { if (mapping_end <= current) { continue; } if (mapping_start >= region_end) { break; } if (mapping_start > current) { reserve_range(current, std::min(mapping_start, region_end)); } current = std::max(current, mapping_end); if (current >= region_end) { break; } } if (current < region_end) { reserve_range(current, region_end); } } #else for (const auto& [start, end]: regions) { int flags = MAP_PRIVATE | MAP_ANON | MAP_NORESERVE; #if defined(KYTY_LINKED_GUEST_ADDRESS_SPACE) flags |= MAP_FIXED; #elif defined(MAP_FIXED_NOREPLACE) flags |= MAP_FIXED_NOREPLACE; #endif void* ptr = mmap(reinterpret_cast(start), end - start, PROT_NONE, flags, -1, 0); if (ptr == MAP_FAILED || reinterpret_cast(ptr) != start) { if (ptr != MAP_FAILED) { munmap(ptr, end - start); } EXIT("failed to reserve guest address space at 0x%016" PRIx64 ", size 0x%016" PRIx64 "\n", start, end - start); } AddFreeUnlocked(start, end - start); m_owned.emplace_back(start, end - start); } #endif #endif } uint64_t FindFreeAlignedUnlocked(uint64_t search_start, uint64_t search_end, uint64_t size, uint64_t alignment) { if (search_start >= search_end || size > search_end - search_start) { return 0; } for (const auto& [base, range_size]: m_free) { const auto candidate = AlignUp(std::max(base, search_start), alignment); if (candidate < search_end && size <= search_end - candidate && GuestFreeRangeContains(base, range_size, candidate, size)) { return candidate; } } return 0; } std::map::iterator FindContaining(uint64_t vaddr, uint64_t size) { auto next = m_free.upper_bound(vaddr); if (next == m_free.begin()) { return m_free.end(); } auto it = std::prev(next); auto end = it->first + it->second; return (vaddr >= it->first && vaddr + size <= end ? it : m_free.end()); } bool SplitUnlocked(std::map::iterator it, uint64_t vaddr, uint64_t size) { const auto base = it->first; const auto end = it->first + it->second; if (base == vaddr && end == vaddr + size) { return true; } if (base != vaddr) { const auto left_size = vaddr - base; #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS if (VirtualFree(reinterpret_cast(base), left_size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER) == 0) { LOGF_COLOR(Log::Color::Red, "\t guest placeholder split failed: base=0x%016" PRIx64 " size=0x%016" PRIx64 " requested=0x%016" PRIx64 " request_size=0x%016" PRIx64 " error=0x%08" PRIx32 "\n", base, end - base, vaddr, size, static_cast(GetLastError())); return false; } #endif it->second = left_size; it = m_free.emplace(vaddr, end - vaddr).first; } if (it->second != size) { #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS if (VirtualFree(reinterpret_cast(it->first), size, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER) == 0) { LOGF_COLOR(Log::Color::Red, "\t guest placeholder split failed: base=0x%016" PRIx64 " size=0x%016" PRIx64 " requested=0x%016" PRIx64 " request_size=0x%016" PRIx64 " error=0x%08" PRIx32 "\n", it->first, it->second, vaddr, size, static_cast(GetLastError())); return false; } #endif const auto right_start = it->first + size; const auto right_size = it->second - size; it->second = size; m_free.emplace(right_start, right_size); } return true; } void AddFreeUnlocked(uint64_t vaddr, uint64_t size) { if (vaddr == 0 || size == 0) { return; } auto [it, inserted] = m_free.emplace(vaddr, size); if (!inserted) { it->second = size; } auto base = it->first; auto end = it->first + it->second; auto first = it; while (first != m_free.begin()) { auto prev = std::prev(first); if (prev->first + prev->second != base) { break; } base = prev->first; first = prev; } auto last = std::next(it); while (last != m_free.end() && end == last->first) { end = last->first + last->second; last = std::next(last); } if (base != it->first || end != it->first + it->second) { #if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS if (CoalescePlaceholders(base, end - base)) { m_free.erase(first, last); m_free.emplace(base, end - base); } #else m_free.erase(first, last); m_free.emplace(base, end - base); #endif } } void RestorePlaceholder(uint64_t vaddr, uint64_t size) { #if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS // The address remains owned for the process lifetime; MAP_FIXED only replaces one of our // mappings and never acquires a new guest address. void* ptr = mmap(reinterpret_cast(vaddr), size, PROT_NONE, MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0); EXIT_IF(ptr == MAP_FAILED || reinterpret_cast(ptr) != vaddr); #else (void)vaddr; (void)size; #endif } std::mutex m_mutex; std::map m_free; std::map m_mapped; std::vector> m_owned; uint64_t m_granularity = 0x10000; GuestBackingStore* m_backing = nullptr; };