feat(libkernel): implement socketpair

AF_UNIX pairs as FreeBSD 11 makes them: SOCK_STREAM, SOCK_DGRAM and
SOCK_SEQPACKET with protocol 0, SOCK_NONBLOCK applied to both ends and
SOCK_CLOEXEC accepted, EAFNOSUPPORT for an unknown family, EOPNOTSUPP
for AF_INET and AF_INET6, EPROTONOSUPPORT for type 0 or a protocol,
EPROTOTYPE for another type, EFAULT for a null output, and nothing
written on failure. The ends are guest sockets, so send, recv,
shutdown, fcntl, poll and close work on them; getsockname and
getpeername report the unnamed AF_UNIX address, recvfrom and recvmsg
no address for a stream and the unnamed one for a datagram. On Linux
the pair is the host's socketpair; on Windows a loopback TCP connection
or two connected UDP sockets, and SOCK_SEQPACKET throws. The remaining
differences from the console are recorded in TechnicalDebt.

OpenSSL's QUIC notifier in 15 catalog titles creates such a pair.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jan Šlégl 2026-10-08 19:04:31 +02:00
parent 4b005b229e
commit 8f31696450
4 changed files with 314 additions and 1 deletions

View File

@ -408,6 +408,16 @@ if(BUILD_TESTING)
set_tests_properties(guest_tcp PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_socket_pair_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketPair.cpp)
target_include_directories(guest_socket_pair_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_socket_pair_tests PRIVATE libkernel libc)
configure_windows_unwind(guest_socket_pair_tests)
add_test(NAME guest_socket_pair COMMAND guest_socket_pair_tests)
set_tests_properties(guest_socket_pair PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(guest_socket_pair PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_elf_phdr_match_tests ${LIBS_INCLUDE_DIR}/tests/GuestElfPhdrMatch.cpp)
target_include_directories(guest_elf_phdr_match_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_elf_phdr_match_tests PRIVATE libkernel libc)

View File

@ -10,6 +10,7 @@
#include <unistd.h>
#include <cerrno>
#endif
#include "prx/libc/include/GuestArena.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libkernel/Socket/include/SocketRuntime.hpp"
#include <algorithm>
@ -208,6 +209,106 @@ std::shared_ptr<Socket> Find(int descriptor) {
}
}
namespace {
constexpr int GuestUnix = 1;
constexpr int GuestCloseOnExec = 0x10000000;
constexpr int GuestNonBlocking = 0x20000000;
int PairError() {
#ifdef _WIN32
switch (WSAGetLastError()) {
case WSAEMFILE: return 24;
default: return NativeError();
}
#else
switch (errno) {
case EMFILE: return 24;
case ENFILE: return 23;
case ENOMEM: return 12;
default: return NativeError();
}
#endif
}
int SetNonBlocking(Socket& socket) {
#ifdef _WIN32
unsigned long value = 1;
if (ioctlsocket(socket.value, FIONBIO, &value)) return Fail(NativeError());
#else
int value = 1;
if (::ioctl(socket.value, FIONBIO, &value)) return Fail(NativeError());
#endif
socket.nonblocking = true;
return 0;
}
#ifdef _WIN32
bool LoopbackName(NativeSocket socket, sockaddr_in& name) {
int size = sizeof(name);
return getsockname(socket, reinterpret_cast<sockaddr*>(&name), &size) == 0;
}
int NativePair(int type, NativeSocket (&pair)[2]) {
static const int startup = [] { WSADATA data{}; return WSAStartup(MAKEWORD(2, 2), &data); }();
if (startup) return 5;
if (type == 5) throw std::runtime_error("socketpair: SOCK_SEQPACKET is not supported on Windows");
sockaddr_in loopback{};
loopback.sin_family = AF_INET;
loopback.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
const int nativeType = type == 1 ? SOCK_STREAM : SOCK_DGRAM;
Socket first(::socket(AF_INET, nativeType, 0), 2, type);
if (first.value == Invalid) return PairError();
const int exclusive = 1;
setsockopt(first.value, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, reinterpret_cast<const char*>(&exclusive), sizeof(exclusive));
if (bind(first.value, reinterpret_cast<sockaddr*>(&loopback), sizeof(loopback))) return PairError();
sockaddr_in firstName{};
if (!LoopbackName(first.value, firstName)) return PairError();
Socket second(::socket(AF_INET, nativeType, 0), 2, type);
if (second.value == Invalid) return PairError();
if (type == 1) {
if (listen(first.value, 1)) return PairError();
if (connect(second.value, reinterpret_cast<sockaddr*>(&firstName), sizeof(firstName))) return PairError();
sockaddr_in peer{};
int peerSize = sizeof(peer);
Socket accepted(accept(first.value, reinterpret_cast<sockaddr*>(&peer), &peerSize), 2, type);
if (accepted.value == Invalid) return PairError();
sockaddr_in secondName{};
if (!LoopbackName(second.value, secondName)) return PairError();
if (peer.sin_port != secondName.sin_port) return 53;
const int noDelay = 1;
setsockopt(second.value, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<const char*>(&noDelay), sizeof(noDelay));
setsockopt(accepted.value, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast<const char*>(&noDelay), sizeof(noDelay));
pair[0] = second.value;
pair[1] = accepted.value;
second.value = Invalid;
accepted.value = Invalid;
return 0;
}
if (bind(second.value, reinterpret_cast<sockaddr*>(&loopback), sizeof(loopback))) return PairError();
sockaddr_in secondName{};
if (!LoopbackName(second.value, secondName)) return PairError();
if (connect(first.value, reinterpret_cast<sockaddr*>(&secondName), sizeof(secondName))) return PairError();
if (connect(second.value, reinterpret_cast<sockaddr*>(&firstName), sizeof(firstName))) return PairError();
pair[0] = first.value;
pair[1] = second.value;
first.value = Invalid;
second.value = Invalid;
return 0;
}
#else
int NativePair(int type, NativeSocket (&pair)[2]) {
const int nativeType = type == 1 ? SOCK_STREAM : type == 2 ? SOCK_DGRAM : SOCK_SEQPACKET;
int native[2];
if (::socketpair(AF_UNIX, nativeType, 0, native)) return PairError();
pair[0] = native[0];
pair[1] = native[1];
return 0;
}
#endif
int UnnamedAddress(void* address, std::uint32_t* length) {
const unsigned char bytes[16] = {16, GuestUnix};
*length = std::min<std::uint32_t>(*length, sizeof(bytes));
std::memcpy(address, bytes, *length);
return 0;
}
}
extern "C" {
int APS5_VABI fcntl_nid_postfix(int descriptor, int command, ...) {
const auto socket = Lookup(descriptor);
@ -286,6 +387,48 @@ int APS5_VABI socket_nid_postfix(int family, int type, int protocol) {
return Fail(12);
}
}
int APS5_VABI socketpair_nid_postfix(int family, int type, int protocol, int* descriptors) {
const bool nonblocking = (type & GuestNonBlocking) != 0;
const int plainType = type & ~(GuestCloseOnExec | GuestNonBlocking);
if (family != GuestUnix && family != 2 && family != 28) return Fail(47);
if (plainType == 0) return Fail(43);
if (family != GuestUnix) {
if (plainType != 1 && plainType != 2) return Fail(41);
return Fail(protocol == 0 || protocol == (plainType == 1 ? 6 : 17) ? 45 : 43);
}
if (protocol != 0) return Fail(43);
if (plainType != 1 && plainType != 2 && plainType != 5) return Fail(41);
if (!descriptors) return Fail(14);
const GuestArena::HostWrite destination(descriptors, 2 * sizeof(int));
if (!destination.Open()) return Fail(14);
NativeSocket native[2];
if (const int error = NativePair(plainType, native)) return Fail(error);
Socket firstGuard(native[0], GuestUnix, plainType);
Socket secondGuard(native[1], GuestUnix, plainType);
try {
auto first = std::make_shared<Socket>(native[0], GuestUnix, plainType);
firstGuard.value = Invalid;
auto second = std::make_shared<Socket>(native[1], GuestUnix, plainType);
secondGuard.value = Invalid;
if (nonblocking && (SetNonBlocking(*first) || SetNonBlocking(*second))) return -1;
std::lock_guard lock(socketsMutex);
if (nextDescriptor >= INT_MAX - 1) return Fail(24);
const int firstDescriptor = nextDescriptor++;
const int secondDescriptor = nextDescriptor++;
sockets.emplace(firstDescriptor, std::move(first));
try {
sockets.emplace(secondDescriptor, std::move(second));
} catch (const std::bad_alloc&) {
sockets.erase(firstDescriptor);
return Fail(12);
}
descriptors[0] = firstDescriptor;
descriptors[1] = secondDescriptor;
return 0;
} catch (const std::bad_alloc&) {
return Fail(12);
}
}
int APS5_VABI connect_nid_postfix(int descriptor, const void* address, std::uint32_t length) {
const auto socket = Lookup(descriptor);
if (!socket) return -1;
@ -361,6 +504,7 @@ int APS5_VABI getsockname_nid_postfix(int descriptor, void* address, std::uint32
const auto socket = Lookup(descriptor);
if (!socket) return -1;
if (!address || !length) return Fail(14);
if (socket->family == GuestUnix) return UnnamedAddress(address, length);
sockaddr_storage native{};
socklen_t size = sizeof(native);
if (::getsockname(socket->value, reinterpret_cast<sockaddr*>(&native), &size)) return Fail(NativeError());
@ -371,6 +515,7 @@ int APS5_VABI getpeername_nid_postfix(int descriptor, void* address, std::uint32
const auto socket = Lookup(descriptor);
if (!socket) return -1;
if (!address || !length) return Fail(14);
if (socket->family == GuestUnix) return UnnamedAddress(address, length);
sockaddr_storage native{};
socklen_t size = sizeof(native);
if (::getpeername(socket->value, reinterpret_cast<sockaddr*>(&native), &size)) return Fail(NativeError());
@ -430,6 +575,10 @@ std::int64_t APS5_VABI recvfrom_nid_postfix(int descriptor, void* buffer, std::u
const auto result = ::recvfrom(socket->value, static_cast<char*>(buffer), static_cast<int>(length),
flags & 2 ? MSG_PEEK : 0, reinterpret_cast<sockaddr*>(&native), &size);
if (result < 0) return Fail(NativeError());
if (address && socket->family == GuestUnix) {
if (socket->type == 1) *addressLength = 0; else UnnamedAddress(address, addressLength);
return result;
}
if (address) GuestAddress(native, address, addressLength);
return result;
}
@ -513,7 +662,9 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message,
std::memcpy(message->iov[i].base, buffer.data() + offset, count);
offset += count;
}
if (message->name) {
if (message->name && socket->family == GuestUnix) {
if (socket->type == 1) message->nameLength = 0; else UnnamedAddress(message->name, &message->nameLength);
} else if (message->name) {
if (size > 0 && (native.ss_family == AF_INET || native.ss_family == AF_INET6)) {
GuestAddress(native, message->name, &message->nameLength);
} else {

View File

@ -0,0 +1,151 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <cstdint>
#include <cstdlib>
#include <cstring>
struct PollDescriptor {
int descriptor;
short events;
short revents;
};
extern "C" {
int APS5_VABI socketpair_nid_postfix(int, int, int, int*);
std::int64_t APS5_VABI send_nid_postfix(int, const void*, std::uint64_t, int);
std::int64_t APS5_VABI recv_nid_postfix(int, void*, std::uint64_t, int);
std::int64_t APS5_VABI recvfrom_nid_postfix(int, void*, std::uint64_t, int, void*, std::uint32_t*);
int APS5_VABI getsockname_nid_postfix(int, void*, std::uint32_t*);
int APS5_VABI getpeername_nid_postfix(int, void*, std::uint32_t*);
int APS5_VABI shutdown_nid_postfix(int, int);
int APS5_VABI fcntl_nid_postfix(int, int, ...);
int APS5_VABI poll_nid_postfix(PollDescriptor*, std::uint32_t, int);
int APS5_VABI close_nid_postfix(int);
int* APS5_VABI __error_nid_postfix();
}
static void Require(bool value) { if (!value) std::abort(); }
constexpr int Unix = 1;
constexpr int Stream = 1;
constexpr int Datagram = 2;
constexpr int SequencedPacket = 5;
constexpr int CloseOnExec = 0x10000000;
constexpr int NonBlocking = 0x20000000;
constexpr int NoSignal = 0x20000;
constexpr int Peek = 2;
constexpr int FirstSocket = 0x10000000;
constexpr short In = 0x1;
constexpr short Out = 0x4;
constexpr short Hup = 0x10;
static bool Fails(int domain, int type, int protocol, int error) {
int pair[2] = {-7, -7};
*__error_nid_postfix() = 0;
const int result = socketpair_nid_postfix(domain, type, protocol, pair);
return result == -1 && *__error_nid_postfix() == error && pair[0] == -7 && pair[1] == -7;
}
static bool Readable(int descriptor, short accepted = In) {
PollDescriptor waited{descriptor, In, 0};
return poll_nid_postfix(&waited, 1, 2000) == 1 && (waited.revents & accepted) != 0;
}
static bool Exchange(int from, int to, const char* text) {
char received[32]{};
const auto length = static_cast<std::uint64_t>(std::strlen(text) + 1);
if (send_nid_postfix(from, text, length, NoSignal) != static_cast<std::int64_t>(length)) return false;
if (!Readable(to)) return false;
if (recv_nid_postfix(to, received, sizeof(received), 0) != static_cast<std::int64_t>(length)) return false;
return std::strcmp(received, text) == 0;
}
static bool Unnamed(int descriptor, int (APS5_VABI *query)(int, void*, std::uint32_t*)) {
std::uint8_t name[28];
std::memset(name, 0xaa, sizeof(name));
std::uint32_t length = sizeof(name);
if (query(descriptor, name, &length) != 0 || length != 16 || name[0] != 16 || name[1] != 1 || name[16] != 0xaa) return false;
length = 4;
std::memset(name, 0xaa, sizeof(name));
return query(descriptor, name, &length) == 0 && length == 4 && name[0] == 16 && name[1] == 1 && name[4] == 0xaa;
}
int main() {
int pair[2] = {-1, -1};
Require(socketpair_nid_postfix(Unix, Stream, 0, pair) == 0);
Require(pair[0] >= FirstSocket && pair[1] >= FirstSocket && pair[0] != pair[1]);
Require(Exchange(pair[0], pair[1], "one way"));
Require(Exchange(pair[1], pair[0], "other way"));
Require(Unnamed(pair[0], getsockname_nid_postfix) && Unnamed(pair[1], getpeername_nid_postfix));
PollDescriptor polled[2] = {{pair[1], In, 0}, {pair[0], Out, 0}};
Require(poll_nid_postfix(polled, 2, 0) == 1 && polled[0].revents == 0 && (polled[1].revents & Out) != 0);
const char ping[] = "ping";
Require(send_nid_postfix(pair[0], ping, sizeof(ping), 0) == sizeof(ping));
polled[0].revents = 0;
Require(poll_nid_postfix(polled, 1, 1000) == 1 && (polled[0].revents & In) != 0);
char peeked[8]{};
Require(recv_nid_postfix(pair[1], peeked, sizeof(peeked), Peek) == sizeof(ping) && std::strcmp(peeked, ping) == 0);
char received[8]{};
std::uint8_t from[28];
std::uint32_t fromLength = sizeof(from);
Require(recvfrom_nid_postfix(pair[1], received, sizeof(received), 0, from, &fromLength) == sizeof(ping) && fromLength == 0);
Require(std::strcmp(received, ping) == 0);
Require(fcntl_nid_postfix(pair[1], 3) == 2);
Require(fcntl_nid_postfix(pair[1], 4, 4) == 0);
Require(fcntl_nid_postfix(pair[1], 3) == 6);
Require(recv_nid_postfix(pair[1], received, sizeof(received), 0) == -1 && *__error_nid_postfix() == 35);
Require(shutdown_nid_postfix(pair[0], 1) == 0);
Require(Readable(pair[1], In | Hup) && recv_nid_postfix(pair[1], received, sizeof(received), 0) == 0);
Require(Exchange(pair[1], pair[0], "still open"));
Require(close_nid_postfix(pair[0]) == 0);
Require(Readable(pair[1], In | Hup) && recv_nid_postfix(pair[1], received, sizeof(received), 0) == 0);
#ifndef _WIN32
Require(send_nid_postfix(pair[1], ping, sizeof(ping), NoSignal) == -1 && *__error_nid_postfix() == 32);
#endif
Require(close_nid_postfix(pair[1]) == 0);
Require(close_nid_postfix(pair[1]) == -1 && *__error_nid_postfix() == 9);
Require(socketpair_nid_postfix(Unix, Stream | NonBlocking | CloseOnExec, 0, pair) == 0);
Require(fcntl_nid_postfix(pair[0], 3) == 6 && fcntl_nid_postfix(pair[1], 3) == 6);
Require(recv_nid_postfix(pair[0], received, sizeof(received), 0) == -1 && *__error_nid_postfix() == 35);
Require(Exchange(pair[1], pair[0], "wake up"));
Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0);
Require(socketpair_nid_postfix(Unix, Datagram, 0, pair) == 0);
Require(fcntl_nid_postfix(pair[0], 3) == 2);
Require(send_nid_postfix(pair[0], "a", 1, 0) == 1);
Require(send_nid_postfix(pair[0], "bb", 2, 0) == 2);
char datagram[16]{};
std::uint8_t sender[28];
std::uint32_t senderLength = sizeof(sender);
Require(Readable(pair[1]));
Require(recvfrom_nid_postfix(pair[1], datagram, sizeof(datagram), 0, sender, &senderLength) == 1 && datagram[0] == 'a');
Require(senderLength == 16 && sender[0] == 16 && sender[1] == 1);
Require(recv_nid_postfix(pair[1], datagram, sizeof(datagram), 0) == 2 && datagram[0] == 'b' && datagram[1] == 'b');
Require(Exchange(pair[1], pair[0], "back"));
Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0);
#ifndef _WIN32
Require(socketpair_nid_postfix(Unix, SequencedPacket, 0, pair) == 0);
Require(send_nid_postfix(pair[0], "a", 1, 0) == 1);
Require(send_nid_postfix(pair[0], "bb", 2, 0) == 2);
Require(recv_nid_postfix(pair[1], datagram, sizeof(datagram), 0) == 1);
Require(recv_nid_postfix(pair[1], datagram, sizeof(datagram), 0) == 2);
Require(close_nid_postfix(pair[0]) == 0 && close_nid_postfix(pair[1]) == 0);
#endif
Require(Fails(2, Stream, 0, 45));
Require(Fails(28, Datagram, 0, 45));
Require(Fails(3, Stream, 0, 47));
Require(Fails(Unix, 0, 0, 43));
Require(Fails(Unix, 3, 0, 41));
Require(Fails(Unix, 3, 6, 43));
Require(Fails(Unix, Stream | 0x01000000, 0, 41));
Require(Fails(Unix, Stream, 6, 43));
*__error_nid_postfix() = 0;
Require(socketpair_nid_postfix(Unix, Stream, 0, nullptr) == -1 && *__error_nid_postfix() == 14);
return 0;
}

View File

@ -328,6 +328,7 @@ Throughout the project, every function at every stage either **does exactly what
- [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached
- [The narrow printf family](../../core/libs/prx/libc/src/Formatting.cpp) (libc) passes `%ls` and `%S` to glibc on Linux, which reads them as strings of 32-bit `wchar_t`; the guest's `wchar_t` is 16 bits (UTF-16), so these conversions fail with -1. The Windows path converts them to UTF-8, and `guest_formatting` checks wide conversions on Windows only
- [collate<char>::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation.
- [socketpair](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the pair is the host's `AF_UNIX` pair on Linux and, on Windows, a loopback TCP connection for `SOCK_STREAM` or two connected UDP sockets for `SOCK_DGRAM`; `SOCK_SEQPACKET` throws on Windows. A datagram sent to a full receiver gets `ENOBUFS` on the console, waits or gets `EAGAIN` on Linux and is dropped on Windows, and on Windows a datagram `recv` after the peer closed reports `ECONNRESET` instead of the console's `ENOTCONN`, and a stream `send` after the peer closed reports `ECONNABORTED` instead of the console's `EPIPE`. `read` and `write` on a pair end go to the host like on every guest socket. `getpeername` on a pair end succeeds after the peer closed, where the console returns `ENOTCONN`, and `connect`, `bind` and `sendto` with an address on a pair end report an address-family error instead of the console's `EISCONN`. On Windows `poll` on a stream end reports `POLLHUP` without `POLLIN` after the peer's `shutdown(SHUT_WR)` or `close`, where the console reports `POLLIN`, with `POLLHUP` only after `close`
- [poll](../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) waits only on guest sockets, through `WSAPoll` on Windows and `poll` on Linux; the `pollfd` layout and flag values are FreeBSD's. A descriptor below the socket range (files, pipes, 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 to wait on. A closed socket descriptor reports `POLLNVAL`; the console's limit on the descriptor count is unknown and not checked
- [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped
- [sceFontOpenFontSet](../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) opens only the primary file of a system font set. Characters the console takes from the set's other files (Hangul, Thai and Arabic in Japanese and Chinese sets, Arabic in European sets) are reported as unsupported glyphs. The console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK) is loaded instead. It is not the system font: its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for.