Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion core/libs/prx/libkernel/File/src/DescriptorControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
#include <string>

#ifdef _WIN32
#include <fcntl.h>
#include <io.h>
#include <windows.h>
#include "prx/libc/include/General.hpp"
#include "prx/libc/include/WindowsDescriptor.hpp"
#else
#include <fcntl.h>
Expand Down Expand Up @@ -180,8 +182,33 @@ Entry InitialEntry(int fd) {
return {false, std::make_shared<int>(status)};
}

void SetHostAppend(int fd, bool append, const char* function) {
const int translation = ::_setmode(fd, _O_BINARY);
if (translation == -1 || ::_setmode(fd, translation) == -1)
throw std::runtime_error(std::string(function) + ": cannot read the translation mode of descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno));
HANDLE copy = nullptr;
if (!::DuplicateHandle(::GetCurrentProcess(), WindowsDescriptorHandle_nid_no_patch(fd), ::GetCurrentProcess(), &copy, 0, TRUE, DUPLICATE_SAME_ACCESS))
throw std::runtime_error(std::string(function) + ": cannot duplicate the handle of descriptor " + std::to_string(fd) + ", error=" + std::to_string(::GetLastError()));
const int replacement = ::_open_osfhandle(reinterpret_cast<std::intptr_t>(copy), (append ? _O_APPEND : 0) | translation);
if (replacement < 0) {
::CloseHandle(copy);
throw std::runtime_error(std::string(function) + ": cannot open a replacement for descriptor " + std::to_string(fd) + ", errno=" + std::to_string(errno));
}
auto* const standard = HostStandardHandle_nid_no_patch(fd);
const int moved = ::_dup2(replacement, fd);
const int error = errno;
::_close(replacement);
KeepStandardHandle_nid_no_patch(fd, standard);
if (moved != 0)
throw std::runtime_error(std::string(function) + ": cannot replace descriptor " + std::to_string(fd) + ", errno=" + std::to_string(error));
}

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 & Append) && kind == Kind::File) {
const auto status = g_entries.at(fd).status;
for (const auto& [descriptor, entry] : g_entries)
if (entry.status == status && DescriptorIsOpen_nid_no_patch(descriptor)) SetHostAppend(descriptor, (next & Append) != 0, function);
}
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);
}
Expand Down
23 changes: 19 additions & 4 deletions core/libs/tests/GuestKernelFcntl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ static void SocketsKeepTheirPath() {
}

