From 6c1f63674e24295d089fa014491c3ef5e4e75b82 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:07:35 +0000 Subject: [PATCH 1/6] test(libc): time shared guest mappings on Windows windows_shared_map_cost maps 256, 16384 and 65536 pages of shared memory (4 MiB, 256 MiB and 1 GiB) through WindowsMappings::Map, then releases them, five times each, and prints the median map and release times, the time per page and the number of mapped views. It only measures and always passes, so a build of this commit and of a later one can be compared on the same machine. --- core/libs/CMakeLists.txt | 8 +++ core/libs/tests/WindowsSharedMapCost.cpp | 78 ++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 core/libs/tests/WindowsSharedMapCost.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index f84fa93f40f3..c7ef05332398 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -323,6 +323,14 @@ if(BUILD_TESTING) endif() add_test(NAME windows_write_watch COMMAND windows_write_watch_tests) set_tests_properties(windows_write_watch PROPERTIES TIMEOUT 20) + add_test_executable(windows_shared_map_cost_tests tests/WindowsSharedMapCost.cpp) + target_include_directories(windows_shared_map_cost_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_compile_definitions(windows_shared_map_cost_tests PRIVATE _WIN32_WINNT=0x0A00) + if(MINGW) + target_compile_options(windows_shared_map_cost_tests PRIVATE -fasynchronous-unwind-tables) + endif() + add_test(NAME windows_shared_map_cost COMMAND windows_shared_map_cost_tests) + set_tests_properties(windows_shared_map_cost PROPERTIES TIMEOUT 120) endif() if(WIN32) diff --git a/core/libs/tests/WindowsSharedMapCost.cpp b/core/libs/tests/WindowsSharedMapCost.cpp new file mode 100644 index 000000000000..ce6c25b6e7ed --- /dev/null +++ b/core/libs/tests/WindowsSharedMapCost.cpp @@ -0,0 +1,78 @@ +#define NOMINMAX +#include "prx/libc/include/WindowsMappings.hpp" +#include +#include +#include +#include +#include +#include + +namespace { +using Mappings = GuestArena::WindowsMappings; +constexpr std::size_t PageBytes = 16384; +constexpr int Rounds = 5; + +[[noreturn]] void Fail(const char* what) { + std::fprintf(stderr, "windows shared map cost: %s (error %lu)\n", what, GetLastError()); + std::exit(1); +} + +std::size_t MappedViews(const unsigned char* begin, std::size_t bytes) { + std::set bases; + for (auto cursor = begin; cursor < begin + bytes;) { + MEMORY_BASIC_INFORMATION info{}; + if (VirtualQuery(cursor, &info, sizeof(info)) != sizeof(info)) Fail("view query failed"); + if (info.Type == MEM_MAPPED) bases.insert(info.AllocationBase); + cursor = static_cast(info.BaseAddress) + info.RegionSize; + } + return bases.size(); +} + +struct Sample { + double mapMs = 0; + double releaseMs = 0; + std::size_t views = 0; +}; + +Sample MapOnce(std::size_t pages) { + const auto bytes = pages * PageBytes; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, static_cast(static_cast(bytes) >> 32), static_cast(bytes), nullptr); + auto* memory = static_cast(Mappings::Get().Reserve(nullptr, bytes)); + if (section == nullptr || memory == nullptr) Fail("cannot allocate shared memory"); + const auto start = std::chrono::steady_clock::now(); + Mappings::Get().Map(memory, bytes, section, 0, PAGE_READWRITE); + Sample sample; + sample.mapMs = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + sample.views = MappedViews(memory, bytes); + memory[0] = 1; + memory[bytes - 1] = 1; + const auto released = std::chrono::steady_clock::now(); + Mappings::Get().Reset(memory, bytes); + sample.releaseMs = std::chrono::duration(std::chrono::steady_clock::now() - released).count(); + VirtualFree(memory, 0, MEM_RELEASE); + CloseHandle(section); + return sample; +} + +double Median(std::vector values) { + std::sort(values.begin(), values.end()); + return values[values.size() / 2]; +} + +} + +int main() { + for (const std::size_t pages : {std::size_t{256}, std::size_t{16384}, std::size_t{65536}}) { + std::vector map; + std::vector release; + std::size_t views = 0; + for (int round = 0; round < Rounds; ++round) { + const auto sample = MapOnce(pages); + map.push_back(sample.mapMs); + release.push_back(sample.releaseMs); + views = sample.views; + } + std::printf("shared map of %zu pages (%zu MiB): map median %.2f ms (%.2f us per page), release median %.2f ms, %zu views\n", pages, pages * PageBytes >> 20, Median(map), Median(map) * 1000.0 / static_cast(pages), Median(release), views); + } + return 0; +} From 312fb247a9b8f63c3ce03e5f11d3b0b9bfcbf342 Mon Sep 17 00:00:00 2001 From: fablevoid <340827264+fable-void@users.noreply.github.com> Date: Mon, 19 Oct 2026 02:27:04 +0000 Subject: [PATCH 2/6] perf(libc): map a run of shared guest pages without per-page queries Mapping shared guest memory page by page queried the address space three times for every 16 KiB page: twice to find the placeholder the page splits from, and once more to see whether the section page still had to be committed, which it then committed on its own. For a range of fresh direct memory that is seven system calls a page, and with shared imports attached every direct mapping takes this path on the guest thread that maps it, under the mapping lock. A mapping that goes page by page now commits its section range once through one window, and splits each page off the placeholder the previous page left, whose extent it already knows; only a page after a resident run or a demotion looks the placeholder up again. A page now costs a placeholder split, the view and its protection. Tested: built; windows_write_watch gains a case that maps 256 shared pages and counts the address-space queries the mapping makes, then stores to and reads back every page. With the per-page lookups it fails (775 queries for 256 pages) and with this change it passes. agc_driver_committed_imports, agc_write_watch_coverage, agc_private_collect, agc_write_guards, agc_driver_dcc_keys_first_write, agc_driver_residency_dirty_mirrors, agc_driver_residency_driver, guest_memory and the host import, window import and alias store tests pass. --- .../libs/prx/libc/include/WindowsMappings.hpp | 43 ++++++++++++++++--- core/libs/tests/WindowsWriteWatch.cpp | 20 +++++++++ 2 files changed, 57 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 624685e8a97d..64e3b76ceb45 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +29,10 @@ class WindowsMappings { return *mappings; } + static std::uint64_t MemoryQueries() { + return queries.load(std::memory_order_relaxed); + } + void* Reserve(void* address, std::size_t bytes) { return allocate(GetCurrentProcess(), address, bytes, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0); } @@ -105,9 +110,14 @@ class WindowsMappings { const auto shared = slot.page.lock(); return resident && !slot.stored && (!shared || shared->aliases.empty()); }; + const bool perPage = !resident; + if (perPage) commitSection(section, offset, bytes); + std::uintptr_t rest = 0; + std::uintptr_t restEnd = 0; for (std::size_t done = 0; done < bytes;) { const auto base = cursor + done; if (single(done)) { + rest = 0; auto runBytes = pageBytes; while (done + runBytes < bytes && (base + runBytes) % chunkBytes != 0 && single(done + runBytes)) runBytes += pageBytes; split(base, runBytes); @@ -138,11 +148,18 @@ class WindowsMappings { const auto shared = pageOf(*slot); const auto aliases = shared->aliases; for (const auto alias : aliases) { - if (views.at(alias).resident) demote(alias); + if (!views.at(alias).resident) continue; + demote(alias); + rest = 0; } const auto viewBytes = std::min(pageBytes, sectionBytes - done); - split(base, pageBytes); - mapShared(base, section, offset + done, protection, viewProtection, viewBytes); + if (base == rest && base + pageBytes <= restEnd) { + if (base + pageBytes != restEnd && !VirtualFree(reinterpret_cast(base), pageBytes, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("split guest placeholder suffix"); + } else { + restEnd = split(base, pageBytes); + } + rest = base + pageBytes; + mapShared(base, section, offset + done, protection, viewProtection, viewBytes, perPage); slot->stored = true; shared->aliases.push_back(base); views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, slot, hostPages(viewBytes)}); @@ -694,12 +711,23 @@ class WindowsMappings { return (bytes + subpageBytes - 1) & ~(subpageBytes - 1); } - void mapShared(std::uintptr_t base, HANDLE section, std::uint64_t offset, DWORD protection, DWORD viewProtection = PAGE_EXECUTE_READWRITE, std::size_t sectionBytes = pageBytes) { + void commitSection(HANDLE section, std::uint64_t offset, std::size_t bytes) { + auto window = open(section, offset, bytes); + const auto memory = query(reinterpret_cast(window.bytes)); + if ((memory.State != MEM_COMMIT || memory.RegionSize < bytes) && !VirtualAlloc(window.bytes, bytes, MEM_COMMIT, PAGE_READWRITE)) { + const auto error = GetLastError(); + close(window); + throw std::system_error(static_cast(error), std::system_category(), "commit direct memory pages"); + } + close(window); + } + + void mapShared(std::uintptr_t base, HANDLE section, std::uint64_t offset, DWORD protection, DWORD viewProtection = PAGE_EXECUTE_READWRITE, std::size_t sectionBytes = pageBytes, bool committed = false) { void* page = reinterpret_cast(base); const auto viewBytes = hostPages(sectionBytes); if (viewBytes != pageBytes) split(base, viewBytes); if (!map(section, GetCurrentProcess(), page, offset, sectionBytes, MEM_REPLACE_PLACEHOLDER, viewProtection, nullptr, 0)) fail("map shared guest page"); - if (query(base).State == MEM_RESERVE && !VirtualAlloc(page, viewBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); + if (!committed && query(base).State == MEM_RESERVE && !VirtualAlloc(page, viewBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); DWORD previous; if (!VirtualProtect(page, viewBytes, protection, &previous)) fail("protect shared guest page"); if (viewBytes == pageBytes) return; @@ -890,11 +918,12 @@ class WindowsMappings { static MEMORY_BASIC_INFORMATION query(std::uintptr_t address) { MEMORY_BASIC_INFORMATION memory{}; + queries.fetch_add(1, std::memory_order_relaxed); 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) { + static std::uintptr_t 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"); @@ -905,6 +934,7 @@ class WindowsMappings { } 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"); + return address + memory.RegionSize; } void reset(std::uintptr_t address, std::size_t bytes) { @@ -1023,6 +1053,7 @@ class WindowsMappings { std::map splitAliases; std::size_t granularity = 0x10000; bool watchResident = false; + static inline std::atomic queries{0}; std::mutex mutex; AllocateFunction allocate = nullptr; MapFunction map = nullptr; diff --git a/core/libs/tests/WindowsWriteWatch.cpp b/core/libs/tests/WindowsWriteWatch.cpp index b44c48af61db..8d95c9e30a71 100644 --- a/core/libs/tests/WindowsWriteWatch.cpp +++ b/core/libs/tests/WindowsWriteWatch.cpp @@ -218,6 +218,25 @@ void CheckConcurrentWrites() { } } +namespace { +void CheckSharedMapQueriesPerMapping() { + constexpr std::size_t Pages = 256; + constexpr std::size_t Span = Pages * 16384; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Span, nullptr); + auto* memory = static_cast(Mappings::Get().Reserve(nullptr, Span)); + Require(section && memory, "cannot allocate shared test memory"); + const auto before = Mappings::MemoryQueries(); + Mappings::Get().Map(memory, Span, section, 0, PAGE_READWRITE); + const auto queries = Mappings::MemoryQueries() - before; + for (std::size_t page = 0; page < Pages; ++page) memory[page * 16384 + 8] = static_cast(page); + for (std::size_t page = 0; page < Pages; ++page) Require(memory[page * 16384 + 8] == static_cast(page), "a shared page lost its store"); + Mappings::Get().Reset(memory, Span); + VirtualFree(memory, 0, MEM_RELEASE); + CloseHandle(section); + Require(queries < Pages, ("a shared mapping queried the address space for every page: " + std::to_string(queries) + " queries for " + std::to_string(Pages) + " pages").c_str()); +} +} + int main() { auto* handler = AddVectoredExceptionHandler(1, HandleWrite); try { @@ -230,6 +249,7 @@ int main() { CheckPrivate(); CheckConcurrentWrites(); CheckForgottenSectionKeepsNoPages(); + CheckSharedMapQueriesPerMapping(); std::cout << "Windows deferred write-watch tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From a16262ba76fa99b9d20dc3afe53f698db7ae43b7 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:03:25 +0000 Subject: [PATCH 3/6] fix(libc): commit a shared mapping's section at once only for whole sections Committing the section range through one read-write window before the pages are mapped works for direct and shared memory, whose sections are committable and at least as long as the mapping. A file mapping passes the bytes its file still has: the window can run past the end of a short file, and a read-only file section refuses a read-write window, so every file mapping would fail. Only mappings that pass no section size commit the range up front; file mappings keep committing each view as before. guest_memory's regular file mapping case maps a read-only file and a file tail shorter than a page on Windows. Adapts fable-void's 027bff519 to main's file mappings. --- core/libs/prx/libc/include/WindowsMappings.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 64e3b76ceb45..68e2ac324feb 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -110,7 +110,7 @@ class WindowsMappings { const auto shared = slot.page.lock(); return resident && !slot.stored && (!shared || shared->aliases.empty()); }; - const bool perPage = !resident; + const bool perPage = !resident && sectionBytes == SIZE_MAX; if (perPage) commitSection(section, offset, bytes); std::uintptr_t rest = 0; std::uintptr_t restEnd = 0; From bdf9bfe14a49d0a0bc13ef02db15226321df9264 Mon Sep 17 00:00:00 2001 From: fablevoid <340827264+fable-void@users.noreply.github.com> Date: Mon, 19 Oct 2026 06:21:22 +0000 Subject: [PATCH 4/6] perf(libc): map a run of shared guest pages as one view Mapping shared direct memory while shared maps are imported placed one view per 16 KiB page: a placeholder split, a map and a protect for every page. A 1 GiB map made about 200,000 system calls, held the mapping thread for about 0.4 s and took and dropped the mappings mutex on every page, so threads taking write faults queued behind it. Map now maps each run of shared pages that need a view as one view: one placeholder split, one map at read-write-execute (the cap later guest protections need) and one protect per run, and builds the per-page bookkeeping under the mutex while yielding to waiting faults. The views move the run's page references instead of copying them, so releasing the run does not hold the mutex. Protections and write tracking change subpages inside the view as before. A view is split only where a release or a remap cuts it: reset unmaps the view, maps the two pieces again over the same section offsets and restores every 4 KiB protection, so armed write tracking and guest protections survive the split. An alias of a page elsewhere and a per-page protection leave the view whole. Tested: windows_write_watch gains a case that maps 1 GiB of shared memory (65536 views before, one after), then protects one page, aliases one page, arms write tracking on one page, releases one page and remaps another: the run splits only at the released and remapped pages (2 and 4 views), every other page keeps its bytes and protection, and the armed page still faults and is collected. It fails before ("a shared gibibyte was mapped as 65536 views instead of one") and passes after. agc_driver_committed_imports: the 1 GiB shared map and import takes 190-196 ms on the mapping thread instead of 418-427 ms, and the longest write fault on another thread stays at 46-97 us. Built; the write watch, committed import, guest memory, write guard, host and window import, residency and mapping tests pass (58 tests). --- .../libs/prx/libc/include/WindowsMappings.hpp | 103 +++++++++++++----- core/libs/tests/WindowsWriteWatch.cpp | 79 ++++++++++++++ 2 files changed, 153 insertions(+), 29 deletions(-) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 68e2ac324feb..e573f3154caa 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -112,12 +112,9 @@ class WindowsMappings { }; const bool perPage = !resident && sectionBytes == SIZE_MAX; if (perPage) commitSection(section, offset, bytes); - std::uintptr_t rest = 0; - std::uintptr_t restEnd = 0; for (std::size_t done = 0; done < bytes;) { const auto base = cursor + done; if (single(done)) { - rest = 0; auto runBytes = pageBytes; while (done + runBytes < bytes && (base + runBytes) % chunkBytes != 0 && single(done + runBytes)) runBytes += pageBytes; split(base, runBytes); @@ -144,28 +141,34 @@ class WindowsMappings { done += runBytes; continue; } - const auto slot = slotOf(done); - const auto shared = pageOf(*slot); - const auto aliases = shared->aliases; - for (const auto alias : aliases) { - if (!views.at(alias).resident) continue; - demote(alias); - rest = 0; - } - const auto viewBytes = std::min(pageBytes, sectionBytes - done); - if (base == rest && base + pageBytes <= restEnd) { - if (base + pageBytes != restEnd && !VirtualFree(reinterpret_cast(base), pageBytes, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("split guest placeholder suffix"); - } else { - restEnd = split(base, pageBytes); + std::vector, std::shared_ptr>> pages; + pages.reserve((bytes - done) / pageBytes); + auto runBytes = std::size_t{0}; + do { + const auto slot = slotOf(done + runBytes); + const auto shared = pageOf(*slot); + const auto aliases = shared->aliases; + for (const auto alias : aliases) { + if (views.at(alias).resident) demote(alias); + } + pages.emplace_back(slot, shared); + runBytes += pageBytes; + } while (done + runBytes < bytes && !single(done + runBytes)); + if (!perPage) commitSection(section, offset + done, runBytes); + split(base, runBytes); + mapRun(base, runBytes, section, offset + done); + DWORD previous; + if (!VirtualProtect(reinterpret_cast(base), runBytes, protection, &previous)) fail("protect shared guest pages"); + collectableRanges.clear(); + for (std::size_t at = 0; at < runBytes; at += pageBytes) { + auto& [slot, shared] = pages[at / pageBytes]; + slot->stored = true; + shared->aliases.push_back(base + at); + invalidate(*shared); + views.emplace(base + at, View{std::move(shared), protection, {}, 0, owned, offset + done + at, 0, 0, false, std::move(slot)}); } - rest = base + pageBytes; - mapShared(base, section, offset + done, protection, viewProtection, viewBytes, perPage); - slot->stored = true; - shared->aliases.push_back(base); - views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, slot, hostPages(viewBytes)}); - remember(freshRanges, base, base + pageBytes); - invalidate(*shared); - done += pageBytes; + remember(freshRanges, base, base + runBytes); + done += runBytes; } } @@ -736,6 +739,47 @@ class WindowsMappings { if (!VirtualProtect(zeros, pageBytes - viewBytes, protection, &previous)) fail("protect guest pages past the end of a file"); } + void mapRun(std::uintptr_t base, std::size_t bytes, HANDLE section, std::uint64_t offset) { + if (!map(section, GetCurrentProcess(), reinterpret_cast(base), offset, bytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map a run of shared guest pages"); + } + + void breakView(std::uintptr_t at) { + const auto memory = query(at); + if (memory.State == MEM_FREE || memory.Type != MEM_MAPPED) return; + const auto start = reinterpret_cast(memory.AllocationBase); + if (start == at) return; + const auto first = views.find(start); + if (first == views.end()) return; + struct Part { + std::uintptr_t base; + std::uintptr_t end; + DWORD protection; + bool committed; + }; + std::vector parts; + auto stop = start; + for (;;) { + const auto part = query(stop); + if (part.AllocationBase != memory.AllocationBase) break; + const auto partEnd = reinterpret_cast(part.BaseAddress) + part.RegionSize; + parts.push_back({stop, partEnd, part.Protect, part.State == MEM_COMMIT}); + stop = partEnd; + } + const auto section = first->second.section; + const auto offset = first->second.offset; + if (!unmap(GetCurrentProcess(), reinterpret_cast(start), MEM_PRESERVE_PLACEHOLDER)) fail("unmap a run of shared guest pages to split it"); + split(start, at - start); + mapRun(start, at - start, section->handle, offset); + mapRun(at, stop - at, section->handle, offset + (at - start)); + for (const auto& part : parts) { + if (!part.committed) continue; + for (const auto [from, to] : {std::pair{part.base, std::min(part.end, at)}, std::pair{std::max(part.base, at), part.end}}) { + DWORD previous; + if (from < to && !VirtualProtect(reinterpret_cast(from), to - from, part.protection, &previous)) fail("protect a split run of shared guest pages"); + } + } + } + static bool blank(std::uintptr_t page) { const auto* words = reinterpret_cast(page); for (std::size_t i = 0; i < pageBytes / sizeof(std::uint64_t); ++i) { @@ -943,6 +987,8 @@ class WindowsMappings { forgetClean(address, end); breakRun(address); breakRun(end); + breakView(address); + breakView(end); if (const auto run = residentRuns.find(address); run != residentRuns.end() && run->second.end == end) { const auto memory = query(address); if (reinterpret_cast(memory.AllocationBase) != address) throw std::runtime_error("cannot release part of a host allocation"); @@ -974,12 +1020,11 @@ class WindowsMappings { 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); - forget(freshRanges, cursor, cursor + pageBytes); + for (auto found = views.lower_bound(cursor); found != views.end() && found->first < allocationEnd;) { + std::erase(found->second.page->aliases, found->first); + found = views.erase(found); } + forget(freshRanges, cursor, allocationEnd); } else if (memory.Type == MEM_PRIVATE) { if (const auto run = residentRuns.find(cursor); run != residentRuns.end()) evict(run); if (!VirtualFree(reinterpret_cast(cursor), allocationEnd - cursor, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory"); diff --git a/core/libs/tests/WindowsWriteWatch.cpp b/core/libs/tests/WindowsWriteWatch.cpp index 8d95c9e30a71..c29b5c7bbee3 100644 --- a/core/libs/tests/WindowsWriteWatch.cpp +++ b/core/libs/tests/WindowsWriteWatch.cpp @@ -235,6 +235,84 @@ void CheckSharedMapQueriesPerMapping() { CloseHandle(section); Require(queries < Pages, ("a shared mapping queried the address space for every page: " + std::to_string(queries) + " queries for " + std::to_string(Pages) + " pages").c_str()); } + +std::size_t MappedViews(const unsigned char* begin, std::size_t bytes) { + std::set bases; + for (auto cursor = begin; cursor < begin + bytes;) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(cursor, &info, sizeof(info)) == sizeof(info), "view query failed"); + if (info.Type == MEM_MAPPED) bases.insert(info.AllocationBase); + cursor = static_cast(info.BaseAddress) + info.RegionSize; + } + return bases.size(); +} + +DWORD ProtectionAt(const unsigned char* address) { + MEMORY_BASIC_INFORMATION info{}; + Require(VirtualQuery(address, &info, sizeof(info)) == sizeof(info), "protection query failed"); + return info.State == MEM_COMMIT ? info.Protect : 0; +} + +void CheckSharedRunIsOneView() { + constexpr std::size_t Gib = std::size_t{1} << 30; + HANDLE large = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, static_cast(Gib), nullptr); + auto* whole = static_cast(Mappings::Get().Reserve(nullptr, Gib)); + Require(large && whole, "cannot allocate a shared gibibyte"); + Mappings::Get().Map(whole, Gib, large, 0, PAGE_READWRITE); + const auto wholeViews = MappedViews(whole, Gib); + whole[Gib - 1] = 1; + Mappings::Get().Reset(whole, Gib); + VirtualFree(whole, 0, MEM_RELEASE); + CloseHandle(large); + Require(wholeViews == 1, ("a shared gibibyte was mapped as " + std::to_string(wholeViews) + " views instead of one").c_str()); + + constexpr std::size_t Page = 16384; + constexpr std::size_t Pages = 64; + constexpr std::size_t Span = Pages * Page; + HANDLE section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE | SEC_RESERVE, 0, Span + Page, nullptr); + auto* memory = static_cast(Mappings::Get().Reserve(nullptr, Span)); + auto* other = static_cast(Mappings::Get().Reserve(nullptr, Page)); + Require(section && memory && other, "cannot allocate shared test memory"); + Mappings::Get().Map(memory, Span, section, 0, PAGE_READWRITE); + for (std::size_t page = 0; page < Pages; ++page) memory[page * Page + 8] = static_cast(page + 1); + Require(MappedViews(memory, Span) == 1, "a shared run was not mapped as one view"); + Mappings::Get().SetProtection(reinterpret_cast(memory + 5 * Page), Page, PAGE_READONLY); + DWORD previous; + Require(VirtualProtect(memory + 5 * Page, Page, PAGE_READONLY, &previous), "guest protection of one page failed"); + Require(MappedViews(memory, Span) == 1, "a per-page protection split the run"); + Mappings::Get().Map(other, Page, section, 30 * Page, PAGE_READWRITE); + Require(MappedViews(memory, Span) == 1, "an alias of one page split the run"); + other[16] = 0x5a; + Require(memory[30 * Page + 16] == 0x5a, "an alias store did not reach the run"); + std::array collected{}; + std::size_t available = collected.size(); + Require(Mappings::Get().Collect(reinterpret_cast(memory + 50 * Page), Page, collected.data(), &available, true, Collection::All), "run collection failed"); + Require(ProtectionAt(memory + 50 * Page) == PAGE_READONLY, "collection did not arm the run's page"); + Mappings::Get().Reset(memory + 10 * Page, Page); + Require(MappedViews(memory, Span) == 2, ("a partial unmap left " + std::to_string(MappedViews(memory, Span)) + " views instead of two").c_str()); + Require(ProtectionAt(memory + 10 * Page) == 0, "the unmapped page is still mapped"); + Mappings::Get().Map(memory + 20 * Page, Page, section, Span, PAGE_READWRITE); + Require(MappedViews(memory, Span) == 4, ("a remap of one page left " + std::to_string(MappedViews(memory, Span)) + " views instead of four").c_str()); + Require(memory[20 * Page + 8] == 0, "the remapped page shows the old page"); + for (std::size_t page = 0; page < Pages; ++page) { + if (page == 10 || page == 20) continue; + Require(memory[page * Page + 8] == static_cast(page + 1), ("a split lost page " + std::to_string(page)).c_str()); + } + Require(ProtectionAt(memory + 5 * Page) == PAGE_READONLY && ProtectionAt(memory + 6 * Page) == PAGE_READWRITE, "a split lost a page protection"); + Require(ProtectionAt(memory + 50 * Page) == PAGE_READONLY, "a split dropped a write-tracking protection"); + const auto before = faults.load(); + memory[50 * Page + 8] = 0x77; + Require(faults == before + 1, "a write to an armed page of a split run did not fault"); + memory[51 * Page + 8] = 0x78; + available = collected.size(); + Require(Mappings::Get().Collect(reinterpret_cast(memory + 50 * Page), 2 * Page, collected.data(), &available, true, Collection::Armed), "armed collection failed"); + Require(available == 1 && collected[0] == memory + 50 * Page, "the split run lost a tracked write"); + Mappings::Get().Reset(memory, Span); + Mappings::Get().Reset(other, Page); + VirtualFree(memory, 0, MEM_RELEASE); + VirtualFree(other, 0, MEM_RELEASE); + CloseHandle(section); +} } int main() { @@ -250,6 +328,7 @@ int main() { CheckConcurrentWrites(); CheckForgottenSectionKeepsNoPages(); CheckSharedMapQueriesPerMapping(); + CheckSharedRunIsOneView(); std::cout << "Windows deferred write-watch tests passed\n"; } catch (const std::exception& error) { std::cerr << error.what() << '\n'; From 1f19ce0f2abf679063630761c87469ace6064159 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:05:03 +0000 Subject: [PATCH 5/6] fix(libc): keep file mappings on one view per page A run view maps the section at read-write-execute over the whole run and commits the run through a read-write window. A file mapping passes the protection its file allows and the bytes its file still has, so a read-only file or a file that ends inside the run cannot be mapped that way. File mappings now take the per-page path they took before the run views: one view per page at the file's view protection, with the last page cut at the end of the file. Mappings that pass no section size, direct and shared memory, still map each run as one view. guest_memory's regular file mapping case maps a read-only file and a file tail shorter than a page on Windows. Adapts fable-void's 3d695fbba to main's file mappings. --- core/libs/prx/libc/include/WindowsMappings.hpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index e573f3154caa..f394e187d142 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -141,6 +141,24 @@ class WindowsMappings { done += runBytes; continue; } + if (sectionBytes != SIZE_MAX) { + const auto slot = slotOf(done); + const auto shared = pageOf(*slot); + const auto aliases = shared->aliases; + for (const auto alias : aliases) { + if (views.at(alias).resident) demote(alias); + } + const auto viewBytes = std::min(pageBytes, sectionBytes - done); + split(base, pageBytes); + mapShared(base, section, offset + done, protection, viewProtection, viewBytes); + slot->stored = true; + shared->aliases.push_back(base); + views.emplace(base, View{shared, protection, {}, 0, owned, offset + done, 0, 0, false, slot, hostPages(viewBytes)}); + remember(freshRanges, base, base + pageBytes); + invalidate(*shared); + done += pageBytes; + continue; + } std::vector, std::shared_ptr>> pages; pages.reserve((bytes - done) / pageBytes); auto runBytes = std::size_t{0}; From a2e0bf19e62f15dd060745849ad67a7c439bd22d Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:53:39 -0700 Subject: [PATCH 6/6] perf(libc): count address-space queries only in the write-watch test The VirtualQuery counter that windows_write_watch_tests reads is now compiled only into that test, through APS5_COUNT_MAPPING_QUERIES, so the runtime's WindowsMappings::query does no atomic update per query. --- core/libs/CMakeLists.txt | 2 +- core/libs/prx/libc/include/WindowsMappings.hpp | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index c7ef05332398..8024f9c930cf 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -317,7 +317,7 @@ if(BUILD_TESTING) if(WIN32) add_test_executable(windows_write_watch_tests tests/WindowsWriteWatch.cpp) target_include_directories(windows_write_watch_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_compile_definitions(windows_write_watch_tests PRIVATE _WIN32_WINNT=0x0A00) + target_compile_definitions(windows_write_watch_tests PRIVATE _WIN32_WINNT=0x0A00 APS5_COUNT_MAPPING_QUERIES=1) if(MINGW) target_compile_options(windows_write_watch_tests PRIVATE -fasynchronous-unwind-tables) endif() diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index f394e187d142..3a9f2cefac89 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -5,7 +5,9 @@ #include #include #include +#ifdef APS5_COUNT_MAPPING_QUERIES #include +#endif #include #include #include @@ -29,9 +31,11 @@ class WindowsMappings { return *mappings; } +#ifdef APS5_COUNT_MAPPING_QUERIES static std::uint64_t MemoryQueries() { return queries.load(std::memory_order_relaxed); } +#endif void* Reserve(void* address, std::size_t bytes) { return allocate(GetCurrentProcess(), address, bytes, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0); @@ -980,7 +984,9 @@ class WindowsMappings { static MEMORY_BASIC_INFORMATION query(std::uintptr_t address) { MEMORY_BASIC_INFORMATION memory{}; +#ifdef APS5_COUNT_MAPPING_QUERIES queries.fetch_add(1, std::memory_order_relaxed); +#endif if (VirtualQuery(reinterpret_cast(address), &memory, sizeof(memory)) != sizeof(memory)) fail("query guest memory"); return memory; } @@ -1116,7 +1122,9 @@ class WindowsMappings { std::map splitAliases; std::size_t granularity = 0x10000; bool watchResident = false; +#ifdef APS5_COUNT_MAPPING_QUERIES static inline std::atomic queries{0}; +#endif std::mutex mutex; AllocateFunction allocate = nullptr; MapFunction map = nullptr;