#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 #else #include #endif #include #include #include #include #include #include #include #include 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(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(raw); const auto aligned = ((begin + prefix + alignment - 1) & ~static_cast(alignment - 1)) - prefix; if (aligned != begin) munmap(raw, aligned - begin); if (aligned + bytes != begin + bytes + alignment) munmap(reinterpret_cast(aligned + bytes), begin + alignment - aligned); void* block = reinterpret_cast(aligned); GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); #endif Require(Watched(reinterpret_cast(block), bytes), "the test block is not watched"); return block; } void CheckSharedBlock() { void* memory = AllocateWatched(3 * Block); const auto base = reinterpret_cast(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(boundary - firstBegin)) != 0, "a driver store into watched memory is not stamped"); std::array generations{synced, synced}; std::array 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(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(sharedFirst), synced), "the first surface's own store is not seen over its part of the shared block"); Require(!StoredOver(boundary, static_cast(secondEnd - boundary), synced), "the second surface reads as stored over"); const auto touched = MarkWritten(boundary + 0x100, 4); Require(StoredOver(boundary, static_cast(sharedSecond), synced), "a store over the second surface's bytes is not seen"); Require(!StoredOver(boundary, static_cast(sharedSecond), touched), "a store older than the generation is seen"); const auto whole = TrackerGeneration(); MarkWritten(base + Block, Block); Require(StoredOver(boundary, static_cast(sharedSecond), whole), "a store over the whole block is not seen"); const auto afterWhole = TrackerGeneration(); MarkWritten(base + Block, static_cast(sharedFirst)); Require(!StoredOver(boundary, static_cast(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(store) * 64, 4); Require(StoredOver(boundary, static_cast(sharedSecond), many), "forgotten driver stores are taken as missing the range"); const auto beforeCpu = CollectWrites(base, 3 * Block); static_cast(memory)[Block + 8] = 0x22; CollectWritesUncached(base, 3 * Block); Require(!StoredOver(boundary, static_cast(sharedSecond), beforeCpu), "a CPU store outside the shared range counts for it"); std::array unwatched{}; const auto outside = reinterpret_cast(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(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 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(memory)[Block + 3 * 4096 + 8] = 0x22; const std::array 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(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(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(imported) = 0x42; }); Require(TrackerGeneration() > beforeOwnWrite, "an unwatched CPU store did not invalidate byte comparisons"); std::array generations{before}; std::array 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(memory)[0] = 0x33; CollectWritesUncached(base, Block); Require(!UnchangedSince(base, Block, adjacent), "unwatch hid a CPU edit in unrelated memory"); } void CheckNewestStamp() { void* memory = AllocateWatched(4 * Block); const auto base = reinterpret_cast(memory); const auto first = base + Block; const auto second = base + 2 * Block; std::memset(memory, 0x11, 4 * Block); BumpCollectEpoch(); CollectWrites(base, 4 * Block); Unwatch(first, 2 * Block); const auto firstBefore = NewestStamp(first, Block); const auto secondBefore = NewestStamp(second, Block); MarkWritten(second + 16, 4); Require(NewestStamp(second, Block) > secondBefore, "a driver store into unwatched memory did not advance its stamp"); Require(NewestStamp(first, Block) == firstBefore, "a driver store advanced the stamp of other unwatched memory"); const auto generation = TrackerGeneration(); static_cast(memory)[0] = 0x22; CollectWritesUncached(base, Block); Require(TrackerGeneration() > generation && NewestStamp(first, Block) == firstBefore, "a CPU store into watched memory advanced the stamp of unwatched memory"); StoreOwnBytes(first, 1, [&] { *reinterpret_cast(first) = 0x42; }); Require(NewestStamp(first, Block) > firstBefore, "an own store into unwatched memory did not advance its stamp"); const auto partial = NewestStamp(base + 3 * Block - 8, Block + 8); static_cast(memory)[3 * Block] = 0x33; CollectWritesUncached(base + 3 * Block, Block); Require(NewestStamp(base + 3 * Block - 8, Block + 8) > partial, "a CPU store into the watched part of a partly watched range did not advance its stamp"); } #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); } std::uint64_t ArenaHostHole() { std::uintptr_t arena = 0; std::size_t arenaBytes = 0; GuestArena::GuestArenaRange_nid_postfix(&arena, &arenaBytes); for (auto cursor = arena; cursor < arena + arenaBytes;) { MEMORY_BASIC_INFORMATION info{}; Require(VirtualQuery(reinterpret_cast(cursor), &info, sizeof(info)) == sizeof(info), "cannot enumerate arena host holes"); if (GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(cursor, 1)) return cursor; const auto next = reinterpret_cast(info.BaseAddress) + info.RegionSize; Require(next > cursor, "arena enumeration did not advance"); cursor = next; } return 0; } void CheckHostHole() { constexpr std::size_t bytes = 64; const auto hole = ArenaHostHole(); if (hole == 0) { std::cout << "host-hole query checks skipped: the arena contains no host hole\n"; return; } const auto before = TrackerGeneration(); Require(!Watched(hole, bytes), "an arena host hole reads as watched"); Require(CollectWrites(hole, bytes) == 0 && CollectWritesUncached(hole, bytes) == 0, "an arena host hole was collected"); auto* watched = static_cast(AllocateWatched(Block, true)); struct Cleanup { void* memory; ~Cleanup() { GuestArena::GuestArenaRelease_nid_postfix(memory, Block); } } cleanup{watched}; const auto base = reinterpret_cast(watched); for (unsigned epoch = 0; epoch < 2; ++epoch) { BumpCollectEpoch(); watched[7] ^= 1; Require(CollectWrites(base + 7, 1) != 0, "a warm host-hole check could not collect its watched range"); for (unsigned repeat = 0; repeat < 3; ++repeat) { Require(CollectWrites(base + 7, 1) != 0, "a warm host-hole check lost its watched range"); Require(CollectWrites(hole, bytes) == 0, "a repeated host-hole query hit a collect memo"); Require(CollectWrites(hole + 7, 1) == 0, "a page-rounded host-hole query hit a collect memo"); } } Require(StoredOver(hole, bytes, before), "an arena host hole reads as not stored over"); Require(!UnchangedSince(hole, bytes, before), "an arena host hole reads as unchanged"); const std::array queries{UnchangedQuery{hole, bytes, before}}; Require(!UnchangedSinceAll(queries), "an arena host hole reads as unchanged in a batch"); Require(!UnchangedSinceCollected(hole, bytes, before), "an arena host hole reads as unchanged since a collect"); Require(MarkWritten(hole, bytes) == 0, "a driver store into an arena host hole is stamped"); Require(StoreOwnBytes(hole, bytes, [] {}) == 0, "an own store into an arena host hole is stamped"); Require(!WrittenSince(hole, bytes, before), "an arena host hole reads its stamps as written"); std::array generations{before}; std::array changed{}; std::array cpu{}; Require(!ChangedBlocks(hole, bytes, generations, changed, cpu) && changed[0] == BlockWritten && cpu[0] == 1, "an arena host hole uses block stamps"); bool imported = false; Require(ImportWatched(hole, bytes, [&] { imported = true; return true; }) && imported, "an import into an arena host hole did not run"); Require(!Watched(hole, bytes), "an import made an arena host hole watched"); } #endif void CheckVersionRanges() { constexpr std::size_t count = 80; constexpr auto bytes = count * Block; auto* memory = static_cast(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(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 history; std::vector 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 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(AllocateWatched(bytes)); struct Cleanup { void* memory; ~Cleanup() { #ifdef _WIN32 GuestArena::GuestArenaRelease_nid_postfix(memory, bytes); #else Unwatch(reinterpret_cast(memory), bytes); munmap(memory, bytes); #endif } } cleanup{memory}; const auto base = reinterpret_cast(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(Block - 8), static_cast(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(); CheckNewestStamp(); #ifdef _WIN32 CheckPrivateMappingReuse(); CheckHostHole(); #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; }