From 76b54cdba42eef1f8516121e8c01cf5a09ee6556 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Sat, 25 Jul 2026 17:37:05 +1000 Subject: [PATCH] exclude: exempt the daemon's own filter parameters from the confinement Confining every parse_filter_file() open to the module root also caught "filter", "include from" and "exclude from" from rsyncd.conf. Those name operator-configured paths and pointing them outside the module -- at /etc/rsync/excludes, say -- is the ordinary way to write them; rsyncd.conf(5) puts no constraint on where the file lives. The result was not a refused rule but a refused connection: failed to open exclude file /etc/rsync/excludes: Too many levels of symbolic links (40) rsync error: error in file IO (code 11) at exclude.c(1582) with no symlink involved anywhere -- just a regular file outside the module. Mark the window in which the daemon loads its own parameters and skip the confinement there. Everything else, in particular the peer-driven dir-merge the leak test exercises, is still confined. Also fix the trailing whitespace in the original hunk. (cherry picked from commit 5eb99bb6b2bfeb7aeec844656a124c38f7adca23) --- clientserver.c | 8 ++++++++ exclude.c | 21 ++++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/clientserver.c b/clientserver.c index abaa26ab..b19f81f9 100644 --- a/clientserver.c +++ b/clientserver.c @@ -42,6 +42,7 @@ extern int munge_symlinks; extern int use_secure_symlinks; extern int open_noatime; extern int sanitize_paths; +extern int daemon_config_filter_file; extern int numeric_ids; extern int filesfrom_fd; extern int remote_protocol; @@ -899,6 +900,11 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char } else set_filter_dir(module_dir, module_dirlen); + /* Everything loaded from here to the end of the exclude block is the + * operator's own configuration, so it keeps the ownership walk without the + * module-confinement parse_filter_file() applies to peer-driven merges. */ + daemon_config_filter_file = 1; + p = lp_filter(module_id); parse_filter_str(&daemon_filter_list, p, rule_template(FILTRULE_WORD_SPLIT), XFLG_ABS_IF_SLASH | XFLG_DIR2WILD3); @@ -920,6 +926,8 @@ static int rsync_module(int f_in, int f_out, int i, const char *addr, const char parse_filter_str(&daemon_filter_list, p, rule_template(FILTRULE_WORD_SPLIT), XFLG_ABS_IF_SLASH | XFLG_DIR2WILD3 | XFLG_OLD_PREFIXES); + daemon_config_filter_file = 0; + log_init(1); #if defined HAVE_SETENV || defined HAVE_PUTENV diff --git a/exclude.c b/exclude.c index ca79fbca..57046c22 100644 --- a/exclude.c +++ b/exclude.c @@ -43,6 +43,9 @@ extern int trust_sender_args; extern int module_id; extern int operator_path_resolve; +/* Set while the daemon loads its own filter parameters; see parse_filter_file(). */ +int daemon_config_filter_file = 0; + extern char curr_dir[MAXPATHLEN]; extern unsigned int curr_dir_len; extern unsigned int module_dirlen; @@ -1554,12 +1557,24 @@ void parse_filter_file(filter_rule_list *listp, const char *fname, const filter_ open_path = line; } else open_path = fname; - + + /* Confine the open to the module root. The ownership walk on its own + * is not enough for a peer-driven merge file: a non-chrooted daemon + * writes --backup-dir entries as root, so a raced backup symlink is + * ROOT-owned -- exactly what open_no_attacker_symlinks() treats as + * trusted -- and naming it in a dir-merge rule would read an + * out-of-module file in as filter rules (their text comes back to the + * peer in "Unknown filter rule" errors). + * + * The daemon's own "filter"/"include from"/"exclude from" parameters + * are exempt: those are operator-configured and legitimately live + * outside the module (/etc/rsync/excludes and the like). */ int save_opr = operator_path_resolve; - operator_path_resolve = 1; + if (!daemon_config_filter_file) + operator_path_resolve = 1; fd = open_no_attacker_symlinks(open_path, O_RDONLY, 0); operator_path_resolve = save_opr; - + if (fd < 0) fp = NULL; else if (!(fp = fdopen(fd, "rb")))