Files
AnyPS5/core/libs/prx/libSceAgcDriver/tests/LiveStack.cpp
T
milescovielloandClaude Opus 5.5 eb6b567fbe perf(agc): answer small guest read checks without /proc/self/maps
On Linux kernels without PROCMAP_QUERY (before 6.11, which includes the
stock Ubuntu 24.04 kernel), every guest range check that misses the page
cache reads and parses /proc/self/maps up to the covering mapping. The
cache only covers the guest arena and the image, so ranges in the host
heap always miss. ASTRO BOT (PPSA21564) checks 96-byte shader headers
there from RegisteredHeader (through SameHeader) on every
sceAgcCreatePrimState and interpolant mapping, about 3,700 times per
second while it loads.

A read-only check of at most 4 KiB on such a kernel now first reads the
range into a scratch buffer with process_vm_readv on the own process.
If every byte is read, the range is readable. Anything else (a fault,
a partial read, a refused call, a write check or a larger range) takes
the path it took before, which also builds the failure message.

agc_driver_graphics runs a second time with APS5_NO_PROCMAP_QUERY=1, so
the fallback path is covered on kernels that have PROCMAP_QUERY too, and
LiveStack gains read-only checks: a readable range, and ranges that end
in a guard page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 16:45:07 +00:00

83 lines
4.2 KiB
C++

#include "BdaTests.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include <cstddef>
#include <cstdint>
#include <exception>
#include <stdexcept>
#include <string>
#if defined(__linux__)
#include <pthread.h>
#include <sys/mman.h>
#include <unistd.h>
#endif
namespace {
using AgcDriver::Graphics::Require;
namespace GuestMemory = AgcDriver::GuestMemory;
void checkLiveStack(std::uintptr_t stackBottom, std::uintptr_t stackTop, std::uintptr_t guardEnd) {
alignas(16) volatile std::uint64_t packet[2] = {1, 2};
const auto* address = const_cast<const std::uint64_t*>(packet);
Require(GuestMemory::Accessible(address, sizeof(packet)) && GuestMemory::Accessible(address, sizeof(packet), true), "a live stack frame is not accessible");
GuestMemory::CheckRange(address, sizeof(packet), alignof(std::uint64_t), true);
if (stackTop == 0) return;
Require(GuestMemory::Accessible(reinterpret_cast<const void*>(stackTop - 16), 16, true), "the top of the live stack is not accessible");
Require(!GuestMemory::Accessible(reinterpret_cast<const void*>(stackTop - 16), 32), "a range past the stack's top counts as accessible");
Require(!GuestMemory::Accessible(reinterpret_cast<const void*>(stackTop), guardEnd - stackTop), "the page above the stack counts as accessible");
Require(!GuestMemory::Accessible(reinterpret_cast<const void*>(stackBottom), 16), "an inaccessible page of the stack below the live frames counts as accessible");
bool rejected = false;
try {
GuestMemory::CheckRange(reinterpret_cast<const void*>(stackTop - 8), 16, 8);
} catch (const std::runtime_error&) {
rejected = true;
}
Require(rejected, "CheckRange accepted a range past the stack's top");
}
}
void RunLiveStackAccessTests() {
checkLiveStack(0, 0, 0);
#if defined(_WIN32)
for (const auto address : {std::uintptr_t{0xf00000000000ull}, std::uintptr_t{0x7fffffffffffull}}) {
Require(!GuestMemory::Accessible(reinterpret_cast<const void*>(address), 4),
"an address above the user address space is reported accessible");
const auto committed = GuestMemory::CommittedRanges(address, 4, false);
Require(committed.empty(), "an address above the user address space reports committed bytes");
}
#endif
#if defined(__linux__)
const auto page = static_cast<std::size_t>(sysconf(_SC_PAGESIZE));
const std::size_t stackBytes = 256 * 1024;
auto* block = static_cast<std::byte*>(mmap(nullptr, stackBytes + page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
Require(block != MAP_FAILED, "cannot map a test stack");
Require(mprotect(block + stackBytes, page, PROT_NONE) == 0 && mprotect(block, page, PROT_NONE) == 0, "cannot protect the test stack's guard pages");
const auto top = reinterpret_cast<std::uintptr_t>(block) + stackBytes;
struct Arguments {
std::uintptr_t bottom;
std::uintptr_t top;
std::uintptr_t guardEnd;
std::string failure;
} arguments{reinterpret_cast<std::uintptr_t>(block), top, top + page, {}};
pthread_attr_t attributes;
Require(pthread_attr_init(&attributes) == 0 && pthread_attr_setstack(&attributes, block, stackBytes) == 0, "cannot set the test stack");
pthread_t thread;
Require(pthread_create(&thread, &attributes, [](void* raw) -> void* {
auto& arguments = *static_cast<Arguments*>(raw);
try {
checkLiveStack(arguments.bottom, arguments.top, arguments.guardEnd);
} catch (const std::exception& error) {
arguments.failure = error.what();
}
return nullptr;
}, &arguments) == 0, "cannot start the stack test thread");
pthread_join(thread, nullptr);
pthread_attr_destroy(&attributes);
Require(GuestMemory::Accessible(block + page, 64, true) && !GuestMemory::Accessible(block, 16) && !GuestMemory::Accessible(block + stackBytes, 16), "the released test stack is misreported");
Require(GuestMemory::Accessible(block + page, 64) && !GuestMemory::Accessible(block + page - 8, 16) && !GuestMemory::Accessible(block + stackBytes - 8, 16), "a read of the released test stack is misreported");
munmap(block, stackBytes + page);
Require(arguments.failure.empty(), arguments.failure.c_str());
#endif
}