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