From 676bbea3486876dd40643327ebad37007ec95ee0 Mon Sep 17 00:00:00 2001 From: Michon <231849@buas.nl> Date: Thu, 1 Oct 2026 18:56:36 +0200 Subject: [PATCH] fix(libkernel): read, pread, AIO and APR write into guest memory as host writes A file read into guest memory is a host write: before the I/O it pins the shared pages it covers, which reopens any page armed for write tracking and keeps write tracking from arming it again until the read ends, when the pages count as written. A destination the guest cannot write fails with EFAULT before any I/O; read and pread throw as for any failed read, AIO reports SCE_KERNEL_ERROR_EFAULT, and APR throws. APR also throws when a read ends before the requested size. Co-Authored-By: Claude Opus 5.5 (1M context) --- core/libs/prx/libc/include/GuestArena.hpp | 18 ++++ .../libs/prx/libc/include/WindowsMappings.hpp | 33 +++++++- core/libs/prx/libc/src/GuestArena.cpp | 19 +++++ core/libs/prx/libkernel/Aio/src/Aio.cpp | 7 ++ core/libs/prx/libkernel/Apr/src/Apr.cpp | 5 ++ core/libs/prx/libkernel/File/src/Open.cpp | 5 +- core/libs/prx/libkernel/File/src/Stdio.cpp | 7 +- core/libs/tests/GuestMemory.cpp | 82 +++++++++++++++++++ 8 files changed, 172 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/include/GuestArena.hpp b/core/libs/prx/libc/include/GuestArena.hpp index 8d7972549..2eab958d6 100644 --- a/core/libs/prx/libc/include/GuestArena.hpp +++ b/core/libs/prx/libc/include/GuestArena.hpp @@ -29,9 +29,27 @@ void GuestArenaMap_nid_postfix(void* pointer, std::size_t bytes, void* section, void* GuestArenaMapAlias_nid_postfix(std::uintptr_t address, std::size_t bytes); void GuestArenaUnmapAlias_nid_postfix(void* alias); #endif +bool GuestArenaBeginHostWrite_nid_postfix(void* pointer, std::size_t bytes); +void GuestArenaEndHostWrite_nid_postfix(void* pointer, std::size_t bytes); } +class HostWrite { +public: + HostWrite(void* pointer, std::size_t bytes) : pointer(pointer), bytes(bytes), open(GuestArenaBeginHostWrite_nid_postfix(pointer, bytes)) {} + ~HostWrite() { + if (open) GuestArenaEndHostWrite_nid_postfix(pointer, bytes); + } + HostWrite(const HostWrite&) = delete; + HostWrite& operator=(const HostWrite&) = delete; + bool Open() const { return open; } + +private: + void* pointer; + std::size_t bytes; + bool open; +}; + } #endif diff --git a/core/libs/prx/libc/include/WindowsMappings.hpp b/core/libs/prx/libc/include/WindowsMappings.hpp index d2bf6c862..6fb6018c7 100644 --- a/core/libs/prx/libc/include/WindowsMappings.hpp +++ b/core/libs/prx/libc/include/WindowsMappings.hpp @@ -88,7 +88,7 @@ public: } const auto base = cursor + done; shared->aliases.push_back(base); - views.emplace(base, View{shared, protection, 0, false, owned, offset + done}); + views.emplace(base, View{shared, protection, 0, false, owned, offset + done, 0}); invalidate(*shared); } } @@ -115,6 +115,34 @@ public: return true; } + bool BeginHostWrite(std::uintptr_t address, std::size_t bytes) { + std::lock_guard lock(mutex); + const auto first = views.lower_bound(address & ~(pageBytes - 1)); + const auto end = address + bytes; + for (auto it = first; it != views.end() && it->first < end; ++it) { + if (!writable(it->second.protection)) return false; + } + for (auto it = first; it != views.end() && it->first < end; ++it) { + auto& view = it->second; + ++view.hostWrites; + invalidate(*view.page); + if (!view.armed) continue; + DWORD previous; + if (!VirtualProtect(reinterpret_cast(it->first), pageBytes, view.protection, &previous)) fail("open shared memory to a host write"); + view.armed = false; + } + return true; + } + + void EndHostWrite(std::uintptr_t address, std::size_t bytes) { + std::lock_guard lock(mutex); + const auto end = address + bytes; + for (auto it = views.lower_bound(address & ~(pageBytes - 1)); it != views.end() && it->first < end; ++it) { + --it->second.hostWrites; + invalidate(*it->second.page); + } + } + void* MapAlias(std::uintptr_t address, std::size_t bytes) { std::lock_guard lock(mutex); const auto refuse = [&](const char* reason) { @@ -190,7 +218,7 @@ public: if (clear) { for (const auto alias : view.page->aliases) { auto& other = views.at(alias); - if (!writable(other.protection) || other.armed) continue; + if (!writable(other.protection) || other.armed || other.hostWrites != 0) continue; DWORD previous; const DWORD protection = other.protection == PAGE_EXECUTE_READWRITE ? PAGE_EXECUTE_READ : PAGE_READONLY; if (!VirtualProtect(reinterpret_cast(alias), pageBytes, protection, &previous)) fail("arm shared memory write tracking"); @@ -236,6 +264,7 @@ private: bool armed; std::shared_ptr
section; std::uint64_t offset; + std::uint32_t hostWrites; }; void forgetClean(std::uintptr_t start, std::uintptr_t end) { auto it = cleanRanges.lower_bound(start); diff --git a/core/libs/prx/libc/src/GuestArena.cpp b/core/libs/prx/libc/src/GuestArena.cpp index dc87e282e..48f4709dd 100644 --- a/core/libs/prx/libc/src/GuestArena.cpp +++ b/core/libs/prx/libc/src/GuestArena.cpp @@ -191,4 +191,23 @@ bool GuestArenaWriteWatched_nid_postfix() { return Arena::Get().WriteWatched(); } +bool GuestArenaBeginHostWrite_nid_postfix(void* pointer, std::size_t bytes) { +#ifdef _WIN32 + return WindowsMappings::Get().BeginHostWrite(reinterpret_cast(pointer), bytes); +#else + (void)pointer; + (void)bytes; + return true; +#endif +} + +void GuestArenaEndHostWrite_nid_postfix(void* pointer, std::size_t bytes) { +#ifdef _WIN32 + WindowsMappings::Get().EndHostWrite(reinterpret_cast(pointer), bytes); +#else + (void)pointer; + (void)bytes; +#endif +} + } diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index baff62d79..b592c1643 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -8,6 +8,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/KernelErrors.hpp" #ifdef _WIN32 @@ -49,6 +50,11 @@ void SetState(std::int32_t id, std::int32_t state) { } std::int64_t NativePread(std::int32_t fd, void* buf, std::size_t nbyte, std::int64_t offset) { + const GuestArena::HostWrite destination(buf, nbyte); + if (!destination.Open()) { + errno = EFAULT; + return -1; + } #ifdef _WIN32 if (nbyte > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelAioSubmitReadCommands: nbytes exceeds platform limit"); @@ -110,6 +116,7 @@ int SubmitCommands(KernelAioRwRequest* req, std::int32_t size, bool write, std:: const int error = errno; req[i].result->return_value = static_cast(SCE_KERNEL_ERROR_EIO); if (error == EBADF) req[i].result->return_value = static_cast(SCE_KERNEL_ERROR_EBADF); + if (error == EFAULT) req[i].result->return_value = static_cast(SCE_KERNEL_ERROR_EFAULT); req[i].result->state = AioAborted; aborted = true; } else { diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 0c19fdbb0..e2f11ae0e 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -2,6 +2,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" @@ -128,8 +129,12 @@ void _readFile(const Apr::ReadFileCommand& command) { std::ifstream stream(file.path, std::ios::binary); if (!stream) throw std::runtime_error("APR: cannot open " + file.path.string()); 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"); 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()); + if (read != command.size) 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"); } void _writeAddress(const Apr::WriteAddressCommand& command) { diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 469c4ad12..49d2b44cb 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -1,6 +1,7 @@ #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -125,7 +126,9 @@ std::int64_t APS5_VABI sceKernelRead(int d, void* buf, std::size_t nbytes) { if (buf == nullptr) { throw std::invalid_argument(std::string(__func__) + ": buf is null"); } - auto n = NativeRead(d, buf, nbytes); + const GuestArena::HostWrite destination(buf, nbytes); + if (!destination.Open()) errno = EFAULT; + auto n = destination.Open() ? NativeRead(d, buf, nbytes) : -1; if (n < 0) { throw std::runtime_error(std::string(__func__) + ": read failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 0846da2ae..75696033a 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -3,6 +3,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/GuestArena.hpp" #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "prx/libkernel/File/include/NativeStat.hpp" @@ -241,7 +242,9 @@ int64_t APS5_VABI pread_nid_postfix(int d, void* buf, size_t nbytes, int64_t off if (offset < 0) { APS5_INVALID_ARG_EX; } - auto n = NativePread(d, buf, nbytes, offset); + const GuestArena::HostWrite destination(buf, nbytes); + if (!destination.Open()) errno = EFAULT; + auto n = destination.Open() ? NativePread(d, buf, nbytes, offset) : -1; if (n < 0) { throw std::runtime_error(std::string(__func__) + ": pread failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); } @@ -398,6 +401,8 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset) { if (buf == nullptr && nbytes != 0) return SceErrorFromErrno(GUEST_EFAULT); if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + const GuestArena::HostWrite destination(buf, nbytes); + if (!destination.Open()) return SceErrorFromErrno(GUEST_EFAULT); const auto result = NativePread(d, buf, nbytes, offset); return result < 0 ? SceErrorFromErrno(errno) : result; } diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 94fd6eb02..c5bcc5d5a 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -3,10 +3,16 @@ #include "prx/libc/include/GuestHeap.hpp" #include "prx/libc/include/GuestArena.hpp" #include "prx/libc/include/GuestWriteWatch.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include #include "SceTypes.hpp" +#include #include #include +#include +#include +#include #include #include #include @@ -38,6 +44,14 @@ int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64 int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t); int APS5_VABI sceKernelMemoryPoolReserve(void*, std::size_t, std::size_t, int, void**); +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); +std::int64_t APS5_VABI sceKernelPread(int, void*, std::size_t, std::int64_t); +int APS5_VABI sceKernelAioInitializeImpl(void*, std::int32_t); +int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest*, std::int32_t, std::int32_t, std::int32_t*); +int APS5_VABI sceKernelAioWaitRequest(std::int32_t, std::int32_t*, std::uint32_t*); +int APS5_VABI sceKernelAioDeleteRequest(std::int32_t, std::int32_t*); } static void Require(bool condition, std::source_location location = std::source_location::current()) { @@ -274,6 +288,73 @@ static void CheckSharedWriteTracking() { #endif } +static void CheckReadsIntoSharedWriteTracking() { +#ifdef _WIN32 + constexpr std::size_t page = 0x4000; + const auto path = std::filesystem::path("anyps5-tracked-read-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".bin"); + std::vector seed(page * 2); + for (std::size_t i = 0; i < seed.size(); ++i) seed[i] = static_cast(i * 7 + 1); + { + std::ofstream stream(path, std::ios::binary); + stream.write(seed.data(), static_cast(seed.size())); + } + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 2, 3, 0, phys, 0) == 0); + auto* bytes = static_cast(mapped); + const auto collect = [&] { + std::array pages{}; + std::size_t count = pages.size(); + Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast(mapped), page * 2, pages.data(), &count, true)); + return count; + }; + const int fd = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(fd >= 0); + collect(); + Require(collect() == 0); + Require(sceKernelPread(fd, mapped, page * 2, 0) == static_cast(page * 2)); + Require(std::memcmp(bytes, seed.data(), page * 2) == 0); + Require(collect() == 8); + const int sequential = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDONLY, 0); + Require(sequential >= 0); + Require(sceKernelRead(sequential, bytes + page, page) == static_cast(page)); + Require(sceKernelClose(sequential) == 0); + Require(std::memcmp(bytes + page, seed.data(), page) == 0); + Require(collect() == 4); + Require(sceKernelAioInitializeImpl(nullptr, 0) == 0); + KernelAioResult result{-1, 0}; + KernelAioRwRequest request{static_cast(page), page, bytes, &result, fd}; + std::int32_t id = 0; + std::int32_t state = 0; + Require(sceKernelAioSubmitReadCommands(&request, 1, 0, &id) == 0); + Require(sceKernelAioWaitRequest(id, &state, nullptr) == 0); + Require(result.state == 3 && result.return_value == static_cast(page)); + Require(std::memcmp(bytes, seed.data() + page, page) == 0); + Require(collect() == 4); + std::int32_t deleted = -1; + Require(sceKernelAioDeleteRequest(id, &deleted) == 0); + Require(sceKernelMprotect(mapped, page * 2, 1) == 0); + bool refused = false; + try { + sceKernelPread(fd, mapped, page, 0); + } catch (const std::exception&) { + refused = true; + } + Require(refused); + KernelAioResult refusedResult{-1, 0}; + KernelAioRwRequest refusedRequest{0, page, bytes, &refusedResult, fd}; + Require(sceKernelAioSubmitReadCommands(&refusedRequest, 1, 0, &id) == 0); + Require(sceKernelAioWaitRequest(id, &state, nullptr) == 0); + Require(refusedResult.return_value == static_cast(SCE_KERNEL_ERROR_EFAULT)); + Require(sceKernelAioDeleteRequest(id, &deleted) == 0); + Require(sceKernelClose(fd) == 0); + Require(sceKernelMunmap(mapped, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); + std::filesystem::remove(path); +#endif +} + #if defined(__linux__) using PageRuns = std::vector>; @@ -457,6 +538,7 @@ int main() { CheckSharedDirectMemoryLifecycle(); CheckHeapAfterMappingReuse(); CheckSharedWriteTracking(); + CheckReadsIntoSharedWriteTracking(); #if defined(__linux__) CheckWriteWatch(); CheckDirectMemoryWriteWatch();