static void FileFlagsRoundTrip() {
std::remove("kernel_fcntl_probe.bin");
const int file = sceKernelOpen("kernel_fcntl_probe.bin", CreateReadWrite, 0644);
Require(file >= 0);
Require(sceKernelFcntl(file, GetFlags) == ReadWrite);
Expand Down Expand Up @@ -275,12 +276,26 @@ static void FileFlagsRoundTrip() {
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(write_nid_postfix(appending, "ab", 2) == 2);
Require(fcntl_nid_postfix(appending, SetFlags, 0) == 0);
Require(fcntl_nid_postfix(appending, GetFlags) == WriteOnly);
#endif
Require(sceKernelLseek(appending, 0, FromStart) == 0);
Require(write_nid_postfix(appending, "X", 1) == 1);
const int sharing = fcntl_nid_postfix(appending, DuplicateCommand, 0);
Require(sharing >= 0);
Require(fcntl_nid_postfix(sharing, SetFlags, Append) == 0);
Require(fcntl_nid_postfix(appending, GetFlags) == (WriteOnly | Append));
Require(sceKernelLseek(appending, 0, FromStart) == 0);
Require(write_nid_postfix(appending, "c", 1) == 1);
Require(fcntl_nid_postfix(sharing, SetFlags, 0) == 0);
Require(sceKernelLseek(appending, 0, FromStart) == 0);
Require(write_nid_postfix(appending, "Y", 1) == 1);
Require(close_nid_postfix(sharing) == 0);
const int reading = sceKernelOpen("kernel_fcntl_probe.bin", ReadOnly, 0);
Require(reading >= 0);
char written[16]{};
Require(sceKernelRead(reading, written, sizeof(written)) == 8 && std::memcmp(written, "Ybcdeabc", 8) == 0);
Require(sceKernelClose(reading) == 0);
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");
Expand Down
2 changes: 1 addition & 1 deletion docs/dev/debt/libkernel.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@
- [kill, killpg](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follow FreeBSD's `kern_kill` and `killpg1` for the only guest process: `EINVAL` for signals outside 0 to 128 and for negative `killpg` process groups, 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
- [Creating through a trailing slash](../../../core/libs/prx/libkernel/File/src/Open.cpp) (libkernel) - `open` and `sceKernelOpen` with `O_CREAT` on a missing name followed by a slash (`new/`) fail with `EINVAL` on Windows and with the host's `EISDIR` on Linux, where FreeBSD 11.4's `lookup()` (sys/kern/vfs_lookup.c) fails with `ENOENT` because only a directory may be created through a trailing slash
- [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_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. 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_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 macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. 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
- [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_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFL`, `F_SETFL` with `O_NONBLOCK`, `F_GETFD` and `F_SETFD`, `EINVAL` for every other command, see the next entry). On Linux and macOS a host socket that is not a guest socket, such as a standard descriptor a service manager connected to its journal, takes the flag handling of a pipe end and `FIONREAD` asks the host for its queued bytes (for a datagram socket Linux counts only the next datagram, where FreeBSD's [`soo_ioctl`](https://github.com/freebsd/freebsd-src/blob/release/11.4.0/sys/kern/sys_socket.c#L202-L205) counts every buffered byte); its record locks and read-ahead hints fail with `EBADF`, as FreeBSD fails them on a descriptor without a vnode, and `O_ASYNC` on it throws. 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_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_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 macOS. `F_GETLK` with `F_UNLCK` asks the host with `F_RDLCK`: as in [`lf_blocks`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/kern_lockf.c#L866) only another owner's overlapping write lock conflicts with either request. 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

Check warning on line 233 in docs/dev/debt/libkernel.md

View workflow job for this annotation

GitHub Actions / conventions

debt-length

Technical debt entry over 1000 characters: state the gap first (what differs from the console, what is unverified, what throws) and keep only the behaviour it needs: 3912 characters
- [fcntl on a guest socket](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) answers `F_DUPFD`, `F_DUP2FD`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC`, `F_GETFD`, `F_SETFD`, `F_GETFL` and `F_SETFL` and fails every other command with `EINVAL`; FreeBSD 11.4's `kern_fcntl` also handles `F_GETOWN` and `F_SETOWN` on a socket. `FD_CLOEXEC` is only recorded (there is no `exec`): `dup`, `dup2`, `F_DUPFD`, `F_DUP2FD` onto another descriptor and an accepted socket start without it, `F_DUP2FD` onto the same descriptor keeps it, and `SOCK_CLOEXEC`, `F_DUPFD_CLOEXEC`, `F_DUP2FD_CLOEXEC` and `F_SETFD` set it. None of this was measured 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, `FIOCLEX` and `FIONCLEX` set and clear the close-on-exec flag `F_GETFD` reports, 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 well-formed request throws. An input request with length 0 is not rejected as malformed: [`sys_ioctl`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/sys/kern/sys_generic.c#L703-L718) rejects it only in a kernel built without `COMPAT_FREEBSD4`, `COMPAT_FREEBSD5` and `COMPAT_43`, and which of these the PS5 kernel has is unknown. Stremio Plus (PPSA74126) imports `_ioctl` for its statically linked `tc*` functions, which send `TIOCGETA`, `TIOCSETA`, `TIOCSPGRP` and the other termios requests
- [ioctl, _ioctl](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) do not pass a `sceNetSocket` socket to libSceNet as `fcntl` does. On Linux its number duplicates the host socket, so `FIONBIO` switches the host `O_NONBLOCK` but not the library's `SO_NBIO` state; on Windows it is a `NUL` placeholder, so `FIONREAD` and enabling `FIONBIO` throw as on a character device. `sceNetIoctl` handles both requests on such a socket.
Expand Down
Loading