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) <noreply@anthropic.com>
This commit is contained in:
Michon 2026-10-01 18:56:36 +02:00
parent bc51b337ad
commit 676bbea348
8 changed files with 172 additions and 4 deletions

View File

@ -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

View File

@ -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<void*>(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<void*>(alias), pageBytes, protection, &previous)) fail("arm shared memory write tracking");
@ -236,6 +264,7 @@ private:
bool armed;
std::shared_ptr<Section> 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);

View File

@ -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<std::uintptr_t>(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<std::uintptr_t>(pointer), bytes);
#else
(void)pointer;
(void)bytes;
#endif
}
}

View File

@ -8,6 +8,7 @@
#include <thread>
#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::size_t>(std::numeric_limits<unsigned int>::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<std::int64_t>(SCE_KERNEL_ERROR_EIO);
if (error == EBADF) req[i].result->return_value = static_cast<std::int64_t>(SCE_KERNEL_ERROR_EBADF);
if (error == EFAULT) req[i].result->return_value = static_cast<std::int64_t>(SCE_KERNEL_ERROR_EFAULT);
req[i].result->state = AioAborted;
aborted = true;
} else {

View File

@ -2,6 +2,7 @@
#include <cstddef>
#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<std::streamoff>(command.offset));
const GuestArena::HostWrite destination(reinterpret_cast<void*>(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<char*>(command.destination), static_cast<std::streamsize>(command.size));
if (stream.bad()) throw std::runtime_error("APR: read failed for " + file.path.string());
const auto read = static_cast<std::uint64_t>(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) {

View File

@ -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));
}

View File

@ -3,6 +3,7 @@
#include <limits>
#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;
}

View File

@ -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 <array>
#include "SceTypes.hpp"
#include <chrono>
#include <cstring>
#include <exception>
#include <filesystem>
#include <fstream>
#include <string>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
@ -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<char> seed(page * 2);
for (std::size_t i = 0; i < seed.size(); ++i) seed[i] = static_cast<char>(i * 7 + 1);
{
std::ofstream stream(path, std::ios::binary);
stream.write(seed.data(), static_cast<std::streamsize>(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<unsigned char*>(mapped);
const auto collect = [&] {
std::array<void*, 32> pages{};
std::size_t count = pages.size();
Require(GuestArena::GuestArenaCollectWrites_nid_postfix(reinterpret_cast<std::uintptr_t>(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<std::int64_t>(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<std::int64_t>(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<std::int64_t>(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<std::int64_t>(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<std::int64_t>(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<std::pair<std::uintptr_t, std::uintptr_t>>;
@ -457,6 +538,7 @@ int main() {
CheckSharedDirectMemoryLifecycle();
CheckHeapAfterMappingReuse();
CheckSharedWriteTracking();
CheckReadsIntoSharedWriteTracking();
#if defined(__linux__)
CheckWriteWatch();
CheckDirectMemoryWriteWatch();