From b7a6823f39a795729b76254a40e714e64e70e117 Mon Sep 17 00:00:00 2001 From: RockYou-cmd <59610755+RockYou-cmd@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:38:04 +0000 Subject: [PATCH] fix(net): implement SO_ERROR for connection completion --- core/libs/prx/libSceNet/CMakeLists.txt | 10 ++ core/libs/prx/libSceNet/Export.cpp | 66 ++++++++- core/libs/tests/GuestSceNetConnect.cpp | 191 +++++++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 262 insertions(+), 6 deletions(-) create mode 100644 core/libs/tests/GuestSceNetConnect.cpp diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index 2516da1e1a0c..8895552bce31 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -13,4 +13,14 @@ if(BUILD_TESTING) set_tests_properties(guest_sce_net PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sce_net_connect_tests ${LIBS_INCLUDE_DIR}/tests/GuestSceNetConnect.cpp) + target_include_directories(guest_sce_net_connect_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sce_net_connect_tests PRIVATE libSceNet libc) + configure_windows_unwind(guest_sce_net_connect_tests) + add_test(NAME guest_sce_net_connect COMMAND guest_sce_net_connect_tests) + set_tests_properties(guest_sce_net_connect PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_sce_net_connect PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 05a06ea00cc1..4d9e56fd391a 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -72,6 +72,7 @@ constexpr int NET_SOCK_RAW = 3; constexpr int NET_SOL_SOCKET = 0xFFFF; constexpr int NET_SO_SNDTIMEO = 0x1005; constexpr int NET_SO_RCVTIMEO = 0x1006; +constexpr int NET_SO_ERROR = 0x1007; constexpr int NET_SO_NBIO = 0x1200; constexpr int NET_MSG_PEEK = 0x2; constexpr int NET_MSG_TRUNC = 0x10; @@ -90,8 +91,9 @@ bool initialize_sockets() { return result == 0; } void close_socket(NativeSocket socket) { closesocket(socket); } -int native_error() { - switch (WSAGetLastError()) { +int native_error(int error) { + switch (error) { + case 0: return 0; case WSAEWOULDBLOCK: return NET_EAGAIN; case WSAEINPROGRESS: return NET_EINPROGRESS; case WSAEALREADY: return NET_EALREADY; @@ -115,6 +117,7 @@ int native_error() { default: return 5; } } +int native_error() { return native_error(WSAGetLastError()); } #else using NativeSocket = int; using NativeLength = socklen_t; @@ -122,8 +125,9 @@ constexpr NativeSocket INVALID_NATIVE_SOCKET = -1; constexpr int NATIVE_SEND_FLAGS = MSG_NOSIGNAL; bool initialize_sockets() { return true; } void close_socket(NativeSocket socket) { ::close(socket); } -int native_error() { - switch (errno) { +int native_error(int error) { + switch (error) { + case 0: return 0; case EAGAIN: return NET_EAGAIN; #if EWOULDBLOCK != EAGAIN case EWOULDBLOCK: return NET_EAGAIN; @@ -151,10 +155,16 @@ int native_error() { default: return 5; } } +int native_error() { return native_error(errno); } #endif struct NativeSocketHandle { NativeSocket value; +#ifdef _WIN32 + std::mutex connectMutex; + bool connectPending = false; + int consumedConnectError = 0; +#endif explicit NativeSocketHandle(NativeSocket socket) : value(socket) {} ~NativeSocketHandle() { if (value != INVALID_NATIVE_SOCKET) close_socket(value); @@ -568,11 +578,29 @@ int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { NativeLength native_length = 0; if (!guest_to_native_address(addr, addrlen, native, native_length)) return fail(NET_EINVAL); if (native.ss_family != (socket.family == NET_AF_INET ? AF_INET : AF_INET6)) return fail(NET_EAFNOSUPPORT); - if (::connect(socket.native->value, reinterpret_cast(&native), native_length) == 0) return 0; #ifdef _WIN32 - if (WSAGetLastError() == WSAEWOULDBLOCK) return fail(NET_EINPROGRESS); + std::lock_guard connectionLock(socket.native->connectMutex); +#endif + if (::connect(socket.native->value, reinterpret_cast(&native), native_length) == 0) { +#ifdef _WIN32 + socket.native->connectPending = false; + socket.native->consumedConnectError = 0; #endif + return 0; + } +#ifdef _WIN32 + const int error = WSAGetLastError(); + if (error == WSAEWOULDBLOCK) { + socket.native->connectPending = socket.type == NET_SOCK_STREAM; + socket.native->consumedConnectError = 0; + } else if (error != WSAEALREADY && error != WSAEISCONN && error != WSAEINVAL) { + socket.native->connectPending = false; + socket.native->consumedConnectError = 0; + } + return fail(error == WSAEWOULDBLOCK ? NET_EINPROGRESS : native_error(error)); +#else return fail(native_error()); +#endif } int64_t APS5_VABI sceNetRecv(int s, void* buf, size_t len, int flags) { @@ -794,6 +822,32 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint if (level != NET_SOL_SOCKET) return fail(NET_EOPNOTSUPP); int value = 0; switch (optname) { + case NET_SO_ERROR: { +#ifdef _WIN32 + std::lock_guard connectionLock(socket.native->connectMutex); +#endif + int error = 0; + NativeLength size = sizeof(error); + if (::getsockopt(socket.native->value, SOL_SOCKET, SO_ERROR, + reinterpret_cast(&error), &size) != 0) return fail(native_error()); +#ifdef _WIN32 + if (error != 0 && error == socket.native->consumedConnectError) { + error = 0; + } else if (socket.native->connectPending) { + if (error != 0) { + socket.native->consumedConnectError = error; + socket.native->connectPending = false; + } else { + sockaddr_storage peer{}; + NativeLength peerSize = sizeof(peer); + if (::getpeername(socket.native->value, reinterpret_cast(&peer), &peerSize) == 0) + socket.native->connectPending = false; + } + } +#endif + value = native_error(error); + break; + } case NET_SO_NBIO: value = socket.nonblock ? 1 : 0; break; case NET_SO_RCVTIMEO: value = socket.rcv_timeout_us; break; case NET_SO_SNDTIMEO: value = socket.snd_timeout_us; break; diff --git a/core/libs/tests/GuestSceNetConnect.cpp b/core/libs/tests/GuestSceNetConnect.cpp new file mode 100644 index 000000000000..7ac00b6c3315 --- /dev/null +++ b/core/libs/tests/GuestSceNetConnect.cpp @@ -0,0 +1,191 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNetInit_nid_postfix(); +int APS5_VABI sceNetSocket(const char*, int, int, int); +int APS5_VABI sceNetBind_nid_postfix(int, const void*, std::uint32_t); +int APS5_VABI sceNetListen(int, int); +int APS5_VABI sceNetGetsockname(int, void*, std::uint32_t*); +int APS5_VABI sceNetConnect(int, const void*, std::uint32_t); +int APS5_VABI sceNetAccept(int, void*, std::uint32_t*); +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*); +std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); +int* APS5_VABI sceNetErrnoLoc(); +int APS5_VABI sceNetEpollCreate(const char*, int); +int APS5_VABI sceNetEpollControl(int, int, int, const NetEpollEvent*); +int APS5_VABI sceNetEpollWait(int, NetEpollEvent*, int, int); +int APS5_VABI sceNetEpollDestroy(int); +} + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +static bool Failed(std::int64_t result, int error) { + return result == static_cast(0x80410100u | static_cast(error)) && *sceNetErrnoLoc() == error; +} + +struct BoundSocket { + int descriptor; + std::array address; +}; + +static BoundSocket BindLoopback() { + BoundSocket socket{sceNetSocket("connect-test", 2, 1, 6), {16, 2, 0, 0, 127, 0, 0, 1}}; + Require(socket.descriptor >= 0, "create bound socket"); + Require(sceNetBind_nid_postfix(socket.descriptor, socket.address.data(), socket.address.size()) == 0, "bind loopback"); + std::uint32_t size = socket.address.size(); + Require(sceNetGetsockname(socket.descriptor, socket.address.data(), &size) == 0 && size == socket.address.size(), "get bound address"); + return socket; +} + +static int NonblockingSocket() { + const int socket = sceNetSocket("connect-test", 2, 1, 6); + Require(socket >= 0, "create client socket"); + const int enabled = 1; + Require(sceNetSetsockopt(socket, 0xffff, 0x1200, &enabled, sizeof(enabled)) == 0, "enable nonblocking"); + return socket; +} + +static std::uint32_t WaitForConnection(int socket) { + const int epoll = sceNetEpollCreate("connect-test", 0); + Require(epoll >= 0, "create epoll"); + NetEpollEvent registration{}; + registration.events = 4; + registration.ident = static_cast(socket); + Require(sceNetEpollControl(epoll, 1, socket, ®istration) == 0, "register connection"); + NetEpollEvent ready{}; + const int count = sceNetEpollWait(epoll, &ready, 1, 5000000); + Require(sceNetEpollDestroy(epoll) == 0, "destroy epoll"); + Require(count == 1 && ready.ident == static_cast(socket), "connection readiness within five seconds"); + return ready.events; +} + +static void CheckSocketError(int socket, int expected, const char* phase) { + std::array output{-1, 12345}; + std::uint32_t size = sizeof(output); + *sceNetErrnoLoc() = 123; + const int result = sceNetGetsockopt(socket, 0xffff, 0x1007, output.data(), &size); + std::fprintf(stderr, "SO_ERROR [%s]: result=0x%08x, value=%d, expected=%d, length=%u, guest errno=%d\n", + phase, static_cast(result), output[0], expected, size, *sceNetErrnoLoc()); + Require(result == 0, "query SO_ERROR"); + Require(output[0] == expected, "SO_ERROR guest error value"); + Require(output[1] == 12345 && size == sizeof(int), "SO_ERROR output bounds and length"); + Require(*sceNetErrnoLoc() == 123, "SO_ERROR success preserves thread errno"); +} + +static void CheckInvalidArguments(int socket) { + int value = -1; + std::uint32_t size = sizeof(value); + Require(Failed(sceNetGetsockopt(socket, 0xffff, 0x1007, nullptr, &size), 22), "SO_ERROR null output"); + Require(size == sizeof(value), "null output preserves length"); + Require(Failed(sceNetGetsockopt(socket, 0xffff, 0x1007, &value, nullptr), 22), "SO_ERROR null length"); + size = sizeof(value) - 1; + Require(Failed(sceNetGetsockopt(socket, 0xffff, 0x1007, &value, &size), 22), "SO_ERROR short output"); + Require(size == sizeof(value) - 1 && value == -1, "short output is unchanged"); + size = sizeof(value); + Require(Failed(sceNetGetsockopt(-1, 0xffff, 0x1007, &value, &size), 9), "SO_ERROR invalid descriptor"); + Require(Failed(sceNetGetsockopt(socket, 0, 0x1007, &value, &size), 45), "SO_ERROR unsupported level"); + Require(Failed(sceNetGetsockopt(socket, 0xffff, 0x7fffffff, &value, &size), 45), "unsupported socket option"); + Require(value == -1 && size == sizeof(value), "invalid query preserves output"); +} + +static void CheckSuccessfulConnection() { + const auto listener = BindLoopback(); + Require(sceNetListen(listener.descriptor, 1) == 0, "listen"); + const int client = NonblockingSocket(); + const int result = sceNetConnect(client, listener.address.data(), listener.address.size()); + Require(result == 0 || Failed(result, 36), "nonblocking connect reports success or EINPROGRESS"); + Require((WaitForConnection(client) & 4) != 0, "connected socket becomes writable"); + CheckInvalidArguments(client); + CheckSocketError(client, 0, "connected, first read"); + CheckSocketError(client, 0, "connected, second read"); + const int accepted = sceNetAccept(listener.descriptor, nullptr, nullptr); + Require(accepted >= 0, "accept connected client"); + char value = 0; + Require(Failed(sceNetRecv(client, &value, sizeof(value), 0), 35), "empty nonblocking receive still reports EAGAIN"); + Require(sceNetSocketClose(accepted) == 0, "close accepted socket"); + Require(sceNetSocketClose(client) == 0, "close connected client"); + Require(sceNetSocketClose(listener.descriptor) == 0, "close listener"); + std::fprintf(stderr, "PASS: nonblocking success, SO_ERROR arguments, and receive EAGAIN\n"); +} + +#ifdef _WIN32 +static void CheckConnectionRetry(int client, const BoundSocket& refused) { + const int retry = sceNetConnect(client, refused.address.data(), refused.address.size()); + if (Failed(retry, 36)) { + Require((WaitForConnection(client) & (4 | 8 | 16)) != 0, "retried refusal becomes ready"); + std::array errors{-1, -1}; + const auto readError = [&](int index) { + std::uint32_t size = sizeof(int); + Require(sceNetGetsockopt(client, 0xffff, 0x1007, &errors[index], &size) == 0, + "concurrent SO_ERROR query"); + Require(size == sizeof(int), "concurrent SO_ERROR length"); + }; + std::thread first(readError, 0); + std::thread second(readError, 1); + first.join(); + second.join(); + std::fprintf(stderr, "SO_ERROR [retry, concurrent reads]: values=%d,%d, expected=61,0 in either order\n", + errors[0], errors[1]); + Require((errors[0] == 61 && errors[1] == 0) || (errors[0] == 0 && errors[1] == 61), + "a new connection error is consumed exactly once"); + CheckSocketError(client, 0, "retry, third read"); + std::fprintf(stderr, "PASS: same-socket refusal retry and concurrent read-and-clear\n"); + } else { + Require(Failed(retry, 61), "retried connect reports ECONNREFUSED"); + std::fprintf(stderr, "PASS: immediate retry refusal (concurrent SO_ERROR path not exercised)\n"); + } + const auto listener = BindLoopback(); + Require(sceNetListen(listener.descriptor, 1) == 0, "listen for retry"); + const int connected = sceNetConnect(client, listener.address.data(), listener.address.size()); + Require(connected == 0 || Failed(connected, 36), "retry to listening endpoint starts"); + Require((WaitForConnection(client) & 4) != 0, "retry connects to listening endpoint"); + CheckSocketError(client, 0, "successful retry, first read"); + CheckSocketError(client, 0, "successful retry, second read"); + const int accepted = sceNetAccept(listener.descriptor, nullptr, nullptr); + Require(accepted >= 0, "accept retried client"); + char value = 0; + Require(Failed(sceNetRecv(client, &value, sizeof(value), 0), 35), "retried connection receive EAGAIN"); + Require(sceNetSocketClose(accepted) == 0, "close accepted retry"); + Require(sceNetSocketClose(listener.descriptor) == 0, "close retry listener"); + std::fprintf(stderr, "PASS: same-socket retry connects successfully\n"); +} +#endif + +static void CheckRefusedConnection() { + const auto reserved = BindLoopback(); + const int client = NonblockingSocket(); + Require(sceNetSocketClose(reserved.descriptor) == 0, "release refused endpoint"); + const int result = sceNetConnect(client, reserved.address.data(), reserved.address.size()); + if (Failed(result, 36)) { + Require((WaitForConnection(client) & (4 | 8 | 16)) != 0, "refused connection becomes ready"); + CheckInvalidArguments(client); + CheckSocketError(client, 61, "refused, first read"); + CheckSocketError(client, 0, "refused, second read"); + std::fprintf(stderr, "PASS: asynchronous refusal, error translation, and read-and-clear\n"); + } else { + Require(Failed(result, 61), "immediate refusal reports ECONNREFUSED"); + std::fprintf(stderr, "PASS: immediate refusal (asynchronous SO_ERROR path not exercised)\n"); + } +#ifdef _WIN32 + CheckConnectionRetry(client, reserved); +#endif + Require(sceNetSocketClose(client) == 0, "close refused client"); +} + +int main() { + Require(sceNetInit_nid_postfix() == 0, "initialize networking"); + CheckSuccessfulConnection(); + CheckRefusedConnection(); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 8f2178189d5b..317105551643 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -361,3 +361,4 @@ Throughout the project, every function at every stage either **does exactly what - [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require one call per ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and one later `s_setpc_b64` return reading that exact pair. Regions must nest or be disjoint; recursion, shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented. - [s_call_b64](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp) saves `PC + 4` over `GetShaderBase` and branches to `PC + 4 + signext(SIMM16) * 4`, following [AMD RDNA2 ISA](https://gpuopen.com/download/rdna2-shader-instruction-set-architecture.pdf) sections 3.6.3, 12.2 and 12.3. Forward callees after `s_endpgm` are decoded. Native RDNA2/PS5 execution and title compatibility for these bounded scalar calls remain unverified; `s_rfe_b64` still throws in translation. - [sceVideodec2CreateDecoder](../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) decodes HEVC (codec type 974921) and VP9 (2382845) through FFmpeg, besides H.264. The tags come from SharpProspero's `Videodec2CodecType` and EVO-PLAYER-PS5's `sce_videodec2.h`, which records them from hardware runs. HEVC input is converted from length-prefixed NAL units to Annex B as for H.264, and VP9 frames are passed through. Output is the same 8-bit NV12 for every codec; a decoder for HEVC Main10 or VP9 Profile 2 is created, but a frame deeper than 8 bits throws, since the console's 10-bit output layout is unknown, and `QueryDecoderMemoryInfo` reports the 8-bit frame size for it. `sceVideodec2GetPictureInfo` and `sceVideodec2GetAvcPictureInfo` throw for HEVC and VP9 pictures, which use other layouts (`SceVideodec2HevcPictureInfo`, `SceVideodec2Vp9PictureInfo`), and `sceVideodec2GetHevcPictureInfo` and `sceVideodec2GetVp9PictureInfo` are not exported +- [Socket error queries](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - `sceNetGetsockopt` SO_ERROR (0x1007, an int containing the guest errno, read and cleared) follows the FreeBSD 11 socket ABI; not confirmed on PS5 hardware or a title. Windows 10 build 19045.7417 repeatedly returns WSAECONNREFUSED for a failed nonblocking connection. A per-native-socket lock and consumed completion error provide read-and-clear for asynchronous TCP connection failures, reset when a new attempt starts. Once getpeername confirms a peer, tracking stops; established-socket and UDP errors retain host behaviour.