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:
parent
64fe743995
commit
e768d97949
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue