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 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-28 13:09:28 +01:00
1 parent 7da0be4f0a
commit 3ee22fbd3d
4 files changed
+138 -13

No files matched your search

+3
View File
@@ -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;
}
+11 -2
View File
@@ -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;
+122 -11
View File
@@ -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<int>(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) {
+2
View File
@@ -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",