From 49bcd7549e8a4baba5bb6cfdb7ca203013c2ca94 Mon Sep 17 00:00:00 2001 From: orezraey Date: Sun, 25 Oct 2026 16:23:33 -0300 Subject: [PATCH] fix(libkernel): report SCM_BINTIME and SCM_TIMESTAMP from recvmsg as measured on a PS5 Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel/Socket/src/Udp.cpp | 41 ++++++++++- core/libs/tests/GuestUdp.cpp | 81 ++++++++++++++++++++++ docs/dev/debt/libkernel.md | 4 +- 3 files changed, 123 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 8df1339f8d..c17be043f4 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -42,6 +42,7 @@ #include "prx/libkernel/File/include/File.hpp" #include "prx/libkernel/File/include/RandomDevice.hpp" #include "prx/libkernel/File/include/SharedMemory.hpp" +#include "prx/libkernel/Time/include/Time.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" #include "prx/libkernel/Socket/include/SocketConnect.hpp" @@ -1599,6 +1600,31 @@ static bool WriteGuest(void* destination, const void* source, std::size_t bytes) return true; #endif } +static std::vector ArrivalTimestamps(const Socket& socket, int keptFlags) { + std::vector records; + if (socket.type != 2 || (socket.family != 2 && socket.family != 28)) return records; + const bool binary = socket.family == 2 && (keptFlags & 0x2000) != 0; + const bool micro = (keptFlags & 0x400) != 0; + if (!binary && !micro) return records; + KernelTimespec now{}; + if (clock_gettime_nid_postfix(0, &now) != 0) throw std::runtime_error("recvmsg: reading CLOCK_REALTIME for the arrival time failed"); + const auto append = [&records](int type, std::int64_t seconds, std::uint64_t fraction) { + constexpr std::uint32_t length = 32; + constexpr int level = 0xffff; + const auto at = records.size(); + records.resize(at + length); + std::memcpy(records.data() + at, &length, sizeof(length)); + std::memcpy(records.data() + at + 4, &level, sizeof(level)); + std::memcpy(records.data() + at + 8, &type, sizeof(type)); + std::memcpy(records.data() + at + 16, &seconds, sizeof(seconds)); + std::memcpy(records.data() + at + 24, &fraction, sizeof(fraction)); + }; + const auto seconds = static_cast(now.tv_sec); + const auto nanoseconds = static_cast(now.tv_nsec); + if (binary) append(4, seconds, nanoseconds * 18446744073ull); + if (micro) append(2, seconds, nanoseconds / 1000); + return records; +} std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, int flags) { GuestMsghdr header{}; if (!message || !ReadGuest(&header, message, sizeof(header))) return Fail(14); @@ -1621,6 +1647,7 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, sockaddr_storage native{}; socklen_t size = 0; std::int64_t received = 0; + std::vector stamps; if (total != 0 || socket->type == 1 || ReadableNow(*socket)) { size = sizeof(native); if (const int waited = WaitForTransfer(*socket, false, flags)) return Fail(waited); @@ -1634,6 +1661,14 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, return Fail(NativeError()); #endif } + if (header.control) { + int keptFlags = 0; + { + std::lock_guard lock(socket->modeMutex); + keptFlags = socket->options.keptFlags; + } + stamps = ArrivalTimestamps(*socket, keptFlags); + } } header.flags = flags; if (received > total) { @@ -1659,7 +1694,11 @@ std::int64_t APS5_VABI recvmsg_nid_postfix(int descriptor, GuestMsghdr* message, if (!WriteGuest(header.name, address, header.nameLength)) return Fail(14); if (!WriteGuest(&message->nameLength, &header.nameLength, sizeof(header.nameLength))) return Fail(14); } - header.controlLength = 0; + const std::size_t controlCapacity = header.controlLength; + const std::size_t copied = std::min(stamps.size(), controlCapacity); + if (copied != 0 && !WriteGuest(header.control, stamps.data(), copied)) return Fail(14); + if (copied < stamps.size() && controlCapacity != 0) header.flags |= 0x20; + header.controlLength = static_cast(copied); constexpr auto results = offsetof(GuestMsghdr, controlLength); if (!WriteGuest(reinterpret_cast(message) + results, reinterpret_cast(&header) + results, sizeof(header) - results)) return Fail(14); return received; diff --git a/core/libs/tests/GuestUdp.cpp b/core/libs/tests/GuestUdp.cpp index 346f8c1a69..a17278d48a 100644 --- a/core/libs/tests/GuestUdp.cpp +++ b/core/libs/tests/GuestUdp.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/include/Api.hpp" +#include "prx/libkernel/Time/include/Time.hpp" #include "prx/libc/include/Api.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include @@ -149,9 +150,89 @@ static void CheckDontWait() { Require(close_nid_postfix(receiver) == 0 && close_nid_postfix(sender) == 0); } +static void CheckArrivalTimestamps(int family) { + const std::uint32_t addressSize = family == 2 ? 16 : 28; + const int receiver = socket_nid_postfix(family, 2, 0); + if (receiver < 0 && family == 28 && *__error_nid_postfix() == 47) return; + Require(receiver >= 0); + const int sender = socket_nid_postfix(family, 2, 0); + Require(sender >= 0); + std::array destination{}; + destination[0] = static_cast(addressSize); + destination[1] = static_cast(family); + if (family == 2) { destination[4] = 127; destination[7] = 1; } + else destination[23] = 1; + const int bound = bind_nid_postfix(receiver, destination.data(), addressSize); + if (bound < 0 && family == 28 && *__error_nid_postfix() == 49) { + Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); + return; + } + Require(bound == 0); + std::uint32_t size = destination.size(); + Require(getsockname_nid_postfix(receiver, destination.data(), &size) == 0); + Require(connect_nid_postfix(sender, destination.data(), addressSize) == 0); + for (const int options : {0, 0x400, 0x2000, 0x2400}) { + for (const int option : {0x400, 0x2000}) { + const int enabled = (options & option) != 0 ? 1 : 0; + Require(setsockopt_nid_postfix(receiver, 0xffff, option, &enabled, sizeof(enabled)) == 0); + } + const bool binary = family == 2 && (options & 0x2000) != 0; + const bool micro = (options & 0x400) != 0; + const std::uint32_t expected = (binary ? 32u : 0u) + (micro ? 32u : 0u); + for (const std::uint32_t capacity : {0u, 8u, 16u, 40u, 64u, 80u}) { + const char payload[] = "stamped"; + Require(sendto_nid_postfix(sender, payload, sizeof(payload), 0, nullptr, 0) == sizeof(payload)); + std::array control; + control.fill(0xa5); + char received[sizeof(payload)]{}; + KernelIovec vector{received, sizeof(received)}; + GuestMsghdr message{nullptr, 0, &vector, 1, control.data(), capacity, -1}; + KernelTimespec before{}; + Require(clock_gettime_nid_postfix(0, &before) == 0); + Require(recvmsg_nid_postfix(receiver, &message, 0) == sizeof(payload)); + KernelTimespec after{}; + Require(clock_gettime_nid_postfix(0, &after) == 0); + Require(std::memcmp(received, payload, sizeof(payload)) == 0); + const std::uint32_t copied = std::min(capacity, expected); + Require(message.controlLength == copied); + Require(message.flags == (capacity != 0 && capacity < expected ? 0x20 : 0)); + Require(std::all_of(control.begin() + copied, control.end(), [](auto byte) { return byte == 0xa5; })); + std::array whole{}; + std::memcpy(whole.data(), control.data(), copied); + std::uint32_t at = 0; + for (const int type : {4, 2}) { + if ((type == 4 && !binary) || (type == 2 && !micro)) continue; + if (at + 32 > copied) break; + std::uint32_t length = 0; + int level = 0; + int kind = 0; + std::int64_t seconds = 0; + std::uint64_t fraction = 0; + std::memcpy(&length, whole.data() + at, sizeof(length)); + std::memcpy(&level, whole.data() + at + 4, sizeof(level)); + std::memcpy(&kind, whole.data() + at + 8, sizeof(kind)); + std::memcpy(&seconds, whole.data() + at + 16, sizeof(seconds)); + std::memcpy(&fraction, whole.data() + at + 24, sizeof(fraction)); + Require(length == 32 && level == 0xffff && kind == type); + Require(seconds >= before.tv_sec && seconds <= after.tv_sec); + if (type == 2) Require(fraction < 1000000); + at += 32; + } + if (copied >= 8) { + std::uint32_t length = 0; + std::memcpy(&length, whole.data(), sizeof(length)); + Require(length == 32); + } + } + } + Require(close_nid_postfix(sender) == 0 && close_nid_postfix(receiver) == 0); +} + int main() { CheckAddressLengths(2); CheckAddressLengths(28); + CheckArrivalTimestamps(2); + CheckArrivalTimestamps(28); CheckDontWait(); CheckSendToPortZero(); const int receiver = socket_nid_postfix(2, 2, 0); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index dba40128f3..cc08a2872a 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -2,7 +2,7 @@ ### Silent stubs -- [SO_TIMESTAMP, SO_BINTIME](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - set and read back as their bits, as a payload process on a PS5 (firmware 13.60) answered them, but `recvmsg` adds no `SCM_TIMESTAMP` or `SCM_BINTIME` control message, which FreeBSD 11.4's `ip_savecontrol` and `ip6_savecontrol` add to a received datagram while the option is set; what the console's `recvmsg` returns was not measured +- [SO_TIMESTAMP, SO_BINTIME](../../../core/libs/prx/libkernel/Socket/src/Udp.cpp) (libkernel) - the arrival time in their control messages is `CLOCK_REALTIME` read when the host hands the datagram over, not when it reached the socket. Otherwise `recvmsg` on a UDP socket returns them as a payload process on PS5 firmware 13.60 got them: one 32-byte `SCM_BINTIME` (IPv4 only) then one `SCM_TIMESTAMP` at `SOL_SOCKET`, and a control buffer too small for them gets as many bytes as fit, its length, and `MSG_CTRUNC`. An empty buffer gets nothing and no `MSG_CTRUNC`, and a TCP or Unix socket gets no message, as in FreeBSD 11.4; neither was measured - [sceKernelGetSocPowerConsumption](../../../core/libs/prx/libkernel/System/src/ThermalSensors.cpp) (libkernel) - returns `EINVAL` and writes nothing, as `sceKernelGetSocSensorTemperature` does when the host has no sensor: the host's package power is not read (Linux RAPL `energy_uj` is readable by root only, and Windows has no general source). The prototype is not documented. PS5LM Plus (PPSA99581) passes a zeroed 128-byte buffer and, on success, reads its first 32-bit word as milliwatts, accepting 1000 to 350000; on failure it shows 0 W - [scePthreadMutexattrSetprotocol, scePthreadMutexattrGetprotocol, scePthreadMutexattrSetprioceiling, scePthreadMutexattrGetprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - `scePthreadMutexattrSetprotocol` accepts 0 to 2 as a PS5 (firmware 13.60) does and returns `EINVAL` otherwise, but the protocol is not applied: priority inheritance and protection are not modelled. On the console a null or uninitialized mutex attribute returns `EINVAL` from the SCE attribute calls, `scePthreadMutexInit` takes an uninitialized attribute as the default one, and a destroyed mutex returns `EINVAL` from `scePthreadMutexDestroy`; a null mutex pointer crashed the payload and still throws - [pthread_mutexattr_setprotocol, pthread_mutexattr_getprotocol, pthread_mutexattr_setprioceiling, pthread_mutexattr_getprioceiling](../../../core/libs/prx/libkernel/Pthread/Posix/Mutex.cpp) (libkernel) - the protocol and the priority ceiling are stored and reported as in [FreeBSD 11 libthr](https://github.com/freebsd/freebsd-src/blob/releng/11.0/lib/libthr/thread/thr_mutexattr.c#L192-L245) (the ceiling is set or read only under `PTHREAD_PRIO_PROTECT`, and setting a protocol resets it to the highest `SCHED_RR` priority). A null, uninitialized or destroyed attribute returns `EINVAL` before the other arguments are checked; this error behavior is unverified on the console. Mutexes do not apply the protocol or ceiling: a priority-inherit or priority-protect mutex locks like a normal one and does not change its owner's priority @@ -237,7 +237,7 @@ - [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`. 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 - [getsockopt, setsockopt](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - a guest socket has the SOL_SOCKET level, eight `IPPROTO_TCP` options, `IPV6_V6ONLY` and ten `IPPROTO_IP` options. `SO_ACCEPTFILTER` (0x1000) fails with `ENOPROTOOPT` (42), where a payload process on a PS5 (firmware 13.60) failed it with `EINVAL` for a set and a get on a non-listening socket, as FreeBSD 11.4 does; the other options of these levels fail with `ENOPROTOOPT` and were not all measured. `SO_KEEPALIVE`, `SO_DONTROUTE`, `SO_BROADCAST`, `SO_OOBINLINE` and `SO_LINGER` act on the host socket, and a boolean one reads back as its own bit, as there. Winsock does not clear a pending error when `SO_ERROR` reads it, so on Windows a pending error is reported once and again only after a new `connect`; a second failure with the same code without a `connect` in between is hidden - [TCP_NOPUSH, TCP_NOOPT, TCP option 0x80, IP_RECVDSTADDR, IP_RECVIF, IP_RECVTTL](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept and read back as a payload process on a PS5 (firmware 13.60) did: the TCP ones as the bits 0x1000, 0x8 and 0x10000 (so 0x80 is not FreeBSD's `TCP_KEEPINIT` there), the IP ones as 0 or 1. None of them changes what the socket does: `TCP_NOPUSH` does not hold back sends, and the IP ones add no control message to `recvmsg`. `IP_RECVTTL` was only read, never set, on the console, and an accepted socket takes `TCP_NOPUSH` from its listener as FreeBSD 11.4's `syncache_socket` does, which was not measured -- [SO_DEBUG, SO_REUSEADDR, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP, SO_BINTIME and option 0x10000](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept as bits that read back as on a PS5 (firmware 13.60, payload process), and options 0x4000 and 0x8000 are accepted and cannot be read, as there; only `SO_REUSEADDR` and `SO_REUSEPORT` change what the socket does. Both are set on the host, `SO_REUSEPORT` as `SO_REUSEADDR` on Windows, which has no `SO_REUSEPORT` and there also lets the socket bind over one that did not set it; Linux spreads unicast datagrams across the sockets, where FreeBSD gives each to one. Option 0x10000 is only kept: if the console treats it as FreeBSD 12's `SO_REUSEPORT_LB`, two sockets that set it share a port there, where the second `bind` here fails with `EADDRINUSE`. An accepted socket takes the bits from its listener, as FreeBSD 11.4's `sonewconn` copies `so_options`; that was not measured +- [SO_DEBUG, SO_REUSEADDR, SO_USELOOPBACK, SO_REUSEPORT, SO_TIMESTAMP, SO_BINTIME and option 0x10000](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept as bits that read back as on a PS5 (firmware 13.60, payload process), and options 0x4000 and 0x8000 are accepted and cannot be read, as there; only `SO_REUSEADDR`, `SO_REUSEPORT`, `SO_TIMESTAMP` and `SO_BINTIME` change what the socket does. Both are set on the host, `SO_REUSEPORT` as `SO_REUSEADDR` on Windows, which has no `SO_REUSEPORT` and there also lets the socket bind over one that did not set it; Linux spreads unicast datagrams across the sockets, where FreeBSD gives each to one. Option 0x10000 is only kept: if the console treats it as FreeBSD 12's `SO_REUSEPORT_LB`, two sockets that set it share a port there, where the second `bind` here fails with `EADDRINUSE`. An accepted socket takes the bits from its listener, as FreeBSD 11.4's `sonewconn` copies `so_options`; that was not measured - [IP_ADD_MEMBERSHIP, IP_DROP_MEMBERSHIP, IP_MULTICAST_IF](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - set on the host socket after the checks a PS5 (firmware 13.60, payload process) showed: `EOPNOTSUPP` on a stream socket, `EINVAL` for a short value or a group outside 224.0.0.0/4, `EADDRINUSE` for a group joined twice and `EADDRNOTAVAIL` for one not joined, tracked per socket. An interface the host lacks fails with the host's error (`EADDRNOTAVAIL` on the console). A group joined through `INADDR_ANY` and through that interface's address counts as two here. Not measured, after FreeBSD 11.4: a drop through `INADDR_ANY` leaves the first such group on any interface (`inp_leave_group`), and the other multicast options fail with `EOPNOTSUPP` on a stream socket (`inp_setmoptions`). An `ip_mreqn` with a non-zero index or address throws, as only a zeroed one was measured, and so does reading `IP_MULTICAST_IF` into 12 bytes, where FreeBSD returns an `ip_mreqn` - [TCP_MAXSEG](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per stream socket, read as 536 at first and failing with `EINVAL` for 1000, 70000 and 100, as a payload process on a PS5 (firmware 13.60) did. No segment set succeeded there, so the rule here, `EINVAL` above the current segment or below 176 and any segment between accepted, follows FreeBSD 11.4's `tcp_default_ctloutput` (`optval + 40 >= V_tcp_minmss`) and is not measured. An `AF_INET6` socket also starts at 536 here, where FreeBSD's `tcp_newtcpcb` uses 1220; not measured. The segment is not set on the host socket, so the host's segment size is its own, and a connected socket still reads the segment it was given instead of the negotiated one - [TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT](../../../core/libs/prx/libkernel/Socket/src/SocketOptions.cpp) (libkernel) - kept per stream socket and read back as a payload process on a PS5 (firmware 13.60) did: 7200, 75 and 8 at first, 0 restoring a keepalive default, and a count stored as an unsigned value. They are not set on the host socket, so the host's keepalive timers are its own while `SO_KEEPALIVE` is applied there. An interval above 4294967 seconds fails with `EINVAL`, after FreeBSD 11.4's `tcp_default_ctloutput` with `hz` taken as 1000, and an accepted socket takes the keepalive values from its listener as `syncache_socket` copies them; neither was measured