diff --git a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp index 95ede1905..eee2a8077 100644 --- a/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp +++ b/core/libs/prx/libkernel/Equeue/src/FileEvent.cpp @@ -18,6 +18,7 @@ #include #include "prx/libkernel/File/include/DirectoryDescriptor.hpp" #else +#include #include #include #include @@ -331,18 +332,22 @@ int OpenHostWatch(int descriptor, std::uint32_t notes, HostWatch& host) { struct HostWatch { int notify = -1; int target = -1; + int entries = -1; bool directory = false; struct stat last {}; }; +struct HostEvent { + std::uint32_t mask; + std::uint32_t cookie; +}; + void CloseHostWatch(HostWatch& host) { - if (host.notify >= 0) { - ::close(host.notify); - host.notify = -1; - } - if (host.target >= 0) { - ::close(host.target); - host.target = -1; + for (int* descriptor : {&host.notify, &host.target, &host.entries}) { + if (*descriptor >= 0) { + ::close(*descriptor); + *descriptor = -1; + } } } @@ -376,9 +381,73 @@ std::uint32_t ModifyHint(HostWatch& host) { return hint; } +int ReadHostEvents(const HostWatch& host, std::vector& events) { + alignas(inotify_event) char buffer[16384]; + for (;;) { + const auto length = ::read(host.notify, buffer, sizeof(buffer)); + if (length < 0) { + if (errno == EINTR) { + continue; + } + return errno == EAGAIN ? 0 : errno; + } + if (length == 0) { + return 0; + } + for (char* cursor = buffer; cursor < buffer + length;) { + const auto* event = reinterpret_cast(cursor); + events.push_back({event->mask, event->cookie}); + cursor += sizeof(inotify_event) + event->len; + } + } +} + +std::unordered_map CountMoves(const std::vector& events) { + std::unordered_map moves; + for (const auto& event : events) { + if ((event.mask & (IN_MOVED_FROM | IN_MOVED_TO)) != 0) { + ++moves[event.cookie]; + } + } + return moves; +} + +bool HasUnpairedMove(const std::vector& events, std::size_t first) { + const auto moves = CountMoves(events); + for (std::size_t index = first; index < events.size(); ++index) { + if ((events[index].mask & IN_MOVED_FROM) != 0 && moves.at(events[index].cookie) == 1) { + return true; + } + } + return false; +} + +int WaitForRenamesInProgress(const HostWatch& host) { + char entries[1024]; + if (::getdents64(host.entries, entries, sizeof(entries)) < 0 && errno != ENOENT) { + return errno; + } + return 0; +} + +int ReadSettledHostEvents(const HostWatch& host, std::vector& events) { + if (const int error = ReadHostEvents(host, events)) { + return error; + } + for (std::size_t settled = 0; HasUnpairedMove(events, settled);) { + settled = events.size(); + if (const int error = WaitForRenamesInProgress(host)) { + return error; + } + if (const int error = ReadHostEvents(host, events)) { + return error; + } + } + return 0; +} + void RunHostWatch(std::shared_ptr watch, HostWatch host) { pollfd waits[2] = {{host.notify, POLLIN, 0}, {watch->StopEvent(), POLLIN, 0}}; - alignas(inotify_event) char buffer[16384]; bool watching = true; while (watching) { if (::poll(waits, 2, -1) < 0) { @@ -391,48 +460,36 @@ void RunHostWatch(std::shared_ptr watch, HostWatch host) { if (waits[1].revents != 0) { break; } - const auto length = ::read(host.notify, buffer, sizeof(buffer)); - if (length < 0) { - if (errno == EINTR || errno == EAGAIN) { - continue; - } + std::vector events; + if (const int error = ReadSettledHostEvents(host, events)) { CloseHostWatch(host); - throw std::runtime_error("EVFILT_VNODE: reading inotify failed, errno=" + std::to_string(errno)); - } - std::unordered_map moves; - for (char* cursor = buffer; cursor < buffer + length;) { - const auto* event = reinterpret_cast(cursor); - if ((event->mask & (IN_MOVED_FROM | IN_MOVED_TO)) != 0) { - ++moves[event->cookie]; - } - cursor += sizeof(inotify_event) + event->len; + throw std::runtime_error("EVFILT_VNODE: reading inotify failed, errno=" + std::to_string(error)); } + const auto moves = CountMoves(events); std::vector hints; - for (char* cursor = buffer; cursor < buffer + length;) { - const auto* event = reinterpret_cast(cursor); - const std::uint32_t link = (event->mask & IN_ISDIR) != 0 ? NOTE_LINK : 0; - if ((event->mask & IN_MODIFY) != 0) { + for (const auto& event : events) { + const std::uint32_t link = (event.mask & IN_ISDIR) != 0 ? NOTE_LINK : 0; + if ((event.mask & IN_MODIFY) != 0) { hints.push_back(ModifyHint(host)); } - if ((event->mask & IN_ATTRIB) != 0) { + if ((event.mask & IN_ATTRIB) != 0) { hints.push_back(AttributeHint(host)); } - if ((event->mask & (IN_CREATE | IN_DELETE)) != 0) { + if ((event.mask & (IN_CREATE | IN_DELETE)) != 0) { hints.push_back(NOTE_WRITE | link); } - if ((event->mask & (IN_MOVED_FROM | IN_MOVED_TO)) != 0) { - hints.push_back(moves[event->cookie] > 1 ? NOTE_WRITE : NOTE_WRITE | NOTE_EXTEND | link); + if ((event.mask & (IN_MOVED_FROM | IN_MOVED_TO)) != 0) { + hints.push_back(moves.at(event.cookie) > 1 ? NOTE_WRITE : NOTE_WRITE | NOTE_EXTEND | link); } - if ((event->mask & IN_MOVE_SELF) != 0) { + if ((event.mask & IN_MOVE_SELF) != 0) { hints.push_back(NOTE_RENAME); } - if ((event->mask & IN_DELETE_SELF) != 0) { + if ((event.mask & IN_DELETE_SELF) != 0) { hints.push_back(NOTE_DELETE); } - if ((event->mask & IN_IGNORED) != 0) { + if ((event.mask & IN_IGNORED) != 0) { watching = false; } - cursor += sizeof(inotify_event) + event->len; } for (const auto hint : hints) { watch->Deliver(hint); @@ -460,6 +517,14 @@ int OpenHostWatch(int descriptor, std::uint32_t, HostWatch& host) { CloseHostWatch(host); throw std::runtime_error("EVFILT_VNODE: opening the watched file failed, errno=" + std::to_string(error)); } + if (host.directory) { + host.entries = ::open(path.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC); + if (host.entries < 0) { + const int error = errno; + CloseHostWatch(host); + throw std::runtime_error("EVFILT_VNODE: opening the watched directory failed, errno=" + std::to_string(error)); + } + } host.notify = ::inotify_init1(IN_CLOEXEC | IN_NONBLOCK); std::uint32_t mask = IN_MODIFY | IN_ATTRIB | IN_MOVE_SELF | IN_DELETE_SELF; if (host.directory) { diff --git a/core/libs/tests/GuestKqueue.cpp b/core/libs/tests/GuestKqueue.cpp index 0fb59ed0e..78c5bd542 100644 --- a/core/libs/tests/GuestKqueue.cpp +++ b/core/libs/tests/GuestKqueue.cpp @@ -330,6 +330,45 @@ static void RenameAndDeleteWatchedFile() { Require(close_nid_postfix(file) == 0); Require(close_nid_postfix(kq) == 0); } + +static std::uint32_t NotesThrough(int kq, std::uintptr_t ident, std::uint32_t last) { + std::uint32_t notes = 0; + KernelEvent events[4]{}; + while ((notes & last) == 0) { + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident); + notes |= events[0].fflags; + } + return notes; +} + +static void MoveDirectoryEntries() { + const auto path = TemporaryName("moves"); + const auto outside = path + "-outside"; + Require(mkdir_nid_postfix(path.c_str(), 0755) == 0 && mkdir_nid_postfix(outside.c_str(), 0755) == 0); + const int child = open_nid_postfix((path + "/first.bin").c_str(), ReadWrite | Create, 0644); + Require(child >= 0 && close_nid_postfix(child) == 0); + const int directory = open_nid_postfix(path.c_str(), Directory, 0); + Require(directory >= 0); + const int kq = kqueue_nid_postfix(); + KernelEvent events[4]{}; + const auto ident = static_cast(directory); + Require(Apply(kq, Change(ident, EvfiltVnode, EvAdd | EvClear, NoteWrite | NoteExtend | NoteLink)) == 0); + for (int i = 0; i < 64; ++i) { + Require(sceKernelRename((path + "/first.bin").c_str(), (path + "/second.bin").c_str()) == SceOk); + Require(sceKernelRename((path + "/second.bin").c_str(), (path + "/first.bin").c_str()) == SceOk); + } + Require(mkdir_nid_postfix((path + "/sub").c_str(), 0755) == 0); + Require(NotesThrough(kq, ident, NoteLink) == (NoteWrite | NoteLink)); + Require(sceKernelRename((path + "/first.bin").c_str(), (outside + "/first.bin").c_str()) == SceOk); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == (NoteWrite | NoteExtend)); + Require(sceKernelRename((outside + "/first.bin").c_str(), (path + "/first.bin").c_str()) == SceOk); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == (NoteWrite | NoteExtend)); + Require(sceKernelRename((path + "/sub").c_str(), (outside + "/sub").c_str()) == SceOk); + Require(Wait(kq, events, 4) == 1 && events[0].ident == ident && events[0].fflags == (NoteWrite | NoteExtend | NoteLink)); + Require(close_nid_postfix(directory) == 0); + Require(close_nid_postfix(kq) == 0); + Require(std::filesystem::remove_all(path) == 2 && std::filesystem::remove_all(outside) == 2); +} #endif static void DirectoryWatch() { @@ -381,6 +420,7 @@ int main() { FileWatch(); #ifndef _WIN32 RenameAndDeleteWatchedFile(); + MoveDirectoryEntries(); #endif DirectoryWatch(); Timeout(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index b4dec2907..3f13c7b98 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -94,7 +94,7 @@ - [JIT shared memory](../../../core/libs/prx/libkernel/DirectMemory/Jit.cpp) (libkernel) - `sceKernelJitCreateSharedMemory`, `sceKernelJitCreateAliasOfSharedMemory` and `sceKernelJitMapSharedMemory` take the signatures of [Orbital](https://github.com/AlexAltea/orbital)'s `tools/ida/db_types.json` (name, length, maximum protection, descriptor out; descriptor, maximum protection, descriptor out; descriptor, protection, address in and out), which ps4sdk, ps5-jar-loader and the call sites in RetroArch PPSA99169, XPSemu PPSA97358 and VLC PPSA85300 match; SharpProspero declares the alias as returning the descriptor. The object is a host anonymous file shared by its descriptors (a `memfd` on Linux, a delete-on-close temporary file mapped as an executable section on Windows), the alias is a duplicate descriptor, and every map maps the whole object, so writes through one view show through the others; a non-null address is a hint, as in `mmap` without `MAP_FIXED`. Not measured on the console: a length that is zero or not a multiple of 16 KiB and protection bits other than CPU read, write and execute are `EINVAL`; an alias or a map asking for more than the descriptor's maximum protection (write and execute counting as read) is `EACCES`, as FreeBSD 11's `vm_map_protect` treats a protection past `max_protection`; a descriptor that is not a JIT object is `EBADF`. The name is ignored, the objects do not count against the flexible memory budget, `mprotect` of a view is not limited to the maximum protection, and `mmap` of the descriptors is unsupported, as for `shm_open`. On Linux the `memfd` is created without `MFD_EXEC`, which Linux 6.3 added and older kernels reject with `EINVAL`, and relies on the protection of the mapping: the flag only sets the file's execute permission bits, and the direct memory `memfd` is mapped executable the same way; a host with `vm.memfd_noexec` set to 2 refuses the creation and the call throws - [kqueue and kevent](../../../core/libs/prx/libkernel/Equeue/src/Kqueue.cpp) (libkernel) follow FreeBSD 11's [`kern_event.c`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/kern_event.c) (`kqueue_kevent`, `kqueue_register`, `kqueue_scan`, the timer and user filters) over the event queue; nothing is measured on a console. The descriptor is a host null device registered as the kqueue: `close` and `sceKernelClose` close the queue and `dup`/`dup2` of it throw, but `read`, `write` and `fstat` act on the null device instead of failing with `EOPNOTSUPP` or reporting `S_IFIFO`. Read and write events are supported on guest sockets only, plus the timer, user and [vnode](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) filters; other filters, `EV_DISABLE`, `EV_DISPATCH`, `EV_FORCEONESHOT`, `NOTE_LOWAT`, `EV_CLEAR` on a socket, `EVFILT_READ` on another kqueue, a `NOTE_NSECONDS` period above one second with a fraction (11.0 converts the fraction with the microsecond macro), any change to an existing timer (11.0 keeps the armed expiry and applies the new period after it, 11.4's `filt_timertouch` restarts it) and a negative `nevents` throw. A new user event keeps its `fflags` unmasked and is not triggered by `NOTE_TRIGGER`, because `filt_userattach` reads the already cleared `kn_fflags`. The timer limit `kq_calloutmax` (`ENOMEM`) is not enforced, and closing a kqueue wakes a thread waiting on it in `kevent` with `EBADF`, where `kqueue_drain` makes the close wait for that thread. A level-triggered event is reported once per call and moved behind the other events, as in `kqueue_scan`; the `sceKernel*Equeue` calls keep their own collection loop -- [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title +- [EVFILT_VNODE, sceKernelAddFileEvent and sceKernelDeleteFileEvent](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) watch the host file or directory behind the descriptor and turn host changes into FreeBSD 11 vnode hints, which [`filt_vfsvnode`](https://github.com/freebsd/freebsd-src/blob/34bd34750187bae02a0086402c78c031c59dd8b3/sys/kern/vfs_subr.c#L5032) adds whole when they share a bit with the requested `fflags`: a write that grows a file is `NOTE_WRITE | NOTE_EXTEND`, another write `NOTE_WRITE`, a shrinking truncate or an attribute change `NOTE_ATTRIB`, a higher link count `NOTE_LINK`, a lower one or a pending delete `NOTE_DELETE`, a new path `NOTE_RENAME`; in a directory, an entry created, removed or renamed is `NOTE_WRITE`, with `NOTE_LINK` for a subdirectory, and on Linux an entry moved from or to another directory adds `NOTE_EXTEND`; inotify queues the two halves of a rename one after the other, so a watcher woken by the first can read it alone, and a half read without the other is classified only after a `getdents64` on the watched directory, which takes the directory lock that `rename` holds until both halves are queued ([`do_renameat2`](https://github.com/torvalds/linux/blob/v6.8/fs/namei.c#L4975) around `vfs_rename` and its `fsnotify_move`, [`iterate_dir`](https://github.com/torvalds/linux/blob/v6.8/fs/readdir.c#L103)), an ordering taken from the kernel source, not from inotify(7), which promises none; a directory registered with `NOTE_EXTEND` on Windows is accepted, as `vfs_kqfilter` does not check it, but never receives it, because the directory handle cannot tell a move between directories from a removal plus an addition. FreeBSD's hints depend on the filesystem and the console's filesystems are not known: a growing `ftruncate` reports `NOTE_WRITE | NOTE_EXTEND` where `vop_setattr_post` reports `NOTE_ATTRIB`, and on Windows `utimes` on a file reports `NOTE_WRITE`. Windows uses `FindFirstChangeNotification` on the parent directory (and on a watched directory itself) and compares `GetFileInformationByHandleEx` snapshots at least every 50 ms, because a write through an open handle is reported only when the handle is flushed or closed, so file hints can arrive 50 ms late; Linux uses inotify on the file and is not run. `NOTE_REVOKE`, `NOTE_OPEN`, `NOTE_CLOSE`, `NOTE_CLOSE_WRITE`, `NOTE_READ`, unknown bits and descriptors other than files, directories, pipes and sockets throw. `sceKernelAddFileEvent(eq, fd, watch, udata)` and `sceKernelDeleteFileEvent(eq, fd)` take the [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/0a1aaf9d/include/orbis/libkernel.h#L35) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0/src/SharpProspero/Interop/Kernel/KernelEqueue.cs#L164) signatures; `watch` is taken as the `fflags` and the event is added with `EV_CLEAR`, neither confirmed on a title - [sem_timedwait](../../../core/libs/prx/libkernel/Pthread/Posix/Sem.cpp) (libkernel) consumes an available count before checking the timeout and waits indefinitely for a null timeout, following FreeBSD 11's `sem_timedwait`. Console behavior for null and invalid timeouts when a count is available is unverified. - [tcsendbreak, tcdrain, tcflush, tcflow, tcgetpgrp, tcsetpgrp](../../../core/libs/prx/libkernel/File/src/Terminal.cpp) (libkernel) follow FreeBSD 11.4's [`termios.c`](https://github.com/freebsd/freebsd-src/blob/8c639945a44588f6cfdddfb057bb5aad88735da0/lib/libc/gen/termios.c): `tcflush` answers `EINVAL` for a queue other than `TCIFLUSH`, `TCOFLUSH` or `TCIOFLUSH` and `tcflow` for an action outside `TCOOFF`..`TCION` before the descriptor is looked at, and every other call fails as its terminal ioctl does on a descriptor that is not a terminal, with the errors `tcgetattr` gives, so `tcsendbreak` never reaches its 0.4 s pause. FreeBSD's `pipe_ioctl` answers `TIOCGPGRP` and `TIOCSPGRP` on a pipe or FIFO with the descriptor's SIGIO owner, which is not modelled, so `tcgetpgrp` and `tcsetpgrp` throw on a host pipe or FIFO above descriptor 2; descriptors 0 to 2 are taken as non-terminals that are not pipes, whatever the host attaches to them, so a pipe the guest creates on a freed descriptor 0 to 2 gets `ENOTTY` there. The PS5 `libkernel` is assumed to use this FreeBSD code, and `tcdrain` is not a cancellation point here - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes` and `futimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond