fix(memory): restore write-watch tracking for shared mappings

This commit is contained in:
boykopovar 2026-09-30 09:18:52 +03:00
parent abb9c85266
commit f7c53a4481
6 changed files with 151 additions and 10 deletions

View File

@ -506,7 +506,9 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) {
const auto base = reinterpret_cast<std::uintptr_t>(memory.BaseAddress);
if (memory.RegionSize > std::numeric_limits<std::uintptr_t>::max() - base || base + memory.RegionSize <= cursor) return false;
const auto regionEnd = base + memory.RegionSize;
const auto protection = memory.Protect & 0xffu;
std::uint32_t logicalProtection = memory.Protect;
GuestArena::GuestArenaProtection_nid_postfix(cursor, &logicalProtection);
const auto protection = logicalProtection & 0xffu;
const bool committed = memory.State == MEM_COMMIT && (memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) == 0;
const bool readable = committed && readableProtection(protection);
const bool writable = readable && writableProtection(protection);
@ -762,9 +764,9 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi
static const bool singlePass = std::getenv("APS5_NO_SINGLE_PASS_COLLECT") == nullptr;
bool dirty = false;
while (cursor < stop) {
ULONG_PTR count = tracker.pages.size();
DWORD granularity = 0;
if (GetWriteWatch(singlePass ? WRITE_WATCH_FLAG_RESET : 0, reinterpret_cast<void*>(cursor), static_cast<std::size_t>(stop - cursor), tracker.pages.data(), &count, &granularity) != 0) {
std::size_t count = tracker.pages.size();
DWORD granularity = 4096;
if (!GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, static_cast<std::size_t>(stop - cursor), tracker.pages.data(), &count, singlePass)) {
// Uncommitted pages inside the range make the call fail; such ranges are compared instead.
return 0;
}
@ -782,9 +784,9 @@ std::uint64_t collectWrites(std::uint64_t address, std::size_t bytes, bool memoi
// The resetting pass reports pages again, so a write racing the first pass is stamped too.
cursor = address & ~(page - 1);
while (cursor < stop) {
ULONG_PTR count = tracker.pages.size();
DWORD granularity = 0;
if (GetWriteWatch(WRITE_WATCH_FLAG_RESET, reinterpret_cast<void*>(cursor), static_cast<std::size_t>(stop - cursor), tracker.pages.data(), &count, &granularity) != 0) return 0;
std::size_t count = tracker.pages.size();
DWORD granularity = 4096;
if (!GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, static_cast<std::size_t>(stop - cursor), tracker.pages.data(), &count, true)) return 0;
for (ULONG_PTR i = 0; i < count; ++i) {
const auto block = (reinterpret_cast<std::uintptr_t>(tracker.pages[i]) - tracker.base) / WriteBlockBytes;
tracker.blocks[block] = tracker.generation;

View File

@ -19,6 +19,10 @@ void GuestArenaRelease_nid_postfix(const void* pointer, std::size_t bytes);
void GuestArenaRange_nid_postfix(std::uintptr_t* base, std::size_t* bytes);
bool GuestArenaWriteWatched_nid_postfix();
#ifdef _WIN32
void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection);
bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address);
bool GuestArenaProtection_nid_postfix(std::uintptr_t address, std::uint32_t* protection);
bool GuestArenaCollectWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear);
void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_t protection, std::size_t granule);
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);

View File

@ -7,6 +7,9 @@
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <map>
#include <memory>
#include <vector>
#include <stdexcept>
#include <system_error>
@ -44,6 +47,11 @@ public:
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;
}
DWORD previous;
if (!VirtualProtect(reinterpret_cast<void*>(cursor), stop - cursor, protection, &previous)) fail("protect guest memory");
cursor = stop;
@ -66,11 +74,110 @@ public:
if (!map(section, GetCurrentProcess(), page, offset + done, pageBytes, MEM_REPLACE_PLACEHOLDER, PAGE_EXECUTE_READWRITE, nullptr, 0)) fail("map shared guest page");
DWORD previous;
if (!VirtualProtect(page, pageBytes, protection, &previous)) fail("protect shared guest page");
const auto key = std::make_pair(reinterpret_cast<std::uintptr_t>(section), offset + done);
auto shared = physical[key].lock();
if (!shared) {
shared = std::make_shared<SharedPage>();
physical[key] = shared;
}
const auto base = cursor + done;
shared->aliases.push_back(base);
views.emplace(base, View{shared, protection, 0, false});
}
}
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;
}
}
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;
++view.page->generation;
DWORD previous;
if (!VirtualProtect(reinterpret_cast<void*>(base), pageBytes, view.protection, &previous)) fail("resume shared memory write");
view.armed = false;
return true;
}
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;
for (auto cursor = address; cursor < end;) {
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(end, base + pageBytes);
if (view.protection == PAGE_NOACCESS) return false;
if (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<void*>(at);
return true;
}
for (auto at = cursor; at < stop; at += 4096) pages[(*count)++] = reinterpret_cast<void*>(at);
}
if (clear) {
for (const auto alias : view.page->aliases) {
auto& other = views.at(alias);
if (!writable(other.protection) || other.armed) continue;
DWORD previous;
const DWORD protection = other.protection == PAGE_EXECUTE_READWRITE ? PAGE_EXECUTE_READ : PAGE_READONLY;
if (!VirtualProtect(reinterpret_cast<void*>(alias), pageBytes, protection, &previous)) fail("arm shared memory write tracking");
other.armed = true;
}
view.seen = view.page->generation;
}
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<std::uintptr_t>(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<void*>(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:
static constexpr std::size_t pageBytes = 0x4000;
struct SharedPage {
std::uint64_t generation = 1;
std::vector<std::uintptr_t> aliases;
};
struct View {
std::shared_ptr<SharedPage> page;
DWORD protection;
std::uint64_t seen;
bool armed;
};
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);
@ -125,6 +232,11 @@ private:
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<void*>(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<void*>(cursor), allocationEnd - cursor, MEM_RELEASE | MEM_PRESERVE_PLACEHOLDER)) fail("release private guest memory");
} else {
@ -140,6 +252,8 @@ private:
if (query(address).RegionSize != bytes && !VirtualFree(reinterpret_cast<void*>(address), bytes, MEM_RELEASE | MEM_COALESCE_PLACEHOLDERS)) fail("coalesce guest placeholders");
}
std::map<std::uintptr_t, View> views;
std::map<std::pair<std::uintptr_t, std::uint64_t>, std::weak_ptr<SharedPage>> physical;
std::mutex mutex;
AllocateFunction allocate = nullptr;
MapFunction map = nullptr;

