mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-23 17:13:13 -05:00
Scan version stamps within contiguous leaves. On Linux, collect nearby pages within queue epochs only after confirming full watched coverage. Keep the requested range on Windows. Select the range before collection and do not retry a failed walk. Test sparse-leaf boundaries, write histories, partial unwatches and epochs. Fedora 44 Release tests pass with default, disabled and shared caching. Isolated upstream comparison reduces the saved collect replay by 11-12%. Windows was not tested. AI-assisted: yes.
325 lines
16 KiB
C++
325 lines
16 KiB
C++
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
|
#include "prx/libc/include/GuestArena.hpp"
|
|
#include "prx/libc/include/GuestWriteWatch.hpp"
|
|
#ifdef _WIN32
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
#include <windows.h>
|
|
#else
|
|
#include <sys/mman.h>
|
|
#endif
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
using namespace AgcDriver::GuestMemory;
|
|
|
|
constexpr std::size_t Block = 65536;
|
|
|
|
void Require(bool condition, const std::string& message) {
|
|
if (!condition) throw std::runtime_error(message);
|
|
}
|
|
|
|
void* AllocateWatched(std::size_t bytes, bool crossLeaf = false) {
|
|
#ifdef _WIN32
|
|
static_cast<void>(crossLeaf);
|
|
void* block = GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block);
|
|
GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes);
|
|
#else
|
|
const std::size_t alignment = crossLeaf ? std::size_t{1} << 32 : Block;
|
|
const auto prefix = crossLeaf ? Block : 0;
|
|
void* raw = mmap(nullptr, bytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
|
if (raw == MAP_FAILED) throw std::runtime_error("cannot map the watched block");
|
|
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
|
const auto aligned = ((begin + prefix + alignment - 1) & ~static_cast<std::uintptr_t>(alignment - 1)) - prefix;
|
|
if (aligned != begin) munmap(raw, aligned - begin);
|
|
if (aligned + bytes != begin + bytes + alignment) munmap(reinterpret_cast<void*>(aligned + bytes), begin + alignment - aligned);
|
|
void* block = reinterpret_cast<void*>(aligned);
|
|
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes);
|
|
#endif
|
|
Require(Watched(reinterpret_cast<std::uint64_t>(block), bytes), "the test block is not watched");
|
|
return block;
|
|
}
|
|
|
|
void CheckSharedBlock() {
|
|
void* memory = AllocateWatched(3 * Block);
|
|
const auto base = reinterpret_cast<std::uint64_t>(memory);
|
|
const auto boundary = base + 2 * Block - 0xb800;
|
|
const auto firstBegin = base;
|
|
const auto secondEnd = base + 3 * Block;
|
|
const auto sharedFirst = boundary - (base + Block);
|
|
const auto sharedSecond = base + 2 * Block - boundary;
|
|
std::memset(memory, 0x11, 3 * Block);
|
|
|
|
const auto synced = CollectWrites(base, 3 * Block);
|
|
Require(synced != 0, "the test block is not collected");
|
|
|
|
Require(MarkWritten(firstBegin, static_cast<std::size_t>(boundary - firstBegin)) != 0, "a driver store into watched memory is not stamped");
|
|
std::array<std::uint64_t, 2> generations{synced, synced};
|
|
std::array<std::uint8_t, 2> changed{};
|
|
Require(ChangedBlocks(base + Block, 2 * Block, generations, changed) && changed[0] == BlockWritten && changed[1] == BlockUnchanged, "the shared block does not read as written");
|
|
Require(!StoredOver(boundary, static_cast<std::size_t>(sharedSecond), synced), "a store of the neighbour's bytes counts for the second surface's part of the shared block");
|
|
Require(StoredOver(base + Block, static_cast<std::size_t>(sharedFirst), synced), "the first surface's own store is not seen over its part of the shared block");
|
|
Require(!StoredOver(boundary, static_cast<std::size_t>(secondEnd - boundary), synced), "the second surface reads as stored over");
|
|
|
|
const auto touched = MarkWritten(boundary + 0x100, 4);
|
|
Require(StoredOver(boundary, static_cast<std::size_t>(sharedSecond), synced), "a store over the second surface's bytes is not seen");
|
|
Require(!StoredOver(boundary, static_cast<std::size_t>(sharedSecond), touched), "a store older than the generation is seen");
|
|
|
|
const auto whole = TrackerGeneration();
|
|
MarkWritten(base + Block, Block);
|
|
Require(StoredOver(boundary, static_cast<std::size_t>(sharedSecond), whole), "a store over the whole block is not seen");
|
|
const auto afterWhole = TrackerGeneration();
|
|
MarkWritten(base + Block, static_cast<std::size_t>(sharedFirst));
|
|
Require(!StoredOver(boundary, static_cast<std::size_t>(sharedSecond), afterWhole), "a partial store after a whole-block store is taken for the whole block");
|
|
|
|
const auto many = TrackerGeneration();
|
|
for (int store = 0; store < 6; ++store) MarkWritten(base + Block + static_cast<std::uint64_t>(store) * 64, 4);
|
|
Require(StoredOver(boundary, static_cast<std::size_t>(sharedSecond), many), "forgotten driver stores are taken as missing the range");
|
|
|
|
const auto beforeCpu = CollectWrites(base, 3 * Block);
|
|
static_cast<volatile std::uint8_t*>(memory)[Block + 8] = 0x22;
|
|
CollectWritesUncached(base, 3 * Block);
|
|
Require(!StoredOver(boundary, static_cast<std::size_t>(sharedSecond), beforeCpu), "a CPU store outside the shared range counts for it");
|
|
|
|
std::array<std::uint8_t, 64> unwatched{};
|
|
const auto outside = reinterpret_cast<std::uint64_t>(unwatched.data());
|
|
if (!Watched(outside, unwatched.size())) Require(StoredOver(outside, unwatched.size(), TrackerGeneration()), "an unwatched range reads as not stored over");
|
|
}
|
|
|
|
void CheckOwnStore() {
|
|
void* memory = AllocateWatched(2 * Block);
|
|
const auto base = reinterpret_cast<std::uint64_t>(memory);
|
|
const auto second = base + Block;
|
|
std::memset(memory, 0x11, 2 * Block);
|
|
const auto synced = CollectWrites(base, 2 * Block);
|
|
Require(synced != 0, "the own-store block is not collected");
|
|
|
|
std::array<std::byte, 8192> bytes{};
|
|
bytes.fill(std::byte{0x44});
|
|
Write(second, bytes);
|
|
const auto stored = TrackerGeneration();
|
|
Require(!UnchangedSince(second, bytes.size(), synced), "the driver store is not stamped");
|
|
CollectWritesUncached(base, 2 * Block);
|
|
Require(UnchangedSince(second, Block, stored), "a walk after a driver store reports the store again as a newer write");
|
|
Require(UnchangedSinceCollected(second, bytes.size(), synced), "the driver store's own page faults are stamped as a CPU write");
|
|
|
|
static_cast<volatile std::uint8_t*>(memory)[Block + 3 * 4096 + 8] = 0x22;
|
|
const std::array<std::byte, 64> label{};
|
|
Write(second + 3 * 4096 + 64, label);
|
|
Require(!UnchangedSinceCollected(second + 3 * 4096, 4096, stored), "a CPU write before a driver store in its page is not stamped as one");
|
|
|
|
const auto beforeCpu = TrackerGeneration();
|
|
static_cast<volatile std::uint8_t*>(memory)[16] = 0x33;
|
|
CollectWritesUncached(base, 2 * Block);
|
|
Require(!UnchangedSince(base, 64, beforeCpu), "a CPU write after the driver store is not seen");
|
|
}
|
|
|
|
void CheckUnwatch() {
|
|
void* memory = AllocateWatched(3 * Block);
|
|
const auto base = reinterpret_cast<std::uint64_t>(memory);
|
|
const auto imported = base + Block;
|
|
std::memset(memory, 0x11, 3 * Block);
|
|
BumpCollectEpoch();
|
|
const auto before = CollectWrites(base, 3 * Block);
|
|
Require(before != 0 && UnchangedSince(imported, Block, before), "the import range was not initially watched");
|
|
Unwatch(imported, Block);
|
|
Require(!Watched(imported, Block) && !UnchangedSince(imported, Block, before), "unwatch kept trusting import stamps");
|
|
Require(CollectWrites(base, 3 * Block) == 0 && CollectWritesUncached(imported, Block) == 0, "unwatch did not invalidate a cached collect");
|
|
Require(Watched(base, Block) && Watched(base + 2 * Block, Block), "unwatch disabled unrelated memory");
|
|
const auto beforeDriverWrite = TrackerGeneration();
|
|
Require(MarkWritten(imported, Block) == 0, "an unwatched import still produces trusted stamps");
|
|
Require(TrackerGeneration() > beforeDriverWrite, "an unwatched driver write did not invalidate byte comparisons");
|
|
const auto beforeOwnWrite = TrackerGeneration();
|
|
StoreOwnBytes(imported, 1, [&] { *reinterpret_cast<std::uint8_t*>(imported) = 0x42; });
|
|
Require(TrackerGeneration() > beforeOwnWrite, "an unwatched CPU store did not invalidate byte comparisons");
|
|
std::array<std::uint64_t, 1> generations{before};
|
|
std::array<std::uint8_t, 1> changed{};
|
|
Require(!ChangedBlocks(imported, Block, generations, changed), "an unwatched import still uses block stamps");
|
|
Unwatch(imported, Block);
|
|
const auto adjacent = CollectWrites(base, Block);
|
|
Require(adjacent != 0, "unwatch broke the adjacent block's collect");
|
|
static_cast<volatile std::uint8_t*>(memory)[0] = 0x33;
|
|
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<std::uint64_t>(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<volatile std::uint8_t*>(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
|
|
|
|
void CheckVersionRanges() {
|
|
constexpr std::size_t count = 80;
|
|
constexpr auto bytes = count * Block;
|
|
auto* memory = static_cast<std::uint8_t*>(AllocateWatched(bytes, true));
|
|
struct Cleanup {
|
|
void* memory;
|
|
~Cleanup() {
|
|
#ifdef _WIN32
|
|
GuestArena::GuestArenaRelease_nid_postfix(memory, bytes);
|
|
#else
|
|
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(memory, bytes);
|
|
munmap(memory, bytes);
|
|
#endif
|
|
}
|
|
} cleanup{memory};
|
|
const auto base = reinterpret_cast<std::uint64_t>(memory);
|
|
Require(UnchangedSince(base, bytes, TrackerGeneration()), "a new range has newer version stamps");
|
|
std::memset(memory, 17, bytes);
|
|
const auto initial = CollectWritesUncached(base, bytes);
|
|
Require(initial != 0, "version ranges could not be collected");
|
|
struct Write { std::size_t block; std::uint64_t generation; };
|
|
std::vector<Write> history;
|
|
std::vector<std::uint64_t> checkpoints{initial};
|
|
std::uint32_t random = 0x61387a29;
|
|
const auto next = [&] { random = random * 1664525u + 1013904223u; return random; };
|
|
const auto expected = [&](const UnchangedQuery& query) {
|
|
if (query.generation == 0) return false;
|
|
for (const auto& write : history) {
|
|
const auto begin = base + write.block * Block;
|
|
if (write.generation > query.generation && begin < query.address + query.bytes && query.address < begin + Block) return false;
|
|
}
|
|
return true;
|
|
};
|
|
for (unsigned step = 0; step < 512; ++step) {
|
|
const auto block = step < 3 ? step : next() % count;
|
|
std::uint64_t generation;
|
|
if (step % 3 == 0) {
|
|
memory[block * Block + 9] ^= 1;
|
|
generation = CollectWritesUncached(base + block * Block, Block);
|
|
} else generation = MarkWritten(base + block * Block + 13, 4);
|
|
Require(generation > checkpoints.back(), "version range write did not advance the tracker");
|
|
history.push_back({block, generation});
|
|
checkpoints.push_back(generation);
|
|
for (unsigned check = 0; check < 16; ++check) {
|
|
std::array<UnchangedQuery, 3> queries;
|
|
bool all = true;
|
|
for (auto& query : queries) {
|
|
const auto offset = next() % bytes;
|
|
query = {base + offset, 1 + next() % (bytes - offset), checkpoints[next() % checkpoints.size()]};
|
|
const auto same = expected(query);
|
|
Require(UnchangedSince(query.address, query.bytes, query.generation) == same, "version range disagrees with write history");
|
|
all = all && same;
|
|
}
|
|
Require(UnchangedSinceAll(queries) == all, "batched version ranges disagree with write history");
|
|
}
|
|
}
|
|
Require(!UnchangedSince(base, bytes, 0), "an unknown generation was accepted");
|
|
Require(UnchangedSince(base, bytes, checkpoints.back()), "the latest checkpoint was rejected");
|
|
}
|
|
|
|
void CheckCollectBoundaries() {
|
|
constexpr std::size_t bytes = 3 * Block;
|
|
auto* memory = static_cast<std::uint8_t*>(AllocateWatched(bytes));
|
|
struct Cleanup {
|
|
void* memory;
|
|
~Cleanup() {
|
|
#ifdef _WIN32
|
|
GuestArena::GuestArenaRelease_nid_postfix(memory, bytes);
|
|
#else
|
|
Unwatch(reinterpret_cast<std::uint64_t>(memory), bytes);
|
|
munmap(memory, bytes);
|
|
#endif
|
|
}
|
|
} cleanup{memory};
|
|
const auto base = reinterpret_cast<std::uint64_t>(memory);
|
|
std::memset(memory, 17, bytes);
|
|
bool unboundedObserved = false;
|
|
std::thread observer([&] {
|
|
const auto before = CollectWrites(base + 7, 1);
|
|
memory[7] = 19;
|
|
const auto after = CollectWrites(base + 7, 1);
|
|
unboundedObserved = before != 0 && after > before && !UnchangedSince(base + 7, 1, before);
|
|
});
|
|
observer.join();
|
|
Require(unboundedObserved, "a thread without collect epochs reused a stale observation");
|
|
for (const auto offset : {7u, 4096u + 3u, static_cast<unsigned>(Block - 8), static_cast<unsigned>(Block + 3)}) {
|
|
BumpCollectEpoch();
|
|
const auto before = CollectWrites(base + offset, 16);
|
|
Require(before != 0, "an epoch lost watched coverage");
|
|
memory[offset] ^= 1;
|
|
BumpCollectEpoch();
|
|
Require(CollectWrites(base + offset, 16) > before && !UnchangedSince(base + offset, 16, before), "a new epoch missed a CPU write");
|
|
const auto observed = TrackerGeneration();
|
|
memory[offset + 1] ^= 1;
|
|
Require(CollectWritesUncached(base + offset, 16) > observed && !UnchangedSince(base + offset, 16, observed), "an uncached observation reused an epoch result");
|
|
const auto stored = MarkWritten(base + offset + 2, 1);
|
|
Require(CollectWrites(base + offset, 16) >= stored && !UnchangedSince(base + offset, 16, observed), "a memoized observation hid a driver write");
|
|
}
|
|
#ifndef _WIN32
|
|
BumpCollectEpoch();
|
|
Require(CollectWrites(base, 1) != 0, "initial partial-block observation failed");
|
|
Unwatch(base + 4096, 4096);
|
|
Require(CollectWrites(base + 4096, 1) == 0, "an unwatched neighbor retained an epoch observation");
|
|
BumpCollectEpoch();
|
|
const auto before = CollectWrites(base, 16);
|
|
Require(before != 0, "an unwatched neighbor prevented collecting a watched prefix");
|
|
memory[0] ^= 1;
|
|
BumpCollectEpoch();
|
|
Require(CollectWrites(base, 16) > before && !UnchangedSince(base, 16, before), "partial-block collection missed a CPU write");
|
|
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(memory + 4096, 4096);
|
|
Require(CollectWrites(base + 4096, 1) != 0, "a re-registered neighbor stayed unwatched");
|
|
#endif
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
try {
|
|
if (!WriteWatched()) {
|
|
std::cout << "no write watching: skipped\n";
|
|
return 77;
|
|
}
|
|
CheckSharedBlock();
|
|
CheckOwnStore();
|
|
CheckUnwatch();
|
|
#ifdef _WIN32
|
|
CheckPrivateMappingReuse();
|
|
#endif
|
|
CheckVersionRanges();
|
|
CheckCollectBoundaries();
|
|
} catch (const std::exception& error) {
|
|
std::cerr << "write tracking test failed: " << error.what() << "\n";
|
|
return 1;
|
|
}
|
|
std::cout << "write tracking tests passed\n";
|
|
return 0;
|
|
}
|