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:
parent
bc51b337ad
commit
676bbea348
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue