fix(libSceNet): report SO_ERROR from sceNetGetsockopt

This commit is contained in:
babyhuey 2026-10-13 02:55:31 -04:00
parent c2565f0149
commit 0daf1d5ba3
3 changed files with 56 additions and 4 deletions

View File

@ -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<char*>(&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));

View File

@ -463,6 +463,36 @@ static void CheckIpv6SocketsAreIpv6Only() {
Require(sceNetSocketClose(inet) == 0);
}
static void CheckSocketError() {
std::array<std::uint8_t, 16> 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<int>(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);

View File

@ -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