From f7953ecac38defb6e7ec3c88ee252cf468fc723d Mon Sep 17 00:00:00 2001 From: Izamaru <289729452+Izamaru@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:01:06 -0300 Subject: [PATCH] fix(libkernel): stage Windows APR reads through host memory Copy bounded host reads into the destination in user mode so demand-committed pages can be handled by the process. Preserve short-read validation, zero-fill at EOF and HostWrite protection checks. Add synthetic read and Windows page-fault regressions. AI-assisted: yes. --- core/libs/prx/libkernel/Apr/src/Apr.cpp | 14 ++ core/libs/prx/libkernel/CMakeLists.txt | 19 ++ core/libs/tests/GuestAprRead.cpp | 241 ++++++++++++++++++++++++ 3 files changed, 274 insertions(+) create mode 100644 core/libs/tests/GuestAprRead.cpp diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 7208a99967..67847b576f 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -165,9 +165,23 @@ void _readFile(const Apr::ReadFileCommand& command) { stream.seekg(static_cast(command.offset)); const GuestArena::HostWrite destination(reinterpret_cast(command.destination), command.size); if (!destination.Open()) throw std::runtime_error("APR: the read destination of " + file.path.string() + " is not writable guest memory"); +#ifdef _WIN32 + std::array staging; + std::uint64_t read = 0; + while (read < command.size) { + const auto wanted = static_cast(std::min(command.size - read, staging.size())); + stream.read(staging.data(), wanted); + const auto obtained = static_cast(stream.gcount()); + if (obtained != 0) std::memcpy(reinterpret_cast(command.destination) + read, staging.data(), static_cast(obtained)); + read += obtained; + if (stream.bad()) throw std::runtime_error("APR: read failed for " + file.path.string()); + if (obtained != static_cast(wanted)) break; + } +#else stream.read(reinterpret_cast(command.destination), static_cast(command.size)); if (stream.bad()) throw std::runtime_error("APR: read failed for " + file.path.string()); const auto read = static_cast(stream.gcount()); +#endif if (read != command.size && (!stream.eof() || command.offset + read < std::filesystem::file_size(file.path))) throw std::runtime_error("APR: read of " + file.path.string() + " at offset " + std::to_string(command.offset) + " returned " + std::to_string(read) + " of " + std::to_string(command.size) + " bytes"); if (read != command.size) std::memset(reinterpret_cast(command.destination) + read, 0, static_cast(command.size - read)); } diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 0390328af4..2ffe176dcd 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -102,6 +102,25 @@ if(BUILD_TESTING) set_tests_properties(kernel_error PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_apr_read_tests ${LIBS_INCLUDE_DIR}/tests/GuestAprRead.cpp) + target_include_directories(guest_apr_read_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_apr_read_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_apr_read_tests) + set(aprReadCases host direct eof errors) + if(WIN32) + list(APPEND aprReadCases fault) + endif() + foreach(aprReadCase IN LISTS aprReadCases) + set(aprReadRun ${CMAKE_CURRENT_BINARY_DIR}/apr_read_${aprReadCase}) + file(MAKE_DIRECTORY ${aprReadRun}) + add_test(NAME guest_apr_read_${aprReadCase} COMMAND guest_apr_read_tests ${aprReadCase} + WORKING_DIRECTORY ${aprReadRun}) + set_tests_properties(guest_apr_read_${aprReadCase} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_apr_read_${aprReadCase} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + endforeach() if(NOT WIN32) add_library(guest_memory_lifetime_module MODULE ${LIBS_INCLUDE_DIR}/tests/GuestMemoryLifetimeModule.cpp) target_include_directories(guest_memory_lifetime_module PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/tests/GuestAprRead.cpp b/core/libs/tests/GuestAprRead.cpp new file mode 100644 index 0000000000..87a532686b --- /dev/null +++ b/core/libs/tests/GuestAprRead.cpp @@ -0,0 +1,241 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +extern "C" { +int APS5_VABI sceKernelAprResolveFilepathsToIds(const char**, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceKernelAprSubmitCommandBuffer(const Apr::CommandBufferObject*, std::uint32_t); +int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); +int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); +int APS5_VABI sceKernelMunmap(void*, std::size_t); +int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); +int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +} + +namespace { + +constexpr std::size_t FileBytes = (16u << 20u) + 257u; +constexpr std::size_t MappingBytes = 17u << 20u; +constexpr const char* FilePath = "apr_read_generated.bin"; + +void Require(bool condition, const char* message) { + if (!condition) throw std::runtime_error(message); +} + +std::uint8_t FileByte(std::uint64_t offset) { + return static_cast((offset * 37u) ^ (offset >> 8u) ^ (offset >> 16u)); +} + +class Fixture { +public: + Fixture() { + std::ofstream stream(FilePath, std::ios::binary | std::ios::trunc); + std::array chunk; + for (std::size_t offset = 0; offset < FileBytes; offset += chunk.size()) { + const auto bytes = std::min(chunk.size(), FileBytes - offset); + for (std::size_t index = 0; index < bytes; ++index) chunk[index] = static_cast(FileByte(offset + index)); + stream.write(chunk.data(), static_cast(bytes)); + } + stream.close(); + Require(!stream.fail(), "cannot create the synthetic APR file"); + const char* path = FilePath; + std::uint32_t failed = 0; + Require(sceKernelAprResolveFilepathsToIds(&path, 1, &id, &failed) == 0 && failed == 0, "cannot resolve the synthetic APR file"); + } + + ~Fixture() { + std::error_code ignored; + std::filesystem::remove(FilePath, ignored); + } + + void Read(void* destination, std::uint64_t bytes, std::uint64_t offset) const { + const Apr::ReadFileCommand command{{Apr::Opcode::ReadFile, sizeof(Apr::ReadFileCommand)}, id, 0, + reinterpret_cast(destination), bytes, offset}; + alignas(8) std::array memory{}; + std::memcpy(memory.data(), &command, sizeof(command)); + Apr::CommandBufferObject buffer{Apr::BufferType::Generic, 0, sizeof(memory), 1, sizeof(memory), memory.data()}; + Require(sceKernelAprSubmitCommandBuffer(&buffer, 0) == 0, "APR read submission failed"); + } + +private: + std::uint32_t id = 0; +}; + +class DirectBuffer { +public: + DirectBuffer() { + Require(sceKernelAllocateDirectMemory(0, 13824ll << 20, MappingBytes, 0x4000, 0, &physical) == 0, "cannot allocate direct memory"); + if (sceKernelMapDirectMemory(&pointer, MappingBytes, 0x33, 0, physical, 0x4000) != 0) { + sceKernelReleaseDirectMemory(physical, MappingBytes); + throw std::runtime_error("cannot map direct memory"); + } + } + + ~DirectBuffer() { + sceKernelMunmap(pointer, MappingBytes); + sceKernelReleaseDirectMemory(physical, MappingBytes); + } + + std::uint8_t* Data() const { return static_cast(pointer); } + +private: + void* pointer = nullptr; + std::int64_t physical = 0; +}; + +void CheckRead(const Fixture& fixture, std::uint8_t* data, std::size_t bytes, std::uint64_t offset) { + data[-1] = 0xA5; + data[bytes] = 0x5A; + fixture.Read(data, bytes, offset); + Require(data[-1] == 0xA5 && data[bytes] == 0x5A, "APR wrote outside the requested range"); + for (std::size_t index = 0; index < bytes; ++index) { + const auto expected = offset + index < FileBytes ? FileByte(offset + index) : std::uint8_t{0}; + Require(data[index] == expected, "APR returned incorrect bytes or did not zero-fill EOF"); + } +} + +void TestRead(const Fixture& fixture, std::uint8_t* data) { + CheckRead(fixture, data + 17, (16u << 20u) + 13u, 7); + for (const auto bytes : {std::size_t{0}, std::size_t{1}, std::size_t{65535}, std::size_t{65536}, std::size_t{65537}}) { + CheckRead(fixture, data + 17, bytes, 31); + } +} + +void TestEof(const Fixture& fixture) { + std::vector storage(131200, 0xCC); + auto* data = storage.data() + 17; + CheckRead(fixture, data, 131073, FileBytes - 65537); + CheckRead(fixture, data, 65537, FileBytes); + CheckRead(fixture, data, 65537, FileBytes + 19); +} + +void TestErrors(const Fixture& fixture) { + std::array data; + data.fill(0xA5); + bool rejected = false; + try { + fixture.Read(data.data(), data.size(), std::uint64_t{1} << 63u); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected, "APR accepted an unseekable offset"); + for (const auto byte : data) Require(byte == 0xA5, "failed APR read modified the destination"); +#ifdef _WIN32 + { + const DirectBuffer storage; + storage.Data()[17] = 0xA5; + Require(sceKernelMprotect(storage.Data(), MappingBytes, 0x11) == 0, "cannot protect the direct read buffer"); + rejected = false; + try { + fixture.Read(storage.Data() + 17, 65537, 0); + } catch (const std::runtime_error& error) { + rejected = std::string(error.what()).find("not writable guest memory") != std::string::npos; + } + Require(rejected && storage.Data()[17] == 0xA5, "APR did not reject a read-only guest destination"); + } +#endif + Require(std::filesystem::remove(FilePath), "cannot remove the synthetic APR file"); + rejected = false; + try { + fixture.Read(data.data(), data.size(), 0); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected, "APR accepted a missing file"); + for (const auto byte : data) Require(byte == 0xA5, "missing-file APR read modified the destination"); +} + +#ifdef _WIN32 +std::uintptr_t faultBegin = 0; +std::size_t faultBytes = 0; +std::size_t faultPage = 0; +std::size_t committedPages = 0; + +LONG CALLBACK CommitOnWrite(EXCEPTION_POINTERS* exception) { + const auto& record = *exception->ExceptionRecord; + if (record.ExceptionCode != EXCEPTION_ACCESS_VIOLATION || record.NumberParameters < 2 || record.ExceptionInformation[0] != 1) return EXCEPTION_CONTINUE_SEARCH; + const auto address = static_cast(record.ExceptionInformation[1]); + if (address < faultBegin || address - faultBegin >= faultBytes) return EXCEPTION_CONTINUE_SEARCH; + const auto page = address & ~(static_cast(faultPage) - 1); + if (VirtualAlloc(reinterpret_cast(page), faultPage, MEM_COMMIT, PAGE_READWRITE) == nullptr) return EXCEPTION_CONTINUE_SEARCH; + ++committedPages; + return EXCEPTION_CONTINUE_EXECUTION; +} + +class FaultBuffer { +public: + FaultBuffer() { + SYSTEM_INFO system{}; + GetSystemInfo(&system); + pointer = VirtualAlloc(nullptr, MappingBytes, MEM_RESERVE, PAGE_NOACCESS); + Require(pointer != nullptr, "cannot reserve the fault-backed read buffer"); + handler = AddVectoredExceptionHandler(1, CommitOnWrite); + if (handler == nullptr) { + VirtualFree(pointer, 0, MEM_RELEASE); + throw std::runtime_error("cannot install the synthetic page fault handler"); + } + faultPage = system.dwPageSize; + faultBytes = MappingBytes; + faultBegin = reinterpret_cast(pointer); + committedPages = 0; + } + + ~FaultBuffer() { + faultBytes = 0; + RemoveVectoredExceptionHandler(handler); + VirtualFree(pointer, 0, MEM_RELEASE); + } + + std::uint8_t* Data() const { return static_cast(pointer); } + +private: + void* pointer = nullptr; + void* handler = nullptr; +}; +#endif + +} + +int main(int argc, char** argv) { + try { + Require(argc == 2, "expected host, direct, eof, errors or fault"); + const Fixture fixture; + const std::string mode = argv[1]; + if (mode == "host") { + std::vector storage(MappingBytes, 0xCC); + TestRead(fixture, storage.data()); + } else if (mode == "direct") { + const DirectBuffer storage; + TestRead(fixture, storage.Data()); + } else if (mode == "eof") { + TestEof(fixture); + } else if (mode == "errors") { + TestErrors(fixture); +#ifdef _WIN32 + } else if (mode == "fault") { + const FaultBuffer storage; + TestRead(fixture, storage.Data()); + Require(committedPages > 2, "APR did not exercise demand-committed destination pages"); +#endif + } else { + throw std::runtime_error("unknown APR read case"); + } + std::puts("APR synthetic read checks passed"); + return 0; + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + return 1; + } +}