From 59db660f7bc37f65ae3c48b26b742f3493c16657 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Tue, 13 Oct 2026 07:47:05 +1300 Subject: [PATCH] test(agc): reserve the arena host hole the write-tracking checks query On Windows the guest arena is reserved while libc loads, around whatever the process has already mapped in its interval. About one start in forty has nothing mapped there, so the arena has no host hole: agc_driver_graphics then fails with "write-watched arena contained no host exclusion to test" and agc_write_tracking prints that its host-hole checks were skipped. agc_write_tracking now starts itself suspended, reserves one allocation granule in the middle of the child's arena interval before the child's loader runs, and the child runs the host-hole checks on that range. The range is a host hole on every start, so the checks no longer depend on the address layout. The four queries agc_driver_graphics made on an enumerated hole are among them and are removed there. --- .../libSceAgcDriver/tests/BdaResources.cpp | 23 ------- .../libSceAgcDriver/tests/WriteTracking.cpp | 61 ++++++++++++++----- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp index 392eab435f..7ab64fdd32 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp @@ -804,29 +804,6 @@ void sparseHeapTests(const Context& context, const BdaTestAccess& access) { void hostWriteWatchExclusionTests() { #ifdef _WIN32 - if (AgcDriver::GuestMemory::WriteWatched()) { - std::uintptr_t arena = 0; - std::size_t arenaBytes = 0; - GuestArena::GuestArenaRange_nid_postfix(&arena, &arenaBytes); - bool found = false; - 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)) { - Require(!AgcDriver::GuestMemory::Watched(cursor, 1), "an arena host hole was reported as write-watched"); - Require(AgcDriver::GuestMemory::CollectWritesUncached(cursor, 1) == 0, "an arena host hole returned a write generation"); - Require(!AgcDriver::GuestMemory::UnchangedSince(cursor, 1, 1), "an arena host hole passed an unchanged proof"); - const std::array queries{AgcDriver::GuestMemory::UnchangedQuery{cursor, 1, 1}}; - Require(!AgcDriver::GuestMemory::UnchangedSinceAll(queries), "an arena host hole passed a bulk unchanged proof"); - found = true; - break; - } - const auto next = reinterpret_cast(info.BaseAddress) + info.RegionSize; - Require(next > cursor, "arena enumeration did not advance"); - cursor = next; - } - Require(found, "write-watched arena contained no host exclusion to test"); - } std::vector host(64, 0x31); const auto address = reinterpret_cast(host.data()); Require(!AgcDriver::GuestMemory::Watched(address, host.size()), "ordinary host heap memory was reported as write-watched"); diff --git a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp index 8528450d4e..3bc7c74a56 100644 --- a/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/WriteTracking.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -23,6 +24,9 @@ namespace { using namespace AgcDriver::GuestMemory; constexpr std::size_t Block = 65536; +#ifdef _WIN32 +constexpr wchar_t HostHoleMode[] = L"--host-hole"; +#endif void Require(bool condition, const std::string& message) { if (!condition) throw std::runtime_error(message); @@ -211,28 +215,49 @@ void CheckPrivateMappingReuse() { GuestArena::GuestArenaRelease_nid_postfix(replacement, 3 * Block); } -std::uint64_t ArenaHostHole() { +std::uint64_t ChildHostHole() { 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 (arena + arenaBytes / 2) & ~(std::uint64_t{Block} - 1); +} + +void CheckHostHoleInChild() { + std::array executable{}; + const DWORD length = GetModuleFileNameW(nullptr, executable.data(), static_cast(executable.size())); + Require(length != 0 && length < executable.size(), "cannot name the test executable"); + const std::wstring command = L"\"" + std::wstring(executable.data(), length) + L"\" " + HostHoleMode; + std::vector commandLine(command.begin(), command.end()); + commandLine.push_back(L'\0'); + STARTUPINFOW startup{}; + startup.cb = sizeof(startup); + startup.dwFlags = STARTF_USESTDHANDLES; + startup.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + startup.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE); + startup.hStdError = GetStdHandle(STD_ERROR_HANDLE); + PROCESS_INFORMATION process{}; + Require(CreateProcessW(executable.data(), commandLine.data(), nullptr, nullptr, TRUE, CREATE_NO_WINDOW | CREATE_SUSPENDED, nullptr, nullptr, &startup, &process) != 0, "cannot create the host-hole child"); + const bool reserved = VirtualAllocEx(process.hProcess, reinterpret_cast(ChildHostHole()), Block, MEM_RESERVE, PAGE_NOACCESS) != nullptr; + const bool resumed = reserved && ResumeThread(process.hThread) != static_cast(-1); + const bool exited = resumed && WaitForSingleObject(process.hProcess, 60000) == WAIT_OBJECT_0; + if (!exited) { + TerminateProcess(process.hProcess, 1); + WaitForSingleObject(process.hProcess, INFINITE); } - return 0; + DWORD exitCode = 1; + const bool reported = GetExitCodeProcess(process.hProcess, &exitCode) != 0; + CloseHandle(process.hThread); + CloseHandle(process.hProcess); + Require(reserved, "cannot reserve a host range in the child's arena interval"); + Require(resumed, "cannot resume the host-hole child"); + Require(exited, "the host-hole child did not exit"); + Require(reported && exitCode == 0, "the host-hole child failed"); } 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 hole = ChildHostHole(); + Require(GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(hole, bytes), "the range reserved before the arena is not an arena host region"); 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"); @@ -394,13 +419,19 @@ int main() { std::cout << "no write watching: skipped\n"; return 77; } +#ifdef _WIN32 + if (std::wstring_view(GetCommandLineW()).ends_with(HostHoleMode)) { + CheckHostHole(); + return 0; + } +#endif CheckSharedBlock(); CheckOwnStore(); CheckUnwatch(); CheckNewestStamp(); #ifdef _WIN32 CheckPrivateMappingReuse(); - CheckHostHole(); + CheckHostHoleInChild(); #endif CheckVersionRanges(); CheckCollectBoundaries();