From 591e8ddadc25bfe7f54d563e8890d351fc601bb3 Mon Sep 17 00:00:00 2001 From: screamm Date: Sun, 11 Oct 2026 18:00:28 +0200 Subject: [PATCH 1/3] feat(libkernel): support fcntl on non-socket descriptors fcntl and sceKernelFcntl answered EBADF (sceKernelFcntl threw) for every descriptor outside the libSceNet socket table, so files, pipes, directories and descriptors 0 to 2 could not be queried or changed. Observed: Yamagi Quake II (PPSA99007) stops with "Sys_Error: change stdin to blocking Bad file descriptor", from fcntl(fileno(stdin), F_SETFL, fcntl(0, F_GETFL, 0) & ~FNDELAY) (yquake2 src/backends/unix/system.c:81-86). Predicted from the titles' sources and decoded call sites: XPSemu (PPSA97358, 97 fcntl sites: F_GETFD 12, F_SETFD 20, F_GETFL 8, F_SETFL 9, F_DUPFD 1, F_DUPFD_CLOEXEC 7, F_GETLK 9) fails in qemu_init_main_loop, where its pipe2 (pipe plus F_SETFD FD_CLOEXEC) and glib's g_unix_set_fd_nonblocking (F_GETFL, F_SETFL) build the event notifier (util/event_notifier-posix.c:44-67); VLC (PPSA85300, 43 sites, F_RDAHEAD on every file it opens), RetroArch (PPSA99169, 17), Porpoise (PPSA99764, 13) and Ruffle (9) call it on files and pipes. The commands follow FreeBSD 11.4 kern_fcntl and kern_dup (sys/kern/kern_descrip.c:479-799, 812, release/11.4.0 8c639945): F_GETFD/F_SETFD keep the close-on-exec flag per descriptor, F_GETFL reports the host access mode with the status flags, F_SETFL changes only O_NONBLOCK, O_APPEND, O_ASYNC, O_FSYNC, O_DIRECT and FRDAHEAD, F_DUPFD/F_DUPFD_CLOEXEC give the lowest free descriptor at or above the start through dup (EINVAL for a negative start), F_DUP2FD goes through dup2, F_RDAHEAD/F_READAHEAD set FRDAHEAD on files (EBADF on pipes, ENOTTY elsewhere), and unknown commands fail with EINVAL. Status flags live in a guest table shared by dup, dup2 and F_DUPFD like an open file description. O_NONBLOCK switches a pipe (PIPE_NOWAIT on Windows, where an empty read and a full write now report EAGAIN; the host flag on Linux). Record locks use the host POSIX locks on Linux and throw on Windows. Changes the host cannot model throw naming the flag and descriptor kind. Sockets keep their path unchanged. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libkernel/CMakeLists.txt | 1 + .../File/include/DescriptorControl.hpp | 16 + .../libkernel/File/src/DescriptorControl.cpp | 421 ++++++++++++++++++ core/libs/prx/libkernel/File/src/Open.cpp | 48 +- core/libs/prx/libkernel/File/src/Stdio.cpp | 29 ++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 15 + core/libs/tests/GuestDup.cpp | 30 ++ core/libs/tests/GuestKernelFcntl.cpp | 178 +++++++- docs/dev/TechnicalDebt.md | 3 +- 9 files changed, 708 insertions(+), 33 deletions(-) create mode 100644 core/libs/prx/libkernel/File/include/DescriptorControl.hpp create mode 100644 core/libs/prx/libkernel/File/src/DescriptorControl.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 58c5364b0..ecd85b4b9 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -3,6 +3,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Apr/src/Apr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/Equeue.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Equeue/src/EventAccessors.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DescriptorControl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/DirectoryDescriptor.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/FileLock.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/NativeStat.cpp diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp new file mode 100644 index 000000000..195fa099d --- /dev/null +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -0,0 +1,16 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_DESCRIPTORCONTROL_HPP +#define CORE_LIBS_PRX_LIBKERNEL_FILE_DESCRIPTORCONTROL_HPP + +#include + +namespace File { + +int Fcntl(int fd, int command, std::intptr_t argument); +int DuplicateFrom(int fd, int start); +void RememberDescriptor(int fd, int openFlags); +void ShareDescriptor(int from, int to); +void ForgetDescriptor(int fd); + +} + +#endif diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp new file mode 100644 index 000000000..ee8b4e0d0 --- /dev/null +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -0,0 +1,421 @@ +#include "prx/libkernel/File/include/DescriptorControl.hpp" +#include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/File.hpp" +#include "prx/libkernel/File/include/RandomDevice.hpp" +#include "prx/libc/include/GuestArena.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#else +#include +#include +#include +#endif + +extern "C" int* APS5_VABI __error_nid_postfix(); +extern "C" int APS5_VABI dup2_nid_postfix(int from, int to); +#ifdef _WIN32 +extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); +#endif + +namespace { + +constexpr int GuestEintr = 4; +constexpr int GuestEbadf = 9; +constexpr int GuestEdeadlk = 11; +constexpr int GuestEfault = 14; +constexpr int GuestEinval = 22; +constexpr int GuestEnotty = 25; +constexpr int GuestEagain = 35; +constexpr int GuestEnolck = 77; +constexpr int GuestEoverflow = 84; + +constexpr int DuplicateCommand = 0; +constexpr int GetDescriptorFlags = 1; +constexpr int SetDescriptorFlags = 2; +constexpr int GetStatusFlags = 3; +constexpr int SetStatusFlags = 4; +constexpr int GetOwner = 5; +constexpr int SetOwner = 6; +constexpr int OldGetLock = 7; +constexpr int OldSetLock = 8; +constexpr int OldSetLockWait = 9; +constexpr int DuplicateToCommand = 10; +constexpr int GetLock = 11; +constexpr int SetLock = 12; +constexpr int SetLockWait = 13; +constexpr int SetLockRemote = 14; +constexpr int ReadAhead = 15; +constexpr int DarwinReadAhead = 16; +constexpr int DuplicateCloseOnExec = 17; +constexpr int DuplicateToCloseOnExec = 18; + +constexpr int CloseOnExec = 1; +constexpr int OpenCloseOnExec = 0x00100000; + +constexpr int ReadOnly = 0; +constexpr int WriteOnly = 1; +constexpr int ReadWrite = 2; +constexpr int Nonblocking = 0x0004; +constexpr int Append = 0x0008; +constexpr int Asynchronous = 0x0040; +constexpr int Synchronous = 0x0080; +constexpr int ReadAheadFlag = 0x0200; +constexpr int Direct = 0x00010000; +constexpr int OpenStatusFlags = Nonblocking | Append | Asynchronous | Synchronous | Direct; +constexpr int SettableStatusFlags = OpenStatusFlags | ReadAheadFlag; + +constexpr std::int16_t ReadLock = 1; +constexpr std::int16_t Unlock = 2; +constexpr std::int16_t WriteLock = 3; +constexpr std::int16_t FromCurrent = 1; + +struct GuestLock { + std::int64_t start; + std::int64_t length; + std::int32_t pid; + std::int16_t type; + std::int16_t whence; + std::int32_t system; +}; +static_assert(sizeof(GuestLock) == 32); +static_assert(offsetof(GuestLock, type) == 20); +constexpr std::size_t OldGuestLockSize = 24; + +enum class Kind { File, Directory, Pipe, Device, RandomDevice }; + +struct Entry { + bool closeOnExec = false; + std::shared_ptr status; +}; + +std::mutex g_mutex; +std::map g_entries; + +int Fail(int error) { + *__error_nid_postfix() = error; + return -1; +} + +const char* KindName(Kind kind) { + switch (kind) { + case Kind::File: return "file"; + case Kind::Directory: return "directory"; + case Kind::Pipe: return "pipe"; + case Kind::Device: return "character device"; + case Kind::RandomDevice: return "random device"; + } + return "descriptor"; +} + +[[noreturn]] void Unsupported(const char* function, const std::string& what, Kind kind, int fd) { + throw std::runtime_error(std::string(function) + ": " + what + " on a " + KindName(kind) + " is not implemented, fd=" + std::to_string(fd)); +} + +#ifdef _WIN32 +void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} + +HANDLE NativeHandle(int fd) { + const auto previous = _set_thread_local_invalid_parameter_handler(IgnoreInvalidParameter); + const auto handle = ::_get_osfhandle(fd); + _set_thread_local_invalid_parameter_handler(previous); + return reinterpret_cast(handle); +} + +std::optional Classify(int fd, const char* function) { + if (fd < 0 || !DescriptorIsOpen_nid_no_patch(fd)) return std::nullopt; + if (File::DirectoryDescriptorPath(fd)) return Kind::Directory; + if (File::IsRandomDevice(fd)) return Kind::RandomDevice; + switch (::GetFileType(NativeHandle(fd))) { + case FILE_TYPE_DISK: return Kind::File; + case FILE_TYPE_PIPE: return Kind::Pipe; + case FILE_TYPE_CHAR: return Kind::Device; + default: throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " has an unknown Windows file type, error=" + std::to_string(::GetLastError())); + } +} + +int AccessMode(int fd, const char* function) { + using QueryObject = NTSTATUS(NTAPI*)(HANDLE, OBJECT_INFORMATION_CLASS, PVOID, ULONG, PULONG); + static const auto query = reinterpret_cast(reinterpret_cast(::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "NtQueryObject"))); + PUBLIC_OBJECT_BASIC_INFORMATION information{}; + const NTSTATUS status = query == nullptr ? -1 : query(NativeHandle(fd), ObjectBasicInformation, &information, sizeof(information), nullptr); + if (status < 0) + throw std::runtime_error(std::string(function) + ": cannot read the access of descriptor " + std::to_string(fd) + ", status=" + std::to_string(status)); + const bool read = (information.GrantedAccess & FILE_READ_DATA) != 0; + const bool write = (information.GrantedAccess & (FILE_WRITE_DATA | FILE_APPEND_DATA)) != 0; + if (read && write) return ReadWrite; + if (write) return WriteOnly; + if (read) return ReadOnly; + throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " grants neither read nor write access"); +} + +DWORD PipeState(int fd, const char* function) { + DWORD state = 0; + if (!::GetNamedPipeHandleStateW(NativeHandle(fd), &state, nullptr, nullptr, nullptr, nullptr, 0)) + throw std::runtime_error(std::string(function) + ": cannot read the mode of pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(::GetLastError())); + return state; +} + +void SetPipeNonblocking(int fd, bool nonblocking, const char* function) { + DWORD mode = (PipeState(fd, function) & PIPE_READMODE_MESSAGE) | (nonblocking ? PIPE_NOWAIT : PIPE_WAIT); + if (!::SetNamedPipeHandleState(NativeHandle(fd), &mode, nullptr, nullptr)) + throw std::runtime_error(std::string(function) + ": cannot change the mode of pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(::GetLastError())); +} + +Entry InitialEntry(int fd) { + int status = 0; + if (::GetFileType(NativeHandle(fd)) == FILE_TYPE_PIPE && (PipeState(fd, "fcntl") & PIPE_NOWAIT) != 0) status |= Nonblocking; + return {false, std::make_shared(status)}; +} + +void ApplyHostStatus(int fd, Kind kind, int changed, int next, const char* function) { + if ((changed & Append) && kind == Kind::File) Unsupported(function, "changing O_APPEND", kind, fd); + if ((changed & Nonblocking) && kind == Kind::Device) Unsupported(function, "changing O_NONBLOCK", kind, fd); + if ((changed & Nonblocking) && kind == Kind::Pipe) SetPipeNonblocking(fd, (next & Nonblocking) != 0, function); +} + +#else +std::optional Classify(int fd, const char* function) { + if (fd < 0) return std::nullopt; + struct stat status{}; + if (::fstat(fd, &status) != 0) { + if (errno == EBADF) return std::nullopt; + throw std::runtime_error(std::string(function) + ": fstat failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); + } + if (File::IsRandomDevice(fd)) return Kind::RandomDevice; + if (S_ISREG(status.st_mode)) return Kind::File; + if (S_ISDIR(status.st_mode)) return Kind::Directory; + if (S_ISFIFO(status.st_mode)) return Kind::Pipe; + if (S_ISCHR(status.st_mode)) return Kind::Device; + throw std::runtime_error(std::string(function) + ": descriptor " + std::to_string(fd) + " is neither a file, a directory, a pipe nor a character device"); +} + +int HostStatus(int fd, const char* function) { + const int flags = ::fcntl(fd, F_GETFL); + if (flags < 0) throw std::runtime_error(std::string(function) + ": host F_GETFL failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return flags; +} + +int AccessMode(int fd, const char* function) { + return HostStatus(fd, function) & O_ACCMODE; +} + +Entry InitialEntry(int fd) { + const int descriptorFlags = ::fcntl(fd, F_GETFD); + if (descriptorFlags < 0) throw std::runtime_error("fcntl: host F_GETFD failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); + const int host = HostStatus(fd, "fcntl"); + int status = 0; + if (host & O_NONBLOCK) status |= Nonblocking; + if (host & O_APPEND) status |= Append; + if (host & O_ASYNC) status |= Asynchronous; + if ((host & O_SYNC) == O_SYNC) status |= Synchronous; + if (host & O_DIRECT) status |= Direct; + return {(descriptorFlags & FD_CLOEXEC) != 0, std::make_shared(status)}; +} + +void ApplyHostStatus(int fd, Kind, int changed, int next, const char* function) { + if ((changed & (Nonblocking | Append)) == 0) return; + const int host = HostStatus(fd, function); + const int wanted = (host & ~(O_NONBLOCK | O_APPEND)) | ((next & Nonblocking) ? O_NONBLOCK : 0) | ((next & Append) ? O_APPEND : 0); + if (::fcntl(fd, F_SETFL, wanted) != 0) + throw std::runtime_error(std::string(function) + ": host F_SETFL failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); +} + + +int LockError(int error) { + switch (error) { + case EACCES: + case EAGAIN: return GuestEagain; + case EDEADLK: return GuestEdeadlk; + case EINTR: return GuestEintr; + case ENOLCK: return GuestEnolck; + case EBADF: return GuestEbadf; + case EINVAL: return GuestEinval; + case EOVERFLOW: return GuestEoverflow; + default: throw std::runtime_error("fcntl: host record lock failed, errno=" + std::to_string(error)); + } +} +#endif + +Entry& Materialize(int fd) { + const auto found = g_entries.find(fd); + if (found != g_entries.end()) return found->second; + return g_entries.emplace(fd, InitialEntry(fd)).first->second; +} + +int ChangeStatus(int fd, Kind kind, int mask, int value, const char* function) { + std::lock_guard lock(g_mutex); + auto& status = *Materialize(fd).status; + const int next = (status & ~mask) | (value & mask); + const int changed = status ^ next; + if ((changed & Asynchronous) && (kind == Kind::Pipe || kind == Kind::Device)) Unsupported(function, "O_ASYNC", kind, fd); + if ((changed & Synchronous) && kind == Kind::File) Unsupported(function, "changing O_FSYNC", kind, fd); + ApplyHostStatus(fd, kind, changed, next, function); + status = next; + return 0; +} + +int CurrentStatus(int fd) { + std::lock_guard lock(g_mutex); + return *Materialize(fd).status; +} + +void SetCloseOnExec(int fd, bool value) { + std::lock_guard lock(g_mutex); + Materialize(fd).closeOnExec = value; +} + +int RecordLock(int fd, int command, std::intptr_t argument) { + const bool old = command <= OldSetLockWait; + const int current = old ? command + (GetLock - OldGetLock) : command; + const std::size_t size = old ? OldGuestLockSize : sizeof(GuestLock); + auto* pointer = reinterpret_cast(argument); + if (pointer == nullptr) return Fail(GuestEfault); + GuestLock lock{}; + std::memcpy(&lock, pointer, size); + const auto kind = Classify(fd, "fcntl"); + if (!kind || *kind == Kind::Pipe) return Fail(GuestEbadf); + if (*kind != Kind::File && *kind != Kind::Directory) Unsupported("fcntl", "a record lock", *kind, fd); +#ifdef _WIN32 + static_cast(current); + throw std::runtime_error("fcntl: record locks are not implemented on Windows, command=" + std::to_string(command) + ", fd=" + std::to_string(fd)); +#else + struct flock host{}; + if (lock.type == ReadLock) host.l_type = F_RDLCK; + else if (lock.type == WriteLock) host.l_type = F_WRLCK; + else if (lock.type == Unlock) host.l_type = F_UNLCK; + else return Fail(GuestEinval); + if (current == GetLock && lock.type == Unlock) + throw std::runtime_error("fcntl: F_GETLK with F_UNLCK is not implemented, fd=" + std::to_string(fd)); + host.l_whence = lock.whence; + host.l_start = static_cast(lock.start); + host.l_len = static_cast(lock.length); + const int hostCommand = current == GetLock ? F_GETLK : current == SetLock ? F_SETLK : F_SETLKW; + if (::fcntl(fd, hostCommand, &host) != 0) return Fail(LockError(errno)); + if (current != GetLock) return 0; + if (host.l_type == F_UNLCK) { + if (lock.whence == FromCurrent) { + const auto offset = ::lseek(fd, 0, SEEK_CUR); + if (offset < 0) throw std::runtime_error("fcntl: cannot read the offset of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + lock.start += offset; + } + lock.type = Unlock; + } else { + lock.type = host.l_type == F_RDLCK ? ReadLock : WriteLock; + lock.whence = SEEK_SET; + lock.start = host.l_start; + lock.length = host.l_len; + lock.pid = host.l_pid; + lock.system = 0; + } + const GuestArena::HostWrite destination(pointer, size); + if (!destination.Open()) return Fail(GuestEfault); + std::memcpy(pointer, &lock, size); + return 0; +#endif +} + +int ReadAheadControl(int fd, int command, std::intptr_t argument) { + const auto kind = Classify(fd, "fcntl"); + if (!kind || *kind == Kind::Pipe) return Fail(GuestEbadf); + if (*kind != Kind::File) return Fail(GuestEnotty); + std::lock_guard lock(g_mutex); + auto& status = *Materialize(fd).status; + if (command == DarwinReadAhead || argument >= 0) status |= ReadAheadFlag; + else status &= ~ReadAheadFlag; + return 0; +} + +} + +namespace File { + +int Fcntl(int fd, int command, std::intptr_t argument) { + switch (command) { + case DuplicateCommand: + case DuplicateCloseOnExec: { + if (fd < 0) return Fail(GuestEbadf); + if (static_cast(argument) < 0) return Fail(GuestEinval); + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + const int duplicate = DuplicateFrom(fd, static_cast(argument)); + if (duplicate >= 0 && command == DuplicateCloseOnExec) SetCloseOnExec(duplicate, true); + return duplicate; + } + case DuplicateToCommand: + case DuplicateToCloseOnExec: { + const int duplicate = dup2_nid_postfix(fd, static_cast(argument)); + if (duplicate >= 0 && command == DuplicateToCloseOnExec) SetCloseOnExec(duplicate, true); + return duplicate; + } + case GetDescriptorFlags: { + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + std::lock_guard lock(g_mutex); + return Materialize(fd).closeOnExec ? CloseOnExec : 0; + } + case SetDescriptorFlags: + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + SetCloseOnExec(fd, (argument & CloseOnExec) != 0); + return 0; + case GetStatusFlags: { + if (!Classify(fd, "fcntl")) return Fail(GuestEbadf); + const int access = AccessMode(fd, "fcntl"); + return access | CurrentStatus(fd); + } + case SetStatusFlags: { + const auto kind = Classify(fd, "fcntl"); + if (!kind) return Fail(GuestEbadf); + return ChangeStatus(fd, *kind, SettableStatusFlags, static_cast(argument), "fcntl"); + } + case GetOwner: + case SetOwner: { + const auto kind = Classify(fd, "fcntl"); + if (!kind) return Fail(GuestEbadf); + Unsupported("fcntl", command == GetOwner ? "F_GETOWN" : "F_SETOWN", *kind, fd); + } + case OldGetLock: + case OldSetLock: + case OldSetLockWait: + case GetLock: + case SetLock: + case SetLockWait: + return RecordLock(fd, command, argument); + case SetLockRemote: + throw std::runtime_error("fcntl: F_SETLK_REMOTE needs a privilege whose state on the console is not known, fd=" + std::to_string(fd)); + case ReadAhead: + case DarwinReadAhead: + return ReadAheadControl(fd, command, argument); + default: + return Fail(GuestEinval); + } +} + +void RememberDescriptor(int fd, int openFlags) { + std::lock_guard lock(g_mutex); + g_entries[fd] = Entry{(openFlags & OpenCloseOnExec) != 0, std::make_shared(openFlags & OpenStatusFlags)}; +} + +void ShareDescriptor(int from, int to) { + std::lock_guard lock(g_mutex); + auto status = Materialize(from).status; + g_entries[to] = Entry{false, std::move(status)}; +} + +void ForgetDescriptor(int fd) { + std::lock_guard lock(g_mutex); + g_entries.erase(fd); +} + +} diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index caae32d96..f8a287b67 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -6,6 +6,7 @@ #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" @@ -80,14 +81,19 @@ static int NativeRead(int fd, void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelRead: nbytes exceeds platform limit"); } - return ::_read(fd, buf, static_cast(n)); + const int result = ::_read(fd, buf, static_cast(n)); + if (result < 0 && _doserrno == ERROR_NO_DATA) errno = EAGAIN; + return result; } static int NativeWrite(int fd, const void* buf, std::size_t n) { if (n > static_cast(std::numeric_limits::max())) { throw std::runtime_error("sceKernelWrite: nbytes exceeds platform limit"); } - const auto result = File::WriteSharedMemory_nid_no_patch(fd, buf, n); - return result == -2 ? ::_write(fd, buf, static_cast(n)) : static_cast(result); + const auto shared = File::WriteSharedMemory_nid_no_patch(fd, buf, n); + if (shared != -2) return static_cast(shared); + const int result = ::_write(fd, buf, static_cast(n)); + if (result < 0 && errno == ENOSPC && _doserrno == 0 && ::GetFileType(reinterpret_cast(::_get_osfhandle(fd))) == FILE_TYPE_PIPE) errno = EAGAIN; + return result; } extern "C" _invalid_parameter_handler _set_thread_local_invalid_parameter_handler(_invalid_parameter_handler); static void IgnoreInvalidParameter(const wchar_t*, const wchar_t*, const wchar_t*, unsigned int, std::uintptr_t) {} @@ -209,6 +215,7 @@ static int OpenNative(const std::filesystem::path& native, int flags, std::uint1 } if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY || (flags & (SCE_KERNEL_O_CREAT | SCE_KERNEL_O_TRUNC))) RecordWrittenPath_nid_no_patch(native); + File::RememberDescriptor(fd, flags); return fd; } @@ -220,7 +227,9 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { if (path != nullptr && File::IsRandomDevicePath(path)) { if ((flags & SCE_KERNEL_O_ACCMODE) != SCE_KERNEL_O_RDONLY) throw std::runtime_error(std::string(__func__) + ": writing to " + path + " is not implemented"); const int fd = File::OpenRandomDevice(); - return fd < 0 ? SceErrorFromErrno(errno) : fd; + if (fd < 0) return SceErrorFromErrno(errno); + File::RememberDescriptor(fd, flags); + return fd; } std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); @@ -236,6 +245,7 @@ int OpenInDirectory_nid_no_patch(int directory, const char* path, int flags, std int APS5_VABI sceKernelClose(int d) { File::ForgetRandomDevice(d); + File::ForgetDescriptor(d); #ifdef _WIN32 File::ForgetDirectoryDescriptor(d); File::ForgetFileLock(d); @@ -332,27 +342,21 @@ int APS5_VABI sceKernelUnlink(const char* path) { } int APS5_VABI sceKernelFcntl(int d, int command, ...) { - if (d < GuestSockets::FirstDescriptor) { - if (!DescriptorIsOpen_nid_no_patch(d)) return SCE_KERNEL_ERROR_EBADF; - throw std::runtime_error(std::string(__func__) + ": file descriptors are not implemented, fd=" + std::to_string(d)); - } - int result; - if (command == GuestSetFlags) { #ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - const int flags = __builtin_va_arg(arguments, int); - __builtin_sysv_va_end(arguments); + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + const auto argument = __builtin_va_arg(arguments, long long); + __builtin_sysv_va_end(arguments); #else - std::va_list arguments; - va_start(arguments, command); - const int flags = va_arg(arguments, int); - va_end(arguments); + std::va_list arguments; + va_start(arguments, command); + const auto argument = va_arg(arguments, long); + va_end(arguments); #endif - result = fcntl_nid_postfix(d, command, flags); - } else { - result = fcntl_nid_postfix(d, command); - } + int result; + if (d < GuestSockets::FirstDescriptor) result = File::Fcntl(d, command, static_cast(argument)); + else if (command == GuestSetFlags) result = fcntl_nid_postfix(d, command, static_cast(argument)); + else result = fcntl_nid_postfix(d, command); return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index fb0fef9e6..ab343dadf 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -12,6 +12,7 @@ #include "prx/libkernel/File/include/NativeStat.hpp" #include "prx/libkernel/File/include/KernelStatfs.hpp" #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/File/include/FileLock.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/KernelErrors.hpp" @@ -410,6 +411,8 @@ extern "C" int APS5_VABI pipe_nid_postfix(int* descriptors) { const int result = ::pipe(native); #endif if (result != 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + File::RememberDescriptor(native[0], 0); + File::RememberDescriptor(native[1], 0); std::memcpy(descriptors, native, sizeof(native)); return 0; } @@ -454,6 +457,28 @@ static bool HasNonDirectoryPrefix(const std::filesystem::path& native) { return false; } +extern "C" int APS5_VABI dup_nid_postfix(int d); +extern "C" int APS5_VABI close_nid_postfix(int d); + +int File::DuplicateFrom(int fd, int start) { +#ifdef _WIN32 + std::vector below; + int duplicate; + while ((duplicate = dup_nid_postfix(fd)) >= 0 && duplicate < start) below.push_back(duplicate); + const int error = *__error_nid_postfix(); + for (const int descriptor : below) close_nid_postfix(descriptor); + return duplicate < 0 ? PosixFailure(error) : duplicate; +#else + const int duplicate = ::fcntl(fd, F_DUPFD, start); + if (duplicate < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); + if (duplicate >= GuestSockets::FirstDescriptor) + throw std::runtime_error("fcntl: host descriptor reached the guest socket range"); + if (File::IsRandomDevice(fd)) File::RememberRandomDevice(duplicate); + File::ShareDescriptor(fd, duplicate); + return duplicate; +#endif +} + extern "C" { int APS5_VABI chmod_nid_postfix(const char* path, int mode) { @@ -467,6 +492,7 @@ int APS5_VABI chmod_nid_postfix(const char* path, int mode) { int APS5_VABI close_nid_postfix(int d) { if (d >= GuestSockets::FirstDescriptor) return GuestSockets::Close(d); File::ForgetRandomDevice(d); + File::ForgetDescriptor(d); #ifdef _WIN32 File::ForgetDirectoryDescriptor(d); File::ForgetFileLock(d); @@ -493,6 +519,7 @@ int APS5_VABI dup_nid_postfix(int d) { if (duplicate >= GuestSockets::FirstDescriptor) throw std::runtime_error(std::string(__func__) + ": host descriptor reached the guest socket range"); if (File::IsRandomDevice(d)) File::RememberRandomDevice(duplicate); + File::ShareDescriptor(d, duplicate); return duplicate; } @@ -517,6 +544,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { File::ForgetFileLock(to); File::ForgetRandomDevice(to); if (sourceIsRandom) File::RememberRandomDevice(to); + File::ShareDescriptor(from, to); return to; #else if (from == to) { @@ -527,6 +555,7 @@ int APS5_VABI dup2_nid_postfix(int from, int to) { if (result < 0) return PosixFailure(SceErrorFromErrno(errno) & 0xffff); File::ForgetRandomDevice(to); if (sourceIsRandom) File::RememberRandomDevice(to); + File::ShareDescriptor(from, to); return result; #endif } diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 436c88e28..03892b0de 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -17,6 +17,7 @@ #include "prx/libc/include/HostThreadLocal.hpp" #include "prx/libkernel/Socket/include/SocketError.hpp" #include "prx/libkernel/Socket/include/SocketRuntime.hpp" +#include "prx/libkernel/File/include/DescriptorControl.hpp" #include "prx/libkernel/Equeue/Equeue.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include @@ -435,6 +436,20 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e extern "C" { int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { + if (descriptor < GuestSockets::FirstDescriptor) { +#ifdef _WIN32 + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + const auto argument = __builtin_va_arg(arguments, long long); + __builtin_sysv_va_end(arguments); +#else + std::va_list arguments; + va_start(arguments, command); + const auto argument = va_arg(arguments, long); + va_end(arguments); +#endif + return File::Fcntl(descriptor, command, static_cast(argument)); + } const auto socket = Lookup(descriptor); if (!socket) return -1; std::lock_guard lock(socket->modeMutex); diff --git a/core/libs/tests/GuestDup.cpp b/core/libs/tests/GuestDup.cpp index 1388a600d..e4241dc52 100644 --- a/core/libs/tests/GuestDup.cpp +++ b/core/libs/tests/GuestDup.cpp @@ -15,6 +15,7 @@ int APS5_VABI socketpair_nid_postfix(int, int, int, int*); std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int); std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int); int APS5_VABI open_nid_postfix(const char*, int, int); +int APS5_VABI fcntl_nid_postfix(int, int, ...); int* APS5_VABI __error_nid_postfix(); } @@ -178,10 +179,39 @@ static void RandomDeviceDuplicates() { for (const int descriptor : {random, dupRandom, pipe[1], secondRandom}) Require(close_nid_postfix(descriptor) == 0); } +static void DuplicatesShareStatusFlags() { + constexpr int GetDescriptorFlags = 1; + constexpr int SetDescriptorFlags = 2; + constexpr int GetFlags = 3; + constexpr int SetFlags = 4; + constexpr int NonBlocking = 4; + constexpr int GuestEagain = 35; + int pipe[2]; + int other[2]; + Require(pipe_nid_postfix(pipe) == 0); + Require(pipe_nid_postfix(other) == 0); + Require(fcntl_nid_postfix(pipe[0], SetDescriptorFlags, 1) == 0); + const int duplicate = dup_nid_postfix(pipe[0]); + Require(duplicate >= 0); + Require(fcntl_nid_postfix(duplicate, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(duplicate, SetFlags, NonBlocking) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == NonBlocking); + Require(dup2_nid_postfix(pipe[0], other[0]) == other[0]); + Require(fcntl_nid_postfix(other[0], GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(other[0], GetFlags) == NonBlocking); + char buffer[4] = {}; + RequireFailure(static_cast(read_nid_postfix(other[0], buffer, sizeof(buffer))), GuestEagain); + Require(fcntl_nid_postfix(pipe[0], SetFlags, 0) == 0); + Require(fcntl_nid_postfix(duplicate, GetFlags) == 0); + Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 1); + for (const int descriptor : {pipe[0], pipe[1], other[0], other[1], duplicate}) Require(close_nid_postfix(descriptor) == 0); +} + int main() { DupOutlivesOriginal(); Dup2ReplacesTarget(); BadDescriptors(); SocketDuplicates(); RandomDeviceDuplicates(); + DuplicatesShareStatusFlags(); } diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 0c80bb893..834b38236 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -9,19 +9,68 @@ extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI close_nid_postfix(int); +int APS5_VABI pipe_nid_postfix(int*); +int APS5_VABI fcntl_nid_postfix(int, int, ...); +int APS5_VABI ioctl_nid_postfix(int, std::uint64_t, void*); +std::int64_t APS5_VABI read_nid_postfix(int, void*, std::uint64_t); +std::int64_t APS5_VABI write_nid_postfix(int, const char*, std::int64_t); +int* APS5_VABI __error_nid_postfix(); int APS5_VABI sceKernelFcntl(int, int, ...); int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); } static void Require(bool value) { if (!value) std::abort(); } +static constexpr int DuplicateCommand = 0; +static constexpr int GetDescriptorFlags = 1; +static constexpr int SetDescriptorFlags = 2; static constexpr int GetFlags = 3; static constexpr int SetFlags = 4; +static constexpr int GetLock = 11; +static constexpr int SetLock = 12; +static constexpr int DarwinReadAhead = 16; +static constexpr int DuplicateCloseOnExec = 17; +static constexpr int ReadOnly = 0; +static constexpr int WriteOnly = 1; static constexpr int ReadWrite = 2; static constexpr int NonBlocking = 4; +static constexpr int Append = 8; +static constexpr int ReadAheadFlag = 0x200; +static constexpr int CreateReadWrite = 0x202; +static constexpr int CreateWriteAppend = 0x209; +static constexpr int GuestEbadf = 9; +static constexpr int GuestEfault = 14; +static constexpr int GuestEinval = 22; +static constexpr int GuestEagain = 35; +static constexpr std::uint64_t ReadableRequest = 0x4004667f; static constexpr int SceEbadf = static_cast(0x80020009u); static constexpr int SceEinval = static_cast(0x80020016u); static constexpr int SceEnotsup = static_cast(0x8002002du); -int main() { + +struct GuestLock { + std::int64_t start; + std::int64_t length; + std::int32_t pid; + std::int16_t type; + std::int16_t whence; + std::int32_t system; +}; + +static void RequireFailure(int result, int error) { + Require(result == -1); + Require(*__error_nid_postfix() == error); +} + +template static bool ThrowsWith(TCall call, const std::string& prefix) { + try { + call(); + } catch (const std::runtime_error& error) { + return std::string(error.what()).rfind(prefix, 0) == 0; + } + return false; +} + +static void SocketsKeepTheirPath() { const int socket = socket_nid_postfix(2, 2, 0); Require(socket >= 0); Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); @@ -33,21 +82,130 @@ int main() { Require(sceKernelFcntl(socket, GetFlags) == ReadWrite); Require(sceKernelFcntl(socket, 5) == SceEinval); Require(sceKernelFcntl(socket + 4096, GetFlags) == SceEbadf); + int queued = -1; + Require(ioctl_nid_postfix(socket, ReadableRequest, &queued) == 0 && queued == 0); Require(close_nid_postfix(socket) == 0); Require(sceKernelFcntl(socket, GetFlags) == SceEbadf); Require(sceKernelFcntl(-1, GetFlags) == SceEbadf); Require(sceKernelFcntl(12345, GetFlags) == SceEbadf); - const int file = sceKernelOpen("kernel_fcntl_probe.bin", 0x202, 0644); +} + +static void FileFlagsRoundTrip() { + const int file = sceKernelOpen("kernel_fcntl_probe.bin", CreateReadWrite, 0644); Require(file >= 0); - bool rejected = false; - try { - sceKernelFcntl(file, GetFlags); - } catch (const std::runtime_error& error) { - rejected = std::string(error.what()).rfind("sceKernelFcntl: file descriptors are not implemented", 0) == 0; - } - Require(rejected); - Require(sceKernelClose(file) == 0); + Require(sceKernelFcntl(file, GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(file, SetFlags, ReadWrite | NonBlocking | 0x8000) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | NonBlocking)); + Require(fcntl_nid_postfix(file, SetFlags, 0) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(file, SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 1); + Require(sceKernelFcntl(file, SetDescriptorFlags, 0) == 0); + Require(sceKernelFcntl(file, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(file, DarwinReadAhead, 1) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | ReadAheadFlag)); + + const int duplicate = fcntl_nid_postfix(file, DuplicateCommand, 20); + Require(duplicate >= 20); + Require(fcntl_nid_postfix(duplicate, GetDescriptorFlags) == 0); + Require(fcntl_nid_postfix(file, SetFlags, NonBlocking) == 0); + Require(fcntl_nid_postfix(duplicate, GetFlags) == (ReadWrite | NonBlocking)); + const int closing = fcntl_nid_postfix(file, DuplicateCloseOnExec, duplicate); + Require(closing > duplicate); + Require(fcntl_nid_postfix(closing, GetDescriptorFlags) == 1); + Require(fcntl_nid_postfix(file, GetDescriptorFlags) == 0); + + Require(write_nid_postfix(file, "abcde", 5) == 5); + Require(sceKernelLseek(file, 2, 0) == 2); + + RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, -1), GuestEinval); + Require(sceKernelFcntl(file, DuplicateCommand, -1) == SceEinval); + RequireFailure(fcntl_nid_postfix(file, 99), GuestEinval); + RequireFailure(fcntl_nid_postfix(file, GetLock, nullptr), GuestEfault); +#ifdef _WIN32 + GuestLock lock{0, 0, 0, 3, 0, 0}; + Require(ThrowsWith([file, &lock] { fcntl_nid_postfix(file, SetLock, &lock); }, "fcntl: record locks are not implemented on Windows")); +#else + GuestLock lock{0, 0, 0, 3, 0, 0}; + Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); + GuestLock probe{0, 0, 0, 1, 0, 0}; + Require(fcntl_nid_postfix(file, GetLock, &probe) == 0 && probe.type == 2); + lock.type = 2; + Require(fcntl_nid_postfix(file, SetLock, &lock) == 0); + lock.type = 7; + RequireFailure(fcntl_nid_postfix(file, SetLock, &lock), GuestEinval); +#endif + + for (const int descriptor : {closing, duplicate, file}) Require(close_nid_postfix(descriptor) == 0); + RequireFailure(fcntl_nid_postfix(file, GetDescriptorFlags), GuestEbadf); + RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, 0), GuestEbadf); Require(sceKernelFcntl(file, GetFlags) == SceEbadf); + + const int appending = sceKernelOpen("kernel_fcntl_probe.bin", CreateWriteAppend, 0644); + Require(appending >= 0); + Require(fcntl_nid_postfix(appending, GetFlags) == (WriteOnly | Append)); + Require(fcntl_nid_postfix(appending, SetFlags, Append | NonBlocking) == 0); + Require(fcntl_nid_postfix(appending, GetFlags) == (WriteOnly | Append | NonBlocking)); +#ifdef _WIN32 + Require(ThrowsWith([appending] { fcntl_nid_postfix(appending, SetFlags, 0); }, "fcntl: changing O_APPEND on a file is not implemented")); +#else + Require(fcntl_nid_postfix(appending, SetFlags, 0) == 0); + Require(fcntl_nid_postfix(appending, GetFlags) == WriteOnly); +#endif + Require(ThrowsWith([appending] { fcntl_nid_postfix(appending, SetFlags, Append | 0x80); }, "fcntl: changing O_FSYNC on a file is not implemented")); + Require(sceKernelClose(appending) == 0); std::remove("kernel_fcntl_probe.bin"); +} + +static void PipeFlagsRoundTrip() { + int pipe[2]; + Require(pipe_nid_postfix(pipe) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadOnly); + Require(fcntl_nid_postfix(pipe[1], GetFlags) == WriteOnly); + Require(fcntl_nid_postfix(pipe[1], SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(pipe[1], GetDescriptorFlags) == 1); + Require(fcntl_nid_postfix(pipe[0], GetDescriptorFlags) == 0); + + Require(fcntl_nid_postfix(pipe[0], SetFlags, fcntl_nid_postfix(pipe[0], GetFlags) | NonBlocking) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadOnly | NonBlocking)); + char buffer[8] = {}; + RequireFailure(static_cast(read_nid_postfix(pipe[0], buffer, sizeof(buffer))), GuestEagain); + Require(write_nid_postfix(pipe[1], "abc", 3) == 3); + Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 3); + RequireFailure(static_cast(read_nid_postfix(pipe[0], buffer, sizeof(buffer))), GuestEagain); + + Require(fcntl_nid_postfix(pipe[1], SetFlags, NonBlocking) == 0); + char block[1024] = {}; + std::int64_t written = 0; + std::int64_t result = 0; + while (written < (1 << 22) && (result = write_nid_postfix(pipe[1], block, sizeof(block))) > 0) written += result; + Require(written > 0); + RequireFailure(static_cast(result), GuestEagain); + + RequireFailure(fcntl_nid_postfix(pipe[0], DarwinReadAhead, 1), GuestEbadf); + GuestLock lock{0, 0, 0, 3, 0, 0}; + RequireFailure(fcntl_nid_postfix(pipe[1], SetLock, &lock), GuestEbadf); + Require(ThrowsWith([&pipe] { fcntl_nid_postfix(pipe[0], SetFlags, NonBlocking | 0x40); }, "fcntl: O_ASYNC on a pipe is not implemented")); + for (const int descriptor : pipe) Require(close_nid_postfix(descriptor) == 0); +} + +static void StandardInputFlags() { + const int flags = fcntl_nid_postfix(0, GetFlags); + Require(flags >= 0); + Require((flags & 3) != WriteOnly); + Require(fcntl_nid_postfix(0, SetFlags, fcntl_nid_postfix(0, GetFlags) & ~NonBlocking) == 0); + Require(fcntl_nid_postfix(0, GetFlags) == (flags & ~NonBlocking)); + Require(fcntl_nid_postfix(0, SetDescriptorFlags, 1) == 0); + Require(fcntl_nid_postfix(0, GetDescriptorFlags) == 1); + Require(fcntl_nid_postfix(0, SetDescriptorFlags, 0) == 0); + Require(fcntl_nid_postfix(0, GetDescriptorFlags) == 0); +} + +int main() { + SocketsKeepTheirPath(); + FileFlagsRoundTrip(); + PipeFlagsRoundTrip(); + StandardInputFlags(); return 0; } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index f42d54e58..3514a6021 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -652,7 +652,8 @@ Throughout the project, every function at every stage either **does exactly what - [getsid](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) throws: process sessions are not modelled. PPSA74126 imports it and fails to load without it; the title made no call to it in a 60 s run - [sigaction](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) records the handler, flags and mask of every signal from 1 to 128 and reports them back, following FreeBSD's `sigaction` (`EINVAL` for other signals, and for a handler on `SIGKILL` or `SIGSTOP`); `signal` records its handler with `SA_RESTART`, as FreeBSD's libc does. Only handlers without `SA_SIGINFO` on the signals `signal` already delivers (`SIGINT`, `SIGILL`, `SIGABRT`, `SIGFPE`, `SIGSEGV`, `SIGTERM`) run, the same way `signal` handlers do; their mask and flags are not applied. `SA_SIGINFO` handlers are recorded but never called, since that needs FreeBSD's `siginfo_t` and `ucontext_t`, and neither are the handlers of other signals (`SIGBUS`, `SIGHUP`, ...), whether they are set by `sigaction` or by `signal`, which records them too (every number from 1 to 128, with `SA_RESTART` and an empty mask replacing what `sigaction` stored; `SIGKILL` and `SIGSTOP` accept only the default handler in both calls, as in FreeBSD's `kern_sigaction`). `raise` of an `SA_SIGINFO` handler on a signal `signal` delivers throws instead of terminating, and `raise` and `kill` of the other signals still fail with `EINVAL`, recorded handler or not - [kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported -- [sceKernelFcntl](../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) handles socket descriptors through the POSIX `fcntl` export (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`) and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use; the command semantics follow FreeBSD's [fcntl](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_descrip.c), and a closed descriptor returns `SCE_KERNEL_ERROR_EBADF`. File descriptors throw: their open flags are not tracked, and `F_DUPFD`, `F_GETFD` and `F_SETFD` are unsupported on all descriptors. The return values and error codes are unverified on a console +- [sceKernelFcntl](../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console +- [fcntl](../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `EINVAL` for every other command, `F_GETFD` and `F_SETFD` included). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and throw on Windows, and `F_GETLK` with `F_UNLCK` throws. The return values and error codes are unverified on a console - [ioctl, _ioctl](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket. Any other request on a socket fails with `ENOTTY`, and any request on a file or pipe descriptor fails with `EBADF`, where FreeBSD answers `FIONBIO` and `FIONREAD` there too and fails other requests, termios ones included, with `ENOTTY`. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [sendfile](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it - [pthread_kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread throws, since guest signals are not delivered to a chosen thread From 01b7bb3e2f2568a263fc014eabfc199a5452d466 Mon Sep 17 00:00:00 2001 From: screamm Date: Sun, 11 Oct 2026 18:01:27 +0200 Subject: [PATCH 2/3] feat(libkernel): support ioctl on non-socket descriptors ioctl and _ioctl answered EBADF for every descriptor outside the libSceNet socket table. Decoded call sites: PS5MC (PPSA99420, FIONBIO x3, FIONREAD x2, TIOCGWINSZ x3), ProsperoTV (PPSA99003, FIONBIO on a pipe once poll on pipes is in), XPSemu (PPSA97358, 21 sites). Following FreeBSD 11.4 kern_ioctl (sys/kern/sys_generic.c:758), vn_ioctl (sys/kern/vfs_vnops.c:1488), pipe_ioctl (sys/kern/sys_pipe.c:1346) and randomdev_ioctl (sys/dev/random/randomdev.c:301), release/11.4.0 8c639945: FIONBIO sets or clears O_NONBLOCK through the same status flags as fcntl F_SETFL (EFAULT for a null argument before EBADF); FIONREAD reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end, ENOTTY on the random device, and throws on a directory or another character device; termios requests (group 't', TIOCGWINSZ included) fail with ENOTTY, as tcgetattr already takes these descriptors as not a terminal; any other request throws naming the request and descriptor kind. Sockets keep their path unchanged. Co-Authored-By: Claude Fable 5.1 --- .../File/include/DescriptorControl.hpp | 1 + .../libkernel/File/src/DescriptorControl.cpp | 64 +++++++++++++++++++ core/libs/prx/libkernel/Socket/src/Udp.cpp | 1 + core/libs/tests/GuestKernelFcntl.cpp | 23 +++++++ docs/dev/TechnicalDebt.md | 2 +- 5 files changed, 90 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index 195fa099d..a0969b442 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -6,6 +6,7 @@ namespace File { int Fcntl(int fd, int command, std::intptr_t argument); +int Ioctl(int fd, std::uint64_t request, void* argument); int DuplicateFrom(int fd, int start); void RememberDescriptor(int fd, int openFlags); void ShareDescriptor(int from, int to); diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index ee8b4e0d0..847a315b7 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #else #include +#include #include #include #endif @@ -83,6 +85,11 @@ constexpr std::int16_t Unlock = 2; constexpr std::int16_t WriteLock = 3; constexpr std::int16_t FromCurrent = 1; +constexpr std::uint32_t NonblockingRequest = 0x8004667e; +constexpr std::uint32_t ReadableRequest = 0x4004667f; +constexpr std::uint32_t RequestIn = 0x80000000u; +constexpr std::uint32_t TerminalGroup = 't'; + struct GuestLock { std::int64_t start; std::int64_t length; @@ -187,6 +194,23 @@ void ApplyHostStatus(int fd, Kind kind, int changed, int next, const char* funct if ((changed & Nonblocking) && kind == Kind::Pipe) SetPipeNonblocking(fd, (next & Nonblocking) != 0, function); } +int Readable(int fd, Kind kind, const char* function) { + if (kind == Kind::File) { + const auto size = ::_filelengthi64(fd); + const auto offset = ::_telli64(fd); + if (size < 0 || offset < 0) + throw std::runtime_error(std::string(function) + ": cannot read the size and offset of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return static_cast(size - offset); + } + if (AccessMode(fd, function) == WriteOnly) return 0; + DWORD available = 0; + if (!::PeekNamedPipe(NativeHandle(fd), nullptr, 0, nullptr, &available, nullptr)) { + const DWORD error = ::GetLastError(); + if (error == ERROR_BROKEN_PIPE) return 0; + throw std::runtime_error(std::string(function) + ": cannot peek pipe descriptor " + std::to_string(fd) + ", error=" + std::to_string(error)); + } + return static_cast(available); +} #else std::optional Classify(int fd, const char* function) { if (fd < 0) return std::nullopt; @@ -234,6 +258,20 @@ void ApplyHostStatus(int fd, Kind, int changed, int next, const char* function) throw std::runtime_error(std::string(function) + ": host F_SETFL failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); } +int Readable(int fd, Kind kind, const char* function) { + if (kind == Kind::File) { + struct stat status{}; + const auto offset = ::lseek(fd, 0, SEEK_CUR); + if (::fstat(fd, &status) != 0 || offset < 0) + throw std::runtime_error(std::string(function) + ": cannot read the size and offset of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return static_cast(status.st_size - offset); + } + if (AccessMode(fd, function) == WriteOnly) return 0; + int available = 0; + if (::ioctl(fd, FIONREAD, &available) != 0) + throw std::runtime_error(std::string(function) + ": host FIONREAD failed, fd=" + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return available; +} int LockError(int error) { switch (error) { @@ -402,6 +440,32 @@ int Fcntl(int fd, int command, std::intptr_t argument) { } } +int Ioctl(int fd, std::uint64_t request, void* argument) { + const auto command = static_cast(request); + const std::uint32_t length = (command >> 16) & 0x1fff; + if ((command & RequestIn) && length != 0 && argument == nullptr) return Fail(GuestEfault); + const auto kind = Classify(fd, "ioctl"); + if (!kind) return Fail(GuestEbadf); + if (command == NonblockingRequest) { + const bool enable = *static_cast(argument) != 0; + return ChangeStatus(fd, *kind, Nonblocking, enable ? Nonblocking : 0, "ioctl"); + } + if (command == ReadableRequest) { + if (*kind == Kind::RandomDevice) return Fail(GuestEnotty); + if (*kind != Kind::File && *kind != Kind::Pipe) Unsupported("ioctl", "FIONREAD", *kind, fd); + const int available = Readable(fd, *kind, "ioctl"); + if (argument == nullptr) return Fail(GuestEfault); + const GuestArena::HostWrite destination(argument, sizeof(available)); + if (!destination.Open()) return Fail(GuestEfault); + std::memcpy(argument, &available, sizeof(available)); + return 0; + } + if (((command >> 8) & 0xff) == TerminalGroup) return Fail(GuestEnotty); + char hex[11]; + std::snprintf(hex, sizeof(hex), "0x%08X", command); + Unsupported("ioctl", std::string("request ") + hex, *kind, fd); +} + void RememberDescriptor(int fd, int openFlags) { std::lock_guard lock(g_mutex); g_entries[fd] = Entry{(openFlags & OpenCloseOnExec) != 0, std::make_shared(openFlags & OpenStatusFlags)}; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 03892b0de..b8923bd35 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -665,6 +665,7 @@ int APS5_VABI getpeername_nid_postfix(int descriptor, void* address, std::uint32 return 0; } int APS5_VABI ioctl_nid_postfix(int descriptor, std::uint64_t request, void* argument) { + if (descriptor < GuestSockets::FirstDescriptor) return File::Ioctl(descriptor, request, argument); const auto socket = Lookup(descriptor); if (!socket) return -1; if (!argument) return Fail(14); diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index 834b38236..c46f6a711 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -41,8 +41,12 @@ static constexpr int CreateWriteAppend = 0x209; static constexpr int GuestEbadf = 9; static constexpr int GuestEfault = 14; static constexpr int GuestEinval = 22; +static constexpr int GuestEnotty = 25; static constexpr int GuestEagain = 35; +static constexpr std::uint64_t NonblockingRequest = 0x8004667e; static constexpr std::uint64_t ReadableRequest = 0x4004667f; +static constexpr std::uint64_t WindowSizeRequest = 0x40087468; +static constexpr std::uint64_t AsyncRequest = 0x8004667d; static constexpr int SceEbadf = static_cast(0x80020009u); static constexpr int SceEinval = static_cast(0x80020016u); static constexpr int SceEnotsup = static_cast(0x8002002du); @@ -118,6 +122,12 @@ static void FileFlagsRoundTrip() { Require(write_nid_postfix(file, "abcde", 5) == 5); Require(sceKernelLseek(file, 2, 0) == 2); + int readable = -1; + Require(ioctl_nid_postfix(file, ReadableRequest, &readable) == 0 && readable == 3); + char size[8] = {}; + RequireFailure(ioctl_nid_postfix(file, WindowSizeRequest, size), GuestEnotty); + RequireFailure(ioctl_nid_postfix(file, NonblockingRequest, nullptr), GuestEfault); + Require(ThrowsWith([file] { int on = 1; ioctl_nid_postfix(file, AsyncRequest, &on); }, "ioctl: request 0x8004667D on a file is not implemented")); RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, -1), GuestEinval); Require(sceKernelFcntl(file, DuplicateCommand, -1) == SceEinval); @@ -140,6 +150,7 @@ static void FileFlagsRoundTrip() { for (const int descriptor : {closing, duplicate, file}) Require(close_nid_postfix(descriptor) == 0); RequireFailure(fcntl_nid_postfix(file, GetDescriptorFlags), GuestEbadf); RequireFailure(fcntl_nid_postfix(file, DuplicateCommand, 0), GuestEbadf); + RequireFailure(ioctl_nid_postfix(file, ReadableRequest, &readable), GuestEbadf); Require(sceKernelFcntl(file, GetFlags) == SceEbadf); const int appending = sceKernelOpen("kernel_fcntl_probe.bin", CreateWriteAppend, 0644); @@ -172,9 +183,19 @@ static void PipeFlagsRoundTrip() { char buffer[8] = {}; RequireFailure(static_cast(read_nid_postfix(pipe[0], buffer, sizeof(buffer))), GuestEagain); Require(write_nid_postfix(pipe[1], "abc", 3) == 3); + int readable = -1; + Require(ioctl_nid_postfix(pipe[0], ReadableRequest, &readable) == 0 && readable == 3); + Require(ioctl_nid_postfix(pipe[1], ReadableRequest, &readable) == 0 && readable == 0); Require(read_nid_postfix(pipe[0], buffer, sizeof(buffer)) == 3); RequireFailure(static_cast(read_nid_postfix(pipe[0], buffer, sizeof(buffer))), GuestEagain); + int off = 0; + Require(ioctl_nid_postfix(pipe[0], NonblockingRequest, &off) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == ReadOnly); + int on = 1; + Require(ioctl_nid_postfix(pipe[0], NonblockingRequest, &on) == 0); + Require(fcntl_nid_postfix(pipe[0], GetFlags) == (ReadOnly | NonBlocking)); + Require(fcntl_nid_postfix(pipe[1], SetFlags, NonBlocking) == 0); char block[1024] = {}; std::int64_t written = 0; @@ -186,6 +207,8 @@ static void PipeFlagsRoundTrip() { RequireFailure(fcntl_nid_postfix(pipe[0], DarwinReadAhead, 1), GuestEbadf); GuestLock lock{0, 0, 0, 3, 0, 0}; RequireFailure(fcntl_nid_postfix(pipe[1], SetLock, &lock), GuestEbadf); + char size[8] = {}; + RequireFailure(ioctl_nid_postfix(pipe[0], WindowSizeRequest, size), GuestEnotty); Require(ThrowsWith([&pipe] { fcntl_nid_postfix(pipe[0], SetFlags, NonBlocking | 0x40); }, "fcntl: O_ASYNC on a pipe is not implemented")); for (const int descriptor : pipe) Require(close_nid_postfix(descriptor) == 0); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 3514a6021..fe45a0e2b 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -654,7 +654,7 @@ Throughout the project, every function at every stage either **does exactly what - [kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128, signal 0 only checks that the target exists, and its own pid, 0 and minus its own pid (the guest is taken to lead its own process group) signal it through `raise`, so they share `raise`'s limits above. Any other pid, and -1, which skips the caller, report `ESRCH`, so `EPERM` is never reported - [sceKernelFcntl](../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) runs the POSIX `fcntl` below and returns its guest errno as `SCE_KERNEL_ERROR_*`, the form the other `sceKernel` file exports use. The return values and error codes are unverified on a console - [fcntl](../../core/libs/prx/libkernel/File/src/DescriptorControl.cpp) (libkernel) follows FreeBSD 11.4's [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L479-L799) and [`kern_dup`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L812) on file, directory, pipe and character device descriptors; a socket keeps the libSceNet path (`F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `EINVAL` for every other command, `F_GETFD` and `F_SETFD` included). The close-on-exec flag and the status flags live in a guest table, the status flags shared by `dup`, `dup2` and `F_DUPFD` as in an open file description; a descriptor the table has not seen, such as 0 to 2, starts from the host's flags, since the console's are not known. `F_GETFL` takes the access mode from the host (the granted access of the handle on Windows), so pipe ends report `O_RDONLY` and `O_WRONLY` where FreeBSD's bidirectional pipes report `O_RDWR`. `F_DUPFD` and `F_DUPFD_CLOEXEC` return the lowest free host descriptor at or above the start, so the bound is the host table (`EMFILE` on Windows, `EINVAL` past `RLIMIT_NOFILE` on Linux), not the console's unknown `RLIMIT_NOFILE` and `kern.maxfilesperproc`. `O_NONBLOCK` switches a host pipe (`PIPE_NOWAIT` on Windows, where an empty read and a full write then report `EAGAIN`) and is only recorded on files, directories and the random device, as their reads never block; on a Windows console or other character device it throws. Changing `O_APPEND` on a Windows file, changing `O_FSYNC` on a file, `O_ASYNC` on a pipe or character device (`SIGIO` is not delivered), `F_GETOWN`, `F_SETOWN` and `F_SETLK_REMOTE` throw; `O_DIRECT`, `F_RDAHEAD` and `F_READAHEAD` are recorded as the hints they are. Record locks (`F_GETLK`, `F_SETLK`, `F_SETLKW` and their forms without `l_sysid`) use the host's POSIX locks on Linux and throw on Windows, and `F_GETLK` with `F_UNLCK` throws. The return values and error codes are unverified on a console -- [ioctl, _ioctl](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket. Any other request on a socket fails with `ENOTTY`, and any request on a file or pipe descriptor fails with `EBADF`, where FreeBSD answers `FIONBIO` and `FIONREAD` there too and fails other requests, termios ones included, with `ENOTTY`. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests +- [ioctl, _ioctl](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) handle only `FIONBIO` and `FIONREAD` on a socket, and any other request there fails with `ENOTTY`. On the other descriptors they follow FreeBSD's [`kern_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L758), [`vn_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/vfs_vnops.c#L1488) and [`pipe_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_pipe.c#L1346): `FIONBIO` sets `O_NONBLOCK` as `fcntl` does, and `FIONREAD` reports the size past the offset of a file, the queued bytes of a pipe's read end, 0 on its write end and `ENOTTY` on the random device, while it throws on a directory or another character device. Every termios request (group `t`, `TIOCGWINSZ` included) fails with `ENOTTY`, since those descriptors are taken as not a terminal, as in `tcgetattr`, and any other request throws. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests - [sendfile](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) follows FreeBSD 11's `sendfile` for a connected stream socket: headers, the file from `offset` (to its end when `nbytes` is 0) and trailers, the file offset left unchanged, `EAGAIN` with `*sbytes` set after a partial send on a non-blocking socket. It reads the file through `sceKernelPread` in 64 KiB blocks and sends each with `send`. A socket as the file fails with `EINVAL`, as on FreeBSD, but any other descriptor `sceKernelPread` reads, such as `/dev/urandom`, is sent where FreeBSD requires a regular file or shared memory, and a file descriptor as the socket fails with `EBADF`, as for `send`, where FreeBSD answers `ENOTSOCK`. `flags` are ignored: reads complete before sending, so `SF_NODISKIO` never fails with `EBUSY`, and `SF_SYNC` returns once the data is queued on the socket rather than sent. PS5MC (PPSA99420) imports it - [pthread_kill](../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) returns the error number like FreeBSD's `pthread_kill`: `EINVAL` for signals outside 0 to 128 and `ESRCH` for a null thread. Signal 0 reports any other thread handle as existing, without checking that the thread is still alive. A signal to the calling thread goes through `raise`, so it shares `raise`'s limits; a signal to another thread throws, since guest signals are not delivered to a chosen thread - [select](../../core/libs/prx/libkernel/Socket/src/Socket.cpp) (libkernel) validates `tv_usec` but saturates a valid timeout beyond the remaining `steady_clock` range at `time_point::max()`, so the wait can continue effectively indefinitely. The console's behavior for a valid timeout outside the host clock's range is unknown. From 99b1b32f315d78730d2ed91b2f8d33b077a34fba Mon Sep 17 00:00:00 2001 From: Ahir <191682168+ahir061@users.noreply.github.com> Date: Sun, 11 Oct 2026 18:10:01 +0000 Subject: [PATCH 3/3] fix(libkernel): read the fcntl argument by command fcntl and sceKernelFcntl read a third argument as a 64-bit integer for every command on a non-socket descriptor. F_GETFD and F_GETFL pass no third argument, and the commands that take one pass an int or a pointer. They now read nothing, an int or a pointer, by command. An int read as a 64-bit value kept whatever the caller left in the upper half of the register, so F_READAHEAD -1 enabled read-ahead instead of disabling it. --- .../File/include/DescriptorControl.hpp | 3 +++ .../libkernel/File/src/DescriptorControl.cpp | 25 +++++++++++++++++++ core/libs/prx/libkernel/File/src/Open.cpp | 24 +++++++++++------- core/libs/prx/libkernel/Socket/src/Udp.cpp | 24 +++++++++++------- core/libs/tests/GuestKernelFcntl.cpp | 8 ++++++ 5 files changed, 66 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp index a0969b442..cb327dff5 100644 --- a/core/libs/prx/libkernel/File/include/DescriptorControl.hpp +++ b/core/libs/prx/libkernel/File/include/DescriptorControl.hpp @@ -5,6 +5,9 @@ namespace File { +enum class FcntlArgument { None, Int, Pointer }; + +FcntlArgument FcntlArgumentOf(int command); int Fcntl(int fd, int command, std::intptr_t argument); int Ioctl(int fd, std::uint64_t request, void* argument); int DuplicateFrom(int fd, int start); diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index 847a315b7..7bdd8c8bc 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -381,6 +381,31 @@ int ReadAheadControl(int fd, int command, std::intptr_t argument) { namespace File { +FcntlArgument FcntlArgumentOf(int command) { + switch (command) { + case DuplicateCommand: + case SetDescriptorFlags: + case SetStatusFlags: + case SetOwner: + case DuplicateToCommand: + case ReadAhead: + case DarwinReadAhead: + case DuplicateCloseOnExec: + case DuplicateToCloseOnExec: + return FcntlArgument::Int; + case OldGetLock: + case OldSetLock: + case OldSetLockWait: + case GetLock: + case SetLock: + case SetLockWait: + case SetLockRemote: + return FcntlArgument::Pointer; + default: + return FcntlArgument::None; + } +} + int Fcntl(int fd, int command, std::intptr_t argument) { switch (command) { case DuplicateCommand: diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index f8a287b67..4d0135c0f 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -342,19 +342,25 @@ int APS5_VABI sceKernelUnlink(const char* path) { } int APS5_VABI sceKernelFcntl(int d, int command, ...) { + const auto type = File::FcntlArgumentOf(command); + std::intptr_t argument = 0; + if (type != File::FcntlArgument::None) { #ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - const auto argument = __builtin_va_arg(arguments, long long); - __builtin_sysv_va_end(arguments); + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? __builtin_va_arg(arguments, int) + : reinterpret_cast(__builtin_va_arg(arguments, void*)); + __builtin_sysv_va_end(arguments); #else - std::va_list arguments; - va_start(arguments, command); - const auto argument = va_arg(arguments, long); - va_end(arguments); + std::va_list arguments; + va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? va_arg(arguments, int) + : reinterpret_cast(va_arg(arguments, void*)); + va_end(arguments); #endif + } int result; - if (d < GuestSockets::FirstDescriptor) result = File::Fcntl(d, command, static_cast(argument)); + if (d < GuestSockets::FirstDescriptor) result = File::Fcntl(d, command, argument); else if (command == GuestSetFlags) result = fcntl_nid_postfix(d, command, static_cast(argument)); else result = fcntl_nid_postfix(d, command); return result < 0 ? SceKernelError(*__error_nid_postfix()) : result; diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index b8923bd35..8f9420d8e 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -437,18 +437,24 @@ bool GuestSockets::Ready(int descriptor, bool write, std::int64_t* data, bool* e extern "C" { int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) { if (descriptor < GuestSockets::FirstDescriptor) { + const auto type = File::FcntlArgumentOf(command); + std::intptr_t argument = 0; + if (type != File::FcntlArgument::None) { #ifdef _WIN32 - __builtin_sysv_va_list arguments; - __builtin_sysv_va_start(arguments, command); - const auto argument = __builtin_va_arg(arguments, long long); - __builtin_sysv_va_end(arguments); + __builtin_sysv_va_list arguments; + __builtin_sysv_va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? __builtin_va_arg(arguments, int) + : reinterpret_cast(__builtin_va_arg(arguments, void*)); + __builtin_sysv_va_end(arguments); #else - std::va_list arguments; - va_start(arguments, command); - const auto argument = va_arg(arguments, long); - va_end(arguments); + std::va_list arguments; + va_start(arguments, command); + argument = type == File::FcntlArgument::Int ? va_arg(arguments, int) + : reinterpret_cast(va_arg(arguments, void*)); + va_end(arguments); #endif - return File::Fcntl(descriptor, command, static_cast(argument)); + } + return File::Fcntl(descriptor, command, argument); } const auto socket = Lookup(descriptor); if (!socket) return -1; diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index c46f6a711..c2c6030b3 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -28,6 +28,7 @@ static constexpr int GetFlags = 3; static constexpr int SetFlags = 4; static constexpr int GetLock = 11; static constexpr int SetLock = 12; +static constexpr int ReadAhead = 15; static constexpr int DarwinReadAhead = 16; static constexpr int DuplicateCloseOnExec = 17; static constexpr int ReadOnly = 0; @@ -109,6 +110,13 @@ static void FileFlagsRoundTrip() { Require(sceKernelFcntl(file, GetDescriptorFlags) == 0); Require(fcntl_nid_postfix(file, DarwinReadAhead, 1) == 0); Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | ReadAheadFlag)); + Require(fcntl_nid_postfix(file, ReadAhead, -1) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == ReadWrite); + Require(sceKernelFcntl(file, ReadAhead, 4096) == 0); + Require(fcntl_nid_postfix(file, GetFlags) == (ReadWrite | ReadAheadFlag)); + Require(sceKernelFcntl(file, ReadAhead, -1) == 0); + Require(sceKernelFcntl(file, GetFlags) == ReadWrite); + Require(fcntl_nid_postfix(file, DarwinReadAhead, 1) == 0); const int duplicate = fcntl_nid_postfix(file, DuplicateCommand, 20); Require(duplicate >= 20);