View File

@ -147,6 +147,22 @@ void GuestArenaRange_nid_postfix(std::uintptr_t* base, std::size_t* bytes) {
}
#ifdef _WIN32
void GuestArenaSetProtection_nid_postfix(std::uintptr_t address, std::size_t bytes, std::uint32_t protection) {
WindowsMappings::Get().SetProtection(address, bytes, protection);
}
bool GuestArenaHandleWrite_nid_postfix(std::uintptr_t address) {
return WindowsMappings::Get().HandleWrite(address);
}
bool GuestArenaProtection_nid_postfix(std::uintptr_t address, std::uint32_t* protection) {
return WindowsMappings::Get().Protection(address, protection);
}
bool GuestArenaCollectWrites_nid_postfix(std::uintptr_t address, std::size_t bytes, void** pages, std::size_t* count, bool clear) {
return WindowsMappings::Get().Collect(address, bytes, pages, count, clear);
}
void GuestArenaCommit_nid_postfix(void* pointer, std::size_t bytes, std::uint32_t protection, std::size_t granule) {
if (!Arena::Get().Contains(pointer, bytes)) throw std::invalid_argument("commit outside the guest arena");
WindowsMappings::Get().Commit(pointer, bytes, protection, granule, Arena::Get().WriteWatched());

View File

@ -1,5 +1,6 @@
#ifdef _WIN32
#include <windows.h>
#include "prx/libc/include/GuestArena.hpp"
#include <atomic>
#include <chrono>
#include <csignal>
@ -235,6 +236,8 @@ bool HandleSse4a(EXCEPTION_POINTERS* info) {
LONG WINAPI ReportCrash(EXCEPTION_POINTERS* info) {
static std::atomic<bool> reported{false};
const auto* fault = info->ExceptionRecord;
if (fault->ExceptionCode == EXCEPTION_ACCESS_VIOLATION && fault->NumberParameters >= 2 && fault->ExceptionInformation[0] == 1 && GuestArena::GuestArenaHandleWrite_nid_postfix(fault->ExceptionInformation[1])) return EXCEPTION_CONTINUE_EXECUTION;
if (HandleWatch(info)) return EXCEPTION_CONTINUE_EXECUTION;
if (HandleSse4a(info)) return EXCEPTION_CONTINUE_EXECUTION;
const auto* record = info->ExceptionRecord;

View File

@ -112,6 +112,7 @@ static int munmap_release(void* addr) {
}
static int mprotect(void* addr, size_t len, int prot) {
GuestArena::GuestArenaSetProtection_nid_postfix(reinterpret_cast<std::uintptr_t>(addr), len, WinProtFromPosix(prot));
if (prot != PROT_NONE && KernelArena::Get().Contains(addr, len)) CommitArenaRange(addr, len, WinProtFromPosix(prot));
auto cursor = reinterpret_cast<std::uintptr_t>(addr);
const auto end = cursor + len;
@ -278,6 +279,7 @@ struct DirectMapping {
std::uintptr_t end;
std::uint64_t phys;
int memoryType;
std::shared_ptr<PhysicalBacking> backing;
};
std::map<std::uintptr_t, DirectMapping> g_directMappings;
@ -289,8 +291,8 @@ void EraseMappings(std::uintptr_t start, std::uintptr_t end) {
const auto base = it->first;
const auto mapping = it->second;
it = g_directMappings.erase(it);
if (base < start) g_directMappings.emplace(base, DirectMapping{start, mapping.phys, mapping.memoryType});
if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType}).first;
if (base < start) g_directMappings.emplace(base, DirectMapping{start, mapping.phys, mapping.memoryType, mapping.backing});
if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}).first;
}
}
@ -317,7 +319,7 @@ void AddMapping(std::uintptr_t address, std::size_t len, std::uint64_t phys, int
bytes += PS5_PAGE_SIZE;
}
page.backing->Map(address + offset, bytes, page.offset, nativeProt);
g_directMappings.emplace(address + offset, DirectMapping{address + offset + bytes, phys + offset, page.backing->MemoryType()});
g_directMappings.emplace(address + offset, DirectMapping{address + offset + bytes, phys + offset, page.backing->MemoryType(), page.backing});
offset += bytes;
}
}