From 3fe1ed512cbd886b170ed2de5545798660cea549 Mon Sep 17 00:00:00 2001 From: Andrew Tridgell Date: Wed, 22 Jul 2026 16:56:28 +1000 Subject: [PATCH] rsync: confine the daemon files-from open to the module root A daemon serving a writable, non-chrooted module reads a client-requested --files-from=:LIST through open_no_attacker_symlinks(), which follows a symlink owned by uid 0 or the euid. The module-root confinement in that resolver (abspath_excluded_by_module) only fires when operator_path_resolve is set, and this open left it clear -- so a trusted-owned symlink whose target escapes the module was followed. An attacker can obtain such a symlink without owning it: a --backup-dir push makes the daemon back up the old destination symlink with the daemon's own (root) ownership, and a parent-swap race can leave that root-owned backup symlink pointing outside the module. A later --files-from=:backup/... then reads out-of-module file content as the file list, bypassing the same-uid ownership constraint that normally protects files-from. Set operator_path_resolve around the files-from open so the ownership walk also refuses a trusted-owned symlink that redirects the list outside the module root. A daemon has no rsyncd.conf "files from" of its own, so this path is always client-requested and confining it is unconditional. No-op off a daemon (the module-root check only fires when am_daemon). The same ownership-walk opener backs the daemon merge/--exclude-from reads in exclude.c, but those also load the module's own "include from"/"exclude from" admin files, which on a non-chrooted module may legitimately live outside the module; confining them there needs a client-vs-admin distinction and is left to a separate change. --- options.c | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/options.c b/options.c index d4462757..88db933c 100644 --- a/options.c +++ b/options.c @@ -27,6 +27,7 @@ extern int module_id; extern int local_server; extern int sanitize_paths; +extern int operator_path_resolve; extern int trust_sender_args; extern int trust_sender_filter; extern unsigned int module_dirlen; @@ -2550,8 +2551,17 @@ int parse_arguments(int *argc_p, const char ***argv_p) } /* Operator-supplied path that may transit attacker-writable * parents; refuse symlinks not owned by uid 0 or our euid, - * as for --exclude-from/--include-from/--filter in exclude.c. */ + * as for --exclude-from/--include-from/--filter in exclude.c. + * A daemon reads this list from a CLIENT-requested path + * (--files-from=:LIST) and it must stay inside the module: + * operator_path_resolve makes the ownership walk also refuse a + * (trusted-owned) symlink that redirects the list outside the + * module root -- e.g. a root-owned backup symlink. No-op off a + * daemon (the module-root check only fires when am_daemon). */ + int save_opr = operator_path_resolve; + operator_path_resolve = 1; filesfrom_fd = open_no_attacker_symlinks(files_from, O_RDONLY|O_BINARY, 0); + operator_path_resolve = save_opr; if (filesfrom_fd < 0) { snprintf(err_buf, sizeof err_buf, "failed to open files-from file %s: %s\n",