#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSMAPPINGS_HPP #define CORE_LIBS_PRX_LIBC_INCLUDE_WINDOWSMAPPINGS_HPP #ifdef _WIN32 #include #include #include #include #include #include #include #include #include #include #include #include namespace GuestArena { class WindowsMappings { public: static WindowsMappings& Get() { static WindowsMappings mappings; return mappings; } void* Reserve(void* address, std::size_t bytes) { return allocate(GetCurrentProcess(), address, bytes, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0); } std::vector> Commit(void* address, std::size_t bytes, DWORD protection, std::size_t granule, bool watched) { std::vector> created; std::lock_guard lock(mutex); const auto end = reinterpret_cast(address) + bytes; for (auto cursor = reinterpret_cast(address); cursor < end;) { const auto memory = query(cursor); const auto stop = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); if (memory.State == MEM_RESERVE) { const auto limit = std::min(end, cursor + granule); auto placeholderEnd = stop; while (placeholderEnd < limit) { const auto next = query(placeholderEnd); if (next.State != MEM_RESERVE) break; placeholderEnd = reinterpret_cast(next.BaseAddress) + next.RegionSize; } const auto size = std::min(limit, placeholderEnd) - cursor; reset(cursor, size); const DWORD flags = MEM_RESERVE | MEM_COMMIT | MEM_REPLACE_PLACEHOLDER | (watched ? MEM_WRITE_WATCH : 0); if (!allocate(GetCurrentProcess(), reinterpret_cast(cursor), size, flags, protection, nullptr, 0)) fail("replace guest placeholder with private memory"); if (!created.empty() && created.back().first + created.back().second == cursor) created.back().second += size; else created.emplace_back(cursor, size); cursor += size; } else { if (memory.State != MEM_COMMIT) throw std::runtime_error("guest memory is not committed"); const auto mapped = views.find(cursor & ~(pageBytes - 1)); if (mapped != views.end()) { mapped->second.protection = protection; mapped->second.armed = false; invalidate(*mapped->second.page); } DWORD previous; if (!VirtualProtect(reinterpret_cast(cursor), stop - cursor, protection, &previous)) fail("protect guest memory"); cursor = stop; } } return created; } void Reset(void* address, std::size_t bytes) { std::lock_guard lock(mutex); reset(reinterpret_cast(address), bytes); } void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection) { std::lock_guard lock(mutex); auto cursor = reinterpret_cast(address); reset(cursor, bytes); HANDLE duplicate = nullptr; if (!DuplicateHandle(GetCurrentProcess(), section, GetCurrentProcess(), &duplicate, 0, FALSE, DUPLICATE_SAME_ACCESS)) fail("keep shared guest section"); const auto owned = std::make_shared
(duplicate); for (std::size_t done = 0; done < bytes; done += pageBytes) { split(cursor + done, pageBytes); void* page = reinterpret_cast(cursor + done); if (!map(section, GetCurrentProcess(), page, offset + done, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page"); MEMORY_BASIC_INFORMATION memory{}; if (VirtualQuery(page, &memory, sizeof(memory)) != sizeof(memory)) fail("query shared guest page"); if (memory.State == MEM_RESERVE && !VirtualAlloc(page, pageBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); DWORD previous; if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page"); const auto key = std::make_pair(reinterpret_cast(section), offset + done); auto shared = physical[key].lock(); if (!shared) { shared = std::make_shared(); physical[key] = shared; } const auto base = cursor + done; shared->aliases.push_back(base); views.emplace(base, View{shared, protection, 0, false, owned, offset + done, 0}); invalidate(*shared); } } void SetProtection(std::uintptr_t address, std::size_t bytes, DWORD protection) { std::lock_guard lock(mutex); for (auto it = views.lower_bound(address); it != views.end() && it->first < address + bytes; ++it) { it->second.protection = protection; it->second.armed = false; invalidate(*it->second.page); } } void Pin(std::uintptr_t address, std::size_t bytes) { std::lock_guard lock(mutex); for (auto it = views.lower_bound(address & ~(pageBytes - 1)); it != views.end() && it->first < address + bytes; ++it) { auto& page = *it->second.page; ++page.pins; for (const auto alias : page.aliases) { auto& view = views.at(alias); if (!view.armed) continue; DWORD previous; if (!VirtualProtect(reinterpret_cast(alias), pageBytes, view.protection, &previous)) fail("pin shared guest page writable"); view.armed = false; } invalidate(page); } } void Unpin(std::uintptr_t address, std::size_t bytes) { std::lock_guard lock(mutex); for (auto it = views.lower_bound(address & ~(pageBytes - 1)); it != views.end() && it->first < address + bytes; ++it) { auto& page = *it->second.page; if (page.pins != 0) --page.pins; invalidate(page); } } bool HandleWrite(std::uintptr_t address) { std::lock_guard lock(mutex); const auto base = address & ~(pageBytes - 1); const auto found = views.find(base); if (found == views.end() || !writable(found->second.protection)) return false; auto& view = found->second; invalidate(*view.page); DWORD previous; if (!VirtualProtect(reinterpret_cast(base), pageBytes, view.protection, &previous)) fail("resume shared memory write"); view.armed = false; return true; } bool BeginHostWrite(std::uintptr_t address, std::size_t bytes) { std::lock_guard lock(mutex); const auto first = views.lower_bound(address & ~(pageBytes - 1)); const auto end = address + bytes; for (auto it = first; it != views.end() && it->first < end; ++it) { if (!writable(it->second.protection)) return false; } for (auto it = first; it != views.end() && it->first < end; ++it) { auto& view = it->second; ++view.hostWrites; invalidate(*view.page); if (!view.armed) continue; DWORD previous; if (!VirtualProtect(reinterpret_cast(it->first), pageBytes, view.protection, &previous)) fail("open shared memory to a host write"); view.armed = false; } return true; } void EndHostWrite(std::uintptr_t address, std::size_t bytes) { std::lock_guard lock(mutex); const auto end = address + bytes; for (auto it = views.lower_bound(address & ~(pageBytes - 1)); it != views.end() && it->first < end; ++it) { --it->second.hostWrites; invalidate(*it->second.page); } } void* MapAlias(std::uintptr_t address, std::size_t bytes) { std::lock_guard lock(mutex); const auto refuse = [&](const char* reason) { char text[192]; std::snprintf(text, sizeof(text), "read-write alias of shared guest memory 0x%llx+0x%llx: %s", static_cast(address), static_cast(bytes), reason); return std::runtime_error(text); }; if (address % pageBytes != 0 || bytes % pageBytes != 0 || bytes == 0) throw refuse("the range is not made of whole shared pages"); auto view = views.find(address); if (view == views.end()) throw refuse("the range does not start at a shared view"); const auto section = view->second.section; const auto offset = view->second.offset; SYSTEM_INFO system{}; GetSystemInfo(&system); for (std::size_t done = 0; done < bytes; done += pageBytes, ++view) { if (view == views.end() || view->first != address + done || view->second.offset != offset + done) throw refuse("the range is not one contiguous run of views of a section"); if (view->second.section != section && !sameSection(view->second.section->handle, section->handle)) throw refuse("the range spans several sections"); } const auto lead = offset % system.dwAllocationGranularity; void* alias = map(section->handle, GetCurrentProcess(), nullptr, offset - lead, lead + bytes, 0, PAGE_READWRITE, nullptr, 0); if (alias == nullptr) { char text[160]; std::snprintf(text, sizeof(text), "MapViewOfFile3 of a read-write alias of shared guest memory 0x%llx+0x%llx", static_cast(address), static_cast(bytes)); throw std::system_error(static_cast(GetLastError()), std::system_category(), text); } return static_cast(alias) + lead; } void UnmapAlias(void* alias) { if (alias == nullptr) return; SYSTEM_INFO system{}; GetSystemInfo(&system); const auto base = reinterpret_cast(alias) & ~(static_cast(system.dwAllocationGranularity) - 1); if (!unmap(GetCurrentProcess(), reinterpret_cast(base), 0)) fail("unmap shared guest alias"); } bool Protection(std::uintptr_t address, std::uint32_t* protection) { std::lock_guard lock(mutex); const auto found = views.find(address & ~(pageBytes - 1)); if (found == views.end()) return false; *protection = found->second.protection; return true; } bool Collect(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear) { std::lock_guard lock(mutex); const auto capacity = *count; *count = 0; const auto end = address + bytes; if (clear && !collectable(address, end)) return false; for (auto cursor = address; cursor < end;) { const auto nextClean = cleanRanges.upper_bound(cursor); if (nextClean != cleanRanges.begin()) { const auto clean = std::prev(nextClean); if (cursor < clean->second) { cursor = std::min(end, clean->second); continue; } } const auto base = cursor & ~(pageBytes - 1); const auto found = views.find(base); if (found != views.end()) { auto& view = found->second; const auto stop = std::min(std::min(end, base + pageBytes), nextClean == cleanRanges.end() ? end : nextClean->first); if (view.protection == PAGE_NOACCESS) return false; const bool pinned = view.page->pins != 0; if (pinned || view.seen != view.page->generation) { const auto needed = (stop - cursor + 4095) / 4096; if (needed > capacity - *count) { for (auto at = cursor; *count < capacity; at += 4096) pages[(*count)++] = reinterpret_cast(at); return true; } for (auto at = cursor; at < stop; at += 4096) pages[(*count)++] = reinterpret_cast(at); } if (clear && !pinned) { for (const auto alias : view.page->aliases) { auto& other = views.at(alias); if (!writable(other.protection) || other.armed || other.hostWrites != 0) continue; DWORD previous; const DWORD protection = other.protection == PAGE_EXECUTE_READWRITE ? PAGE_EXECUTE_READ : PAGE_READONLY; if (!VirtualProtect(reinterpret_cast(alias), pageBytes, protection, &previous)) fail("arm shared memory write tracking"); other.armed = true; } if (cursor == base && stop == base + pageBytes) view.seen = view.page->generation; rememberClean(cursor, stop); } cursor = stop; } else { const auto memory = query(cursor); if (memory.State != MEM_COMMIT || memory.Type != MEM_PRIVATE) return false; const auto stop = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); ULONG_PTR available = capacity - *count; if (available == 0) return true; DWORD granularity = 0; if (GetWriteWatch(clear ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast(cursor), stop - cursor, pages + *count, &available, &granularity) != 0) fail("collect private guest writes"); *count += available; if (*count == capacity) return true; cursor = stop; } } return true; } private: bool collectable(std::uintptr_t address, std::uintptr_t end) { for (auto cursor = address; cursor < end;) { const auto base = cursor & ~(pageBytes - 1); if (const auto found = views.find(base); found != views.end()) { if (found->second.protection == PAGE_NOACCESS) return false; cursor = std::min(end, base + pageBytes); continue; } const auto memory = query(cursor); if (memory.State != MEM_COMMIT || memory.Type != MEM_PRIVATE) return false; cursor = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); } return true; } static constexpr std::size_t pageBytes = 0x4000; struct SharedPage { std::uint64_t generation = 1; std::vector aliases; std::uint32_t pins = 0; }; struct Section { HANDLE handle; explicit Section(HANDLE handle) : handle(handle) {} Section(const Section&) = delete; Section& operator=(const Section&) = delete; ~Section() { CloseHandle(handle); } }; struct View { std::shared_ptr page; DWORD protection; std::uint64_t seen; bool armed; std::shared_ptr
section; std::uint64_t offset; std::uint32_t hostWrites; }; void forgetClean(std::uintptr_t start, std::uintptr_t end) { auto it = cleanRanges.lower_bound(start); if (it != cleanRanges.begin() && std::prev(it)->second > start) --it; while (it != cleanRanges.end() && it->first < end) { const auto first = it->first; const auto last = it->second; it = cleanRanges.erase(it); if (first < start) cleanRanges.emplace(first, start); if (last > end) it = cleanRanges.emplace(end, last).first; } } void rememberClean(std::uintptr_t start, std::uintptr_t end) { auto it = cleanRanges.lower_bound(start); if (it != cleanRanges.begin() && std::prev(it)->second >= start) --it; while (it != cleanRanges.end() && it->first <= end) { start = std::min(start, it->first); end = std::max(end, it->second); it = cleanRanges.erase(it); } cleanRanges.emplace(start, end); } void invalidate(SharedPage& page) { ++page.generation; for (const auto alias : page.aliases) forgetClean(alias, alias + pageBytes); } bool sameSection(HANDLE first, HANDLE second) const { return compare != nullptr && compare(first, second); } static bool writable(DWORD protection) { return protection == PAGE_READWRITE || protection == PAGE_EXECUTE_READWRITE; } using AllocateFunction = PVOID (WINAPI*)(HANDLE, PVOID, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER*, ULONG); using MapFunction = PVOID (WINAPI*)(HANDLE, HANDLE, PVOID, ULONG64, SIZE_T, ULONG, ULONG, MEM_EXTENDED_PARAMETER*, ULONG); using UnmapFunction = BOOL (WINAPI*)(HANDLE, PVOID, ULONG); using CompareFunction = BOOL (WINAPI*)(HANDLE, HANDLE); WindowsMappings() { const auto module = GetModuleHandleW(L"KernelBase.dll"); if (!module) fail("load Windows memory API"); allocate = reinterpret_cast(GetProcAddress(module, "VirtualAlloc2")); map = reinterpret_cast(GetProcAddress(module, "MapViewOfFile3")); unmap = reinterpret_cast(GetProcAddress(module, "UnmapViewOfFile2")); if (!allocate || !map || !unmap) throw std::runtime_error("Windows placeholder memory APIs are required"); compare = reinterpret_cast(GetProcAddress(module, "CompareObjectHandles")); } [[noreturn]] static void fail(const char* operation) { throw std::system_error(static_cast(GetLastError()), std::system_category(), operation); } static MEMORY_BASIC_INFORMATION query(std::uintptr_t address) { MEMORY_BASIC_INFORMATION memory{}; if (VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) != sizeof(memory)) fail("query guest memory"); return memory; } static void split(std::uintptr_t address, std::size_t bytes) { auto memory = query(address); memory = query(reinterpret_cast(memory.AllocationBase)); if (memory.State != MEM_RESERVE) throw std::runtime_error("guest mapping requires a placeholder"); const auto base = reinterpret_cast(memory.BaseAddress); if (address != base) { if (!VirtualFree(reinterpret_cast(base), address - base, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("split guest placeholder prefix"); memory = query(address); } if (memory.RegionSize < bytes) throw std::runtime_error("guest placeholder is too small"); if (memory.RegionSize != bytes && !VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("split guest placeholder suffix"); } void reset(std::uintptr_t address, std::size_t bytes) { const auto end = address + bytes; forgetClean(address, end); for (auto cursor = address; cursor < end;) { const auto memory = query(cursor); if (memory.State == MEM_RESERVE) { cursor = std::min(end, reinterpret_cast(memory.BaseAddress) + memory.RegionSize); continue; } if (reinterpret_cast(memory.AllocationBase) != cursor) throw std::runtime_error("cannot release part of a host allocation"); auto allocationEnd = cursor; do { const auto part = query(allocationEnd); if (part.AllocationBase != memory.AllocationBase) break; allocationEnd = reinterpret_cast(part.BaseAddress) + part.RegionSize; } while (allocationEnd < end); if (allocationEnd > end || query(allocationEnd).AllocationBase == memory.AllocationBase) throw std::runtime_error("guest release truncates a host allocation"); if (memory.Type == MEM_MAPPED) { if (!unmap(GetCurrentProcess(), reinterpret_cast(cursor), MEM_PRESERVE_PLACEHOLDER)) fail("unmap shared guest page"); const auto found = views.find(cursor); if (found != views.end()) { std::erase(found->second.page->aliases, cursor); views.erase(found); } } else if (memory.Type == MEM_PRIVATE) { if (!VirtualFree(reinterpret_cast(cursor), allocationEnd - cursor, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); } else { throw std::runtime_error("unsupported guest mapping type"); } cursor = allocationEnd; } const auto last = query(reinterpret_cast(query(end - 1).AllocationBase)); const auto lastBase = reinterpret_cast(last.BaseAddress); if (lastBase + last.RegionSize > end) split(lastBase, end - lastBase); const auto first = query(address); split(address, std::min(bytes, reinterpret_cast(first.BaseAddress) + first.RegionSize - address)); if (query(address).RegionSize != bytes && !VirtualFree(reinterpret_cast(address), bytes, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) fail("coalesce guest placeholders"); } std::map cleanRanges; std::map views; std::map, std::weak_ptr> physical; std::mutex mutex; AllocateFunction allocate = nullptr; MapFunction map = nullptr; UnmapFunction unmap = nullptr; CompareFunction compare = nullptr; }; } #endif #endif