From eebedb6427ad51434fe48bcb8d7c7e13b08beae0 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:00:01 -0300 Subject: [PATCH 1/3] feat(libSceNet): receive urgent TCP data inline --- core/libs/prx/libSceNet/CMakeLists.txt | 11 +++ core/libs/prx/libSceNet/Export.cpp | 24 +++++- core/libs/tests/GuestNetOobInline.cpp | 107 +++++++++++++++++++++++++ docs/dev/debt/libSceNet.md | 1 + 4 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 core/libs/tests/GuestNetOobInline.cpp diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index 425f382c63..90129eac83 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -116,6 +116,17 @@ if(BUILD_TESTING) set_tests_properties(guest_refused_connect PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_net_oob_inline_tests ${LIBS_INCLUDE_DIR}/tests/GuestNetOobInline.cpp) + target_include_directories(guest_net_oob_inline_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_net_oob_inline_tests PRIVATE libSceNet libkernel libc) + configure_windows_unwind(guest_net_oob_inline_tests) + add_test(NAME guest_net_oob_inline COMMAND guest_net_oob_inline_tests) + set_tests_properties(guest_net_oob_inline PROPERTIES TIMEOUT 20) + if(WIN32) + target_link_libraries(guest_net_oob_inline_tests PRIVATE ws2_32) + set_tests_properties(guest_net_oob_inline PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(WIN32) add_test_executable(guest_socket_host_write_tests ${LIBS_INCLUDE_DIR}/tests/GuestSocketHostWrite.cpp) target_include_directories(guest_socket_host_write_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index c4dca4740f..ad5ab932d3 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -149,6 +149,7 @@ constexpr int NET_SO_REUSEADDR = 0x4; constexpr int NET_SO_KEEPALIVE = 0x8; constexpr int NET_SO_BROADCAST = 0x20; constexpr int NET_SO_ONESBCAST = 0x10000; +constexpr int NET_SO_OOBINLINE = 0x100; constexpr int NET_SO_SNDBUF = 0x1001; constexpr int NET_SO_RCVBUF = 0x1002; constexpr int NET_SO_SCE_SNDTIMEO = 0x1105; @@ -272,6 +273,7 @@ struct Sock { bool aborted = false; bool sending_shut = false; bool read_shut = false; + bool oobInline = false; int reported_error = 0; int snd_buffer = 0; int rcv_buffer = 0; @@ -1472,7 +1474,12 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval if (!optval || optlen < sizeof(int)) return fail(NET_EINVAL); int value = 0; std::memcpy(&value, optval, sizeof(value)); - if (optname == NET_SO_NBIO) { + if (optname == NET_SO_OOBINLINE) { + const int enabled = value != 0; + if (socket.type == NET_SOCK_STREAM && + ::setsockopt(socket.native->value, SOL_SOCKET, SO_OOBINLINE, reinterpret_cast(&enabled), sizeof(enabled)) != 0) + return fail(native_error()); + } else if (optname == NET_SO_NBIO) { #ifndef _WIN32 const int old_flags = fcntl(socket.native->value, F_GETFL, 0); if (old_flags < 0 || fcntl(socket.native->value, F_SETFL, @@ -1498,6 +1505,7 @@ int APS5_VABI sceNetSetsockopt(int s, int level, int optname, const void* optval std::lock_guard lk(g_mutex); auto it = g_socks.find(s); if (it != g_socks.end()) { + if (optname == NET_SO_OOBINLINE && it->second.native == socket.native) it->second.oobInline = value != 0; if (optname == NET_SO_NBIO) it->second.native->nonblock = value != 0; if (optname == NET_SO_RCVTIMEO) it->second.native->receiveTimeout = value; if (optname == NET_SO_SNDTIMEO) it->second.native->sendTimeout = value; @@ -1524,6 +1532,20 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint std::memcpy(optval, &value, *optlen); return 0; } + if (level == NET_SOL_SOCKET && optname == NET_SO_OOBINLINE) { + int enabled = 0; + if (socket.type == NET_SOCK_STREAM) { + NativeLength size = sizeof(enabled); + if (::getsockopt(socket.native->value, SOL_SOCKET, SO_OOBINLINE, reinterpret_cast(&enabled), &size) != 0) + return fail(native_error()); + } else { + enabled = socket.oobInline; + } + const int value = enabled != 0 ? NET_SO_OOBINLINE : 0; + *optlen = std::min(*optlen, static_cast(sizeof(value))); + std::memcpy(optval, &value, *optlen); + return 0; + } if (level == NET_SOL_SOCKET && optname == NET_SO_PROTOCOL) { if (socket.type == NET_SOCK_RAW) NotImplemented_nid_no_patch(__func__); *optlen = std::min(*optlen, static_cast(sizeof(socket.protocol))); diff --git a/core/libs/tests/GuestNetOobInline.cpp b/core/libs/tests/GuestNetOobInline.cpp new file mode 100644 index 0000000000..4080b61d00 --- /dev/null +++ b/core/libs/tests/GuestNetOobInline.cpp @@ -0,0 +1,107 @@ +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include +#else +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Socket/include/SocketPoll.hpp" + +extern "C" { +int APS5_VABI sceNetInit_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); +int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +int APS5_VABI sceNetListen(int, int); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); +std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); +int* APS5_VABI sceNetErrnoLoc(); +} + +static void Require(bool condition, std::source_location location = std::source_location::current()) { + if (!condition) { + std::fprintf(stderr, "check failed at line %u\n", location.line()); + std::abort(); + } +} + +static void CheckFlag(int socket, int expected) { + for (const std::uint32_t capacity : {0u, 1u, 3u, 4u, 8u}) { + std::array bytes; + bytes.fill(0xa5); + std::uint32_t size = capacity; + Require(sceNetGetsockopt(socket, 0xffff, 0x100, bytes.data(), &size) == 0 && size == std::min(capacity, 4u)); + Require(std::memcmp(bytes.data(), &expected, size) == 0); + Require(std::all_of(bytes.begin() + size, bytes.end(), [](auto byte) { return byte == 0xa5; })); + } +} + +int main() { + Require(sceNetInit_nid_postfix() == 0); + for (const int family : {2, 28}) { + const int datagram = sceNetSocket("inline-datagram", family, 2, 17); + const int listener = sceNetSocket("inline-listener", family, 1, 6); + const int client = sceNetSocket("inline-client", family, 1, 6); + Require(datagram >= 0 && listener >= 0 && client >= 0); + for (const int socket : {datagram, listener}) { + CheckFlag(socket, 0); + for (const int setting : {1, -1, 0, 256, 0, 1}) { + Require(sceNetSetsockopt(socket, 0xffff, 0x100, &setting, sizeof(setting)) == 0); + CheckFlag(socket, setting != 0 ? 256 : 0); + } + const int on = 1; + Require(sceNetSetsockopt(socket, 0xffff, 0x100, &on, 3) == static_cast(0x80410116u)); + Require(*sceNetErrnoLoc() == 22); + Require(sceNetSetsockopt(socket, 0xffff, 0x100, nullptr, 4) == static_cast(0x80410116u)); + } + CheckFlag(client, 0); + std::array address{}; + std::uint32_t size = family == 2 ? 16 : 28; + address[0] = size; + address[1] = family; + if (family == 2) { address[4] = 127; address[7] = 1; } + else address[23] = 1; + Require(sceNetBind_nid_postfix(listener, address.data(), size) == 0); + Require(sceNetListen(listener, 1) == 0); + Require(sceNetGetsockname(listener, address.data(), &size) == 0); + Require(sceNetConnect(client, address.data(), size) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + CheckFlag(accepted, 256); + const int timeout = 500000; + Require(sceNetSetsockopt(accepted, 0xffff, 0x1106, &timeout, sizeof(timeout)) == 0); + std::shared_ptr owner; + std::uintptr_t native = 0; + Require(KernelFindSocketNative_nid_no_patch(client, &owner, &native)); + Require(::send(native, "A", 1, 0) == 1); + Require(::send(native, "!", 1, MSG_OOB) == 1); + Require(::send(native, "B", 1, 0) == 1); + std::array received{}; + std::size_t count = 0; + while (count < received.size()) { + const auto result = sceNetRecv(accepted, received.data() + count, received.size() - count, 0); + Require(result > 0); + count += result; + } + Require(std::memcmp(received.data(), "A!B", 3) == 0); + const int off = 0; + Require(sceNetSetsockopt(listener, 0xffff, 0x100, &off, sizeof(off)) == 0); + CheckFlag(listener, 0); + CheckFlag(accepted, 256); + for (const int socket : {accepted, client, listener, datagram}) Require(sceNetSocketClose(socket) == 0); + int value = 0; + size = sizeof(value); + Require(sceNetGetsockopt(listener, 0xffff, 0x100, &value, &size) == static_cast(0x80410109u)); + } +} diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index f584ebcb33..3814d8f57b 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -39,6 +39,7 @@ - [sceNetConnect to AF_UNSPEC](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) dissolves a datagram association as FreeBSD 11.4's `udp_disconnect()` does, which resets the local address to the wildcard address and keeps the port. On Linux and on Windows a socket bound to an explicit port keeps its local address as well (`127.0.0.1:` stays `127.0.0.1:`), because a bound socket cannot be bound again, and `sceNetGetsockname` reports that host address. On Windows a `sendto` on such a host socket to a non-loopback address fails with Winsock error 10051 (measured on the host socket, not through `sceNetSendto`), where FreeBSD sends it from the wildcard address. One bound to port 0 is bound again to the wildcard address with the port it had - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. Windows sockets stay non-blocking and the library waits for readiness until the stored timeout. Blocking stream sends return the queued byte count on timeout, or `EAGAIN` when no bytes were queued. - [sceNetSetsockopt, sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) take `SO_SNDTIMEO` and `SO_RCVTIMEO` (0x1005, 0x1006) as a 16-byte `struct timeval`, as FreeBSD 11.4's [`sosetopt` and `sogetopt`](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/sys/kern/uipc_socket.c#L2657-L2690) do, and Sony's 0x1105 and 0x1106 as an `int` of microseconds. A [console log of ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/issues/49) (firmware 12.20) shows receives timing out every 100 ms after it gave 0x1006 a `{0, 100000}` timeval. The errors for a value shorter than a timeval (`EINVAL`) or out of range (`EDOM`), and `sceNetGetsockopt` cutting the timeval to the caller's length, follow FreeBSD's `sooptcopyin` and `sooptcopyout` and are not measured on a console +- [SO_OOBINLINE](../../../core/libs/prx/libSceNet/Export.cpp) uses native TCP urgent-data handling; its urgent-pointer and signalling behavior has not been compared with PS5. The flag bit (0x100), non-stream flag storage and truncated readback follow FreeBSD 11.4 [sosetopt/sogetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c). Null pointers retain the existing libSceNet `EINVAL` result. - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) stores `SO_ONESBCAST` (`0x10000`, [shadPS4's `ORBIS_NET_SO_ONESBCAST`](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net_types.h)) per socket and `sceNetGetsockopt` reads back the value that was set, as shadPS4 does; PPSA34501 sets it and crashes later when it fails with `EOPNOTSUPP`. Sends ignore it. FreeBSD 11.4's [ip(4)](https://man.freebsd.org/cgi/man.cgi?manpath=FreeBSD+11.4-RELEASE&query=ip&sektion=4) documents the like-named `IPPROTO_IP` option `IP_ONESBCAST`: a datagram sent to an interface's directed broadcast address, such as 192.168.2.255, leaves through that interface with its destination rewritten to 255.255.255.255, and only on a socket allowed to set `SO_BROADCAST`. Here host routing selects the outgoing interface and the destination is not rewritten. shadPS4 also leaves sends unchanged, and its [TODO](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/net/net.cpp#L2467-L2470) reads the option the other way round: a send to 255.255.255.255 would be converted to the interface's broadcast address. Whether the console's `SOL_SOCKET` option behaves like `IP_ONESBCAST`, and what it reads back, is not verified on hardware. FreeBSD 11.4's `sogetopt()` reads several boolean socket options back as their flag bit (`so_options & optname`, `sys/kern/uipc_socket.c`), not as the value set; whether the console does so for `0x10000` is unknown - [SO_REUSEADDR and SO_BROADCAST readback](../../../core/libs/prx/libSceNet/Export.cpp) follows FreeBSD 11.4's [sogetopt](https://github.com/freebsd/freebsd-src/blob/releng/11.4/sys/kern/uipc_socket.c#L2674-L2688): enabled options return their flag bit, and output is truncated to the supplied length. PS5 results are unverified. Values reflect the native socket; host restrictions on setting the options are unchanged. Null pointers retain libSceNet's existing `EINVAL` result. Windows passes `SO_REUSEADDR` directly to Winsock, which permits a second bind to an occupied address and port under its [reuse rules](https://learn.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse), unlike FreeBSD's separate `SO_REUSEPORT` requirement for duplicate unicast binds. Guest `SO_REUSEPORT` (`0x200`) still fails with `EOPNOTSUPP`. - [sceNetGetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) reads `SO_ERROR` from the host socket. Winsock does not clear a socket's pending error when it is read, so on Windows the library reports a pending error once and shows it again only after a new `sceNetConnect`; a second failure with the same code on the same socket without a `sceNetConnect` in between would be hidden. From 4be44fade08445f01c37cd3d67798d49f99778c8 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:32:03 -0300 Subject: [PATCH 2/3] test(net): cover inline OOB accept inheritance --- core/libs/tests/GuestNetOobInline.cpp | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/core/libs/tests/GuestNetOobInline.cpp b/core/libs/tests/GuestNetOobInline.cpp index 4080b61d00..995a40ed5e 100644 --- a/core/libs/tests/GuestNetOobInline.cpp +++ b/core/libs/tests/GuestNetOobInline.cpp @@ -47,9 +47,51 @@ static void CheckFlag(int socket, int expected) { } } +static void CheckInheritance(int family, int beforeListen, bool changeAfterListen) { + std::fprintf(stderr, "inheritance family=%d beforeListen=%d changeAfterListen=%d\n", + family, beforeListen, changeAfterListen); + const int listener = sceNetSocket("inheritance-listener", family, 1, 6); + const int client = sceNetSocket("inheritance-client", family, 1, 6); + Require(listener >= 0 && client >= 0); + Require(sceNetSetsockopt(listener, 0xffff, 0x100, &beforeListen, sizeof(beforeListen)) == 0); + CheckFlag(listener, beforeListen != 0 ? 256 : 0); + std::array address{}; + std::uint32_t size = family == 2 ? 16 : 28; + address[0] = size; + address[1] = family; + if (family == 2) { address[4] = 127; address[7] = 1; } + else address[23] = 1; + Require(sceNetBind_nid_postfix(listener, address.data(), size) == 0); + Require(sceNetListen(listener, 1) == 0); + const int setting = changeAfterListen ? 1 - beforeListen : beforeListen; + if (changeAfterListen) + Require(sceNetSetsockopt(listener, 0xffff, 0x100, &setting, sizeof(setting)) == 0); + CheckFlag(listener, setting != 0 ? 256 : 0); + Require(sceNetGetsockname(listener, address.data(), &size) == 0); + Require(sceNetConnect(client, address.data(), size) == 0); + const int accepted = sceNetAccept(listener, nullptr, nullptr); + Require(accepted >= 0); + CheckFlag(accepted, setting != 0 ? 256 : 0); + CheckFlag(client, 0); + const int opposite = 1 - setting; + Require(sceNetSetsockopt(listener, 0xffff, 0x100, &opposite, sizeof(opposite)) == 0); + CheckFlag(listener, opposite != 0 ? 256 : 0); + CheckFlag(accepted, setting != 0 ? 256 : 0); + Require(sceNetSetsockopt(listener, 0xffff, 0x100, &setting, sizeof(setting)) == 0); + Require(sceNetSetsockopt(accepted, 0xffff, 0x100, &opposite, sizeof(opposite)) == 0); + CheckFlag(accepted, opposite != 0 ? 256 : 0); + CheckFlag(listener, setting != 0 ? 256 : 0); + CheckFlag(client, 0); + for (const int socket : {accepted, client, listener}) Require(sceNetSocketClose(socket) == 0); +} + int main() { Require(sceNetInit_nid_postfix() == 0); for (const int family : {2, 28}) { + for (const int beforeListen : {0, 1}) { + for (const bool changeAfterListen : {false, true}) + CheckInheritance(family, beforeListen, changeAfterListen); + } const int datagram = sceNetSocket("inline-datagram", family, 2, 17); const int listener = sceNetSocket("inline-listener", family, 1, 6); const int client = sceNetSocket("inline-client", family, 1, 6); From 6c0bb5a3434acd1accc0170950a65b947235d2d3 Mon Sep 17 00:00:00 2001 From: Queuze <58829992+lucaslima430@users.noreply.github.com> Date: Sun, 25 Oct 2026 07:28:20 -0300 Subject: [PATCH 3/3] test(net): use canonical public declarations --- core/libs/tests/GuestNetOobInline.cpp | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/core/libs/tests/GuestNetOobInline.cpp b/core/libs/tests/GuestNetOobInline.cpp index 995a40ed5e..6a8e4c4cf5 100644 --- a/core/libs/tests/GuestNetOobInline.cpp +++ b/core/libs/tests/GuestNetOobInline.cpp @@ -11,23 +11,9 @@ #include #include #include -#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNet/libSceNet.hpp" #include "prx/libkernel/Socket/include/SocketPoll.hpp" -extern "C" { -int APS5_VABI sceNetInit_nid_postfix(); -int APS5_VABI sceNetSocket(const char*, int, int, int); -int APS5_VABI sceNetSocketClose(int); -int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); -int APS5_VABI sceNetGetsockopt(int, int, int, void*, std::uint32_t*); -int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); -int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); -int APS5_VABI sceNetListen(int, int); -int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); -int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); -std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); -int* APS5_VABI sceNetErrnoLoc(); -} static void Require(bool condition, std::source_location location = std::source_location::current()) { if (!condition) {