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 <noreply@anthropic.com>
This commit is contained in:
screamm 2026-10-11 18:00:28 +02:00 committed by Ahir Barman Maji
parent d06c065657
commit 591e8ddadc
9 changed files with 708 additions and 33 deletions

View File

@ -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

View File

@ -0,0 +1,16 @@
#ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_DESCRIPTORCONTROL_HPP
#define CORE_LIBS_PRX_LIBKERNEL_FILE_DESCRIPTORCONTROL_HPP
#include <cstdint>
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

View File

@ -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 <cerrno>
#include <cstddef>
#include <cstring>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <string>
#ifdef _WIN32
#include <io.h>
#include <windows.h>
#include <winternl.h>
#else
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#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<int> status;
};
std::mutex g_mutex;
std::map<int, Entry> 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>(handle);
}
std::optional<Kind> 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<QueryObject>(reinterpret_cast<void*>(::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<int>(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<Kind> 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<int>(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<void*>(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<void>(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<off_t>(lock.start);
host.l_len = static_cast<off_t>(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<int>(argument) < 0) return Fail(GuestEinval);
if (!Classify(fd, "fcntl")) return Fail(GuestEbadf);
const int duplicate = DuplicateFrom(fd, static_cast<int>(argument));
if (duplicate >= 0 && command == DuplicateCloseOnExec) SetCloseOnExec(duplicate, true);
return duplicate;
}
case DuplicateToCommand:
case DuplicateToCloseOnExec: {
const int duplicate = dup2_nid_postfix(fd, static_cast<int>(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<int>(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<int>(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);
}
}

View File

@ -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::size_t>(std::numeric_limits<unsigned int>::max())) {
throw std::runtime_error("sceKernelRead: nbytes exceeds platform limit");
}
return ::_read(fd, buf, static_cast<unsigned int>(n));
const int result = ::_read(fd, buf, static_cast<unsigned int>(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::size_t>(std::numeric_limits<unsigned int>::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<unsigned int>(n)) : static_cast<int>(result);
const auto shared = File::WriteSharedMemory_nid_no_patch(fd, buf, n);
if (shared != -2) return static_cast<int>(shared);
const int result = ::_write(fd, buf, static_cast<unsigned int>(n));
if (result < 0 && errno == ENOSPC && _doserrno == 0 && ::GetFileType(reinterpret_cast<HANDLE>(::_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<std::intptr_t>(argument));
else if (command == GuestSetFlags) result = fcntl_nid_postfix(d, command, static_cast<int>(argument));
else result = fcntl_nid_postfix(d, command);
return result < 0 ? SceKernelError(*__error_nid_postfix()) : result;
}

View File

@ -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<int> 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
}

View File

@ -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 <algorithm>
@ -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<std::intptr_t>(argument));
}
const auto socket = Lookup(descriptor);
if (!socket) return -1;
std::lock_guard lock(socket->modeMutex);

View File

@ -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<int>(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();
}

View File

@ -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<int>(0x80020009u);
static constexpr int SceEinval = static_cast<int>(0x80020016u);
static constexpr int SceEnotsup = static_cast<int>(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 <typename TCall> 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<int>(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<int>(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<int>(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;
}

View File

@ -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