Files
AnyPS5/core/libs/tests/WriteWatch.cpp
T
Utku Halis 99ddfa762a fix(libc): track never-collected pages in the macOS write watch
On macOS, GuestWriteWatchCollectFresh reported nothing and returned true, so
MarkWritten took pages no collection had armed yet for clean, and they stayed
writable without being tracked. GuestWriteWatchCollectArmed was a full collect,
which also took those pages.

Pages now carry a fresh bit from registration until a collection takes them, as
the Linux watch keeps its fresh ranges: CollectFresh reports and arms only fresh
pages, CollectArmed skips them, and Collect takes both. Protection changes, host
writes and faults keep the bit.

The write_watch fresh and armed tests check both collections.
2026-10-11 18:34:59 +03:00

253 lines
14 KiB
C++

#include "prx/libc/include/GuestWriteWatch.hpp"
#include <array>
#include <atomic>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <set>
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <sys/mman.h>
#include <unistd.h>
#if !defined(__APPLE__)
#error The page protection write watch is the macOS backend
#endif
using namespace GuestWriteWatch;
namespace {
void Require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
struct Region {
explicit Region(std::size_t pageCount) : page(static_cast<std::size_t>(::getpagesize())), bytes(pageCount * page) {
base = static_cast<std::uint8_t*>(::mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0));
Require(base != MAP_FAILED, "cannot map the test region");
}
~Region() {
GuestWriteWatchUnregister_nid_postfix(base, bytes);
::munmap(base, bytes);
}
std::uintptr_t Address(std::size_t index) const {
return reinterpret_cast<std::uintptr_t>(base) + index * page;
}
using Collector = bool (*)(std::uintptr_t, std::size_t, void (*)(void*, std::uintptr_t, std::uintptr_t), void*);
std::set<std::size_t> Collect(Collector collector = &GuestWriteWatchCollect_nid_postfix, std::size_t first = 0, std::size_t count = 0) {
std::set<std::size_t> pages;
struct Sink {
Region* region;
std::set<std::size_t>* pages;
} sink{this, &pages};
const bool complete = collector(Address(first), count == 0 ? bytes - first * page : count * page, [](void* context, std::uintptr_t begin, std::uintptr_t end) {
auto& target = *static_cast<Sink*>(context);
for (auto at = begin; at < end; at += target.region->page) target.pages->insert((at - target.region->Address(0)) / target.region->page);
}, &sink);
Require(complete, "collection of a watched range is incomplete");
return pages;
}
std::size_t page;
std::size_t bytes;
std::uint8_t* base = nullptr;
};
std::set<std::size_t> All(std::size_t count) {
std::set<std::size_t> pages;
for (std::size_t index = 0; index < count; ++index) pages.insert(index);
return pages;
}
void CheckCollect() {
Region region(8);
Require(GuestWriteWatchAvailable_nid_postfix(), "write watch is unavailable");
Require(!GuestWriteWatchCovers_nid_postfix(region.Address(0), region.bytes), "unregistered range is covered");
GuestWriteWatchRegister_nid_postfix(region.base, region.bytes);
Require(GuestWriteWatchCovers_nid_postfix(region.Address(0), region.bytes), "registered range is not covered");
Require(region.Collect() == All(8), "a new range is not reported in full");
Require(region.Collect().empty(), "an untouched range reports writes");
region.base[3 * region.page + 17] = 1;
region.base[5 * region.page] = 2;
region.base[5 * region.page + 9] = 3;
Require(region.Collect() == std::set<std::size_t>({3, 5}), "written pages are not reported exactly");
Require(region.Collect().empty(), "collected writes are reported again");
Require(region.base[3 * region.page + 17] == 1 && region.base[5 * region.page + 9] == 3, "watched writes were lost");
std::memset(region.base + region.page - 4, 7, 8);
Require(region.Collect() == std::set<std::size_t>({0, 1}), "a write across a page boundary is not reported on both pages");
}
void CheckFresh() {
Region region(8);
GuestWriteWatchRegister_nid_postfix(region.base, region.bytes);
Require(region.Collect(&GuestWriteWatchCollect_nid_postfix, 0, 4) == All(4), "the collected half is not reported in full");
region.base[region.page] = 1;
Require(region.Collect(&GuestWriteWatchCollectFresh_nid_postfix) == std::set<std::size_t>({4, 5, 6, 7}), "a fresh collection does not report exactly the never collected pages");
Require(region.Collect(&GuestWriteWatchCollectFresh_nid_postfix).empty(), "a fresh collection reports pages again");
region.base[6 * region.page] = 1;
Require(region.Collect() == std::set<std::size_t>({1, 6}), "a fresh collection does not arm the pages it reports");
Require(region.Collect().empty(), "collected writes are reported again");
}
void CheckArmed() {
Region region(8);
GuestWriteWatchRegister_nid_postfix(region.base, region.bytes);
Require(region.Collect(&GuestWriteWatchCollect_nid_postfix, 0, 4) == All(4), "the collected half is not reported in full");
region.base[region.page] = 1;
region.base[6 * region.page] = 1;
Require(region.Collect(&GuestWriteWatchCollectArmed_nid_postfix) == std::set<std::size_t>({1}), "an armed collection does not report exactly the written armed pages");
Require(region.Collect() == std::set<std::size_t>({4, 5, 6, 7}), "an armed collection takes never collected pages");
Require(region.Collect().empty(), "collected writes are reported again");
}
void CheckRegisterAgain() {
Region region(4);
GuestWriteWatchRegister_nid_postfix(region.base, region.bytes);
Require(region.Collect() == All(4), "a new range is not reported in full");
GuestWriteWatchRegister_nid_postfix(region.base, region.bytes);
Require(region.Collect() == All(4), "a range registered again is not reported in full");
region.base[2 * region.page] = 1;
Require(region.Collect() == std::set<std::size_t>({2}), "a range registered while protected stops reporting writes");
GuestWriteWatchRegister_nid_postfix(region.base + region.page, region.page);
Require(region.Collect() == std::set<std::size_t>({1}), "registering part of a range does not report that part alone");
region.base[0] = 1;
region.base[3 * region.page] = 1;
Require(region.Collect() == std::set<std::size_t>({0, 3}), "the rest of a split range stops reporting writes");
}
int KernelProtection(const void* page) {
mach_vm_address_t address = reinterpret_cast<mach_vm_address_t>(page);
mach_vm_size_t size = 0;
vm_region_basic_info_data_64_t info{};
mach_msg_type_number_t count = VM_REGION_BASIC_INFO_COUNT_64;
mach_port_t object = MACH_PORT_NULL;
Require(mach_vm_region(mach_task_self(), &address, &size, VM_REGION_BASIC_INFO_64, reinterpret_cast<vm_region_info_t>(&info), &count, &object) == KERN_SUCCESS && address == reinterpret_cast<mach_vm_address_t>(page), "cannot read the kernel protection of a page");
return ((info.protection & VM_PROT_READ) ? PROT_READ : 0) | ((info.protection & VM_PROT_WRITE) ? PROT_WRITE : 0) | ((info.protection & VM_PROT_EXECUTE) ? PROT_EXEC : 0);
}
void CheckRegisterMapped() {
Region region(4);
std::uintptr_t runEnd = 0;
Require(::mprotect(region.base, region.bytes, PROT_NONE) == 0, "cannot reserve the test region");
GuestWriteWatchRegister_nid_postfix(region.base, region.bytes);
Require(::mmap(region.base, region.bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0) == region.base, "cannot map over the reservation");
GuestWriteWatchRegister_nid_postfix(region.base, region.bytes);
Require(GuestWriteWatchProtection_nid_postfix(region.Address(0), region.Address(4), &runEnd) == (PROT_READ | PROT_WRITE) && runEnd == region.Address(4), "a range mapped over its reservation keeps the reservation's protection");
Require(region.Collect() == All(4), "a mapped range is not reported in full");
region.base[2 * region.page] = 1;
Require(region.Collect() == std::set<std::size_t>({2}), "a range mapped over its reservation stops reporting writes");
Require(::mmap(region.base + region.page, region.page, PROT_NONE, MAP_PRIVATE | MAP_ANON | MAP_FIXED, -1, 0) == region.base + region.page, "cannot reserve a watched page again");
GuestWriteWatchRegister_nid_postfix(region.base + region.page, region.page);
Require(GuestWriteWatchProtection_nid_postfix(region.Address(1), region.Address(2), &runEnd) == PROT_NONE, "a protected page reserved again reports its old protection");
Require(KernelProtection(region.base + region.page) == PROT_NONE, "registering a protected page reserved again restored its old protection");
}
void CheckHostWrite() {
Region region(4);
GuestWriteWatchRegister_nid_postfix(region.base, region.bytes);
Require(region.Collect() == All(4), "a new range is not reported in full");
int descriptors[2];
Require(::pipe(descriptors) == 0, "cannot create a pipe");
const std::array<char, 8> payload{'w', 'a', 't', 'c', 'h', 'e', 'd', '!'};
Require(::write(descriptors[1], payload.data(), payload.size()) == static_cast<ssize_t>(payload.size()), "cannot fill the pipe");
errno = 0;
Require(::read(descriptors[0], region.base + region.page, payload.size()) == -1 && errno == EFAULT, "a system call wrote a protected page without a host write");
GuestWriteWatchBeginHostWrite_nid_postfix(region.base + region.page, payload.size());
Require(region.Collect() == std::set<std::size_t>({1}), "a page under a host write is not reported");
Require(::read(descriptors[0], region.base + region.page, payload.size()) == static_cast<ssize_t>(payload.size()), "a collection protected a page during a host write");
GuestWriteWatchEndHostWrite_nid_postfix(region.base + region.page, payload.size());
Require(std::memcmp(region.base + region.page, payload.data(), payload.size()) == 0, "the host write did not reach the page");
Require(region.Collect() == std::set<std::size_t>({1}), "a host write is not reported after it ends");
Require(region.Collect().empty(), "a finished host write is reported again");
::close(descriptors[0]);
::close(descriptors[1]);
}
void CheckProtection() {
Region region(6);
std::uintptr_t runEnd = 0;
Require(GuestWriteWatchProtection_nid_postfix(region.Address(0), region.Address(6), &runEnd) == -1 && runEnd == region.Address(6), "an unwatched range reports a protection");
GuestWriteWatchRegister_nid_postfix(region.base + 2 * region.page, 3 * region.page);
Require(GuestWriteWatchProtection_nid_postfix(region.Address(0), region.Address(6), &runEnd) == -1 && runEnd == region.Address(2), "the run before a watched range does not end at it");
static_cast<void>(GuestWriteWatchCollect_nid_postfix(region.Address(2), 3 * region.page, [](void*, std::uintptr_t, std::uintptr_t) {}, nullptr));
Require(GuestWriteWatchProtection_nid_postfix(region.Address(2), region.Address(6), &runEnd) == (PROT_READ | PROT_WRITE) && runEnd == region.Address(5), "a protected page does not report the protection it was given");
Require(::mprotect(region.base + 3 * region.page, region.page, PROT_READ) == 0, "cannot change the protection");
GuestWriteWatchProtectionChanged_nid_postfix(region.base + 3 * region.page, region.page, PROT_READ);
Require(GuestWriteWatchProtection_nid_postfix(region.Address(2), region.Address(6), &runEnd) == (PROT_READ | PROT_WRITE) && runEnd == region.Address(3), "a run does not end where the protection changes");
Require(GuestWriteWatchProtection_nid_postfix(region.Address(3), region.Address(6), &runEnd) == PROT_READ && runEnd == region.Address(4), "a changed protection is not reported");
std::set<std::size_t> pages;
static_cast<void>(GuestWriteWatchCollect_nid_postfix(region.Address(2), 3 * region.page, [](void* context, std::uintptr_t begin, std::uintptr_t) { static_cast<std::set<std::size_t>*>(context)->insert(begin); }, &pages));
Require(pages == std::set<std::size_t>({region.Address(3)}), "a protection change is not reported as a write");
Require(::mprotect(region.base + 3 * region.page, region.page, PROT_READ | PROT_WRITE) == 0, "cannot restore the protection");
GuestWriteWatchProtectionChanged_nid_postfix(region.base + 3 * region.page, region.page, PROT_READ | PROT_WRITE);
region.base[3 * region.page] = 1;
GuestWriteWatchUnregister_nid_postfix(region.base + 2 * region.page, 3 * region.page);
region.base[2 * region.page] = 1;
region.base[4 * region.page] = 1;
Require(!GuestWriteWatchCovers_nid_postfix(region.Address(2), region.page), "an unregistered range is covered");
}
void CheckThreads() {
constexpr std::size_t Threads = 4;
constexpr std::size_t PagesPerThread = 64;
constexpr int Rounds = 50;
Region region(Threads * PagesPerThread);
GuestWriteWatchRegister_nid_postfix(region.base, region.bytes);
Require(region.Collect() == All(Threads * PagesPerThread), "a new range is not reported in full");
std::atomic<bool> start{false};
std::atomic<std::size_t> finished{0};
std::array<std::thread, Threads> writers;
for (std::size_t thread = 0; thread < Threads; ++thread) {
writers[thread] = std::thread([&, thread] {
while (!start.load()) std::this_thread::yield();
for (int round = 1; round <= Rounds; ++round)
for (std::size_t index = 0; index < PagesPerThread; ++index) region.base[(thread * PagesPerThread + index) * region.page] = static_cast<std::uint8_t>(round);
finished.fetch_add(1);
});
}
std::vector<std::uint8_t> seen(Threads * PagesPerThread, 0);
const auto take = [&] {
for (const auto index : region.Collect()) seen[index] = region.base[index * region.page];
};
start.store(true);
while (finished.load() != Threads) {
take();
std::this_thread::yield();
}
for (auto& writer : writers) writer.join();
take();
for (std::size_t index = 0; index < seen.size(); ++index)
Require(seen[index] == static_cast<std::uint8_t>(Rounds), "a write that raced a collection was not reported");
Require(region.Collect().empty(), "writes are reported after the writers stopped");
}
}
int main(int argc, char** argv) {
try {
Require(argc == 2, "expected collect, fresh, armed, register, mapped, host, protection or threads");
const std::string_view mode(argv[1]);
if (mode == "collect") CheckCollect();
else if (mode == "fresh") CheckFresh();
else if (mode == "armed") CheckArmed();
else if (mode == "register") CheckRegisterAgain();
else if (mode == "mapped") CheckRegisterMapped();
else if (mode == "host") CheckHostWrite();
else if (mode == "protection") CheckProtection();
else if (mode == "threads") CheckThreads();
else throw std::runtime_error("unknown write watch test");
std::printf("write watch %s passed\n", argv[1]);
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}