From 71da166a1808033e977eba6581e81d5ec6a41009 Mon Sep 17 00:00:00 2001 From: thibaut Date: Wed, 14 Oct 2026 00:24:09 +0200 Subject: [PATCH] fix(libc): keep resident mappings to direct memory backings on Windows Map made every singly mapped section page private resident memory. A JIT or shm_open section is file backed and committed: its bytes live in the file, and its alias descriptors map duplicated handles, which the physical page slots do not match, so a resident copy diverged from the file, and committing its view on a demote failed with access denied (guest_jit_shared_memory). Only direct memory backings, mapped through the new GuestArenaMapDirect, now become resident; other sections keep main's shared views. Shared pages are committed only while their section page is reserved, as on main. The socket host write tests map their direct memory a second time, so the write-protected shared path they check is still the one they exercise. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01V1W4jdNSKTV7xwuS6xQPJV --- core/libs/prx/libc/include/GuestArena.hpp | 1 + core/libs/prx/libc/include/WindowsMappings.hpp | 10 +++++----- core/libs/prx/libc/src/GuestArena.cpp | 7 ++++++- core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp | 2 +- core/libs/tests/GuestSocketHostWrite.cpp | 7 +++++++ core/libs/tests/WindowsWriteWatch.cpp | 6 +++--- 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 8a9e07a37..3c96845e4 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -41,6 +41,7 @@ bool GuestArenaHostRegionOverlaps_nid_postfix(std::uintptr_t address, std::size_ std::uint64_t GuestArenaCommitGeneration_nid_postfix(); void GuestArenaSetPrivateMappingObserver_nid_postfix(void (*callback)(std::uintptr_t address, std::size_t bytes, std::uint64_t generation)); void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection); +void GuestArenaMapDirect_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection); void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes); void GuestArenaUnmapAlias_nid_postfix(void* alias); bool GuestArenaResidentSpan_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uintptr_t* start, std::uintptr_t* end); diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 08df34a85..87f9f7830 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -75,11 +75,11 @@ public: reset(reinterpret_cast(address), bytes); } - void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched) { + void Map(void* address, std::size_t bytes, HANDLE section, std::uint64_t offset, DWORD protection, bool watched = false, bool resident = false) { std::lock_guard lock(mutex); auto cursor = reinterpret_cast(address); reset(cursor, bytes); - watchResident = watched; + if (resident) watchResident = watched; 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); @@ -95,7 +95,7 @@ public: const auto single = [&](std::size_t done) { const auto& slot = slotOf(done); const auto shared = slot.page.lock(); - return !slot.stored && (!shared || shared->aliases.empty()); + return resident && !slot.stored && (!shared || shared->aliases.empty()); }; for (std::size_t done = 0; done < bytes;) { const auto base = cursor + done; @@ -589,7 +589,7 @@ private: void mapShared(std::uintptr_t base, HANDLE section, std::uint64_t offset, DWORD protection) { void* page = reinterpret_cast(base); if (!map(section, GetCurrentProcess(), page, offset, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page"); - if (!VirtualAlloc(page, pageBytes, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) fail("commit shared guest page"); + if (query(base).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"); } @@ -610,7 +610,7 @@ private: if (!VirtualProtect(reinterpret_cast(start), stop - start, PAGE_READONLY, &previous)) fail("fence resident guest pages"); const auto section = views.at(start).section; auto window = open(section->handle, views.at(start).offset, stop - start); - if (!VirtualAlloc(window.bytes, stop - start, MEM_COMMIT, PAGE_READWRITE)) fail("commit direct memory pages"); + if (query(reinterpret_cast(window.bytes)).State == MEM_RESERVE && !VirtualAlloc(window.bytes, stop - start, MEM_COMMIT, PAGE_READWRITE)) fail("commit direct memory pages"); std::memcpy(window.bytes, reinterpret_cast(start), stop - start); close(window); if (!VirtualFree(reinterpret_cast(start), stop - start, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release resident guest pages"); diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index 44df388a3..5387ae039 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -378,7 +378,12 @@ void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes) { void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection) { if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("shared mapping", pointer, bytes); - WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched()); + WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), false); +} + +void GuestArenaMapDirect_nid_postfix(void* pointer, std::size_t bytes, void* section, std::uint64_t offset, std::uint32_t protection) { + if (!Arena::Get().Contains(pointer, bytes)) throw OutsideArena("direct memory mapping", pointer, bytes); + WindowsMappings::Get().Map(pointer, bytes, section, offset, protection, Arena::Get().WriteWatched(), true); } void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes) { diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index cba67b8f5..4b7e9a094 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -314,7 +314,7 @@ public: void Map(std::uintptr_t address, std::size_t bytes, std::uint64_t offset, int protection) const { #ifdef _WIN32 - GuestArena::GuestArenaMap_nid_postfix(reinterpret_cast(address), bytes, section, offset, WinProtFromPosix(protection)); + GuestArena::GuestArenaMapDirect_nid_postfix(reinterpret_cast(address), bytes, section, offset, WinProtFromPosix(protection)); #else if (::mmap(reinterpret_cast(address), bytes, protection, MAP_SHARED | MAP_FIXED, file, static_cast(fileOffset + offset)) == MAP_FAILED) throw std::system_error(errno, std::generic_category(), "map direct memory backing"); #endif diff --git a/core/libs/tests/GuestSocketHostWrite.cpp b/core/libs/tests/GuestSocketHostWrite.cpp index ff2b0b5d2..06e23b986 100644 --- a/core/libs/tests/GuestSocketHostWrite.cpp +++ b/core/libs/tests/GuestSocketHostWrite.cpp @@ -141,6 +141,8 @@ int main(int argc, char** argv) { Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &physical) == 0, "allocate direct memory"); void* mapping = nullptr; Require(sceKernelMapDirectMemory(&mapping, page * 2, 3, 0, physical, 0) == 0, "map read/write memory"); + void* twin = nullptr; + Require(sceKernelMapDirectMemory(&twin, page * 2, 3, 0, physical, 0) == 0, "map read/write memory a second time"); auto* buffer = static_cast(mapping) + page - 7; const auto collect = [&] { std::array pages{}; @@ -199,6 +201,9 @@ int main(int argc, char** argv) { void* outputMapping = nullptr; Require(sceKernelMapDirectMemory(&outputMapping, page * 2, 3, 0, outputPhysical, 0) == 0, "map sender output memory"); + void* outputTwin = nullptr; + Require(sceKernelMapDirectMemory(&outputTwin, page * 2, 3, 0, outputPhysical, 0) == 0, + "map sender output memory a second time"); const auto collectOutputs = [&] { std::array pages{}; std::size_t count = pages.size(); @@ -238,6 +243,7 @@ int main(int argc, char** argv) { if (mask & 1) Require(Protection(address) == PAGE_READONLY, "sender address rearmed"); if (mask & 2) Require(Protection(length) == PAGE_READONLY, "sender length rearmed"); } + Require(sceKernelMunmap(outputTwin, page * 2) == 0, "unmap the second sender output mapping"); Require(sceKernelMunmap(outputMapping, page * 2) == 0, "unmap sender outputs"); Require(sceKernelReleaseDirectMemory(outputPhysical, page * 2) == 0, "release sender output memory"); } @@ -300,6 +306,7 @@ int main(int argc, char** argv) { Require(Protection(buffer) == PAGE_READONLY, "completed receive can be rearmed"); Require((net ? sceNetSocketClose(sender) : close_nid_postfix(sender)) == 0, "close sender"); Require((net ? sceNetSocketClose(receiver) : close_nid_postfix(receiver)) == 0, "close receiver"); + Require(sceKernelMunmap(twin, page * 2) == 0, "unmap the second direct memory mapping"); Require(sceKernelMunmap(mapping, page * 2) == 0, "unmap direct memory"); Require(sceKernelReleaseDirectMemory(physical, page * 2) == 0, "release direct memory"); } diff --git a/core/libs/tests/WindowsWriteWatch.cpp b/core/libs/tests/WindowsWriteWatch.cpp index f6e799bf6..08b6d4ea1 100644 --- a/core/libs/tests/WindowsWriteWatch.cpp +++ b/core/libs/tests/WindowsWriteWatch.cpp @@ -32,8 +32,8 @@ struct Region { Region() { Require(section && first && alias, "cannot allocate shared test memory"); - Mappings::Get().Map(first, Bytes, section, 0, PAGE_READWRITE, true); - Mappings::Get().Map(alias, Bytes, section, 0, PAGE_READWRITE, true); + Mappings::Get().Map(first, Bytes, section, 0, PAGE_READWRITE); + Mappings::Get().Map(alias, Bytes, section, 0, PAGE_READWRITE); } ~Region() { Mappings::Get().Reset(first, Bytes); @@ -113,7 +113,7 @@ void CheckAliasesAndCapacity() { Require(region.Collect(Collection::Armed) == Pages(0, 1), "alias write was missed or fresh neighbors were consumed"); Require(region.Collect(Collection::Fresh) == Pages(1, 16), "alias collection changed another view's history"); Mappings::Get().Reset(region.first, Bytes); - Mappings::Get().Map(region.first, Bytes, region.section, 0, PAGE_READWRITE, true); + Mappings::Get().Map(region.first, Bytes, region.section, 0, PAGE_READWRITE); Require(region.Collect(Collection::Armed).empty(), "remapped address kept old collection history"); Require(region.Collect(Collection::Fresh) == Pages(0, 16), "remapped memory was not fresh"); }