From 3ee22fbd3dfb751fa91e1dd12061f7c64cb281a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Wed, 28 Oct 2026 09:13:27 +0100 Subject: [PATCH] sceNetInet: TCP socket states and bind's port sharing Following pspautotests net/inet/errors, states and reuse: - bind decides itself whether a port is in use, as 4.4BSD's in_pcbbind() does with SO_REUSEADDR and SO_REUSEPORT. The host can't, since its sockets have both on for running several instances, so any number of sockets could bind the same port. A socket that's bound already is EINVAL. - Shutting down the sending side of an unconnected TCP socket, or a failed connect, throws away its TCP state as in 4.4BSD's tcp_usrreq(), after which bind, listen, connect, accept and shutdown fail with EINVAL, and its port is free again. shutdown otherwise only fails for a bad how, and still reaches the host, so a thread waiting in accept wakes up. - listen on an unbound TCP socket binds one, which Winsock didn't, and on a connected one succeeds. - connect on a listening socket is EOPNOTSUPP. Winsock's WSAEINVAL for it looked like a connect in progress, and a blocking socket waited forever. A second connect on a loopback connection the host hasn't noticed finishing is EISCONN once it has. Co-Authored-By: Claude Opus 5.5 --- Core/HLE/SocketManager.cpp | 3 + Core/HLE/SocketManager.h | 13 +++- Core/HLE/sceNetInet.cpp | 133 ++++++++++++++++++++++++++++++++++--- test.py | 2 + 4 files changed, 138 insertions(+), 13 deletions(-) diff --git a/Core/HLE/SocketManager.cpp b/Core/HLE/SocketManager.cpp index 01d9d325ee..85dffeb311 100644 --- a/Core/HLE/SocketManager.cpp +++ b/Core/HLE/SocketManager.cpp @@ -87,6 +87,9 @@ InetSocket *SocketManager::AdoptSocket(int *index, SOCKET hostSocket, const Inet inetSock->addr.clear(); inetSock->port = 0; inetSock->upnpPort = 0; + inetSock->boundPort = 0; + inetSock->tcpClosed = false; + inetSock->listening = false; return inetSock; } diff --git a/Core/HLE/SocketManager.h b/Core/HLE/SocketManager.h index 597d5d911b..845f2921fb 100644 --- a/Core/HLE/SocketManager.h +++ b/Core/HLE/SocketManager.h @@ -31,10 +31,19 @@ struct InetSocket { int rcvBuf; int sndLowat; int rcvLowat; - // IP_TTL, kept here since the host's default differs (Windows' is 128, the PSP's 64). - int ipTtl; // The port bind() asked UPnP to forward, removed on close. 0 if none. u16 upnpPort; + // What the game bound the socket to, for telling when a port is in use. boundPort is 0 if it hasn't + // bound one itself. boundAddr is in network byte order. + u32 boundAddr; + u16 boundPort; + // A TCP socket whose protocol control block 4.4BSD has thrown away: after shutting down the sending + // side while not connected, or a failed connect. Most calls on it then fail with EINVAL. + bool tcpClosed; + // Whether a listen() on it succeeded. + bool listening; + // IP_TTL, kept here since the host's default differs (Windows' is 128, the PSP's 64). + int ipTtl; // Metadata for debug use only. std::string addr; int port; diff --git a/Core/HLE/sceNetInet.cpp b/Core/HLE/sceNetInet.cpp index dfe0c2f793..4d6a021ebd 100644 --- a/Core/HLE/sceNetInet.cpp +++ b/Core/HLE/sceNetInet.cpp @@ -495,6 +495,54 @@ static bool GetHostSocket(int socket, SOCKET *hostSocket) { return true; } +static bool IsTcpSocket(const InetSocket *inetSock) { + return inetSock->type == PSP_NET_INET_SOCK_STREAM; +} + +static bool IsHostSocketConnected(const InetSocket *inetSock) { + SockAddrIN4 peer{}; + socklen_t len = sizeof(peer); + return getpeername(inetSock->sock, &peer.addr, &len) == 0; +} + +// The host's port, 0 if it hasn't bound one, by itself or on request. +static u16 HostSocketPort(const InetSocket *inetSock) { + SockAddrIN4 local{}; + socklen_t len = sizeof(local); + if (getsockname(inetSock->sock, &local.addr, &len) < 0) { + return 0; + } + return ntohs(local.in.sin_port); +} + +// A call on a TCP socket whose control block is gone (see InetSocket::tcpClosed). +static int InetTcpClosed(int socket, const char *func) { + SetInetErrno(ERROR_INET_EINVAL, func); + return hleLogError(Log::sceNet, -1, "socket #%d has no TCP state left", socket); +} + +// Whether another socket has the port, as 4.4BSD's in_pcbbind() decides (pspautotests net/inet/reuse): +// without SO_REUSEADDR or SO_REUSEPORT, a socket on that port whose address is the same or either +// is INADDR_ANY; with one, only the same address. Then SO_REUSEPORT on both lets them share. The host +// sockets all have both options, for running several instances, so the host can't tell. +static bool IsInetPortInUse(const InetSocket *inetSock, u32 addr, u16 port) { + const bool wildcard = (inetSock->soOptions & (PSP_NET_INET_SO_REUSEADDR | PSP_NET_INET_SO_REUSEPORT)) == 0; + for (int i = SocketManager::MIN_VALID_INET_SOCKET; i < SocketManager::VALID_INET_SOCKET_COUNT; i++) { + const InetSocket &other = g_socketManager.Sockets()[i]; + // A socket without TCP state has given its port up, as 4.4BSD's in_pcbdetach() does. + if (&other == inetSock || other.state != SocketState::UsedNetInet || other.boundPort != port || IsTcpSocket(&other) != IsTcpSocket(inetSock) || other.tcpClosed) { + continue; + } + if (other.boundAddr != addr && (!wildcard || (other.boundAddr != INADDR_ANY && addr != INADDR_ANY))) { + continue; + } + if ((inetSock->soOptions & other.soOptions & PSP_NET_INET_SO_REUSEPORT) == 0) { + return true; + } + } + return false; +} + // Sets errno for a failed send. An unconnected UDP socket sending without an address is EDESTADDRREQ, // which Winsock calls ENOTCONN (pspautotests net/inet/errors and msg). static void SetSendErrno(SceUID threadID, const InetSocket *inetSock, int hostErrno, const char *func) { @@ -1261,16 +1309,26 @@ static int sceNetInetBind(MIPSState *mips, int socket, u32 namePtr, int namelen) return hleLogError(Log::sceNet, -1, "bad address %08x", namePtr); } SceNetInetSockaddr* name = (SceNetInetSockaddr*)Memory::GetPointerOrException(namePtr); - // (Only lengths under 16 have been seen to fail. 4.4BSD fails any but 16.) - if (namelen < (int)sizeof(SceNetInetSockaddr)) { - SetInetErrno(ERROR_INET_EINVAL, __FUNCTION__); - return hleLogError(Log::sceNet, -1, "namelen %d", namelen); + // In 4.4BSD's in_pcbbind() order (pspautotests net/inet/errors and reuse). + if (IsTcpSocket(inetSock) && inetSock->tcpClosed) { + return InetTcpClosed(socket, __FUNCTION__); } - if (name->sa_family != PSP_NET_INET_AF_INET && ((const SceNetInetSockaddrIn *)name)->sin_addr != INADDR_ANY) { - // Not found among the local addresses, whose family is compared too (pspautotests net/inet/errors). + // (Only lengths under 16 have been seen to fail. 4.4BSD fails any but 16.) + if (inetSock->boundPort != 0 || HostSocketPort(inetSock) != 0 || namelen < (int)sizeof(SceNetInetSockaddr)) { + SetInetErrno(ERROR_INET_EINVAL, __FUNCTION__); + return hleLogError(Log::sceNet, -1, "already bound, or namelen %d", namelen); + } + const u32 requestedAddr = ((const SceNetInetSockaddrIn *)name)->sin_addr; + const u16 requestedPort = ntohs(((const SceNetInetSockaddrIn *)name)->sin_port); + if (name->sa_family != PSP_NET_INET_AF_INET && requestedAddr != INADDR_ANY) { + // Not found among the local addresses, whose family is compared too. SetInetErrno(ERROR_INET_EADDRNOTAVAIL, __FUNCTION__); return hleLogError(Log::sceNet, -1, "family %d", name->sa_family); } + if (requestedPort != 0 && IsInetPortInUse(inetSock, requestedAddr, requestedPort)) { + SetInetErrno(ERROR_INET_EADDRINUSE, __FUNCTION__); + return hleLogError(Log::sceNet, -1, "port %d in use", requestedPort); + } SockAddrIN4 saddr{}; // TODO: Should've created convertSockaddrPSP2Host (and Host2PSP too) function as it's being used pretty often, thus fixing a bug on it will be tedious when scattered all over the places saddr.addr.sa_family = name->sa_family; @@ -1310,6 +1368,8 @@ static int sceNetInetBind(MIPSState *mips, int socket, u32 namePtr, int namelen) return hleLogError(Log::sceNet, retval); } changeBlockingMode(inetSock->sock, 1); + inetSock->boundAddr = requestedAddr; + inetSock->boundPort = requestedPort; // Update binded port number if it was 0 (any port) memcpy(name->sa_data, saddr.addr.sa_data, sizeof(name->sa_data)); // Enable Port-forwarding @@ -1354,6 +1414,7 @@ static InetTry TryConnectDone(SceUID threadID, const u32 *args) { } if (sockError != 0) { UpdateErrnoFromHost(threadID, sockError, "sceNetInetConnect"); + inetSock->tcpClosed = IsTcpSocket(inetSock); return { -1, false, 0 }; } return { 0, false, 0 }; @@ -1370,6 +1431,15 @@ static int sceNetInetConnect(int socket, u32 sockAddrPtr, int sockAddrLen) { if (!InetValidRange(sockAddrPtr, sizeof(SceNetInetSockaddr), __FUNCTION__)) { return hleLogError(Log::sceNet, -1, "bad address %08x", sockAddrPtr); } + if (IsTcpSocket(inetSock) && inetSock->tcpClosed) { + return InetTcpClosed(socket, __FUNCTION__); + } + if (inetSock->listening) { + // Decided here, since Winsock fails it with WSAEINVAL, which looks like "in progress", and Linux + // with EISCONN (pspautotests net/inet/states). + SetInetErrno(ERROR_INET_EOPNOTSUPP, __FUNCTION__); + return hleLogError(Log::sceNet, -1, "listening"); + } SceNetInetSockaddr* dst = (SceNetInetSockaddr*)Memory::GetPointerOrException(sockAddrPtr); SockAddrIN4 saddr{}; int dstlen = std::min(sockAddrLen > 0 ? sockAddrLen : 0, static_cast(sizeof(saddr))); @@ -1378,7 +1448,12 @@ static int sceNetInetConnect(int socket, u32 sockAddrPtr, int sockAddrLen) { int retval = connect(inetSock->sock, (struct sockaddr*)&saddr.addr, dstlen); int hostErrno = socket_errno; - if (retval < 0 && connectInProgress(hostErrno)) { + if (retval < 0 && hostErrno == EALREADY && IsHostSocketConnected(inetSock)) { + // Done by now, though the host hasn't noticed. The PSP's loopback connects at once, so a second + // connect is EISCONN there (pspautotests net/inet/states). + hostErrno = EISCONN; + } + if (retval < 0 && connectInProgress(hostErrno) && hostErrno != EALREADY) { // A blocking socket waits for the connection to be made, or to fail. const u32 args[] = { (u32)socket }; if (StartInetWait(InetWaitOp::Connect, args, ARRAY_SIZE(args), SocketWaitUs(inetSock, 0, true))) { @@ -1386,6 +1461,10 @@ static int sceNetInetConnect(int socket, u32 sockAddrPtr, int sockAddrLen) { } } if (retval < 0) { + if (IsTcpSocket(inetSock) && !connectInProgress(hostErrno) && hostErrno != EISCONN) { + // A failed connect throws away the TCP state (pspautotests net/inet/errors). + inetSock->tcpClosed = true; + } int pspErrno = UpdateErrnoFromHost(__KernelGetCurThread(), hostErrno, __FUNCTION__); if (connectInProgress(hostErrno)) retval = hleLogDebug(Log::sceNet, retval, "errno = %s Address = %s, Port = %d", convertInetErrno2str(pspErrno), ip2str(saddr.in.sin_addr).c_str(), ntohs(saddr.in.sin_port)); @@ -1409,11 +1488,27 @@ static int sceNetInetListen(int socket, int backlog) { return InetBadSocket(socket, __FUNCTION__); } + if (IsTcpSocket(inetSock)) { + if (inetSock->tcpClosed) { + return InetTcpClosed(socket, __FUNCTION__); + } + if (IsHostSocketConnected(inetSock)) { + // 4.4BSD makes a listener of it, which the host won't (pspautotests net/inet/states). + return hleLogWarning(Log::sceNet, 0, "listen on a connected socket, ignored"); + } + if (HostSocketPort(inetSock) == 0) { + // The PSP binds one itself, as macOS and Linux do. Winsock fails instead. + SockAddrIN4 any{}; + any.in.sin_family = AF_INET; + bind(inetSock->sock, &any.addr, sizeof(any.in)); + } + } int retval = listen(inetSock->sock, (backlog == PSP_NET_INET_SOMAXCONN ? SOMAXCONN : backlog)); if (retval < 0) { UpdateErrnoFromHost(__KernelGetCurThread(), socket_errno, __FUNCTION__); return hleLogError(Log::sceNet, retval); } + inetSock->listening = true; return hleLogInfo(Log::sceNet, retval); } @@ -1425,6 +1520,10 @@ static InetTry TryAccept(SceUID threadID, const u32 *args) { UpdateErrnoFromHost(threadID, EBADF, "sceNetInetAccept"); return { -1, false, 0 }; } + if (IsTcpSocket(inetSock) && inetSock->tcpClosed) { + SetInetErrno(ERROR_INET_EINVAL, "sceNetInetAccept", threadID); + return { -1, false, 0 }; + } // The game's address is written afterwards, as much of it as its length allows. SockAddrIN4 saddr{}; @@ -1488,20 +1587,32 @@ static int sceNetInetShutdown(int socket, int how) { return InetBadSocket(socket, __FUNCTION__); } - // Convert HOW from PSP to Host + // As in 4.4BSD's soshutdown(), it can only fail on a bad how or a TCP socket with no state left + // (pspautotests net/inet/errors and states). int hostHow; switch (how) { case PSP_NET_INET_SHUT_RD: hostHow = SHUT_RD; break; case PSP_NET_INET_SHUT_WR: hostHow = SHUT_WR; break; case PSP_NET_INET_SHUT_RDWR: hostHow = SHUT_RDWR; break; default: - // pspautotests net/inet/errors and states. SetInetErrno(ERROR_INET_EINVAL, __FUNCTION__); return hleLogError(Log::sceNet, -1, "bad how %d", how); } + if (IsTcpSocket(inetSock)) { + if (inetSock->tcpClosed) { + return InetTcpClosed(socket, __FUNCTION__); + } + if (how != PSP_NET_INET_SHUT_RD && !IsHostSocketConnected(inetSock)) { + // Shutting down the sending side of an unconnected socket closes it. The host's shutdown + // still runs, to wake a thread waiting in accept() or connect(). + inetSock->tcpClosed = true; + } + } - int retVal = shutdown(inetSock->sock, hostHow); // no translation - return hleLogInfo(Log::sceNet, retVal); + if (shutdown(inetSock->sock, hostHow) < 0) { + DEBUG_LOG(Log::sceNet, "Host shutdown failed: %d", socket_errno); + } + return hleLogInfo(Log::sceNet, 0); } static int sceNetInetSocketAbort(int socket) { diff --git a/test.py b/test.py index 40c28eb0b3..288b00f0f3 100755 --- a/test.py +++ b/test.py @@ -359,6 +359,7 @@ tests_good = [ "net/inet/msg", "net/inet/limits", "net/inet/polledge", + "net/inet/reuse", "misc/libc", "misc/sdkver", "misc/testgp", @@ -678,6 +679,7 @@ tests_next = [ "net/inet/poll", "net/inet/errors", "net/inet/errnothread", + "net/inet/states", "net/inet/offline", "net/inet/stats", "net/inet/tcp",