fix(libkernel): follow pollscan for a pipe whose other end is closed

FreeBSD's pollscan clears POLLOUT whenever POLLHUP is set, so a write end
whose read end is closed reports POLLHUP alone, with POLLIN and POLLRDNORM
when they were asked for; both host paths reported POLLOUT | POLLHUP.

POLLOUT on a read end now throws. FreeBSD opens both ends FREAD | FWRITE
and pipe_poll reports the read end writable, which a one-way host pipe
cannot back.

guest_poll drained 1 KiB from the full pipe before expecting POLLOUT, but
Linux reports it only once a whole 4 KiB page is free, so the test aborted
there. It now drains the pipe, which is writable on every host.
This commit is contained in:
Dean Galvin committed 2026-10-13 13:37:28 +13:00
1 parent 7250306b77
commit 64ca6e3329
3 files changed
+30 -13

No files matched your search

+15 -8
View File
@@ -300,6 +300,9 @@ constexpr short GuestPollReadable = 0x1 | 0x40;
constexpr short GuestPollWritable = 0x4;
constexpr short GuestPollHangUp = 0x10;
constexpr short GuestPollError = 0x8;
[[noreturn]] void ThrowReadEndWritable(int descriptor) {
throw std::runtime_error("poll: POLLOUT on the read end of pipe descriptor " + std::to_string(descriptor) + " is not supported");
}
#ifdef _WIN32
struct PipeIoStatus {
void* status;
@@ -334,19 +337,24 @@ bool PipeReadiness(int descriptor, short events, short& revents) {
if (query(handle, &status, &access, sizeof(access), FileAccessInformationClass) < 0 ||
query(handle, &status, &pipe, sizeof(pipe), FilePipeLocalInformationClass) < 0)
throw std::runtime_error("poll: querying pipe descriptor " + std::to_string(descriptor) + " failed");
if ((events & GuestPollWritable) && !(access & FILE_WRITE_DATA)) ThrowReadEndWritable(descriptor);
const bool closed = pipe.state == PipeClosingState;
revents = 0;
if ((access & FILE_READ_DATA) && pipe.readDataAvailable > 0)
revents = static_cast<short>(revents | (events & GuestPollReadable));
if ((access & FILE_WRITE_DATA) && (closed || pipe.writeQuotaAvailable >= PipeAtomicBytes))
if ((access & FILE_WRITE_DATA) && !closed && pipe.writeQuotaAvailable >= PipeAtomicBytes)
revents = static_cast<short>(revents | (events & GuestPollWritable));
if (closed) revents = static_cast<short>(revents | (events & GuestPollReadable) | GuestPollHangUp);
return true;
}
#else
bool IsPipe(int descriptor) {
bool IsPipe(int descriptor, short events) {
struct stat status{};
return ::fstat(descriptor, &status) == 0 && S_ISFIFO(status.st_mode);
if (::fstat(descriptor, &status) != 0 || !S_ISFIFO(status.st_mode)) return false;
const int flags = ::fcntl(descriptor, F_GETFL);
if (flags < 0) throw std::runtime_error("poll: querying pipe descriptor " + std::to_string(descriptor) + " failed");
if ((events & GuestPollWritable) && (flags & O_ACCMODE) == O_RDONLY) ThrowReadEndWritable(descriptor);
return true;
}
#endif
}
@@ -1016,7 +1024,7 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c
short revents = 0;
if (!PipeReadiness(entry.descriptor, entry.events, revents))
#else
if (!IsPipe(entry.descriptor))
if (!IsPipe(entry.descriptor, entry.events))
#endif
throw std::runtime_error("poll: descriptor " + std::to_string(entry.descriptor) + " is neither a socket nor a pipe");
pipes.push_back(i);
@@ -1063,10 +1071,9 @@ int APS5_VABI poll_nid_postfix(GuestPollDescriptor* descriptors, std::uint32_t c
entry.revents = static_cast<short>(entry.revents | flag.guest);
for (const auto& flag : StatusFlags)
if (native[i].revents & flag.native) entry.revents = static_cast<short>(entry.revents | flag.guest);
if (pipe && (entry.revents & GuestPollError)) {
entry.revents = static_cast<short>((entry.revents & ~GuestPollError) | (entry.events & GuestPollWritable) | GuestPollHangUp);
}
if (pipe && (entry.revents & GuestPollHangUp)) entry.revents = static_cast<short>(entry.revents | (entry.events & GuestPollReadable));
if (pipe && (entry.revents & GuestPollError)) entry.revents = static_cast<short>((entry.revents & ~GuestPollError) | GuestPollHangUp);
if (pipe && (entry.revents & GuestPollHangUp))
entry.revents = static_cast<short>((entry.revents & ~GuestPollWritable) | (entry.events & GuestPollReadable));
if (entry.revents) ++ready;
};
#ifdef _WIN32
+14 -4
View File
@@ -1,8 +1,10 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <stdexcept>
#include <thread>
struct PollDescriptor {
int descriptor;
@@ -45,7 +47,9 @@ static void TestPipes(int socket) {
PollDescriptor writer{ends[1], In, -1};
Require(poll_nid_postfix(&writer, 1, 0) == 0 && writer.revents == 0);
PollDescriptor readerOut{ends[0], Out, -1};
Require(poll_nid_postfix(&readerOut, 1, 0) == 0 && readerOut.revents == 0);
bool threw = false;
try { poll_nid_postfix(&readerOut, 1, 0); } catch (const std::runtime_error&) { threw = true; }
Require(threw);
PollDescriptor mixed[2]{{socket, In, -1}, {ends[0], In, -1}};
Require(poll_nid_postfix(mixed, 2, 1000) == 1 && mixed[0].revents == 0 && mixed[1].revents == In);
char buffer[8]{};
@@ -78,7 +82,7 @@ static void TestPipes(int socket) {
Require(close_nid_postfix(ends[0]) == 0);
int full[2];
Require(pipe_nid_postfix(full) == 0);
static char block[1024];
static char block[4096];
std::int64_t queued = 0;
PollDescriptor fill{full[1], Out, -1};
while (poll_nid_postfix(&fill, 1, 0) == 1) {
@@ -87,7 +91,11 @@ static void TestPipes(int socket) {
queued += 256;
}
Require(queued > 0 && fill.revents == 0);
Require(read_nid_postfix(full[0], block, sizeof(block)) == sizeof(block));
while (queued > 0) {
const std::int64_t taken = read_nid_postfix(full[0], block, static_cast<std::uint64_t>(std::min<std::int64_t>(queued, sizeof(block))));
Require(taken > 0);
queued -= taken;
}
Require(poll_nid_postfix(&fill, 1, 0) == 1 && fill.revents == Out);
Require(close_nid_postfix(full[0]) == 0);
Require(close_nid_postfix(full[1]) == 0);
@@ -95,7 +103,9 @@ static void TestPipes(int socket) {
Require(pipe_nid_postfix(other) == 0);
Require(close_nid_postfix(other[0]) == 0);
PollDescriptor orphan{other[1], Out, -1};
Require(poll_nid_postfix(&orphan, 1, 0) == 1 && orphan.revents == (Out | Hup));
Require(poll_nid_postfix(&orphan, 1, 0) == 1 && orphan.revents == Hup);
orphan.events = In | Out;
Require(poll_nid_postfix(&orphan, 1, 0) == 1 && orphan.revents == (In | Hup));
Require(close_nid_postfix(other[1]) == 0);
}
int main() {
+1 -1
View File
@@ -135,7 +135,7 @@
- [UDP recv/recvfrom](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) and [sceNetRecv/sceNetRecvfrom](../../../core/libs/prx/libSceNet/Export.cpp) translate Windows WSAEMSGSIZE into the copied byte count for datagram sockets, including MSG_PEEK, and retain the sender address. This follows FreeBSD 11.4 recv(2) and Winsock's documented truncated-datagram behavior; console behavior and existing title impact are unverified.
- [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. Guest socket descriptors start at `0x10000000` and are allocated upward, so the descriptor range selects the guest socket table or the host descriptor table. `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
- [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. 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 `POLLIN` on the read end and `POLLOUT` on the write end), and the write end is writable when `PIPE_BUF` (512) bytes are free on Windows, or when the host kernel says so on Linux. 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 below the socket range reports `POLLNVAL`, and any other descriptor there (files, stdio) 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 or pipe to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked
- [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. 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 below the socket range reports `POLLNVAL`, and any other descriptor there (files, stdio) 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 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
- [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
- [sigtimedwait](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) throws: guest signals are delivered when raised and never left pending, so there is nothing to wait for or take. PPSA85300 imports it and fails to load without it. Its only call site is VLC's `vlc_writev` ([filesystem.c](https://github.com/videolan/vlc/blob/6de05adcbaf2e8b85fe86aad4169393098628119/src/posix/filesystem.c#L243-L278)), which polls with a zero timeout to drain a `SIGPIPE` only after `writev` fails with `EPIPE`