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 5eb99bb6b2)
The opt-out is meant to restore stock-3.2.7 "follow existing symlinks in the
module" behaviour, but several daemon symlink-resolution sites called the
confined resolver unconditionally and never consulted symlink_optout_allowed(),
so a module with "insecure links = yes" still refused to follow a symlinked
directory (change_dir ELOOP) or an alt-dest basis. Gate every such site on the
opt-out -- follow plainly like 3.2.7 when set, confine otherwise:
- change_dir() relative daemon branch (util1.c): plain chdir() under the
opt-out instead of secure_relative_open(), so a peer can read a path through
a symlinked directory again.
- basis_link_stat() (generator.c): the chrooted-relative and non-chroot
branches honour the opt-out. The non-chroot ABSOLUTE branch (a basis rooted
under the module by check_alt_basis_dirs, which can reach an in-module
symlink) is, when NOT opted out, resolved through owner_walk_parent so a
target landing outside the module root is refused -- closing a confirmed
--compare-dest=/symlink out-of-module read oracle. The leaf is taken
O_NOFOLLOW under the confined parent so --copy-links can't follow a leaf
symlink out; --fake-super folds its %stat via the held fd. A relative
sibling basis (--link-dest=../01) keeps the plain path (#915/#930).
- secure_basis_open() (receiver.c): plain do_open() under the opt-out.
- use_secure_symlinks (clientserver.c): cleared under the opt-out, so the
receiver's protected-regular EACCES write fallback is legacy too.
- secure_sender_parent_fd() (sender.c): declines (errno=0) under the opt-out
so --remove-source-files re-stats the plain path.
Default modules (insecure links = no) are unchanged and stay fully confined;
the opt-out is per-module and a client cannot enable it.
Five error-path/cleanup memory leaks found by an external audit:
- flist.c send_file_name: free the ACL loaded by get_acl() when a later
get_xattr() fails (and on the get_acl error path).
- xattrs.c copy_xattrs: free the xattr datum buffer when the setxattr fails.
- generator.c recv_generator: free real_sx at the cleanup label (the
directory branch loaded its ACL via set_file_attrs but only the
regular-file path freed it); zero-init real_sx so the early gotos are safe.
- uidlist.c send_one_list: free the strdup'd id-0 name after send_one_name.
- clientserver.c start_inband_exchange: free modname on the early error
returns (it was freed only on the success path).
ASan/LSan regression tests cover the generator, uidlist and clientserver
leaks; the flist and xattrs leaks need a forced syscall failure and are
covered by the audit's standalone harnesses.
Reported-by: Leonid Bugaev <leonsbox@gmail.com>
(cherry picked from commit 078f3b99f4004510d418ee9d97d9b775dc8587bc)
Route operator-supplied paths (--partial-dir, --backup-dir, alt/copy/link-dest,
--temp-dir, --files-from, --log-file, config and secrets files) and the sender's
own enumeration through the race-safe resolver and an ownership walk that refuses a
target redirected outside the module root. open_tmpfile() now creates the temp via
the cached held dir fd (do_mkstemp_atfd) so an in-tree dir symlink is followed
safely instead of refused. Adds --insecure-links to opt back into legacy following;
the daemon hard-refuses the client flag (use the "insecure links" module param).
- io: bound daemon argument lists so a malicious daemon client cannot grow
argv without limit (DoS);
- socket: bound the PROXY CONNECT request and proxy response header lines;
- daemon: require a trusted-proxy host list for "proxy protocol = true"
(reject untrusted proxy peers, fail-closed), and warn at startup when the
trusted-proxy list is unset so the fail-closed behaviour is not silent.
Tests: daemon-argv-limit, proxy-connect-request-too-long,
proxy-response-header-too-long, proxy-protocol-trusted-peer.
A daemon module with a /./ inner boundary (use chroot=yes, path=.../.inner)
relies on the inner module root, not the kernel chroot, as its trust
boundary: the chroot confines the outer path, but a symlink inside the
module can point to a sibling that is inside the chroot yet outside the
inner module. Extend the receiver finish/rename gate (use_secure_symlinks),
change_dir()'s confined fchdir, and the basis-open gate from
"am_daemon && !am_chrooted" to "am_daemon && (!am_chrooted || module_dirlen)"
so an inner module gets the same confinement as a non-chroot module.
secure_relpath_active() already carries the inner-module clause.
Tests: chroot-basis-forge/receiver-write/special-inner-module.
Route the daemon's operator-supplied path opens through
safe_open_no_attacker_symlinks() / a pinned parent dir so a local attacker
cannot plant a symlink at one and redirect the daemon's privileged
read/write: motd, --early-input, lock and --config paths
(clientserver/connection/params); the --password-file and daemon secrets
file, fstat'ing the opened fd (authenticate); and the pid-file parent dir.
Tests: daemon-config-symlink, early-input-symlink, daemon-secrets-file-symlink,
password-file-symlink, daemon-module-chdir-symlink.
The sender/daemon enumerated each source directory with a plain opendir()
on the full accumulated path -- not through the secure resolver -- so the
enumeration itself was unconfined: a parent component raced into a symlink
after the scan let the listing (and per-entry lstat) escape the transfer
root / module.
- secure_opendir() resolves the directory through the secure resolver and
turns the held fd into a DIR* via fdopendir(); the daemon anchors it at
the identity-pinned module_dirfd (captured at module setup while the
daemon was positioned at the module root, before any privilege drop);
- the per-entry scan stats through the opendir fd (scan_dirfd) with
fstatat(), so the listing can't be redirected out of the tree.
Closes daemon-subdir-climb-symlink and sender-scan-dir-escape.
Backport of the 3.5.0 daemon name-converter hardening:
- reject an empty or non-numeric namecvt response instead of letting it
collapse to uid/gid 0 (Mitchell Benjamin finding 2);
- parse the id with overflow/sign checking;
- reject unsafe (newline/control) tokens in the converter response.
Tests: daemon-namecvt-empty-response, daemon-namecvt-newline-token.
Reported-by: Mitchell Benjamin <mitchell@revampstudio.com.au>
Without --secluded-args, the client's safe_arg() backslash-escapes shell
and wildcard chars in option values before sending them to the server, so
--chown's --usermap=*:user is transmitted as --usermap=\*:user. Over ssh a
remote shell removes the backslashes before rsync parses the args, but a
daemon has no shell and read_args() stored option args verbatim -- so the
receiver saw the literal "\*", the usermap/groupmap wildcard never matched,
and the module's configured uid/gid won instead. A regression from the
secluded-args hardening; rsync 3.2.3 (protocol 31) worked.
Un-backslash option args in read_args() on the daemon's first
(non-protected) read, mirroring what the ssh-side shell does. File args
after the dot are already handled by glob_expand(); the protected (NUL,
already-unescaped) re-read and the server's stdin read pass unescape=0 so
their raw args are left untouched.
Thanks to @elcamlost for the report (#829).
Fixes: #829
(cherry picked from commit 8dc5fd1408)
On an rsync daemon configured with "daemon chroot", the reverse-DNS
lookup of the connecting client was performed *after* the chroot
had been entered. If the chroot did not contain the files glibc
needs for resolution (/etc/resolv.conf, /etc/nsswitch.conf,
/etc/hosts, NSS service modules), the lookup failed and
client_name() returned "UNKNOWN". Hostname-based deny rules
("hosts deny = *.evil.example") therefore could not match, and
an attacker controlling their PTR record could connect from a
hostname the administrator had intended to deny. IP-based ACLs
were unaffected.
Do the reverse DNS lookup before chroot/setuid; client_name()
caches its result, so the post-chroot call uses the cached value
and hostname-based ACLs work even when DNS is unavailable
post-chroot.
Adds testsuite/daemon-chroot-acl.test as end-to-end regression
coverage. The test sets up an empty chroot directory, configures
"hosts deny = <localhost-resolved-name>" with daemon chroot, and
asserts the connection is refused with @ERROR access denied.
Uses unshare --user --map-root-user for non-root CAP_SYS_CHROOT;
skips cleanly on non-Linux or when user namespaces aren't
available.
Reporter: Joshua Rogers (MegaManSec).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CVE-2026-29518: an rsync daemon configured with "use chroot = no"
is exposed to a TOCTOU race on parent path components. A local
attacker with write access to a module can replace a parent
directory component with a symlink between the receiver's check
and its open(), redirecting reads (basis-file disclosure) and
writes (file overwrite) outside the module. Under elevated daemon
privilege this allows privilege escalation. Default
"use chroot = yes" is not exposed.
Add secure_relative_open() in syscall.c. It walks the parent
components under RESOLVE_BENEATH (Linux 5.6+) /
O_RESOLVE_BENEATH (FreeBSD 13+, macOS 15+) / per-component
O_NOFOLLOW elsewhere, anchored at a trusted dirfd, so a parent-
symlink swap is rejected by the kernel. Route the receiver's
basis-file open in receiver.c through it when use_secure_symlinks
is set in clientserver.c rsync_module().
Reporters: Nullx3D (Batuhan SANCAK); Damien Neil; Michael Stapelberg.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
localtime/localtime_r need /etc/localtime for timezone info.
After chroot this file is inaccessible, causing log timestamps
to fall back to UTC. Calling tzset() before chroot ensures the
timezone data is cached by glibc for subsequent calls.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- The sanitize_paths variable was set too often. It only needs to be set
when the "inner" path is not "/". This change avoids sanitizing &
munging things for a path=/ module just because chroot is off.
- The default for "use chroot" is now "unset" instead of "true". When
unset it checks if chrooting works, and if not, it proceeds with a
sanitized copy instead of totally failing to work. This makes it
easier to setup a non-root rsync daemon, for instance. It will have
no effect on a typical Linux root-run daemon where the default will
continue to use chroot (because chrooting works). A config file can
explicitly set "use chroot = true | false" to force the choice.
- Try to improve the "use chroot" manpage.
- Size flist checksum data to hold the active size, not the max.
- Add a negotiated hash method to the daemon auth code.
- Use EVP for all openssl digests. This makes it easy to add more
openssl digest methods and avoids deprecation warnings.
- Support a way to re-enable deprecated digests via openssl conf
file and allow a default file to be configured.
- Supply a simple openssl-rsync.cnf file to enable legacy digests.
Without a DISPLAY var, ssh won't try to forward X11 when making an
ssh connection. This patch also makes use of setenv() and unsetenv()
if they are available.
- Rename daemon_over_rsh -> daemon_connection since it is also used to
indicate if a non-rsh daemon connection is active.
- Move the daemon-over-rsh exception out of server_options() to the one
caller that needs that behavior.
- Don't allow noop_io_until_death() to be short-circuited when talking
to a daemon over a socket, because it can't send errors via stderr.
This is based on the long-standing patch but with the protocol changed
to just use newlines as delimiters instead of null chars (since names
should not contain a newline AND it makes it easier to write a helper
script). Lots of other small improvements and a better default value
for "numeric ids" when using "use chroot" with "name converter".
- All the memory-allocation macros now auto-check for failure and exit
with a failure message that incudes the caller's file and lineno
info. This includes strdup().
- Added the `--max-alloc=SIZE` option to be able to override the memory
allocator's sanity-check limit. It defaults to 1G (as before).
Fixes bugzilla bug 12769.
- Set am_daemon to -1 (from 1) when the daemon is run via rsh.
- Only disable --msgs2stderr for a normal (socket) daemon.
- Forward a -q to the server if --msgs2stderr was also specified.
- Added --no-msgs2stderr option to allow it to be overridden.
I replaced git-set-file-times with an improved version that I wrote
recently (in python3). A new script uses it to figure out the
last-modified year for each *.[ch] file and updates its copyright.
It also puts the latest year into the latest-year.h file for the
output of --version.