diff --git a/clientserver.c b/clientserver.c index 0e9aca36a..e7f5dfd85 100644 --- a/clientserver.c +++ b/clientserver.c @@ -1722,9 +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 = {0}; + 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) { - if (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 new file mode 100644 index 000000000..fc240ad54 --- /dev/null +++ b/testsuite/daemon-stdin-local-socket_test.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""A local stdin socket 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, 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 + # local socket must not be mistaken for an inetd connection. + 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, diff --git a/testsuite/skiplist/cygwin.txt b/testsuite/skiplist/cygwin.txt index 4a56c4229..2f90d25fd 100644 --- a/testsuite/skiplist/cygwin.txt +++ b/testsuite/skiplist/cygwin.txt @@ -28,6 +28,7 @@ daemon-config-symlink 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 diff --git a/testsuite/stdio_daemon.py b/testsuite/stdio_daemon.py index 9737b81b1..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,7 +28,19 @@ 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', '--no-detach', f'--config={conf}'), @@ -60,4 +72,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 '' -