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 <noreply@anthropic.com>
This commit is contained in:
screamm 2026-10-12 14:59:07 +02:00 committed by milescoviello
parent 1acfa7f788
commit 43ffba87e4
3 changed files with 208 additions and 8 deletions

View File

@ -10,6 +10,7 @@
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <limits>
#include <map>
#include <memory>
#include <mutex>
@ -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<int>(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<std::int64_t>::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<std::int64_t>::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<Kind> 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<void>(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;

View File

@ -4,8 +4,11 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <limits>
#include <stdexcept>
#include <string>
#include <thread>
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, &current) == 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<std::int64_t>::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<std::int64_t>::max(), 0, 0, WriteLock, FromEnd, 0};
RequireFailure(fcntl_nid_postfix(file, SetLock, &pastEnd), GuestEoverflow);
GuestLock pastCurrent{std::numeric_limits<std::int64_t>::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;

View File

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