From c01044eeaa3ef59eef330885f12969f79e80bd4d Mon Sep 17 00:00:00 2001 From: ToyuAU Date: Thu, 8 Oct 2026 18:21:30 +1100 Subject: [PATCH] fix(memory): restore write tracking for fresh private backing --- .../Execution/include/WriteWatchCoverage.hpp | 34 ++++++++++++++++- .../Execution/src/GuestMemory.cpp | 22 ++++++++++- .../libSceAgcDriver/tests/WriteTracking.cpp | 37 +++++++++++++++++++ .../tests/WriteWatchCoverage.cpp | 21 +++++++++++ core/libs/prx/libc/include/GuestArena.hpp | 2 + .../libs/prx/libc/include/WindowsMappings.hpp | 6 ++- core/libs/prx/libc/src/GuestArena.cpp | 19 +++++++++- docs/dev/TechnicalDebt.md | 2 +- 8 files changed, 137 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp index c9dd6a364..a35d3fb19 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include namespace AgcDriver::GuestMemory { @@ -14,16 +16,17 @@ public: base = address; size = bytes; excluded.assign(bytes / BlockBytes + (bytes % BlockBytes != 0), 0); + excludedGenerations.clear(); } bool Covers(std::uint64_t address, std::size_t bytes) const { if (bytes == 0 || bytes > size || address < base || address - base > size - bytes) return false; const auto first = (address - base) / BlockBytes; const auto last = (address - base + bytes - 1) / BlockBytes; - return std::find(excluded.begin() + first, excluded.begin() + last + 1, 1) == excluded.begin() + last + 1; + return std::none_of(excluded.begin() + first, excluded.begin() + last + 1, [](auto generation) { return generation != 0; }); } - bool Exclude(std::uint64_t address, std::size_t bytes) { + bool Exclude(std::uint64_t address, std::size_t bytes, std::uint64_t generation = 1) { if (bytes == 0 || size == 0) return false; if (address < base) { if (base - address >= bytes) return false; @@ -38,16 +41,43 @@ public: bool changed = false; for (auto block = first; block <= last; ++block) { changed = changed || excluded[block] == 0; + auto& stamp = excludedGenerations[block]; + stamp = std::max(stamp, std::max(generation, 1)); excluded[block] = 1; } return changed; } + bool Restore(std::uint64_t address, std::size_t bytes, std::uint64_t generation = std::numeric_limits::max()) { + if (bytes == 0 || size == 0) return false; + if (address < base) { + if (base - address >= bytes) return false; + bytes -= base - address; + address = base; + } + const auto offset = address - base; + if (offset >= size) return false; + bytes = static_cast(std::min(bytes, size - offset)); + const auto first = offset / BlockBytes + (offset % BlockBytes != 0); + const auto stop = offset + bytes; + const auto end = stop / BlockBytes + (stop == size && stop % BlockBytes != 0); + bool changed = false; + for (auto block = first; block < end; ++block) { + if (excluded[block] != 0 && excludedGenerations.at(block) < generation) { + changed = true; + excluded[block] = 0; + excludedGenerations.erase(block); + } + } + return changed; + } + private: static constexpr std::size_t BlockBytes = 65536; std::uint64_t base = 0; std::size_t size = 0; std::vector excluded; + std::unordered_map excludedGenerations; }; } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index 811df3a70..ee0c88170 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -749,6 +749,10 @@ constexpr std::size_t WritePagesPerBlock = WriteBlockBytes / WritePageBytes; enum class StampKind : std::uint8_t { Cpu, Driver, ImportWindow }; +#ifdef _WIN32 +void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_t generation); +#endif + struct WriteTracker { std::mutex mutex; bool initialized = false; @@ -809,6 +813,7 @@ struct WriteTracker { writtenBlocks.assign(size / WriteBlockBytes + 1, 0); coverage.Initialize(base, size); pages.resize(1u << 16); + GuestArena::GuestArenaSetPrivateMappingObserver_nid_postfix(&watchPrivateMapping); #else watched = GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix(); if (watched) leaves.resize(LeafCount); @@ -950,6 +955,21 @@ std::unique_lock lockTracker(WriteTracker& tracker) { return lock; } +#ifdef _WIN32 +void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_t generation) { + auto& tracker = Tracker(); + const auto lock = lockTracker(tracker); + if (!tracker.watched || bytes == 0 || bytes > tracker.size || address < tracker.base || address - tracker.base > tracker.size - bytes) return; + tracker.coverage.Restore(address, bytes, generation); + ++tracker.generation; + for (auto block = tracker.blockOf(address); block <= tracker.blockOf(address + bytes - 1); ++block) { + tracker.driverPieces.erase(block); + tracker.stamp(block, tracker.generation, StampKind::Cpu); + } + unwatchSerial.fetch_add(1, std::memory_order_release); +} +#endif + // Collect epochs are per thread and globally unique: every bump takes a fresh value from one counter, // so a memo entry (stamped with the epoch of its walk) can only match the thread that made it, and // only until that thread's next ordering point (the queue workers bump at theirs, see the header). A @@ -1124,7 +1144,7 @@ namespace { void unwatchLocked(WriteTracker& tracker, std::uint64_t address, std::size_t bytes) { if (!tracker.watched) return; #ifdef _WIN32 - if (tracker.coverage.Exclude(address, bytes)) unwatchSerial.fetch_add(1, std::memory_order_release); + if (tracker.coverage.Exclude(address, bytes, GuestArena::GuestArenaCommitGeneration_nid_postfix())) unwatchSerial.fetch_add(1, std::memory_order_release); #else if (GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast(address), bytes)) unwatchSerial.fetch_add(1, std::memory_order_release); #endif diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 8039ae7c8..fff947f23 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -141,6 +141,40 @@ void CheckUnwatch() { CollectWritesUncached(base, Block); Require(!UnchangedSince(base, Block, adjacent), "unwatch hid a CPU edit in unrelated memory"); } + +#ifdef _WIN32 +void CheckPrivateMappingReuse() { + void* memory = AllocateWatched(3 * Block); + const auto base = reinterpret_cast(memory); + std::memset(memory, 0x11, 3 * Block); + BumpCollectEpoch(); + const auto before = CollectWrites(base, 3 * Block); + Require(before != 0, "the original backing was not collected"); + Unwatch(base + Block, Block); + Require(!Watched(base + Block, Block), "the imported block stayed watched"); + GuestArena::GuestArenaCommit_nid_postfix(memory, 3 * Block, PAGE_READWRITE, 3 * Block); + Require(!Watched(base + Block, Block), "a protection-only commit restored an imported block"); + const auto neighbour = CollectWrites(base, Block); + Require(neighbour != 0, "the neighbouring block was not collected before remapping"); + GuestArena::GuestArenaReset_nid_postfix(memory, 3 * Block); + GuestArena::GuestArenaRelease_nid_postfix(memory, 3 * Block); + auto* replacement = GuestArena::GuestArenaAllocateAtOrAbove_nid_postfix(base, 3 * Block, Block); + Require(replacement == memory, "the released arena address was not reused"); + GuestArena::GuestArenaCommit_nid_postfix(replacement, 3 * Block, PAGE_READWRITE, 3 * Block); + Require(Watched(base, 3 * Block), "fresh backing at an excluded address is not watched"); + Require(!UnchangedSince(base + Block, Block, before), "fresh backing reused an old import generation"); + Require(!UnchangedSince(base, Block, neighbour), "fresh backing reused an adjacent block's old generation"); + const auto after = CollectWrites(base, 3 * Block); + Require(after > before && UnchangedSince(base, 3 * Block, after), "fresh backing was not collected"); + static_cast(replacement)[Block + 8] = 0x22; + CollectWritesUncached(base, 3 * Block); + Require(!UnchangedSince(base + Block, Block, after), "a CPU edit in fresh backing was missed"); + Unwatch(base + Block, Block); + Require(!Watched(base + Block, Block) && CollectWrites(base + Block, Block) == 0, "a second import of fresh backing stayed watched"); + GuestArena::GuestArenaReset_nid_postfix(replacement, 3 * Block); + GuestArena::GuestArenaRelease_nid_postfix(replacement, 3 * Block); +} +#endif } int main() { @@ -152,6 +186,9 @@ int main() { CheckSharedBlock(); CheckOwnStore(); CheckUnwatch(); +#ifdef _WIN32 + CheckPrivateMappingReuse(); +#endif } catch (const std::exception& error) { std::cerr << "write tracking test failed: " << error.what() << "\n"; return 1; diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp index d40221a8d..083a984de 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteWatchCoverage.cpp @@ -32,6 +32,27 @@ int main() { coverage.Initialize(base, 2 * block); require(coverage.Exclude(base - 1, 2) && !coverage.Covers(base, block), "an import crossing the arena start was not clipped"); require(coverage.Covers(base + block, block), "clipping an import excluded the rest of the arena"); + coverage.Initialize(base, 4 * block + 1); + coverage.Exclude(base, 4 * block + 1); + require(!coverage.Restore(base + 1, block - 1) && !coverage.Covers(base, block), "a partial replacement restored an excluded block"); + require(coverage.Restore(base + block, block) && coverage.Covers(base + block, block), "fresh private backing stayed excluded"); + require(!coverage.Restore(base + block, block), "restoring an already watched block reports a change"); + require(!coverage.Covers(base, block) && !coverage.Covers(base + 2 * block, block), "restoring one block restored its neighbours"); + require(coverage.Restore(base + block - 1, 2 * block + 2), "a full interior replacement was not restored"); + require(!coverage.Covers(base, block) && coverage.Covers(base + block, 2 * block) && !coverage.Covers(base + 3 * block, block), "a replacement restored partially covered boundary blocks"); + require(!coverage.Restore(base - block, block) && !coverage.Restore(base + 5 * block, block), "an outside replacement changed coverage"); + require(coverage.Restore(base + 3 * block, std::numeric_limits::max()) && coverage.Covers(base + 3 * block, block + 1), "an oversized replacement was not clipped to the arena"); + require(coverage.Restore(base - block, 2 * block) && coverage.Covers(base, block), "a replacement across the arena start was not clipped"); + coverage.Exclude(base + block + 4096, 1); + require(!coverage.Covers(base + block, block), "a re-import did not exclude fresh backing again"); + coverage.Initialize(base, 2 * block); + coverage.Exclude(base, block, 4); + require(!coverage.Restore(base, block, 3) && !coverage.Restore(base, block, 4), "a stale mapping notification restored a newer import"); + require(coverage.Restore(base, block, 5) && coverage.Covers(base, block), "a newer private mapping did not restore an older exclusion"); + coverage.Exclude(base, block, 5); + coverage.Exclude(base, block, 7); + require(!coverage.Restore(base, block, 6) && !coverage.Covers(base, block), "a delayed mapping notification erased a repeated import"); + require(coverage.Restore(base, block, 8), "a fresh replacement did not restore the latest import exclusion"); std::cout << "write watch coverage passed\n"; return 0; } catch (const std::exception& error) { diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 6a54c7980..0cbeac68d 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -31,6 +31,8 @@ void GuestArenaUnpinWritable_nid_postfix(const void* pointer, std::size_t bytes) 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); bool GuestArenaHostRegionOverlaps_nid_postfix(std::uintptr_t address, std::size_t bytes); +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 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); diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index 200e8e3f8..3c423d148 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -28,7 +28,8 @@ public: return allocate(GetCurrentProcess(), address, bytes, MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0); } - void Commit(void* address, std::size_t bytes, DWORD protection, std::size_t granule, bool watched) { + 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;) { @@ -46,6 +47,8 @@ public: 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"); @@ -60,6 +63,7 @@ public: cursor = stop; } } + return created; } void Reset(void* address, std::size_t bytes) { diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index ff7a0b4f0..c14e34c81 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -19,6 +19,11 @@ namespace GuestArena { namespace { +#ifdef _WIN32 +std::atomic commitGeneration{1}; +std::atomic privateMappingObserver{nullptr}; +#endif + constexpr std::uintptr_t ArenaStart = 0x0000000200000000ull; constexpr std::uintptr_t SystemReservedStart = 0x00000007FFFFC000ull; constexpr std::uintptr_t SystemReservedEnd = 0x0000001000000000ull; @@ -221,9 +226,21 @@ std::invalid_argument OutsideArena(const char* operation, const void* pointer, s } +std::uint64_t GuestArenaCommitGeneration_nid_postfix() { + return commitGeneration.load(std::memory_order_acquire); +} + +void GuestArenaSetPrivateMappingObserver_nid_postfix(void (*callback)(std::uintptr_t, std::size_t, std::uint64_t)) { + privateMappingObserver.store(callback, std::memory_order_release); +} + 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 OutsideArena("commit", pointer, bytes); - WindowsMappings::Get().Commit(pointer, bytes, protection, granule, Arena::Get().WriteWatched()); + const auto generation = commitGeneration.fetch_add(1, std::memory_order_acq_rel) + 1; + const auto created = WindowsMappings::Get().Commit(pointer, bytes, protection, granule, Arena::Get().WriteWatched()); + if (const auto callback = privateMappingObserver.load(std::memory_order_acquire)) { + for (const auto& [address, size] : created) callback(address, size, generation); + } } void GuestArenaReset_nid_postfix(void* pointer, std::size_t bytes) { diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 3b8fcd520..99800be64 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -36,7 +36,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp](../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate; `sceHttpSetRedirectCallback` and `sceHttpSetCookieRecvCallback` accept callbacks that are never called, `sceHttpsSetSslVersion` has no TLS connection to configure and `sceHttpsGetSslError` reports error 0 with detail 0 - [sceFontGlyphDefineAttribute](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) checks the glyph and ignores the attribute and its value, whose meaning is unknown. PPSA01325 sets attribute 0x11 to 0 on a glyph from `sceFontGenerateCharGlyph` that it deletes unused, and draws the character with `sceFontRenderCharGlyphImageHorizontal` -- Windows host-pointer imports are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. [GuestMemory](../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block for the process lifetime, including after that allocation is reused. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for those blocks; restoring tracking safely after import retirement is unimplemented. +- Windows host-pointer imports are compared instead of write-watched: a driver can mark imported pages dirty after the synchronous import window. [GuestMemory](../../core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp) excludes every overlapping 64 KiB tracker block while its backing is retained. A full block replaced by fresh private backing regains tracking and gets a new generation; protection changes, partial replacements and shared remappings do not restore it. This preserves CPU edits through byte comparisons but disables snapshot reuse and unit shadows for excluded blocks; restoring tracking on import retirement without replacing the backing and measuring the comparison cost remain unimplemented. ### Unknown function info