#include "prx/libc/include/GuestWriteWatch.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace GuestWriteWatch; namespace { void Require(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); } struct Mapping { Mapping(int flags, int protection) : protection(protection) { char path[] = "/tmp/anyps5-watch-backing-XXXXXX"; file = ::mkstemp(path); Require(file >= 0, "cannot create backing file"); ::unlink(path); Require(::ftruncate(file, 2 * page) == 0, "cannot size backing file"); base = static_cast(::mmap(nullptr, 2 * page, PROT_READ | PROT_WRITE, flags, file, 0)); Require(base != MAP_FAILED, "cannot map backing file"); Require(::mprotect(base, 2 * page, protection) == 0, "cannot set mapping protection"); } ~Mapping() { GuestWriteWatchUnregister_nid_postfix(base, 2 * page); ::munmap(base, 2 * page); ::close(file); } std::set Collect() { std::set pages; Require(GuestWriteWatchCollect_nid_postfix(reinterpret_cast(base), 2 * page, [](void* context, std::uintptr_t begin, std::uintptr_t end) { auto& output = *static_cast*>(context); const auto page = static_cast(::getpagesize()); for (auto at = begin; at < end; at += page) output.insert(at); }, &pages), "cannot collect watched mapping"); return pages; } void Watch(bool armed, bool hostWrite) { GuestWriteWatchRegister_nid_postfix(base, 2 * page); Require(GuestWriteWatchProtect_nid_postfix(base, 2 * page, protection) == 0, "cannot record mapping protection"); if (armed) Require(Collect().size() == 2, "fresh mapping was not reported"); if (hostWrite) GuestWriteWatchBeginHostWrite_nid_postfix(base + page, page); } const std::size_t page = static_cast(::getpagesize()); const int protection; int file = -1; std::uint8_t* base = nullptr; }; template int RunChild(TFunction function) { const auto child = ::fork(); Require(child >= 0, "cannot fork fault test"); if (child == 0) { ::alarm(2); try { function(); } catch (...) { ::_exit(2); } ::_exit(0); } int status = 0; Require(::waitpid(child, &status, 0) == child, "cannot wait for fault test"); return status; } template void RequireFault(TFunction function) { const int status = RunChild(function); Require(WIFSIGNALED(status) && (WTERMSIG(status) == SIGBUS || WTERMSIG(status) == SIGSEGV), "unrecoverable write did not deliver its fault"); } void CheckTruncation(int flags) { for (const int protection : {PROT_READ | PROT_WRITE, PROT_WRITE}) { for (const bool armed : {false, true}) { for (const bool hostWrite : {false, true}) { for (const bool keepFirst : {false, true}) { const auto write = [&](bool watched) { Mapping mapping(flags, protection); if (watched) mapping.Watch(armed, hostWrite); Require(::ftruncate(mapping.file, keepFirst ? mapping.page : 0) == 0, "cannot truncate backing file"); *static_cast(mapping.base + mapping.page + 17) = 42; if (watched) { if (hostWrite) GuestWriteWatchEndHostWrite_nid_postfix(mapping.base + mapping.page, mapping.page); Require(mapping.Collect().contains(reinterpret_cast(mapping.base + mapping.page)), "surviving private write was not tracked"); } }; const int native = RunChild([&] { write(false); }); Require((WIFEXITED(native) && WEXITSTATUS(native) == 0) || (WIFSIGNALED(native) && (WTERMSIG(native) == SIGBUS || WTERMSIG(native) == SIGSEGV)), "native truncation control failed"); const int watched = RunChild([&] { write(true); }); if (watched != native) std::fprintf(stderr, "flags=%d protection=%d armed=%d host=%d keepFirst=%d native=%d watched=%d\n", flags, protection, armed, hostWrite, keepFirst, native, watched); Require(watched == native, "write watch changed the native truncation result"); } } } } } void CheckWritable() { for (const int flags : {MAP_SHARED, MAP_PRIVATE}) { for (const int protection : {PROT_READ | PROT_WRITE, PROT_WRITE}) { for (const bool armed : {false, true}) { for (const bool hostWrite : {false, true}) { Mapping mapping(flags, protection); mapping.Watch(armed, hostWrite); mapping.base[mapping.page + 17] = 42; if (hostWrite) GuestWriteWatchEndHostWrite_nid_postfix(mapping.base + mapping.page, mapping.page); const auto pages = mapping.Collect(); Require(pages.size() == (armed ? 1u : 2u) && pages.contains(reinterpret_cast(mapping.base + mapping.page)), "valid write was not reported"); Require(mapping.Collect().empty(), "valid write was reported twice"); GuestWriteWatchUnregister_nid_postfix(mapping.base, 2 * mapping.page); Require(mach_vm_protect(mach_task_self(), reinterpret_cast(mapping.base), 2 * mapping.page, false, VM_PROT_READ | VM_PROT_WRITE) == KERN_SUCCESS, "cannot read back valid write"); Require(mapping.base[mapping.page + 17] == 42, "valid write was lost"); } } } } } std::atomic arrived{0}; std::atomic firstHandled{false}; struct sigaction previousBus{}; struct sigaction previousSegv{}; static_assert(std::atomic::is_always_lock_free && std::atomic::is_always_lock_free); void DelaySecondFault(int signal, siginfo_t* info, void* context) { const auto order = arrived.fetch_add(1); if (order >= 2) ::_exit(4); while (arrived.load() < 2) {} if (order == 1) while (!firstHandled.load()) {} (signal == SIGBUS ? previousBus : previousSegv).sa_sigaction(signal, info, context); if (order == 0) firstHandled.store(true); } void CheckLateFault() { ::alarm(10); struct sigaction delayed{}; delayed.sa_sigaction = &DelaySecondFault; delayed.sa_flags = SA_SIGINFO | SA_RESTART; sigemptyset(&delayed.sa_mask); Require(::sigaction(SIGBUS, &delayed, &previousBus) == 0, "cannot delay SIGBUS"); Require(::sigaction(SIGSEGV, &delayed, &previousSegv) == 0, "cannot delay SIGSEGV"); for (const int flags : {MAP_SHARED, MAP_PRIVATE}) { for (const int protection : {PROT_READ | PROT_WRITE, PROT_WRITE}) { Mapping mapping(flags, protection); mapping.Watch(true, false); for (unsigned round = 1; round <= 8; ++round) { arrived.store(0); firstHandled.store(false); std::thread first([&] { mapping.base[mapping.page + 32] = static_cast(round); }); std::thread second([&] { mapping.base[mapping.page + 64] = static_cast(round); }); first.join(); second.join(); Require(arrived.load() == 2 && firstHandled.load(), "two simultaneous faults were not exercised"); Require(mapping.Collect() == std::set{reinterpret_cast(mapping.base + mapping.page)}, "late write fault was not tracked"); Require(mapping.Collect().empty(), "late write fault was tracked twice"); } GuestWriteWatchUnregister_nid_postfix(mapping.base, 2 * mapping.page); Require(mach_vm_protect(mach_task_self(), reinterpret_cast(mapping.base), 2 * mapping.page, false, VM_PROT_READ | VM_PROT_WRITE) == KERN_SUCCESS, "cannot read back concurrent writes"); Require(mapping.base[mapping.page + 32] == 8 && mapping.base[mapping.page + 64] == 8, "concurrent write was lost"); } } Require(::sigaction(SIGBUS, &previousBus, nullptr) == 0 && ::sigaction(SIGSEGV, &previousSegv, nullptr) == 0, "cannot restore fault handlers"); ::alarm(0); } void CheckRestoreFailure() { RequireFault([] { Mapping mapping(MAP_SHARED, PROT_READ | PROT_WRITE); mapping.Watch(true, false); Require(mach_vm_protect(mach_task_self(), reinterpret_cast(mapping.base), 2 * mapping.page, true, VM_PROT_READ) == KERN_SUCCESS, "cannot lower maximum protection"); *static_cast(mapping.base + mapping.page + 17) = 42; }); } } int main(int argc, char** argv) { try { Require(argc == 2 && GuestWriteWatchAvailable_nid_postfix(), "expected a mode and available write watch"); const std::string_view mode(argv[1]); if (mode == "shared") CheckTruncation(MAP_SHARED); else if (mode == "private") CheckTruncation(MAP_PRIVATE); else if (mode == "writable") CheckWritable(); else if (mode == "late") CheckLateFault(); else if (mode == "restore") CheckRestoreFailure(); else throw std::runtime_error("unknown write-watch backing test"); std::printf("write watch backing %s passed\n", argv[1]); return 0; } catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; } }