fix(kernel): keep the file offset in Windows AIO reads and writes

Each Windows AIO request is an overlapped ReadFile/WriteFile at the
request's offset, which on a synchronous handle also moves the
descriptor's file pointer; on the console AIO transfers leave it alone.
The requests now go through the positioned path sceKernelPread and
sceKernelPwrite use (NativePositioned in Stdio.cpp, exposed as
NativePositioned_nid_no_patch), which restores the pointer afterwards.
Negative offsets still fail with EINVAL before the transfer.

GuestAio sets the offset to 2 before a write batch and checks it is
still 2 after. TechnicalDebt records that a concurrent read or write on
the same descriptor can see the moved pointer in between, as for the
pread family.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UTjpbMX7BUFMZZaxJE98zV
This commit is contained in:
Raihan Abdul Majid 2026-10-09 09:20:31 +07:00 committed by Adrià Franch
parent 64fe743995
commit e768d97949
5 changed files with 24 additions and 41 deletions

View File

@ -10,13 +10,10 @@
#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/KernelErrors.hpp"
#ifdef _WIN32
#include <io.h>
#include <limits>
#include <windows.h>
#else
#ifndef _WIN32
#include <fcntl.h>
#include <unistd.h>
#endif
@ -54,35 +51,6 @@ void SetState(std::int32_t id, std::int32_t state) {
g_states[id] = state;
}
#ifdef _WIN32
std::int64_t NativePositioned(std::int32_t fd, void* buf, std::size_t nbyte, std::int64_t offset, bool write, const char* name) {
if (nbyte > static_cast<std::size_t>(std::numeric_limits<DWORD>::max())) {
throw std::runtime_error(std::string(name) + ": nbytes exceeds platform limit");
}
if (offset < 0) {
errno = EINVAL;
return -1;
}
const auto handle = reinterpret_cast<HANDLE>(::_get_osfhandle(fd));
if (handle == INVALID_HANDLE_VALUE) {
errno = EBADF;
return -1;
}
OVERLAPPED overlapped{};
overlapped.Offset = static_cast<DWORD>(offset);
overlapped.OffsetHigh = static_cast<DWORD>(static_cast<std::uint64_t>(offset) >> 32u);
DWORD done = 0;
const BOOL ok = write ? ::WriteFile(handle, buf, static_cast<DWORD>(nbyte), &done, &overlapped)
: ::ReadFile(handle, buf, static_cast<DWORD>(nbyte), &done, &overlapped);
if (!ok) {
if (!write && ::GetLastError() == ERROR_HANDLE_EOF) return 0;
errno = EIO;
return -1;
}
return done;
}
#endif
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()) {
@ -90,7 +58,11 @@ std::int64_t NativePread(std::int32_t fd, void* buf, std::size_t nbyte, std::int
return -1;
}
#ifdef _WIN32
return NativePositioned(fd, buf, nbyte, offset, false, "sceKernelAioSubmitReadCommands");
if (offset < 0) {
errno = EINVAL;
return -1;
}
return NativePositioned_nid_no_patch(fd, buf, nbyte, offset, false);
#else
return static_cast<std::int64_t>(::pread(fd, buf, nbyte, static_cast<off_t>(offset)));
#endif
@ -98,7 +70,11 @@ std::int64_t NativePread(std::int32_t fd, void* buf, std::size_t nbyte, std::int
std::int64_t NativePwrite(std::int32_t fd, const void* buf, std::size_t nbyte, std::int64_t offset) {
#ifdef _WIN32
return NativePositioned(fd, const_cast<void*>(buf), nbyte, offset, true, "sceKernelAioSubmitWriteCommands");
if (offset < 0) {
errno = EINVAL;
return -1;
}
return NativePositioned_nid_no_patch(fd, const_cast<void*>(buf), nbyte, offset, true);
#else
return static_cast<std::int64_t>(::pwrite(fd, buf, nbyte, static_cast<off_t>(offset)));
#endif

View File

@ -16,6 +16,9 @@ std::int64_t APS5_VABI sceKernelWrite(int d, const void* buf, std::size_t nbytes
std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence);
int APS5_VABI sceKernelStat(const char* path, FileStat* sb);
int APS5_VABI sceKernelUnlink(const char* path);
#ifdef _WIN32
std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write);
#endif
}

View File

@ -97,7 +97,7 @@ static int NativeFlock(int descriptor, int operation) {
if (operation & 4) flags |= LOCKFILE_FAIL_IMMEDIATELY;
return ::LockFileEx(handle, flags, 0, MAXDWORD, MAXDWORD, &overlapped) ? 0 : -1;
}
static std::int64_t NativePositioned(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) {
std::int64_t NativePositioned_nid_no_patch(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset, bool write) {
if (nbytes > static_cast<std::size_t>(std::numeric_limits<DWORD>::max())) {
throw std::runtime_error("NativePositioned: nbytes exceeds platform limit");
}
@ -139,10 +139,10 @@ static bool NativeIsDisk(int descriptor) {
return handle != INVALID_HANDLE_VALUE && ::GetFileType(handle) == FILE_TYPE_DISK;
}
static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, std::int64_t offset) {
return NativePositioned(descriptor, buf, nbytes, offset, false);
return NativePositioned_nid_no_patch(descriptor, buf, nbytes, offset, false);
}
static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nbytes, std::int64_t offset) {
return NativePositioned(descriptor, const_cast<void*>(buf), nbytes, offset, true);
return NativePositioned_nid_no_patch(descriptor, const_cast<void*>(buf), nbytes, offset, true);
}
#else
#include <unistd.h>
@ -560,7 +560,7 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co
if (!write && !OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT);
if (total == 0) {
char none = 0;
const auto result = offset != nullptr ? NativePositioned(d, &none, 0, *offset, write) : NativeTransfer(d, &none, 0, write);
const auto result = offset != nullptr ? NativePositioned_nid_no_patch(d, &none, 0, *offset, write) : NativeTransfer(d, &none, 0, write);
return result < 0 ? SceErrorFromErrno(errno) : 0;
}
const bool whole = write || offset != nullptr || NativeIsDisk(d);
@ -569,7 +569,7 @@ static std::int64_t TransferIovecs(int d, const KernelIovec* iov, int iovcnt, co
auto* base = static_cast<char*>(iov[i].base);
for (std::size_t position = 0; position < iov[i].length;) {
const auto chunk = std::min<std::size_t>(iov[i].length - position, std::numeric_limits<int>::max());
const auto result = offset != nullptr ? NativePositioned(d, base + position, chunk, *offset + done, write) : NativeTransfer(d, base + position, chunk, write);
const auto result = offset != nullptr ? NativePositioned_nid_no_patch(d, base + position, chunk, *offset + done, write) : NativeTransfer(d, base + position, chunk, write);
if (result < 0) return done != 0 ? done : SceErrorFromErrno(errno);
done += result;
position += static_cast<std::size_t>(result);

View File

@ -17,6 +17,7 @@
extern "C" {
int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode);
int APS5_VABI sceKernelClose(int d);
std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence);
int APS5_VABI sceKernelAioDeleteRequest(std::int32_t id, std::int32_t* ret);
int APS5_VABI sceKernelAioInitializeImpl(void* param, std::int32_t size);
void APS5_VABI sceKernelAioInitializeParam(void* param);
@ -99,6 +100,7 @@ int main() {
Check(sceKernelAioDeleteRequest(id, nullptr) == SCE_KERNEL_ERROR_EFAULT);
const int batchFd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDWR, 0);
Check(batchFd >= 0);
Check(sceKernelLseek(batchFd, 2, 0) == 2);
std::array<char, 4> patch = {'W', 'X', 'Y', 'Z'};
KernelAioResult patchResult{-1, 0};
KernelAioResult badResult{-1, 0};
@ -108,6 +110,7 @@ int main() {
};
std::int32_t writeIds[2] = {0, 0};
Check(sceKernelAioSubmitWriteCommandsMultiple(writeBatch, 2, 0, writeIds) == 0);
Check(sceKernelLseek(batchFd, 0, 1) == 2);
Check(writeIds[0] > 0 && writeIds[1] > 0 && writeIds[0] != writeIds[1]);
Check(patchResult.state == 3 && patchResult.return_value == 4);
Check(badResult.state == 4 && badResult.return_value == SCE_KERNEL_ERROR_EBADF);

View File

@ -258,6 +258,7 @@ Throughout the project, every function at every stage either **does exactly what
- [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported
- [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries
- [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done
- [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console
- [sceKernelGetFsSandboxRandomWord](../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist
- [sceKernelSync](../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows flushes only the host files and directories recorded through `RecordWrittenPath_nid_no_patch` (the libc, libkernel, save data and app content file functions) and their changed parent directories. A host write in a library that does not record its path is not flushed by `sceKernelSync`
- [sceKernelSync](../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way