From ab98cd7f4a37378ac6da46c0863e097fe903472a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Franch?= Date: Tue, 6 Oct 2026 07:32:51 +0200 Subject: [PATCH] feat(libkernel): sceKernelReadv, Writev, Preadv and Pwritev Co-Authored-By: Claude --- core/libs/prx/libkernel/CMakeLists.txt | 4 + core/libs/prx/libkernel/File/src/Stdio.cpp | 75 ++++++++++++++ core/libs/tests/GuestFileVector.cpp | 111 +++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 191 insertions(+) create mode 100644 core/libs/tests/GuestFileVector.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 43949deb4..4af7886fc 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -355,6 +355,10 @@ if(BUILD_TESTING) target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_directory_entry_tests PRIVATE libkernel libc) add_test(NAME guest_directory_entries COMMAND guest_directory_entry_tests) + add_test_executable(guest_file_vector_tests ${LIBS_INCLUDE_DIR}/tests/GuestFileVector.cpp) + target_include_directories(guest_file_vector_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_file_vector_tests PRIVATE libkernel libc) + add_test(NAME guest_file_vector COMMAND guest_file_vector_tests) else() add_test_executable(guest_directory_close_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryClose.cpp) target_include_directories(guest_directory_close_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 5785a3c9b..7defbc421 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -17,6 +17,14 @@ #include #include #include + +struct KernelIovec { + void* base; + std::size_t length; +}; + +static constexpr int KERNEL_IOV_MAX = 1024; + #ifdef _WIN32 #include #include @@ -119,6 +127,8 @@ static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nb #include #include #include +#include +#include #include static int NativeRmdir(const std::filesystem::path& path) { return ::rmdir(path.c_str()); @@ -156,6 +166,12 @@ static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, s static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nbytes, std::int64_t offset) { return static_cast(::pwrite(descriptor, buf, nbytes, static_cast(offset))); } +static_assert(sizeof(KernelIovec) == sizeof(struct iovec)); +static_assert(offsetof(KernelIovec, base) == offsetof(struct iovec, iov_base)); +static_assert(offsetof(KernelIovec, length) == offsetof(struct iovec, iov_len)); +static const struct iovec* NativeIovecs(const KernelIovec* iov) { + return reinterpret_cast(iov); +} #endif static constexpr int GUEST_ENOENT = 2; @@ -471,6 +487,22 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t return pwrite_nid_disambig1_nid_postfix(d, buf, nbytes, offset); } +int64_t APS5_VABI sceKernelReadv(int, const KernelIovec*, int) { + throw std::runtime_error(std::string(__func__) + ": not implemented on Windows"); +} + +int64_t APS5_VABI sceKernelWritev(int, const KernelIovec*, int) { + throw std::runtime_error(std::string(__func__) + ": not implemented on Windows"); +} + +int64_t APS5_VABI sceKernelPreadv(int, const KernelIovec*, int, int64_t) { + throw std::runtime_error(std::string(__func__) + ": not implemented on Windows"); +} + +int64_t APS5_VABI sceKernelPwritev(int, const KernelIovec*, int, int64_t) { + throw std::runtime_error(std::string(__func__) + ": not implemented on Windows"); +} + #else int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset) { @@ -489,6 +521,49 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t return result < 0 ? SceErrorFromErrno(errno) : result; } +static int CheckIovecs(const KernelIovec* iov, int iovcnt) { + if (iovcnt < 0 || iovcnt > KERNEL_IOV_MAX) return SceErrorFromErrno(GUEST_EINVAL); + if (iov == nullptr && iovcnt != 0) return SceErrorFromErrno(GUEST_EFAULT); + return 0; +} + +static bool OpenIovecs(const KernelIovec* iov, int iovcnt, std::deque& destinations) { + for (int i = 0; i < iovcnt; ++i) { + if (!destinations.emplace_back(iov[i].base, iov[i].length).Open()) return false; + } + return true; +} + +int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* iov, int iovcnt) { + if (const int error = CheckIovecs(iov, iovcnt)) return error; + std::deque destinations; + if (!OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); + const auto result = static_cast(::readv(d, NativeIovecs(iov), iovcnt)); + return result < 0 ? SceErrorFromErrno(errno) : result; +} + +int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* iov, int iovcnt) { + if (const int error = CheckIovecs(iov, iovcnt)) return error; + const auto result = static_cast(::writev(d, NativeIovecs(iov), iovcnt)); + return result < 0 ? SceErrorFromErrno(errno) : result; +} + +int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { + if (const int error = CheckIovecs(iov, iovcnt)) return error; + if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + std::deque destinations; + if (!OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); + const auto result = static_cast(::preadv(d, NativeIovecs(iov), iovcnt, static_cast(offset))); + return result < 0 ? SceErrorFromErrno(errno) : result; +} + +int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { + if (const int error = CheckIovecs(iov, iovcnt)) return error; + if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + const auto result = static_cast(::pwritev(d, NativeIovecs(iov), iovcnt, static_cast(offset))); + return result < 0 ? SceErrorFromErrno(errno) : result; +} + #endif int APS5_VABI sceKernelRename(const char* from, const char* to) { diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp new file mode 100644 index 000000000..a3cb50f35 --- /dev/null +++ b/core/libs/tests/GuestFileVector.cpp @@ -0,0 +1,111 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestIovec { + void* base; + std::size_t length; +}; + +extern "C" { +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); +int APS5_VABI sceKernelLseek(int, std::int64_t, int); +std::int64_t APS5_VABI sceKernelReadv(int, const GuestIovec*, int); +std::int64_t APS5_VABI sceKernelWritev(int, const GuestIovec*, int); +std::int64_t APS5_VABI sceKernelPreadv(int, const GuestIovec*, int, std::int64_t); +std::int64_t APS5_VABI sceKernelPwritev(int, const GuestIovec*, int, std::int64_t); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "File vector check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr std::int64_t ErrorEbadf = static_cast(0x80020009u); +static constexpr std::int64_t ErrorEfault = static_cast(0x8002000Eu); +static constexpr std::int64_t ErrorEinval = static_cast(0x80020016u); + +static std::string Contents(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return std::string(std::istreambuf_iterator(stream), std::istreambuf_iterator()); +} + +int main() { + const auto root = std::filesystem::path("anyps5-file-vector-test-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto path = root / "data.bin"; + { std::ofstream stream(path, std::ios::binary); stream << "0123456789"; } + + const int file = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Require(file >= 0); + + char first[3] = {}; + char second[4] = {}; + GuestIovec reads[2] = {{first, sizeof(first)}, {second, sizeof(second)}}; + Require(sceKernelReadv(file, reads, 2) == 7); + Require(std::memcmp(first, "012", 3) == 0 && std::memcmp(second, "3456", 4) == 0); + Require(sceKernelLseek(file, 0, 1) == 7); + Require(sceKernelReadv(file, reads, 2) == 3); + Require(std::memcmp(first, "789", 3) == 0); + Require(sceKernelReadv(file, reads, 2) == 0); + + Require(sceKernelPreadv(file, reads, 2, 1) == 7); + Require(std::memcmp(first, "123", 3) == 0 && std::memcmp(second, "4567", 4) == 0); + Require(sceKernelLseek(file, 0, 1) == 10); + Require(sceKernelPreadv(file, reads, 2, 8) == 2); + Require(std::memcmp(first, "89", 2) == 0); + + char ab[] = "AB"; + char cde[] = "CDE"; + GuestIovec writes[2] = {{ab, 2}, {cde, 3}}; + Require(sceKernelPwritev(file, writes, 2, 2) == 5); + Require(sceKernelLseek(file, 0, 1) == 10); + Require(Contents(path) == "01ABCDE789"); + Require(sceKernelLseek(file, 8, 0) == 8); + Require(sceKernelWritev(file, writes, 2) == 5); + Require(sceKernelLseek(file, 0, 1) == 13); + Require(Contents(path) == "01ABCDE7ABCDE"); + + GuestIovec empty[1] = {{nullptr, 0}}; + Require(sceKernelReadv(file, empty, 1) == 0); + Require(sceKernelReadv(file, reads, 0) == 0); + Require(sceKernelWritev(file, nullptr, 0) == 0); + + Require(sceKernelReadv(file, reads, -1) == ErrorEinval); + Require(sceKernelWritev(file, writes, -1) == ErrorEinval); + Require(sceKernelPreadv(file, reads, -1, 0) == ErrorEinval); + Require(sceKernelPwritev(file, writes, -1, 0) == ErrorEinval); + Require(sceKernelReadv(file, reads, 1025) == ErrorEinval); + Require(sceKernelWritev(file, writes, 1025) == ErrorEinval); + Require(sceKernelReadv(file, nullptr, 1) == ErrorEfault); + Require(sceKernelWritev(file, nullptr, 1) == ErrorEfault); + Require(sceKernelPreadv(file, nullptr, 1, 0) == ErrorEfault); + Require(sceKernelPwritev(file, nullptr, 1, 0) == ErrorEfault); + Require(sceKernelPreadv(file, reads, 2, -1) == ErrorEinval); + Require(sceKernelPwritev(file, writes, 2, -1) == ErrorEinval); + Require(Contents(path) == "01ABCDE7ABCDE"); + + Require(sceKernelClose(file) == 0); + Require(sceKernelReadv(file, reads, 2) == ErrorEbadf); + Require(sceKernelWritev(file, writes, 2) == ErrorEbadf); + Require(sceKernelPreadv(file, reads, 2, 0) == ErrorEbadf); + Require(sceKernelPwritev(file, writes, 2, 0) == ErrorEbadf); + + std::filesystem::remove_all(root); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index cd0f433c1..47ca593d1 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -120,6 +120,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceVoiceSetMuteFlag](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature - [snwprintf_s](../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown. +- [sceKernelReadv / sceKernelWritev / sceKernelPreadv / sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) are implemented on Linux only; on Windows they throw - [_sceUlobjmgrRegisterObject](../../core/libs/prx/ulobjmgr/Export.cpp) (ulobjmgr) - parameters taken from shadPS4 (PS4): a nonzero 64-bit object, a nonzero 32-bit kind and a 32-bit id output; the meaning of the first two and the PS5 signature are unverified. The id bound of `_sceUlobjmgrUnregisterObject` (below 0x4000) and the raw `EINVAL` (22) return also come from shadPS4 - [sceShareGetCurrentStatus](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes)