From ae10ec25733bc106712684322d8e7d807e1aabab Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Thu, 3 Sep 2026 08:27:23 +1000 Subject: [PATCH 1/5] daemon: require IP stream for inetd mode --- clientserver.c | 20 +++++++++++- testsuite/daemon-stdin-local-socket_test.py | 35 +++++++++++++++++++++ testsuite/rsyncfns.py | 8 +++-- 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 testsuite/daemon-stdin-local-socket_test.py diff --git a/clientserver.c b/clientserver.c index 0e9aca36a..feee164b9 100644 --- a/clientserver.c +++ b/clientserver.c @@ -1722,9 +1722,27 @@ static void become_daemon(void) } } +/* Inetd supplies a connected IP stream on stdin. Other launchers may use a + * local socket for process I/O, which must not select inetd mode. */ +static int is_inet_stream_socket(int fd) +{ + struct sockaddr_storage ss; + struct sockaddr *sa = (struct sockaddr *)&ss; + socklen_t ss_len = sizeof ss; + int type; + socklen_t type_len = sizeof type; + + if (getpeername(fd, (struct sockaddr *)&ss, &ss_len) < 0 + || getsockopt(fd, SOL_SOCKET, SO_TYPE, (char *)&type, &type_len) < 0) + return 0; + + return type == SOCK_STREAM + && (sa->sa_family == AF_INET || sa->sa_family == AF_INET6); +} + int daemon_main(void) { - if (is_a_socket(STDIN_FILENO)) { + if (is_inet_stream_socket(STDIN_FILENO)) { int i; /* we are running via inetd - close off stdout and diff --git a/testsuite/daemon-stdin-local-socket_test.py b/testsuite/daemon-stdin-local-socket_test.py new file mode 100644 index 000000000..6ddf0a18c --- /dev/null +++ b/testsuite/daemon-stdin-local-socket_test.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""A local socket on stdin must not make a standalone daemon enter inetd mode.""" + +import socket + +from rsyncfns import ( + RSYNC, SCRATCHDIR, claim_free_port, rmtree, start_rsyncd, + test_skipped, write_daemon_conf, +) + +if not hasattr(socket, 'AF_UNIX') or not hasattr(socket, 'socketpair'): + test_skipped('Unix-domain socket pairs are unavailable') + +base = SCRATCHDIR / 'daemon-stdin-local-socket' +rmtree(base) +module = base / 'module' +module.mkdir(parents=True) +conf = write_daemon_conf([ + ('module', {'path': str(module), 'read only': 'yes'}), +]) +port = claim_free_port(12979) + +try: + parent, child = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM) +except OSError as e: + test_skipped(f'Unix-domain socket pairs are unavailable: {e}') +try: + # ADB shell without a PTY similarly presents a local socket as fd 0. + # The daemon must ignore that process-I/O transport and listen normally. + start_rsyncd(conf, port, rsync_cmd=RSYNC, stdin=child) +finally: + child.close() + parent.close() + +print('daemon ignores a local stdin socket when selecting inetd mode') diff --git a/testsuite/rsyncfns.py b/testsuite/rsyncfns.py index 15f294ef4..7524cfff0 100644 --- a/testsuite/rsyncfns.py +++ b/testsuite/rsyncfns.py @@ -767,7 +767,8 @@ def _cleanup_rsyncd(proc, port: int) -> 'None': _record_port_proc(port, 0, 0) -def start_rsyncd(conf_path, port: int, rsync_cmd: str = None) -> 'subprocess.Popen': +def start_rsyncd(conf_path, port: int, rsync_cmd: str = None, + stdin=subprocess.DEVNULL) -> 'subprocess.Popen': """Spawn `rsync --daemon --no-detach --address=127.0.0.1 --port=N --config=conf` and return the Popen handle after the port is accepting connections. @@ -784,7 +785,8 @@ def start_rsyncd(conf_path, port: int, rsync_cmd: str = None) -> 'subprocess.Pop RSYNC_PEER (the peer side of a two-sided run), so ordinary daemon tests get current-client <-> peer-daemon. The reverse-direction test passes rsync_cmd=RSYNC to put the current build on the daemon side and drive with - the old client. + the old client. stdin may override the default /dev/null input when a test + needs to exercise daemon launch detection. This is only ever reached from start_test_daemon() in --use-tcp mode; the default (pipe) mode never starts a listening daemon. @@ -797,7 +799,7 @@ def start_rsyncd(conf_path, port: int, rsync_cmd: str = None) -> 'subprocess.Pop ] proc = subprocess.Popen( argv, - stdin=subprocess.DEVNULL, + stdin=stdin, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, preexec_fn=_set_pdeathsig, From c336eb0fd23bc193b4c4682c5856a694e573e52d Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Thu, 3 Sep 2026 08:36:12 +1000 Subject: [PATCH 2/5] daemon: preserve bidirectional stdio mode --- clientserver.c | 25 +++++++++------------ testsuite/daemon-stdin-local-socket_test.py | 6 ++--- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/clientserver.c b/clientserver.c index feee164b9..62723149f 100644 --- a/clientserver.c +++ b/clientserver.c @@ -1722,27 +1722,24 @@ static void become_daemon(void) } } -/* Inetd supplies a connected IP stream on stdin. Other launchers may use a - * local socket for process I/O, which must not select inetd mode. */ -static int is_inet_stream_socket(int fd) +/* Inetd supplies one bidirectional socket on stdin and stdout. Other launchers + * may use unrelated local sockets for process I/O, which must not select + * inetd mode. */ +static int is_inetd_socket(void) { - struct sockaddr_storage ss; - struct sockaddr *sa = (struct sockaddr *)&ss; - socklen_t ss_len = sizeof ss; - int type; - socklen_t type_len = sizeof type; - - if (getpeername(fd, (struct sockaddr *)&ss, &ss_len) < 0 - || getsockopt(fd, SOL_SOCKET, SO_TYPE, (char *)&type, &type_len) < 0) + STRUCT_STAT in_st, out_st; + + if (!is_a_socket(STDIN_FILENO) || !is_a_socket(STDOUT_FILENO) + || do_fstat(STDIN_FILENO, &in_st) < 0 + || do_fstat(STDOUT_FILENO, &out_st) < 0) return 0; - return type == SOCK_STREAM - && (sa->sa_family == AF_INET || sa->sa_family == AF_INET6); + return in_st.st_dev == out_st.st_dev && in_st.st_ino == out_st.st_ino; } int daemon_main(void) { - if (is_inet_stream_socket(STDIN_FILENO)) { + if (is_inetd_socket()) { int i; /* we are running via inetd - close off stdout and diff --git a/testsuite/daemon-stdin-local-socket_test.py b/testsuite/daemon-stdin-local-socket_test.py index 6ddf0a18c..500949fbd 100644 --- a/testsuite/daemon-stdin-local-socket_test.py +++ b/testsuite/daemon-stdin-local-socket_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""A local socket on stdin must not make a standalone daemon enter inetd mode.""" +"""An unrelated local stdin socket must not make a daemon enter inetd mode.""" import socket @@ -25,8 +25,8 @@ except OSError as e: test_skipped(f'Unix-domain socket pairs are unavailable: {e}') try: - # ADB shell without a PTY similarly presents a local socket as fd 0. - # The daemon must ignore that process-I/O transport and listen normally. + # ADB shell without a PTY similarly presents a local socket as fd 0 while + # stdout is separate. The daemon must ignore it and listen normally. start_rsyncd(conf, port, rsync_cmd=RSYNC, stdin=child) finally: child.close() From 40c2ce9b4b87e2da1f760008eb39847daf145cb9 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Thu, 3 Sep 2026 08:53:38 +1000 Subject: [PATCH 3/5] daemon: honour explicit no-detach mode --- clientserver.c | 20 ++++---------------- testsuite/daemon-stdin-local-socket_test.py | 6 +++--- testsuite/stdio_daemon.py | 3 +-- 3 files changed, 8 insertions(+), 21 deletions(-) diff --git a/clientserver.c b/clientserver.c index 62723149f..98f5ddab9 100644 --- a/clientserver.c +++ b/clientserver.c @@ -1722,24 +1722,12 @@ static void become_daemon(void) } } -/* Inetd supplies one bidirectional socket on stdin and stdout. Other launchers - * may use unrelated local sockets for process I/O, which must not select - * inetd mode. */ -static int is_inetd_socket(void) -{ - STRUCT_STAT in_st, out_st; - - if (!is_a_socket(STDIN_FILENO) || !is_a_socket(STDOUT_FILENO) - || do_fstat(STDIN_FILENO, &in_st) < 0 - || do_fstat(STDOUT_FILENO, &out_st) < 0) - return 0; - - return in_st.st_dev == out_st.st_dev && in_st.st_ino == out_st.st_ino; -} - int daemon_main(void) { - if (is_inetd_socket()) { + /* --no-detach explicitly requests a foreground standalone daemon. Honour + * that mode before socket auto-detection because launchers such as ADB may + * use a local socket for ordinary process input. */ + if (!no_detach && is_a_socket(STDIN_FILENO)) { int i; /* we are running via inetd - close off stdout and diff --git a/testsuite/daemon-stdin-local-socket_test.py b/testsuite/daemon-stdin-local-socket_test.py index 500949fbd..a75187627 100644 --- a/testsuite/daemon-stdin-local-socket_test.py +++ b/testsuite/daemon-stdin-local-socket_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""An unrelated local stdin socket must not make a daemon enter inetd mode.""" +"""--no-detach must select standalone mode even when stdin is a local socket.""" import socket @@ -25,8 +25,8 @@ except OSError as e: test_skipped(f'Unix-domain socket pairs are unavailable: {e}') try: - # ADB shell without a PTY similarly presents a local socket as fd 0 while - # stdout is separate. The daemon must ignore it and listen normally. + # ADB shell without a PTY similarly presents a local socket as fd 0. The + # explicit --no-detach mode must take precedence and listen normally. start_rsyncd(conf, port, rsync_cmd=RSYNC, stdin=child) finally: child.close() diff --git a/testsuite/stdio_daemon.py b/testsuite/stdio_daemon.py index 9737b81b1..33cfd6d66 100644 --- a/testsuite/stdio_daemon.py +++ b/testsuite/stdio_daemon.py @@ -31,7 +31,7 @@ def start_stdio_daemon(conf, timeout=10, env=None): parent, child = socket.socketpair() try: proc = subprocess.Popen( - rsync_argv('--daemon', '--no-detach', f'--config={conf}'), + rsync_argv('--daemon', f'--config={conf}'), stdin=child.fileno(), stdout=child.fileno(), stderr=subprocess.PIPE, close_fds=True, env=env, ) @@ -60,4 +60,3 @@ def finish_stdio_daemon(client, proc, timeout=5): proc.kill() proc.wait(timeout=timeout) return proc.stderr.read().decode('utf-8', 'replace') if proc.stderr else '' - From 130671b495c9cd1629c209bc03821d48c8ee1ee2 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Thu, 3 Sep 2026 17:21:30 +1000 Subject: [PATCH 4/5] daemon: preserve inetd socket detection --- clientserver.c | 26 +++++++++++++++++---- testsuite/daemon-stdin-local-socket_test.py | 6 ++--- testsuite/skiplist/cygwin.txt | 1 + testsuite/stdio_daemon.py | 18 +++++++++++--- 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/clientserver.c b/clientserver.c index 98f5ddab9..030362c8e 100644 --- a/clientserver.c +++ b/clientserver.c @@ -1722,12 +1722,30 @@ static void become_daemon(void) } } +/* Inetd supplies a connected IP stream on stdin. Other launchers may use a + * local socket for process I/O, which must not select inetd mode. */ +static int is_inetd_socket(int fd) +{ + int type; + struct sockaddr_storage peer; + socklen_t type_len = sizeof type; + socklen_t peer_len = sizeof peer; + + if (getsockopt(fd, SOL_SOCKET, SO_TYPE, (char *)&type, &type_len) != 0 + || type != SOCK_STREAM + || getpeername(fd, (struct sockaddr *)&peer, &peer_len) != 0) + return 0; + + return peer.ss_family == AF_INET +#ifdef INET6 + || peer.ss_family == AF_INET6 +#endif + ; +} + int daemon_main(void) { - /* --no-detach explicitly requests a foreground standalone daemon. Honour - * that mode before socket auto-detection because launchers such as ADB may - * use a local socket for ordinary process input. */ - if (!no_detach && is_a_socket(STDIN_FILENO)) { + if (is_inetd_socket(STDIN_FILENO)) { int i; /* we are running via inetd - close off stdout and diff --git a/testsuite/daemon-stdin-local-socket_test.py b/testsuite/daemon-stdin-local-socket_test.py index a75187627..fc240ad54 100644 --- a/testsuite/daemon-stdin-local-socket_test.py +++ b/testsuite/daemon-stdin-local-socket_test.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""--no-detach must select standalone mode even when stdin is a local socket.""" +"""A local stdin socket must not make a standalone daemon enter inetd mode.""" import socket @@ -22,11 +22,11 @@ try: parent, child = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM) -except OSError as e: +except (OSError, ValueError) as e: test_skipped(f'Unix-domain socket pairs are unavailable: {e}') try: # ADB shell without a PTY similarly presents a local socket as fd 0. The - # explicit --no-detach mode must take precedence and listen normally. + # local socket must not be mistaken for an inetd connection. start_rsyncd(conf, port, rsync_cmd=RSYNC, stdin=child) finally: child.close() diff --git a/testsuite/skiplist/cygwin.txt b/testsuite/skiplist/cygwin.txt index 2648c7c4f..fac941a5b 100644 --- a/testsuite/skiplist/cygwin.txt +++ b/testsuite/skiplist/cygwin.txt @@ -25,6 +25,7 @@ copy-xattrs-symlink-race daemon-auth-group daemon-chroot-munge-default daemon-config-symlink +daemon-stdin-local-socket # Cygwin Python has no AF_UNIX socketpair daemon-module-chdir-symlink daemon-module-private-parent daemon-secrets-file-symlink diff --git a/testsuite/stdio_daemon.py b/testsuite/stdio_daemon.py index 33cfd6d66..d7258d94c 100644 --- a/testsuite/stdio_daemon.py +++ b/testsuite/stdio_daemon.py @@ -1,4 +1,4 @@ -"""Launch one rsync daemon session over a local socketpair.""" +"""Launch one rsync daemon session over a loopback TCP connection.""" import socket import subprocess @@ -28,10 +28,22 @@ def _client_for_socket(sock, timeout=10): def start_stdio_daemon(conf, timeout=10, env=None): """Return ``(DaemonClient, Popen)`` for one daemon connection.""" - parent, child = socket.socketpair() + listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + parent = None + try: + listener.bind(('127.0.0.1', 0)) + listener.listen(1) + parent = socket.create_connection(listener.getsockname(), timeout) + child, _ = listener.accept() + except OSError: + if parent is not None: + parent.close() + raise + finally: + listener.close() try: proc = subprocess.Popen( - rsync_argv('--daemon', f'--config={conf}'), + rsync_argv('--daemon', '--no-detach', f'--config={conf}'), stdin=child.fileno(), stdout=child.fileno(), stderr=subprocess.PIPE, close_fds=True, env=env, ) From c0c54ed40cdfc813a4c615a8452184c5e247c4d3 Mon Sep 17 00:00:00 2001 From: Zen Dodd Date: Thu, 3 Sep 2026 17:30:53 +1000 Subject: [PATCH 5/5] ci: fix daemon socket checks Signed-off-by: Zen Dodd --- clientserver.c | 2 +- testsuite/skiplist/cygwin.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/clientserver.c b/clientserver.c index 030362c8e..e7f5dfd85 100644 --- a/clientserver.c +++ b/clientserver.c @@ -1727,7 +1727,7 @@ static void become_daemon(void) static int is_inetd_socket(int fd) { int type; - struct sockaddr_storage peer; + struct sockaddr_storage peer = {0}; socklen_t type_len = sizeof type; socklen_t peer_len = sizeof peer; diff --git a/testsuite/skiplist/cygwin.txt b/testsuite/skiplist/cygwin.txt index 9a40ffea6..2f90d25fd 100644 --- a/testsuite/skiplist/cygwin.txt +++ b/testsuite/skiplist/cygwin.txt @@ -25,10 +25,10 @@ copy-xattrs-symlink-race daemon-auth-group daemon-chroot-munge-default daemon-config-symlink -daemon-stdin-local-socket # Cygwin Python has no AF_UNIX socketpair daemon-module-chdir-symlink daemon-module-private-parent daemon-secrets-file-symlink +daemon-stdin-local-socket # Cygwin Python has no AF_UNIX socketpair devices dir-sgid early-input-symlink