From 43b49513ea9eecd654da8574b3fc7cece8be6db0 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Mon, 3 Aug 2026 12:51:43 +1000 Subject: [PATCH] socket: don't let open_socket_out() hang in connect() The same kernel-side missed wakeup on the other side of the connection: a blocking connect() can sleep forever on a connection that is already established, with the 4-tuple ESTABLISHED at both ends and the listener's greeting queued unread. Without --contimeout nothing breaks it. Wait for the connect with poll() in slices rather than blocking in the kernel, re-checking the socket on each pass, and take the result from SO_ERROR. A finished slice is not a failure -- looping is what re-examines the socket and recovers a missed wakeup. --contimeout is unchanged: the alarm still fires and the caller still reports RERR_CONTIMEOUT. The per-address errno is now stashed before close()/alarm() can overwrite it. Measured the same way, against a real loopback daemon: 20 hangs in 48,000 connects before, 0 in 48,000 after, with equal wall clock. This is the half of the OpenBSD flakiness that the socketpair_tcp() fix does not cover: the --use-tcp pass talks to a real rsyncd over a port, so it hangs here rather than in accept(). (cherry picked from commit 11e43daf909f421d0447282709926f4f5018d69f) --- socket.c | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 66 insertions(+), 7 deletions(-) diff --git a/socket.c b/socket.c index 3f27e793..2a4b44b0 100644 --- a/socket.c +++ b/socket.c @@ -256,6 +256,68 @@ static void contimeout_handler(UNUSED(int val)) connect_timeout = -1; } +/* How long each poll() pass waits before re-checking a pending connect(). */ +#define CONNECT_POLL_SLICE 100 + +/* connect() to addr, waiting for completion with poll() rather than blocking + * in the kernel. A blocking connect() can sleep forever on a connection that + * is already established (seen on OpenBSD, where the socket shows ESTABLISHED + * at both ends while connect() never returns); with no timeout set that hangs + * rsync for good. Re-checking the socket on each pass costs a loop instead. + * Returns 0 on success, -1 with errno set on failure. */ +static int connect_polled(int s, const struct sockaddr *addr, socklen_t addrlen) +{ + struct pollfd pfd; + int save_errno; + + set_nonblocking(s); + + if (connect(s, addr, addrlen) == 0) + goto connected; + if (errno != EINPROGRESS && errno != EINTR) + goto failed; + + pfd.fd = s; + pfd.events = POLLOUT; + while (1) { + int err = 0; + socklen_t errlen = sizeof err; + + /* the --contimeout alarm fired: let the caller report it */ + if (connect_timeout < 0) { + errno = ETIMEDOUT; + goto failed; + } + pfd.revents = 0; + if (poll(&pfd, 1, CONNECT_POLL_SLICE) < 0) { + if (errno == EINTR) + continue; + goto failed; + } + /* A finished slice is not a failure: loop so that poll() looks + * at the socket again, which is what recovers a missed wakeup. */ + if (!pfd.revents) + continue; + if (getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &errlen) < 0) + goto failed; + if (err) { + errno = err; + goto failed; + } + break; + } + + connected: + set_blocking(s); + return 0; + + failed: + save_errno = errno; + set_blocking(s); + errno = save_errno; + return -1; +} + /* Open a socket to a tcp remote host with the specified port. * * Based on code from Warren. Proxy support by Stephen Rothwell. @@ -363,23 +425,20 @@ int open_socket_out(char *host, int port, const char *bind_addr, int af_hint) } set_socket_options(s, sockopts); - while (connect(s, res->ai_addr, res->ai_addrlen) < 0) { + if (connect_polled(s, res->ai_addr, res->ai_addrlen) < 0) { if (connect_timeout < 0) exit_cleanup(RERR_CONTIMEOUT); - if (errno == EINTR) - continue; + /* stash it before close()/alarm() can overwrite errno */ + errnos[j] = errno; close(s); s = -1; - break; } if (connect_timeout > 0) alarm(0); - if (s < 0) { - errnos[j] = errno; + if (s < 0) continue; - } if (proxied && establish_proxy_connection(s, host, port, proxy_user, proxy_pass) != 0) { close(s);