From 73e317ee17fc6b408cfde231a020f7deedef4ab0 Mon Sep 17 00:00:00 2001 From: Migush Date: Tue, 13 Oct 2026 22:34:04 +0200 Subject: [PATCH 1/2] soc_poll: don't sleep out the timeout when a connect completes or fails The SOC service doesn't wake a poll() waiting for POLLOUT when a non-blocking connect() completes, so poll() only returns once its timeout runs out (curl waits a second per new connection this way). It also reports a refused connection only as readable, so a POLLOUT waiter never learns about it. Wait for POLLOUT in 10 ms slices, and watch those sockets for POLLIN too: POLLIN without POLLOUT on a socket that then fails to receive is a failed connection, reported as POLLERR | POLLHUP. Nothing tells a socket that is still connecting from a connected one (getpeername() succeeds for both), so connected sockets waiting for POLLOUT are sliced as well. Waits for POLLIN alone are unchanged. Fixes the poll() part of devkitPro/libctru#412. --- libctru/source/services/soc/soc_common.c | 13 +++ libctru/source/services/soc/soc_common.h | 2 + libctru/source/services/soc/soc_poll.c | 126 +++++++++++++++++------ 3 files changed, 108 insertions(+), 33 deletions(-) diff --git a/libctru/source/services/soc/soc_common.c b/libctru/source/services/soc/soc_common.c index a9a9e1988..6bcdf9ee9 100644 --- a/libctru/source/services/soc/soc_common.c +++ b/libctru/source/services/soc/soc_common.c @@ -100,3 +100,16 @@ s32 _net_convert_error(s32 sock_retval) return NET_UNKNOWN_ERROR_OFFSET + sock_retval; return -_net_error_code_map[-sock_retval]; } + +// The service reports a failed connection only as readable: receiving then fails, with ENOTCONN when the +// connection was refused. Returns the reason, or 0 while the connection is up or still being made. +int soc_connection_error(int sockfd) +{ + char byte; + int error = 0; + int saved_errno = errno; + if(recv(sockfd, &byte, 1, MSG_PEEK | MSG_DONTWAIT) < 0 && errno != EAGAIN) + error = errno == ENOTCONN ? ECONNREFUSED : errno; + errno = saved_errno; + return error; +} diff --git a/libctru/source/services/soc/soc_common.h b/libctru/source/services/soc/soc_common.h index 85759eb7b..33de2950a 100644 --- a/libctru/source/services/soc/soc_common.h +++ b/libctru/source/services/soc/soc_common.h @@ -28,6 +28,8 @@ soc_get_fd(int fd) s32 _net_convert_error(s32 sock_retval); +int soc_connection_error(int sockfd); + ssize_t soc_recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); ssize_t soc_sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); diff --git a/libctru/source/services/soc/soc_poll.c b/libctru/source/services/soc/soc_poll.c index 145fff2cb..a2c190079 100644 --- a/libctru/source/services/soc/soc_poll.c +++ b/libctru/source/services/soc/soc_poll.c @@ -4,15 +4,67 @@ #include #include #include <3ds/ipc.h> +#include <3ds/os.h> -int poll(struct pollfd *fds, nfds_t nfds, int timeout) +// The SOC service doesn't wake a poll() waiting for POLLOUT when a non-blocking connect() completes: +// it only notices once the poll times out. Waits that include POLLOUT are therefore split into slices. +#define POLLOUT_SLICE_MS 10 + +static int soc_poll(struct pollfd *fds, nfds_t nfds, int timeout) { int ret = 0; - nfds_t i; u32 size = sizeof(struct pollfd)*nfds; u32 *cmdbuf = getThreadCommandBuffer(); u32 saved_threadstorage[2]; + cmdbuf[0] = IPC_MakeHeader(0x14,2,4); // 0x140084 + cmdbuf[1] = (u32)nfds; + cmdbuf[2] = (u32)timeout; + cmdbuf[3] = IPC_Desc_CurProcessId(); + cmdbuf[5] = IPC_Desc_StaticBuffer(size,10); + cmdbuf[6] = (u32)fds; + + u32 * staticbufs = getThreadStaticBuffers(); + saved_threadstorage[0] = staticbufs[0]; + saved_threadstorage[1] = staticbufs[1]; + + staticbufs[0] = IPC_Desc_StaticBuffer(size,0); + staticbufs[1] = (u32)fds; + + ret = svcSendSyncRequest(SOCU_handle); + + staticbufs[0] = saved_threadstorage[0]; + staticbufs[1] = saved_threadstorage[1]; + + if(ret != 0) { + errno = SYNC_ERROR; + return ret; + } + + ret = (int)cmdbuf[1]; + if(ret == 0) + ret = _net_convert_error(cmdbuf[2]); + + if(ret < 0) { + errno = -ret; + return -1; + } + + return ret; +} + +static s64 elapsed_ms(u64 start) +{ + return (svcGetSystemTick() - start) * 1000 / SYSCLOCK_ARM11; +} + +int poll(struct pollfd *fds, nfds_t nfds, int timeout) +{ + int ret = 0; + nfds_t i; + bool sliced = false; + u64 start = svcGetSystemTick(); + if(nfds == 0) { errno = EINVAL; return -1; @@ -34,6 +86,13 @@ int poll(struct pollfd *fds, nfds_t nfds, int timeout) free(tmp_fds); return -1; } + + // a failed connect() is only reported as readable, so watch for that too + if(fds[i].events & POLLOUT) { + tmp_fds[i].events |= POLLIN; + if(timeout != 0) + sliced = true; + } } // negative fds are ignored, however 3ds poll only // ignores fd's that are -1. @@ -41,47 +100,48 @@ int poll(struct pollfd *fds, nfds_t nfds, int timeout) else { tmp_fds[i].fd = -1; } - tmp_fds[i].revents = 0; } - cmdbuf[0] = IPC_MakeHeader(0x14,2,4); // 0x140084 - cmdbuf[1] = (u32)nfds; - cmdbuf[2] = (u32)timeout; - cmdbuf[3] = IPC_Desc_CurProcessId(); - cmdbuf[5] = IPC_Desc_StaticBuffer(size,10); - cmdbuf[6] = (u32)tmp_fds; - - u32 * staticbufs = getThreadStaticBuffers(); - saved_threadstorage[0] = staticbufs[0]; - saved_threadstorage[1] = staticbufs[1]; + for(;;) { + int slice = timeout; + if(sliced) { + slice = POLLOUT_SLICE_MS; + if(timeout > 0) { + s64 left = timeout - elapsed_ms(start); + if(left < slice) + slice = left > 0 ? left : 0; + } + } - staticbufs[0] = IPC_Desc_StaticBuffer(size,0); - staticbufs[1] = (u32)tmp_fds; + for(i = 0; i < nfds; ++i) + tmp_fds[i].revents = 0; - ret = svcSendSyncRequest(SOCU_handle); + ret = soc_poll(tmp_fds, nfds, slice); + if(ret < 0) + break; - staticbufs[0] = saved_threadstorage[0]; - staticbufs[1] = saved_threadstorage[1]; + ret = 0; + for(i = 0; i < nfds; ++i) { + short revents = tmp_fds[i].revents; - if(ret != 0) { - free(tmp_fds); - errno = SYNC_ERROR; - return ret; - } + if((fds[i].events & POLLOUT) && (revents & POLLIN) && !(revents & POLLOUT)) { + if(soc_connection_error(fds[i].fd)) + revents |= POLLERR | POLLHUP; // the connection failed or was reset + else if(!(fds[i].events & POLLIN)) + tmp_fds[i].events &= ~POLLIN; // only data nobody asked for: stop watching for it + } + if(!(fds[i].events & POLLIN)) + revents &= ~POLLIN; - ret = (int)cmdbuf[1]; - if(ret == 0) - ret = _net_convert_error(cmdbuf[2]); + fds[i].revents = revents; + if(revents != 0) + ++ret; + } - if(ret < 0) { - free(tmp_fds); - errno = -ret; - return -1; + if(ret != 0 || !sliced || (timeout > 0 && elapsed_ms(start) >= timeout)) + break; } - for(i = 0; i < nfds; ++i) - fds[i].revents = tmp_fds[i].revents; - free(tmp_fds); return ret; From 1f89c3e80dbfa101317a9f2f2e260ec475e33dd2 Mon Sep 17 00:00:00 2001 From: Migush Date: Tue, 13 Oct 2026 22:34:04 +0200 Subject: [PATCH 2/2] soc_getsockopt: report the pending error for SO_ERROR For SO_ERROR the service returns the result code of an earlier operation on the socket, in its own codes, instead of the socket's pending error: -26 (EINPROGRESS) after a non-blocking connect(), even once it has completed. Convert the code, and for the ones that aren't errors of the socket itself (EINPROGRESS, EALREADY, ENOTCONN, EAGAIN) report whether its connection failed instead: 0, or the reason (ECONNREFUSED for a refused connection). Fixes the SO_ERROR part of devkitPro/libctru#412. --- libctru/source/services/soc/soc_getsockopt.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/libctru/source/services/soc/soc_getsockopt.c b/libctru/source/services/soc/soc_getsockopt.c index e3fe64a86..752340ecc 100644 --- a/libctru/source/services/soc/soc_getsockopt.c +++ b/libctru/source/services/soc/soc_getsockopt.c @@ -6,6 +6,7 @@ int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen) { int ret = 0; + int fd = sockfd; u32 *cmdbuf = getThreadCommandBuffer(); u32 saved_threadstorage[2]; @@ -50,5 +51,17 @@ int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optl *optlen = cmdbuf[3]; + // For SO_ERROR the service returns the result code of an earlier operation on the socket, in its own + // codes: EINPROGRESS after a non-blocking connect() even once it has completed, EAGAIN after a recv() + // that found nothing, ENOTCONN once a refused connection was polled. None of those is an error of the + // socket itself, so report whether its connection failed instead. + if(level == SOL_SOCKET && optname == SO_ERROR && *optlen >= sizeof(int)) { + int *error = (int*)optval; + if(*error < 0) + *error = -_net_convert_error(*error); + if(*error == EINPROGRESS || *error == EALREADY || *error == ENOTCONN || *error == EAGAIN) + *error = soc_connection_error(fd); + } + return ret; }