diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 93f56d7b5..d1825c3d0 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -82,6 +82,7 @@ constexpr int NET_EPOLL_CTL_DEL = 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_SO_REUSEADDR = 0x4; constexpr int NET_SO_BROADCAST = 0x20; @@ -104,8 +105,8 @@ bool initialize_sockets() { return result == 0; } void close_socket(NativeSocket socket) { closesocket(socket); } -int native_error() { - switch (WSAGetLastError()) { +int net_error(int code) { + switch (code) { case WSAEWOULDBLOCK: return NET_EAGAIN; case WSAEINPROGRESS: return NET_EINPROGRESS; case WSAEALREADY: return NET_EALREADY; @@ -130,6 +131,7 @@ int native_error() { default: return 5; } } +int native_error() { return net_error(WSAGetLastError()); } #else using NativeSocket = int; using NativeLength = socklen_t; @@ -137,8 +139,8 @@ 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 net_error(int code) { + switch (code) { case EAGAIN: return NET_EAGAIN; #if EWOULDBLOCK != EAGAIN case EWOULDBLOCK: return NET_EAGAIN; @@ -166,6 +168,7 @@ int native_error() { default: return 5; } } +int native_error() { return net_error(errno); } #endif struct NativeSocketHandle { @@ -241,6 +244,7 @@ struct Sock { bool aborted = false; int rcv_timeout_us = 0; int snd_timeout_us = 0; + int reported_error = 0; }; #ifdef _WIN32 @@ -756,6 +760,7 @@ int APS5_VABI sceNetConnect(int s, const void* addr, uint32_t addrlen) { std::lock_guard lk(g_mutex); auto it = g_socks.find(s); if (it == g_socks.end()) return fail(NET_EBADF); + it->second.reported_error = 0; socket = it->second; } sockaddr_storage native{}; @@ -992,6 +997,21 @@ int APS5_VABI sceNetGetsockopt(int s, int level, int optname, void* optval, uint 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; + case NET_SO_ERROR: { + int pending = 0; + NativeLength size = sizeof(pending); + if (::getsockopt(socket.native->value, SOL_SOCKET, SO_ERROR, reinterpret_cast(&pending), &size) != 0) + return fail(native_error()); +#ifdef _WIN32 + std::lock_guard lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) return fail(NET_EBADF); + if (pending != 0 && pending == it->second.reported_error) pending = 0; + else if (pending != 0) it->second.reported_error = pending; +#endif + value = pending == 0 ? 0 : net_error(pending); + break; + } default: return fail(NET_EOPNOTSUPP); } std::memcpy(optval, &value, sizeof(value)); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index 96d7ded24..fb8e890bb 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -463,6 +463,36 @@ static void CheckIpv6SocketsAreIpv6Only() { Require(sceNetSocketClose(inet) == 0); } +static void CheckSocketError() { + std::array address{16, 2, 0, 0, 127, 0, 0, 1}; + const int listener = sceNetSocket(nullptr, 2, 1, 6); + Require(listener >= 0 && sceNetBind_nid_postfix(listener, address.data(), address.size()) == 0 && sceNetListen(listener, 1) == 0); + std::uint32_t size = address.size(); + Require(sceNetGetsockname(listener, address.data(), &size) == 0 && size == address.size()); + const int connected = sceNetSocket(nullptr, 2, 1, 6); + Require(connected >= 0 && sceNetConnect(connected, address.data(), address.size()) == 0); + int error = -1; + size = sizeof(error); + Require(sceNetGetsockopt(connected, 0xffff, 0x1007, &error, &size) == 0 && size == sizeof(error) && error == 0); + for (const int descriptor : {connected, listener}) Require(sceNetSocketClose(descriptor) == 0); + + const int refused = sceNetSocket(nullptr, 2, 1, 6); + const int nonblocking = 1; + Require(refused >= 0 && sceNetSetsockopt(refused, 0xffff, 0x1200, &nonblocking, sizeof(nonblocking)) == 0); + const int started = sceNetConnect(refused, address.data(), address.size()); + Require(started == static_cast(0x80410124) && *sceNetErrnoLoc() == 36); + error = 0; + for (int attempt = 0; attempt < 500 && error == 0; ++attempt) { + size = sizeof(error); + Require(sceNetGetsockopt(refused, 0xffff, 0x1007, &error, &size) == 0 && size == sizeof(error)); + if (error == 0) std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + Require(error == 61); + size = sizeof(error); + Require(sceNetGetsockopt(refused, 0xffff, 0x1007, &error, &size) == 0 && error == 0); + Require(sceNetSocketClose(refused) == 0); +} + int main() { const auto epoch = std::chrono::steady_clock::time_point{}; Require(KernelSocketSelect::DeadlineAfter(epoch, 2, 250000) == epoch + std::chrono::milliseconds(2250)); @@ -495,6 +525,7 @@ int main() { Require(close_nid_postfix(closed) == 0); Require(Failed(sceNetSocketClose(closed), 9)); CheckKernelTimeouts(); + CheckSocketError(); const int listener = sceNetSocket(nullptr, 2, 1, 6); Require(listener >= 0); diff --git a/docs/dev/debt/libSceNet.md b/docs/dev/debt/libSceNet.md index c70d4323d..5384786b4 100644 --- a/docs/dev/debt/libSceNet.md +++ b/docs/dev/debt/libSceNet.md @@ -9,5 +9,6 @@ ### Functional - [sceNetSetsockopt](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) sets `SO_RCVTIMEO` and `SO_SNDTIMEO` as the host socket's timeouts. On Windows a receive or send on a stream socket that times out fails with `ETIMEDOUT`, where FreeBSD returns `EAGAIN` or the partial count: Winsock leaves the connection in an indeterminate state after a timeout and reports no partial count ([SOL_SOCKET options](https://learn.microsoft.com/en-us/windows/win32/winsock/sol-socket-socket-options)), so FreeBSD's behaviour needs the timeout waited for without the host option. A receive on a datagram socket that times out fails with `EAGAIN` +- [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. - [sceNetSendmsg and sceNetRecvmsg](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported - [sceNetResolverAbort and sceNetResolverStartNtoaMultipleRecords](../../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) follow shadPS4 ([net.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net.cpp), [net_resolver.cpp](https://github.com/shadps4-emu/shadPS4/blob/811bfcd7d17ed8ae975c477d9ff28abc744e1ca8/src/core/libraries/net/net_resolver.cpp)); the `(int rid, int flags)` signature of the abort also matches [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Net/Socket.cs). Unverified on hardware. Lookups run synchronously over the host resolver, which cannot be interrupted: an abort while a lookup runs on another thread returns at once, and that lookup fails with `EINTR` without writing its output only when the host call returns; the abort flags are then ignored, as shadPS4 does for a running lookup. With nothing running, abort flag 1 (NTOA preservation) or 2 (ATON preservation) makes the next `sceNetResolverStartNtoa*` or `sceNetResolverStartAton` call on that resolver fail with `EINTR`, reported by `sceNetResolverGetError`, after its arguments are checked. Unlike shadPS4, which ignores them, other abort flag bits throw, and `sceNetResolverStartNtoaMultipleRecords` throws for any flag, including async (1) and 0x10000 (IP address literals disabled), as `sceNetResolverStartNtoaMultipleRecordsEx` does