From 43ffba87e45bb726571bca76080a3fe53c4122f7 Mon Sep 17 00:00:00 2001 From: screamm Date: Mon, 12 Oct 2026 14:59:07 +0200 Subject: [PATCH] feat(libkernel): implement fcntl record locks on Windows ProsperoRadio (PPSA99001) stops at 33.7 s on Windows, with #3649 applied so that it gets past poll, on the throw from #3840: what(): fcntl: record locks are not implemented on Windows, command=12, fd=3 F_GETLK, F_SETLK, F_SETLKW and the F_OGETLK, F_OSETLK and F_OSETLKW forms now follow FreeBSD 11.4's kern_fcntl and lf_advlockasync (8c639945): an SEEK_CUR start is resolved against the descriptor offset (EOVERFLOW past OFF_MAX), F_RDLCK needs read access and F_WRLCK write access (EBADF), another type is EINVAL, and the range is checked against the file size for SEEK_END (EINVAL for a negative start or an unknown whence, EOVERFLOW past OFF_MAX). The guest is one process, and lf_blocks never lets locks of one owner conflict, so whichever descriptor or thread asks, F_SETLK and F_SETLKW succeed without waiting and F_GETLK reports F_UNLCK, as on the console. Nothing is held on the host: Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors. A lock from the end of a directory throws. Co-Authored-By: Claude Fable 5.1 --- .../libkernel/File/src/DescriptorControl.cpp | 62 ++++++- core/libs/tests/GuestKernelFcntl.cpp | 152 +++++++++++++++++- docs/dev/debt/libkernel.md | 2 +- 3 files changed, 208 insertions(+), 8 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp index d2a62b739..5fa3171ef 100644 --- a/core/libs/prx/libkernel/File/src/DescriptorControl.cpp +++ b/core/libs/prx/libkernel/File/src/DescriptorControl.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -84,7 +85,9 @@ 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 FromStart = 0; constexpr std::int16_t FromCurrent = 1; +constexpr std::int16_t FromEnd = 2; constexpr std::uint32_t NonblockingRequest = 0x8004667e; constexpr std::uint32_t ReadableRequest = 0x4004667f; @@ -212,6 +215,62 @@ int Readable(int fd, Kind kind, const char* function) { } return static_cast(available); } + +std::int64_t LockOffset(int fd, Kind kind) { + if (kind == Kind::Directory) { + const auto cursor = File::SeekDirectoryDescriptor(fd, 0, FromCurrent); + if (!cursor) throw std::runtime_error("fcntl: cannot read the offset of directory descriptor " + std::to_string(fd)); + return *cursor; + } + const auto offset = ::_telli64(fd); + if (offset < 0) throw std::runtime_error("fcntl: cannot read the offset of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return offset; +} + +std::int64_t LockFileSize(int fd, Kind kind) { + if (kind == Kind::Directory) Unsupported("fcntl", "a record lock from the end", kind, fd); + const auto size = ::_filelengthi64(fd); + if (size < 0) throw std::runtime_error("fcntl: cannot read the size of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno)); + return size; +} + +int LockRangeError(int fd, Kind kind, const GuestLock& lock) { + constexpr auto Largest = std::numeric_limits::max(); + std::int64_t start = lock.start; + if (lock.whence == FromEnd) { + const auto size = LockFileSize(fd, kind); + if (lock.start > 0 && size > Largest - lock.start) return GuestEoverflow; + start = size + lock.start; + } else if (lock.whence != FromStart && lock.whence != FromCurrent) { + return GuestEinval; + } + if (start < 0) return GuestEinval; + if (lock.length < 0 && (start == 0 || start + lock.length < 0)) return GuestEinval; + if (lock.length > 0 && lock.length - 1 > Largest - start) return GuestEoverflow; + return 0; +} + +int ProcessRecordLock(int fd, Kind kind, int command, GuestLock& lock, void* pointer, std::size_t size) { + const bool known = lock.type == ReadLock || lock.type == WriteLock || lock.type == Unlock; + if (command == GetLock && !known) return Fail(GuestEinval); + if (lock.whence == FromCurrent) { + const auto offset = LockOffset(fd, kind); + if (lock.start > 0 && offset > std::numeric_limits::max() - lock.start) return Fail(GuestEoverflow); + lock.start += offset; + } + if (command != GetLock) { + if (!known) return Fail(GuestEinval); + const int access = lock.type == Unlock ? ReadWrite : AccessMode(fd, "fcntl"); + if ((lock.type == ReadLock && access == WriteOnly) || (lock.type == WriteLock && access == ReadOnly)) return Fail(GuestEbadf); + } + if (const int error = LockRangeError(fd, kind, lock); error != 0) return Fail(error); + if (command != GetLock) return 0; + lock.type = Unlock; + const GuestArena::HostWrite destination(pointer, size); + if (!destination.Open()) return Fail(GuestEfault); + std::memcpy(pointer, &lock, size); + return 0; +} #else std::optional Classify(int fd, const char* function) { if (fd < 0) return std::nullopt; @@ -329,8 +388,7 @@ int RecordLock(int fd, int command, std::intptr_t argument) { 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)); + return ProcessRecordLock(fd, *kind, current, lock, pointer, size); #else struct flock host{}; if (lock.type == ReadLock) host.l_type = F_RDLCK; diff --git a/core/libs/tests/GuestKernelFcntl.cpp b/core/libs/tests/GuestKernelFcntl.cpp index c2c6030b3..a839d3d03 100644 --- a/core/libs/tests/GuestKernelFcntl.cpp +++ b/core/libs/tests/GuestKernelFcntl.cpp @@ -4,8 +4,11 @@ #include #include #include +#include +#include #include #include +#include extern "C" { int APS5_VABI socket_nid_postfix(int, int, int); int APS5_VABI close_nid_postfix(int); @@ -26,8 +29,12 @@ static constexpr int GetDescriptorFlags = 1; static constexpr int SetDescriptorFlags = 2; static constexpr int GetFlags = 3; static constexpr int SetFlags = 4; +static constexpr int OldGetLock = 7; +static constexpr int OldSetLock = 8; +static constexpr int OldSetLockWait = 9; static constexpr int GetLock = 11; static constexpr int SetLock = 12; +static constexpr int SetLockWait = 13; static constexpr int ReadAhead = 15; static constexpr int DarwinReadAhead = 16; static constexpr int DuplicateCloseOnExec = 17; @@ -39,11 +46,19 @@ static constexpr int Append = 8; static constexpr int ReadAheadFlag = 0x200; static constexpr int CreateReadWrite = 0x202; static constexpr int CreateWriteAppend = 0x209; +static constexpr int OpenDirectory = 0x20000; +static constexpr std::int16_t ReadLock = 1; +static constexpr std::int16_t Unlock = 2; +static constexpr std::int16_t WriteLock = 3; +static constexpr std::int16_t FromStart = 0; +static constexpr std::int16_t FromCurrent = 1; +static constexpr std::int16_t FromEnd = 2; 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 int GuestEoverflow = 84; static constexpr std::uint64_t NonblockingRequest = 0x8004667e; static constexpr std::uint64_t ReadableRequest = 0x4004667f; static constexpr std::uint64_t WindowSizeRequest = 0x40087468; @@ -141,10 +156,6 @@ static void FileFlagsRoundTrip() { 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}; @@ -153,7 +164,6 @@ static void FileFlagsRoundTrip() { 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); @@ -177,6 +187,137 @@ static void FileFlagsRoundTrip() { std::remove("kernel_fcntl_probe.bin"); } +static bool NoConflict(int fd, GuestLock request) { + const GuestLock asked = request; + if (fcntl_nid_postfix(fd, GetLock, &request) != 0) return false; + return request.type == Unlock && request.start == asked.start && request.length == asked.length && request.pid == asked.pid && request.whence == asked.whence && request.system == asked.system; +} + +static void RecordLocksBelongToTheProcess() { + const int file = sceKernelOpen("kernel_fcntl_locks.bin", CreateReadWrite, 0644); + Require(file >= 0); + Require(write_nid_postfix(file, "0123456789", 10) == 10); + + GuestLock whole{0, 0, 0, WriteLock, FromStart, 0}; + Require(fcntl_nid_postfix(file, SetLock, &whole) == 0); + Require(NoConflict(file, {4, 2, 0, WriteLock, FromStart, 77})); + GuestLock middle{2, 3, 0, ReadLock, FromStart, 0}; + Require(fcntl_nid_postfix(file, SetLock, &middle) == 0); + GuestLock tail{5, 0, 0, Unlock, FromStart, 0}; + Require(fcntl_nid_postfix(file, SetLock, &tail) == 0); + GuestLock merged{0, 10, 0, WriteLock, FromStart, 0}; + Require(fcntl_nid_postfix(file, SetLockWait, &merged) == 0); + Require(NoConflict(file, {0, 0, 0, WriteLock, FromStart, 0})); + + const int other = sceKernelOpen("kernel_fcntl_locks.bin", ReadWrite, 0); + Require(other >= 0); + GuestLock same{0, 10, 0, WriteLock, FromStart, 0}; + Require(fcntl_nid_postfix(other, SetLock, &same) == 0); + Require(fcntl_nid_postfix(other, SetLockWait, &same) == 0); + Require(NoConflict(other, {0, 0, 0, WriteLock, FromStart, 0})); + + int threadSet = -1; + int threadWait = -1; + bool threadSeesNoConflict = false; + std::thread thread([&] { + GuestLock request{3, 4, 0, WriteLock, FromStart, 0}; + threadSet = fcntl_nid_postfix(other, SetLock, &request); + threadWait = fcntl_nid_postfix(file, SetLockWait, &request); + threadSeesNoConflict = NoConflict(file, {0, 0, 0, WriteLock, FromStart, 0}); + }); + thread.join(); + Require(threadSet == 0 && threadWait == 0 && threadSeesNoConflict); + + Require(sceKernelLseek(file, 6, 0) == 6); + GuestLock current{-2, 0, 0, ReadLock, FromCurrent, 5}; + Require(fcntl_nid_postfix(file, GetLock, ¤t) == 0); + Require(current.type == Unlock && current.start == 4 && current.whence == FromCurrent && current.length == 0 && current.system == 5); + GuestLock end{-3, 0, 0, WriteLock, FromEnd, 0}; + Require(fcntl_nid_postfix(file, SetLock, &end) == 0); + GuestLock backwards{5, -5, 0, ReadLock, FromStart, 0}; + Require(fcntl_nid_postfix(file, SetLock, &backwards) == 0); + + unsigned char old[32]; + std::memset(old, 0xa5, sizeof(old)); + GuestLock oldLock{0, 1, 0, WriteLock, FromStart, 0}; + std::memcpy(old, &oldLock, 24); + Require(fcntl_nid_postfix(file, OldSetLock, old) == 0); + Require(fcntl_nid_postfix(file, OldSetLockWait, old) == 0); + Require(fcntl_nid_postfix(file, OldGetLock, old) == 0); + std::memcpy(&oldLock, old, 24); + Require(oldLock.type == Unlock && oldLock.start == 0 && oldLock.length == 1); + for (std::size_t index = 24; index < sizeof(old); ++index) Require(old[index] == 0xa5); + + GuestLock badType{0, 0, 0, 0, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &badType), GuestEinval); + badType.type = 4; + RequireFailure(fcntl_nid_postfix(file, SetLock, &badType), GuestEinval); + badType.type = 7; + RequireFailure(fcntl_nid_postfix(file, GetLock, &badType), GuestEinval); + GuestLock negative{-1, 0, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &negative), GuestEinval); + GuestLock beforeStart{0, -1, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &beforeStart), GuestEinval); + GuestLock pastStart{5, -6, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &pastStart), GuestEinval); + GuestLock unknownWhence{0, 0, 0, WriteLock, 3, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &unknownWhence), GuestEinval); + Require(sceKernelFcntl(file, SetLock, &unknownWhence) == SceEinval); + + const int reader = sceKernelOpen("kernel_fcntl_locks.bin", ReadOnly, 0); + Require(reader >= 0); + GuestLock write{0, 0, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(reader, SetLock, &write), GuestEbadf); + RequireFailure(fcntl_nid_postfix(reader, SetLockWait, &write), GuestEbadf); + Require(sceKernelFcntl(reader, SetLock, &write) == SceEbadf); + Require(NoConflict(reader, write)); + GuestLock read{0, 0, 0, ReadLock, FromStart, 0}; + Require(fcntl_nid_postfix(reader, SetLock, &read) == 0); + const int writer = sceKernelOpen("kernel_fcntl_locks.bin", WriteOnly, 0); + Require(writer >= 0); + RequireFailure(fcntl_nid_postfix(writer, SetLock, &read), GuestEbadf); + Require(fcntl_nid_postfix(writer, SetLock, &write) == 0); + GuestLock release{0, 0, 0, Unlock, FromStart, 0}; + Require(fcntl_nid_postfix(writer, SetLock, &release) == 0); + Require(fcntl_nid_postfix(reader, SetLock, &release) == 0); + +#ifdef _WIN32 + Require(NoConflict(file, {0, 0, 0, Unlock, FromStart, 0})); + GuestLock tooLong{std::numeric_limits::max(), 2, 0, WriteLock, FromStart, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &tooLong), GuestEoverflow); + RequireFailure(fcntl_nid_postfix(file, GetLock, &tooLong), GuestEoverflow); + GuestLock pastEnd{std::numeric_limits::max(), 0, 0, WriteLock, FromEnd, 0}; + RequireFailure(fcntl_nid_postfix(file, SetLock, &pastEnd), GuestEoverflow); + GuestLock pastCurrent{std::numeric_limits::max(), 0, 0, ReadLock, FromCurrent, 0}; + Require(fcntl_nid_postfix(reader, SetLock, &pastCurrent) == 0); + pastCurrent.type = WriteLock; + Require(sceKernelLseek(reader, 1, 0) == 1); + RequireFailure(fcntl_nid_postfix(reader, SetLock, &pastCurrent), GuestEoverflow); + GuestLock writeBeforeWhence{0, 0, 0, WriteLock, 3, 0}; + RequireFailure(fcntl_nid_postfix(reader, SetLock, &writeBeforeWhence), GuestEbadf); + RequireFailure(fcntl_nid_postfix(reader, GetLock, &writeBeforeWhence), GuestEinval); + + std::filesystem::create_directory("kernel_fcntl_locks_dir"); + const int directory = sceKernelOpen("kernel_fcntl_locks_dir", OpenDirectory, 0); + Require(directory >= 0); + Require(fcntl_nid_postfix(directory, SetLock, &read) == 0); + RequireFailure(fcntl_nid_postfix(directory, SetLock, &write), GuestEbadf); + Require(NoConflict(directory, {0, 0, 0, WriteLock, FromCurrent, 0})); + GuestLock directoryEnd{0, 0, 0, ReadLock, FromEnd, 0}; + Require(ThrowsWith([directory, &directoryEnd] { fcntl_nid_postfix(directory, SetLock, &directoryEnd); }, "fcntl: a record lock from the end on a directory is not implemented")); + Require(close_nid_postfix(directory) == 0); + std::filesystem::remove("kernel_fcntl_locks_dir"); +#endif + + for (const int descriptor : {writer, reader, other}) Require(close_nid_postfix(descriptor) == 0); + Require(fcntl_nid_postfix(file, SetLock, &whole) == 0); + Require(NoConflict(file, {0, 0, 0, WriteLock, FromStart, 0})); + Require(close_nid_postfix(file) == 0); + RequireFailure(fcntl_nid_postfix(file, SetLock, &whole), GuestEbadf); + RequireFailure(fcntl_nid_postfix(file, GetLock, &whole), GuestEbadf); + std::remove("kernel_fcntl_locks.bin"); +} + static void PipeFlagsRoundTrip() { int pipe[2]; Require(pipe_nid_postfix(pipe) == 0); @@ -236,6 +377,7 @@ static void StandardInputFlags() { int main() { SocketsKeepTheirPath(); FileFlagsRoundTrip(); + RecordLocksBelongToTheProcess(); PipeFlagsRoundTrip(); StandardInputFlags(); return 0; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 132b6b9b4..77a4ccfa4 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -142,7 +142,7 @@ - [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) 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 +- [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, where `F_GETLK` with `F_UNLCK` throws. On Windows they follow [`kern_fcntl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_descrip.c#L607-L752) and [`lf_advlockasync`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L433-L487) for the one guest process, which owns every lock: `EOVERFLOW` for an `SEEK_CUR` start past `OFF_MAX`, then `EBADF` for `F_RDLCK` without read access or `F_WRLCK` without write access and `EINVAL` for another type, then `EINVAL` and `EOVERFLOW` for the range; as [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) never lets locks of one owner conflict, whichever descriptor or thread asks, `F_SETLK` and `F_SETLKW` then succeed without waiting and `F_GETLK` reports `F_UNLCK`, so no lock list is kept and merging, splitting and the release on close have nothing to change. Nothing is held on the host, since Windows byte-range locks are mandatory and would fail the guest's own reads and writes through other descriptors, so other host processes neither see these locks nor are seen; a lock from the end of a directory 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, 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