fix(libkernel): report SCM_BINTIME and SCM_TIMESTAMP from recvmsg as measured on a PS5

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
orezraeyandClaude Opus 5.5 committed 2026-10-25 16:30:34 -03:00
1 parent aeff26bd97
commit 49bcd7549e
3 files changed
+123 -3

No files matched your search

+40 -1
View File
@@ -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<unsigned char> ArrivalTimestamps(const Socket& socket, int keptFlags) {
std::vector<unsigned char> 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<std::int64_t>(now.tv_sec);
const auto nanoseconds = static_cast<std::uint64_t>(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<unsigned char> 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<std::uint32_t>(copied);
constexpr auto results = offsetof(GuestMsghdr, controlLength);
if (!WriteGuest(reinterpret_cast<char*>(message) + results, reinterpret_cast<const char*>(&header) + results, sizeof(header) - results)) return Fail(14);
return received;
+81
View File
@@ -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 <algorithm>
@@ -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<unsigned char, 28> destination{};
destination[0] = static_cast<unsigned char>(addressSize);
destination[1] = static_cast<unsigned char>(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<unsigned char, 80> 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<unsigned char, 64> 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);
+2 -2
View File
@@ -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