From 5db1d8d5e933c5096ed87530898c2989becec4d7 Mon Sep 17 00:00:00 2001 From: screamm Date: Mon, 12 Oct 2026 14:48:12 +0200 Subject: [PATCH] fix(libkernel): accept POLLPRI on guest sockets under WSAPoll Three catalog titles stop on Windows when they poll a guest socket for POLLPRI|POLLOUT to see whether a non-blocking connect has completed: - PPSA97510 PSXS5 (socket 0x10000002, thread gamedb, 13 s): what(): poll: POLLPRI is not supported by WSAPoll - PPSA98510 SwanStation (sockets 0x10000003/0x10000002, threads gamedb and update): what(): poll: POLLPRI is not supported by WSAPoll - PPSA99003 ProsperoTV ({0x10000001, POLLPRI|POLLOUT} at 5.3 s): what(): poll: POLLPRI is not supported by WSAPoll FreeBSD's sopoll_generic (sys/kern/uipc_socket.c, freebsd-src 8c639945) reports POLLPRI and POLLRDBAND alike, only while out-of-band data is pending (so_oobmark or SBS_RCVATMARK). WSAPoll rejects POLLPRI in events with WSAEINVAL but reports pending urgent data as POLLRDBAND, so on Windows the guest POLLPRI is now asked of WSAPoll as POLLRDBAND and POLLIN as POLLRDNORM (Winsock's POLLIN includes POLLRDBAND, which would report urgent data as readable). Measured on this host: WSAPoll reports POLLRDBAND after a peer's send(MSG_OOB), waits for it, and clears it once the byte is read with MSG_OOB. Linux keeps polling POLLPRI on the host. select needs no change: libkernel's select passes exceptfds as Urgent to the libSceNet poller, which already asks WSAPoll for POLLRDBAND, and it does not take libkernel guest sockets on either host. The guest send and recv refuse MSG_OOB, so urgent data only comes from a peer outside the guest's sockets; guest_poll now sends it from a host socket and checks that a connected pair and a non-blocking connect report POLLOUT and not POLLPRI. TechnicalDebt records where Winsock's answer differs from FreeBSD's once the stream passes the mark. Depends on #3649 (poll on file descriptors), whose two commits this branch carries, since the titles reach this stop only with it. Co-Authored-By: Claude Fable 5.1 --- core/libs/prx/libkernel/CMakeLists.txt | 1 + core/libs/prx/libkernel/Socket/src/Udp.cpp | 12 ++-- core/libs/tests/GuestPoll.cpp | 68 ++++++++++++++++++++++ docs/dev/debt/libkernel.md | 3 +- 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 2fcf255d4..955b228c7 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -1001,6 +1001,7 @@ if(BUILD_TESTING) add_test(NAME guest_poll COMMAND guest_poll_tests) set_tests_properties(guest_poll PROPERTIES TIMEOUT 20) if(WIN32) + target_link_libraries(guest_poll_tests PRIVATE ws2_32) set_tests_properties(guest_poll PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index a012fe196..7a0304ce1 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -393,8 +393,15 @@ struct PollFlag { constexpr short GuestPollPriority = 0x2; constexpr short GuestPollInvalid = 0x20; constexpr short GuestPollAccepted = 0x1 | 0x2 | 0x4 | 0x8 | 0x10 | 0x20 | 0x40 | 0x80 | 0x100; +#ifdef _WIN32 +constexpr short NativePollIn = POLLRDNORM; +constexpr short NativePollPriority = POLLRDBAND; +#else +constexpr short NativePollIn = POLLIN; +constexpr short NativePollPriority = POLLPRI; +#endif constexpr PollFlag RequestFlags[] = { - {0x1, POLLIN}, {0x2, POLLPRI}, {0x4, POLLOUT}, {0x40, POLLRDNORM}, {0x80, POLLRDBAND}, {0x100, POLLWRBAND}}; + {0x1, NativePollIn}, {GuestPollPriority, NativePollPriority}, {0x4, POLLOUT}, {0x40, POLLRDNORM}, {0x80, POLLRDBAND}, {0x100, POLLWRBAND}}; constexpr PollFlag StatusFlags[] = {{0x8, POLLERR}, {0x10, POLLHUP}, {GuestPollInvalid, POLLNVAL}}; std::shared_ptr Find(int descriptor) { std::lock_guard lock(socketsMutex); @@ -1199,9 +1206,6 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c pipes.push_back(i); continue; } -#ifdef _WIN32 - if (entry.events & GuestPollPriority) throw std::runtime_error("poll: POLLPRI is not supported by WSAPoll"); -#endif NativePollDescriptor request{}; request.fd = socket->value; for (const auto& flag : RequestFlags) diff --git a/core/libs/tests/GuestPoll.cpp b/core/libs/tests/GuestPoll.cpp index 446ff33dd..37171623d 100644 --- a/core/libs/tests/GuestPoll.cpp +++ b/core/libs/tests/GuestPoll.cpp @@ -1,9 +1,18 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#include +#include +#endif #include "prx/libc/include/general/VabiMacros.hpp" #include #include #include #include #include +#include #include #include struct PollDescriptor { @@ -26,10 +35,22 @@ int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int); int APS5_VABI pipe_nid_postfix(int*); 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 fcntl_nid_postfix(int, int, ...); int* APS5_VABI __error_nid_postfix(); } +#ifdef _WIN32 +using NativeSocket = SOCKET; +constexpr NativeSocket NativeInvalid = INVALID_SOCKET; +static void CloseNative(NativeSocket socket) { closesocket(socket); } +#else +using NativeSocket = int; +constexpr NativeSocket NativeInvalid = -1; +static void CloseNative(NativeSocket socket) { close(socket); } +#endif static void Require(bool value) { if (!value) std::abort(); } constexpr short In = 0x1; +constexpr short Priority = 0x2; +constexpr short ReadBand = 0x80; constexpr short Out = 0x4; constexpr short Hup = 0x10; constexpr short Invalid = 0x20; @@ -109,7 +130,54 @@ static void TestPipes(int socket) { Require(poll_nid_postfix(&orphan, 1, 0) == 1 && orphan.revents == (In | Hup)); Require(close_nid_postfix(other[1]) == 0); } +static void PollsUrgentData() { + const int listener = socket_nid_postfix(2, 1, 0); + Require(listener >= 0); + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + Require(bind_nid_postfix(listener, address.data(), address.size()) == 0); + std::uint32_t size = address.size(); + Require(getsockname_nid_postfix(listener, address.data(), &size) == 0); + Require(listen_nid_postfix(listener, 4) == 0); + const int client = socket_nid_postfix(2, 1, 0); + Require(client >= 0 && fcntl_nid_postfix(client, 4, 4) == 0); + if (connect_nid_postfix(client, address.data(), address.size()) != 0) + Require(*__error_nid_postfix() == 35 || *__error_nid_postfix() == 36); + PollDescriptor connecting{client, Priority | Out, -1}; + Require(poll_nid_postfix(&connecting, 1, 1000) == 1 && connecting.revents == Out); + const int server = accept_nid_postfix(listener, nullptr, nullptr); + Require(server >= 0); + PollDescriptor pair[2]{{server, Priority | Out, -1}, {client, In | Priority | Out | ReadBand, -1}}; + Require(poll_nid_postfix(pair, 2, 0) == 2 && pair[0].revents == Out && pair[1].revents == Out); + char byte = 'u'; + Require(send_nid_postfix(client, &byte, 1, 1) == -1 && *__error_nid_postfix() == 45); + Require(recv_nid_postfix(server, &byte, 1, 1) == -1 && *__error_nid_postfix() == 45); + const NativeSocket peer = ::socket(AF_INET, SOCK_STREAM, 0); + Require(peer != NativeInvalid); + sockaddr_in native{}; + native.sin_family = AF_INET; + std::memcpy(&native.sin_port, address.data() + 2, 2); + std::memcpy(&native.sin_addr, address.data() + 4, 4); + Require(::connect(peer, reinterpret_cast(&native), sizeof(native)) == 0); + const int urgent = accept_nid_postfix(listener, nullptr, nullptr); + Require(urgent >= 0); + PollDescriptor waiting{urgent, Priority, -1}; + Require(poll_nid_postfix(&waiting, 1, 0) == 0 && waiting.revents == 0); + Require(::send(peer, "u", 1, MSG_OOB) == 1); + Require(poll_nid_postfix(&waiting, 1, 1000) == 1 && waiting.revents == Priority); + PollDescriptor bands{urgent, In | Priority | Out, -1}; + Require(poll_nid_postfix(&bands, 1, 0) == 1 && bands.revents == (Priority | Out)); +#ifdef _WIN32 + PollDescriptor band{urgent, ReadBand, -1}; + Require(poll_nid_postfix(&band, 1, 0) == 1 && band.revents == ReadBand); +#endif + CloseNative(peer); + Require(close_nid_postfix(urgent) == 0); + Require(close_nid_postfix(server) == 0); + Require(close_nid_postfix(client) == 0); + Require(close_nid_postfix(listener) == 0); +} int main() { + PollsUrgentData(); const int listener = socket_nid_postfix(2, 1, 0); Require(listener >= 0 && listener < 1024); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 6e33ad17f..695559305 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -142,7 +142,8 @@ - [send, sendto](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - send and sendto without an address on an unconnected datagram socket map Windows `WSAENOTCONN` to FreeBSD's `EDESTADDRREQ` (39). - [read, write and vector I/O](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on a guest socket use `recv` and `send` without flags; `sceKernelReadv` and `sceKernelWritev` gather or scatter through one socket operation, preserving stream short reads and datagram boundaries. The positioned `pread`, `pwrite`, `preadv` and `pwritev` forms, and their `sceKernel` forms, return `ESPIPE` for an open guest socket and `EBADF` for a closed one. `EPIPE` from a write raises the host's `SIGPIPE` on Linux, which the guest can neither ignore nor suppress: `signal(SIGPIPE, SIG_IGN)` and `sigaction` record the disposition, as they do for every number from 1 to 128, but only the six signals with host equivalents (2, 4, 6, 8, 11 and 15) take effect on the host or are delivered, so the host `SIGPIPE` is not stopped; `SO_NOSIGPIPE` is not an option here, and raises nothing on Windows; a `write` longer than `INT_MAX` bytes is `EMSGSIZE` where the console sends it. AIO on a guest socket still goes to the host, which has no such descriptor - [Socket descriptor numbers](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel, libSceNet) - `socket`, `accept`, `socketpair`, `dup`, `sceNetSocket` and `sceNetAccept` take the lowest free number of the host descriptor table that files use, as FreeBSD 11.4 `fdalloc` takes the lowest free entry of the one process table (`kern_descrip.c`), so every number stays below `FD_SETSIZE` (1024) while fewer than 1024 descriptors are open. The number holds a zero-access `NUL` handle on Windows and a duplicate of the host socket on Linux, and the socket tables map it to the socket. The host table also holds the runtime's own files, so the numbers differ from the console's; the first number `socket` and `sceNetSocket` return on a console and its `getdtablesize` are not measured. Paths that bypass the socket tables see the placeholder: libc `fdopen` on a socket opens it, libkernel `fstat` on a socket throws, and libkernel `close` of a libSceNet socket releases the number but leaves the libSceNet socket open. Palworld (PPSA23126), The Mound (PPSA26223) and Dying Light 2 (PPSA02262) set socket bits in a 1024-bit `fd_set` -- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. An open descriptor that is neither a guest socket nor a pipe (files, stdio, libSceNet sockets) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), `POLLPRI` on Windows, which `WSAPoll` rejects, and an infinite timeout with no socket to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked- [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 +- [poll](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits on guest sockets and pipes, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. `WSAPoll` rejects `POLLPRI`, so on Windows a socket's `POLLPRI` is asked as `POLLRDBAND` and `POLLIN` as `POLLRDNORM`; Winsock reports urgent data until it is read with `MSG_OOB`, where FreeBSD 11.4's `sopoll_generic` stops once the normal stream passes the mark. Guest `send` and `recv` refuse `MSG_OOB` with `EOPNOTSUPP`, so urgent data can only come from an outside peer and Windows keeps reporting `POLLPRI` after the stream passes the mark. A pipe follows FreeBSD's `pipe_poll`: the read end is readable when data is buffered, either end reports `POLLHUP` once the other end is closed, with the requested `POLLIN` and `POLLRDNORM` bits and without `POLLOUT`, which `pollscan` clears, and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. `POLLOUT` on a read end throws: FreeBSD opens both ends for reading and writing and reports it writable, and host pipes are one-way. Windows pipes cannot be waited on, so while a pipe is polled `poll` checks it with `NtQueryInformationFile` and waits on the sockets in slices of 1 ms. A closed descriptor reports `POLLNVAL`, and an open descriptor that is neither a guest socket nor a pipe (files, stdio, libSceNet sockets) throws, as do event bits other than `POLLIN`, `POLLPRI`, `POLLOUT`, `POLLRDNORM`, `POLLRDBAND` and `POLLWRBAND` (the `POLLERR`, `POLLHUP` and `POLLNVAL` bits are ignored in `events`), an infinite timeout with no socket or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked +- [Socket error numbers](../../../core/libs/prx/libkernel/Socket/include/SocketError.hpp) (libkernel) - a host socket error becomes the FreeBSD 11 `errno.h` number of the same name; the numbers are not checked on the console. Winsock `WSAESHUTDOWN` becomes `EPIPE`, as a send after `shutdown` does on FreeBSD, and a host error with no FreeBSD socket counterpart becomes `EIO`. A nonblocking `connect` in progress reports `EINPROGRESS` on Linux but `EAGAIN` on Windows (`WSAEWOULDBLOCK`), where FreeBSD reports `EINPROGRESS`. On Windows a refused nonblocking `connect` is never reported (`SO_ERROR` is unsupported), so a repeated one keeps reporting `EALREADY` where FreeBSD reports `ECONNREFUSED`. A receive after `shutdown(SHUT_RD)` reports `EPIPE` on Windows where FreeBSD returns 0 - [connect to AF_UNSPEC](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again; one bound to port 0 is bound again to the wildcard address with the port it had - [msync](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) follows FreeBSD 11's `kern_msync` and `vm_map_sync`, not measured on the console: the address is rounded down to the page, a range that wraps is `EINVAL`, as is `MS_ASYNC` with `MS_INVALIDATE`, and a range whose start or any page is not mapped (as `sceKernelVirtualQuery` reports it, reservations included) is `ENOMEM`; a page unmapped from inside a larger mapping still counts as mapped there, since `sceKernelVirtualQuery` keeps reporting the whole mapping. The guest has no file mappings, so a successful call writes nothing back. `MS_INVALIDATE` on a mapped range throws: FreeBSD returns `EBUSY` when the range holds `mlock`ed pages, and locked pages are not tracked - [madvise](../../../core/libs/prx/libkernel/DirectMemory/PosixMemory.cpp) (libkernel) accepts `MADV_PROTECT` (10) as a no-op hint without modifying process protection. FreeBSD 11's `kern_madvise` (`sys/vm/vm_mmap.c`) handles it before the address check, ignores the address and length, and calls `kern_procctl(..., PROC_SPROTECT, PPROT_SET)`, which requires `PRIV_VM_MADV_PROTECT` and fails with `EPERM` without it. Returning 0 unconditionally assumes the calling process is privileged. Console behaviour is unverified