Files
AnyPS5/core/libs/tests/GuestPoll.cpp
T
Jota 8bed63610b fix(libkernel): report POLLHUP on macOS only once both directions of a stream have ended
XNU's poll reports POLLHUP as soon as either direction of a stream socket has ended, adds POLLPRI and POLLRDBAND to it and then drops POLLOUT. FreeBSD's sopoll_generic reports POLLHUP only once the receive and the send side are both shut, and Linux does the same, so on macOS a guest saw POLLIN|POLLHUP for a stream whose peer had only shut its sending side, and POLLHUP for one that had only shut its own.

When the host reports POLLHUP on a guest stream socket, ask it about each direction alone and keep POLLHUP only when both have ended, report the requested POLLOUT while the sending side is shut or writable, and leave urgent data to SIOCATMARK.
2026-10-27 06:55:16 -03:00

386 lines
20 KiB
C++

#include "prx/libkernel/include/Api.hpp"
#include "prx/libc/include/Api.hpp"
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#else
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#endif
#include "prx/libc/include/general/VabiMacros.hpp"
#include "SceTypes.hpp"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <stdexcept>
#include <string>
#include <thread>
#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;
constexpr short ReadNormal = 0x40;
constexpr short WriteBand = 0x100;
constexpr std::int16_t EvfiltUser = -11;
constexpr std::uint16_t EvAdd = 0x0001;
constexpr std::uint16_t EvClear = 0x0020;
constexpr std::uint32_t NoteTrigger = 0x01000000;
static KernelEvent UserEvent(std::uint16_t flags, std::uint32_t fflags) {
KernelEvent change{};
change.ident = 7;
change.filter = EvfiltUser;
change.flags = flags;
change.fflags = fflags;
return change;
}
static void PollsKqueues(int socket) {
const int queue = kqueue_nid_postfix();
Require(queue >= 0);
GuestPollDescriptor kq{queue, static_cast<short>(In | ReadNormal | Out | Priority | ReadBand | WriteBand), -1};
Require(poll_nid_postfix(&kq, 1, 0) == 0 && kq.revents == 0);
const auto added = UserEvent(EvAdd | EvClear, 0);
Require(kevent_nid_postfix(queue, &added, 1, nullptr, 0, nullptr) == 0);
Require(poll_nid_postfix(&kq, 1, 0) == 0 && kq.revents == 0);
const auto start = std::chrono::steady_clock::now();
Require(poll_nid_postfix(&kq, 1, 30) == 0 && kq.revents == 0);
Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(25));
std::thread trigger([queue] {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
const auto triggered = UserEvent(0, NoteTrigger);
Require(kevent_nid_postfix(queue, &triggered, 1, nullptr, 0, nullptr) == 0);
});
GuestPollDescriptor mixed[2]{{socket, In, -1}, {queue, static_cast<short>(In | Out), -1}};
Require(poll_nid_postfix(mixed, 2, -1) == 1);
trigger.join();
Require(mixed[0].revents == 0 && mixed[1].revents == In);
Require(poll_nid_postfix(&kq, 1, 0) == 1 && kq.revents == (In | ReadNormal));
KernelEvent events[1]{};
const KernelTimespec zero{0, 0};
Require(kevent_nid_postfix(queue, nullptr, 0, events, 1, &zero) == 1);
Require(poll_nid_postfix(&kq, 1, 0) == 0 && kq.revents == 0);
Require(close_nid_postfix(queue) == 0);
Require(poll_nid_postfix(&kq, 1, 0) == 1 && kq.revents == Invalid);
}
static void PollsFilesAndDirectories(int socket) {
constexpr int ReadWrite = 0x0002, Create = 0x0200, DirectoryFlag = 0x00020000;
const auto stamp = std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
const std::string filePath = "anyps5-poll-file-" + stamp;
const std::string directoryPath = "anyps5-poll-directory-" + stamp;
const int file = open_nid_postfix(filePath.c_str(), ReadWrite | Create, 0644);
Require(file >= 0);
Require(mkdir_nid_postfix(directoryPath.c_str(), 0755) == 0);
const int directory = open_nid_postfix(directoryPath.c_str(), DirectoryFlag, 0);
Require(directory >= 0);
GuestPollDescriptor vnode{file, static_cast<short>(In | Out | ReadNormal | Priority | ReadBand | WriteBand), -1};
Require(poll_nid_postfix(&vnode, 1, 0) == 1 && vnode.revents == (In | Out | ReadNormal));
vnode.events = static_cast<short>(Priority | ReadBand | WriteBand);
Require(poll_nid_postfix(&vnode, 1, 0) == 0 && vnode.revents == 0);
const auto start = std::chrono::steady_clock::now();
Require(poll_nid_postfix(&vnode, 1, 30) == 0);
Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(25));
GuestPollDescriptor folder{directory, static_cast<short>(In | Out), -1};
Require(poll_nid_postfix(&folder, 1, -1) == 1 && folder.revents == (In | Out));
GuestPollDescriptor mixed[3]{{socket, In, -1}, {file, Out, -1}, {directory, ReadNormal, -1}};
Require(poll_nid_postfix(mixed, 3, -1) == 2);
Require(mixed[0].revents == 0 && mixed[1].revents == Out && mixed[2].revents == ReadNormal);
Require(close_nid_postfix(directory) == 0 && close_nid_postfix(file) == 0);
Require(rmdir_nid_postfix(directoryPath.c_str()) == 0 && unlink_nid_postfix(filePath.c_str()) == 0);
}
static void StandardDescriptorFile(int descriptor) {
const int saved = dup_nid_postfix(descriptor);
const std::string path = "anyps5-poll-input-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count());
const int file = open_nid_postfix(path.c_str(), 0x0002 | 0x0200, 0644);
Require(file >= 0);
Require(dup2_nid_postfix(file, descriptor) == descriptor);
if (file != descriptor) Require(close_nid_postfix(file) == 0);
GuestPollDescriptor input{descriptor, static_cast<short>(In | Out | ReadNormal | Priority), -1};
const int ready = poll_nid_postfix(&input, 1, 0);
if (ready != 1) std::fprintf(stderr, "poll on a regular file at descriptor %d returned %d instead of 1\n", descriptor, ready);
Require(ready == 1 && input.revents == (In | Out | ReadNormal));
input.events = Priority;
Require(poll_nid_postfix(&input, 1, 0) == 0 && input.revents == 0);
if (saved >= 0) {
Require(dup2_nid_postfix(saved, descriptor) == descriptor);
Require(close_nid_postfix(saved) == 0);
} else Require(close_nid_postfix(descriptor) == 0);
Require(unlink_nid_postfix(path.c_str()) == 0);
}
static void TestPipes(int socket) {
int ends[2];
Require(pipe_nid_postfix(ends) == 0);
GuestPollDescriptor empty[2]{{ends[0], In, -1}, {ends[1], Out, -1}};
Require(poll_nid_postfix(empty, 2, 0) == 1 && empty[0].revents == 0 && empty[1].revents == Out);
GuestPollDescriptor reader{ends[0], In | ReadNormal, -1};
const auto start = std::chrono::steady_clock::now();
Require(poll_nid_postfix(&reader, 1, 50) == 0 && reader.revents == 0);
Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40));
const auto shortWaitStart = std::chrono::steady_clock::now();
for (int i = 0; i < 20; ++i) Require(poll_nid_postfix(&reader, 1, 2) == 0 && reader.revents == 0);
const auto shortWait = std::chrono::steady_clock::now() - shortWaitStart;
Require(shortWait >= std::chrono::milliseconds(40) && shortWait < std::chrono::milliseconds(200));
Require(write_nid_postfix(ends[1], "wake", 4) == 4);
Require(poll_nid_postfix(&reader, 1, 1000) == 1 && reader.revents == (In | ReadNormal));
GuestPollDescriptor writer{ends[1], In, -1};
Require(poll_nid_postfix(&writer, 1, 0) == 0 && writer.revents == 0);
GuestPollDescriptor readerOut{ends[0], Out, -1};
bool threw = false;
try { poll_nid_postfix(&readerOut, 1, 0); } catch (const std::runtime_error&) { threw = true; }
Require(threw);
GuestPollDescriptor 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]{};
Require(read_nid_postfix(ends[0], buffer, sizeof(buffer)) == 4);
Require(poll_nid_postfix(mixed, 2, 0) == 0 && mixed[1].revents == 0);
std::thread late([&] {
std::this_thread::sleep_for(std::chrono::milliseconds(30));
Require(write_nid_postfix(ends[1], "x", 1) == 1);
});
Require(poll_nid_postfix(mixed, 2, 2000) == 1 && mixed[1].revents == In);
late.join();
std::thread lateInfinite([&] {
std::this_thread::sleep_for(std::chrono::milliseconds(30));
Require(write_nid_postfix(ends[1], "y", 1) == 1);
});
Require(read_nid_postfix(ends[0], buffer, sizeof(buffer)) == 1);
const auto waitStart = std::chrono::steady_clock::now();
Require(poll_nid_postfix(&reader, 1, -1) == 1 && reader.revents == (In | ReadNormal));
Require(std::chrono::steady_clock::now() - waitStart >= std::chrono::milliseconds(20));
lateInfinite.join();
Require(read_nid_postfix(ends[0], buffer, sizeof(buffer)) == 1);
Require(write_nid_postfix(ends[1], "z", 1) == 1);
Require(close_nid_postfix(ends[1]) == 0);
Require(poll_nid_postfix(&reader, 1, 0) == 1 && reader.revents == (In | ReadNormal | Hup));
Require(read_nid_postfix(ends[0], buffer, sizeof(buffer)) == 1);
Require(poll_nid_postfix(&reader, 1, 0) == 1 && reader.revents == (In | ReadNormal | Hup));
Require(read_nid_postfix(ends[0], buffer, sizeof(buffer)) == 0);
GuestPollDescriptor closedPipe{ends[1], In, -1};
Require(poll_nid_postfix(&closedPipe, 1, 0) == 1 && closedPipe.revents == Invalid);
Require(close_nid_postfix(ends[0]) == 0);
int full[2];
Require(pipe_nid_postfix(full) == 0);
static char block[4096];
std::int64_t queued = 0;
GuestPollDescriptor fill{full[1], Out, -1};
while (poll_nid_postfix(&fill, 1, 0) == 1) {
Require(fill.revents == Out && queued < (1 << 20));
Require(write_nid_postfix(full[1], block, 256) == 256);
queued += 256;
}
Require(queued > 0 && fill.revents == 0);
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);
int other[2];
Require(pipe_nid_postfix(other) == 0);
Require(close_nid_postfix(other[0]) == 0);
GuestPollDescriptor orphan{other[1], Out, -1};
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);
}
static void PollsUrgentData() {
const int listener = socket_nid_postfix(2, 1, 0);
Require(listener >= 0);
std::array<unsigned char, 16> 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() == 36);
GuestPollDescriptor 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);
GuestPollDescriptor 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<const sockaddr*>(&native), sizeof(native)) == 0);
const int urgent = accept_nid_postfix(listener, nullptr, nullptr);
Require(urgent >= 0);
GuestPollDescriptor 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);
GuestPollDescriptor bands{urgent, In | Priority | Out, -1};
Require(poll_nid_postfix(&bands, 1, 0) == 1 && bands.revents == (Priority | Out));
#ifdef _WIN32
GuestPollDescriptor 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);
}
static void PollsAStreamWhosePeerClosed() {
const int listener = socket_nid_postfix(2, 1, 0);
Require(listener >= 0);
std::array<unsigned char, 16> 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 && listen_nid_postfix(listener, 4) == 0);
const int client = socket_nid_postfix(2, 1, 0);
Require(client >= 0 && connect_nid_postfix(client, address.data(), address.size()) == 0);
const int server = accept_nid_postfix(listener, nullptr, nullptr);
Require(server >= 0);
Require(shutdown_nid_postfix(client, 1) == 0);
GuestPollDescriptor wait{client, static_cast<short>(In | Out), 0};
Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == Out);
wait = {server, In, 0};
Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == In);
wait = {server, static_cast<short>(In | Out), 0};
Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == (In | Out));
wait = {server, ReadNormal, 0};
Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == ReadNormal);
#ifndef _WIN32
wait = {server, static_cast<short>(In | Priority | ReadBand), 0};
Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == In);
#endif
wait = {server, Out, 0};
Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == Out);
char byte;
Require(recv_nid_postfix(server, &byte, 1, 0) == 0);
Require(shutdown_nid_postfix(server, 1) == 0);
wait = {server, In, 0};
Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == (In | Hup));
wait = {client, In, 0};
Require(poll_nid_postfix(&wait, 1, 1000) == 1 && wait.revents == (In | Hup));
for (const int descriptor : {client, server, listener}) Require(close_nid_postfix(descriptor) == 0);
}
#ifndef _WIN32
static void PollsAHungUpSocketWithoutPollout() {
const int listener = socket_nid_postfix(2, 1, 0);
Require(listener >= 0);
std::array<unsigned char, 16> 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 && listen_nid_postfix(listener, 1) == 0);
const int client = socket_nid_postfix(2, 1, 0);
Require(client >= 0 && connect_nid_postfix(client, address.data(), address.size()) == 0);
const int server = accept_nid_postfix(listener, nullptr, nullptr);
Require(server >= 0);
Require(close_nid_postfix(client) == 0);
char byte = 0;
GuestPollDescriptor ended{server, In, 0};
Require(poll_nid_postfix(&ended, 1, 1000) == 1);
Require(recv_nid_postfix(server, &byte, 1, 0) == 0);
Require(shutdown_nid_postfix(server, 1) == 0);
GuestPollDescriptor hungUp{server, static_cast<short>(In | Out), 0};
Require(poll_nid_postfix(&hungUp, 1, 1000) == 1 && hungUp.revents == (In | Hup));
Require(close_nid_postfix(server) == 0 && close_nid_postfix(listener) == 0);
}
#endif
int main() {
for (const int descriptor : {0, 1, 2}) StandardDescriptorFile(descriptor);
PollsUrgentData();
PollsAStreamWhosePeerClosed();
#ifndef _WIN32
PollsAHungUpSocketWithoutPollout();
#endif
const int listener = socket_nid_postfix(2, 1, 0);
Require(listener >= 0 && listener < 1024);
std::array<unsigned char, 16> 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);
GuestPollDescriptor single{listener, In, -1};
Require(poll_nid_postfix(&single, 1, 0) == 0 && single.revents == 0);
const auto start = std::chrono::steady_clock::now();
Require(poll_nid_postfix(&single, 1, 50) == 0);
Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(40));
const int client = socket_nid_postfix(2, 1, 0);
Require(client >= 0 && connect_nid_postfix(client, address.data(), address.size()) == 0);
Require(poll_nid_postfix(&single, 1, 1000) == 1 && single.revents == In);
const int server = accept_nid_postfix(listener, nullptr, nullptr);
Require(server >= 0);
GuestPollDescriptor pair[3]{{server, In, 0}, {-1, In, 7}, {client, Out, 0}};
Require(poll_nid_postfix(pair, 3, 1000) == 1);
Require(pair[0].revents == 0 && pair[1].revents == 0 && pair[2].revents == Out);
const char message[] = "poll";
Require(send_nid_postfix(client, message, sizeof(message), 0) == sizeof(message));
pair[2].events = In;
Require(poll_nid_postfix(pair, 3, 1000) == 1 && pair[0].revents == In && pair[2].revents == 0);
char buffer[8]{};
Require(recv_nid_postfix(server, buffer, sizeof(buffer), 0) == sizeof(message));
Require(close_nid_postfix(client) == 0);
Require(poll_nid_postfix(pair, 1, 1000) == 1 && (pair[0].revents & (In | Hup)) != 0);
Require(recv_nid_postfix(server, buffer, sizeof(buffer), 0) == 0);
GuestPollDescriptor closed[2]{{client, In, 0}, {listener, In, 0}};
Require(poll_nid_postfix(closed, 2, -1) == 1 && closed[0].revents == Invalid && closed[1].revents == 0);
Require(poll_nid_postfix(nullptr, 0, 10) == 0);
const auto shortWaitStart = std::chrono::steady_clock::now();
for (int i = 0; i < 20; ++i) Require(poll_nid_postfix(nullptr, 0, 1) == 0);
const auto shortWait = std::chrono::steady_clock::now() - shortWaitStart;
Require(shortWait >= std::chrono::milliseconds(20) && shortWait < std::chrono::milliseconds(200));
Require(poll_nid_postfix(nullptr, 1, 0) == -1 && *__error_nid_postfix() == 14);
Require(poll_nid_postfix(&single, 1, -2) == -1 && *__error_nid_postfix() == 22);
TestPipes(listener);
PollsFilesAndDirectories(listener);
Require(close_nid_postfix(server) == 0);
for (const char* path : {"/dev/urandom", "/dev/random"}) {
const int random = open_nid_postfix(path, 0, 0);
Require(random >= 0);
GuestPollDescriptor device{random, static_cast<short>(In | Out | Priority), -1};
Require(poll_nid_postfix(&device, 1, 0) == 1 && device.revents == In);
device.events = static_cast<short>(ReadNormal | Out);
Require(poll_nid_postfix(&device, 1, 0) == 1 && device.revents == ReadNormal);
device.events = static_cast<short>(Out | Priority | ReadBand | WriteBand);
Require(poll_nid_postfix(&device, 1, -1) == 1 && device.revents == (Out | Priority | ReadBand | WriteBand));
GuestPollDescriptor mixed[2]{{listener, In, 0}, {random, Out, 0}};
Require(poll_nid_postfix(mixed, 2, -1) == 1 && mixed[0].revents == 0 && mixed[1].revents == Out);
Require(close_nid_postfix(random) == 0);
}
GuestPollDescriptor console[3]{{0, static_cast<short>(In | ReadNormal | Out), -1}, {1, static_cast<short>(In | Out | WriteBand), -1}, {2, Out, -1}};
Require(poll_nid_postfix(console, 3, 0) == 2);
Require(console[0].revents == 0 && console[1].revents == Out && console[2].revents == Out);
GuestPollDescriptor input{0, In, -1};
const auto inputStart = std::chrono::steady_clock::now();
Require(poll_nid_postfix(&input, 1, 30) == 0 && input.revents == 0);
Require(std::chrono::steady_clock::now() - inputStart >= std::chrono::milliseconds(25));
PollsKqueues(listener);
Require(close_nid_postfix(listener) == 0);
}