* log: avoid recursive deletion-message overflow
A quiet daemon receiver queued one MSG_DELETED per path until the generator hit --max-alloc, so skip names the client did not request. If requested output legitimately fills the queue, keep allocator error reporting from re-entering the same buffer and exhausting the stack. Preserve explicit deletion requests across the daemon max-verbosity clamp.
* NEWS: note daemon deletion-message overflow fix
rsyncd.conf(5) says of "timeout": "Using this parameter you can ensure that
rsync won't wait on a dead client forever." That did not hold before a module
was known. set_io_timeout() ran at the very end of rsync_module(), so the
greeting, authentication and the whole argument list were read with no I/O
timeout at all -- a peer could stall at any of them and the child waited
indefinitely. Measured: 20 connections sending "@RSYNCD: 31.0" with no newline
were all still alive well past timeout=5, and only went away when the client
hung up.
The consequence is worse than an idle process. claim_connection() runs BEFORE
auth_server(), so naming a module is enough to take a slot: an attacker with no
credentials could occupy every "max connections" slot of an authenticated
module and hold them for as long as it kept the sockets open, with the
documented control unable to recover them. It costs the attacker nothing --
five stalled children measured 0 CPU ticks over 5s -- so this is descriptor and
slot exhaustion, not load.
Bound the handshake at min(configured, 60s). "timeout" is a Locals parameter,
so lp_timeout(-1) reads the global section -- the same -1 idiom start_daemon()
already uses for lp_reverse_lookup().
Both halves of that minimum matter. "timeout" DEFAULTS TO 0, so honouring only
the configured value would leave the daemon most exposed to this -- one whose
administrator never set a timeout -- exactly as pinnable as before. And capping
matters because an operator who sets "timeout = 86400" for slow links is asking
for patience during a TRANSFER, not for a stranger to hold a pre-auth slot for a
day. The pre-module phase has no legitimate reason to take even a minute.
The bound is retired the moment the module is known, which is what lets the
configured value still govern the transfer. That retirement is load-bearing:
the per-module test only ever LOWERS the timeout (`lp_timeout(module_id) <
io_timeout`), so leaving the handshake bound in place would silently clamp a
module that asked for more -- "timeout = 300" would get 60. It is cleared
before that test runs, and only when io_timeout is still the value we armed,
since the client's own --timeout is parsed in between and must win on its own
terms. Verified: with no global timeout and "timeout = 120" in the module, a
connection idles past 75s rather than being dropped at 60.
Applied only for a real socket daemon (am_daemon > 0): an rsh-run daemon has no
listener to exhaust.
Verified end to end with max connections = 2 and timeout = 5: with two stalled
unauthenticated connections holding both slots, a legitimate client is refused
during the timeout window and served once it elapses. Before this change it was
refused both times.
Reported by Chamal De Silva. Not a regression -- 3.2.7 behaves the same way.
An idle timeout alone is not enough, which the review of the first version of
this change made concrete: safe_read() consults it only when poll() TIMES OUT,
so a peer sending a byte more often than allowed_lull (timeout/2) is never
checked at all. Measured: one byte every 20s held the handshake open for 182s
against a 60s bound, keeping its max-connections slot the whole time -- the
reported attack, merely with the attacker typing.
Non-positive configured values are treated as "use the built-in bound":
"timeout" is parsed with atoi(), so "timeout = -1" would otherwise reach
set_io_timeout() (which reads it as no timeout) and alarm() (which would take it
as a huge unsigned count), disabling the very bound it looks like it configures.
The client's own --timeout is no longer inferred by comparing values, which could
not distinguish it from an identical armed value: io_timeout is zeroed before
parse_arguments(), so anything non-zero afterwards came from the client.
So the bound is absolute and lives in the READ PATH, next to the idle timeout
it complements: safe_read() caps each poll() at whatever is left of it and
gives up when it expires, so it is re-checked on every iteration and a peer
that keeps typing cannot outrun it.
It is deliberately NOT alarm()/SIGALRM. Three earlier attempts used one and
each regressed something: fork() clears pending alarms, so the "post-xfer exec"
parent -- which waits for the ENTIRE transfer -- kept the deadline and _exit()ed
mid-transfer, skipping the hook and releasing the max-connections fcntl lock
while the transfer child ran on; "pre-xfer exec" and the name converter are
operator scripts that may legitimately outlast any handshake bound; and the
cancellation sat inside an exec-environment compile guard, so a build without
setenv/putenv kept it armed through the transfer. A deadline consulted only
where the daemon is already blocked reading a peer has none of those hazards.
It is also kept entirely separate from io_timeout, which is an idle timeout the
module or client may set. Mixing them clamped a module asking for more than the
bound ("timeout = 300" became 60) and leaked the handshake value into the
transfer. Verified: module 300 stays 300, and a client --timeout=7 still wins.
Armed for each peer-driven phase and cleared between them: at the start of the
handshake, tightened by the module's own timeout once the module is known and
its slot claimed, cleared across the hook/fork setup, re-armed before
"@RSYNCD: OK" so it spans BOTH read_args() calls including secluded args, and
cleared before the transfer.
That argument-read coverage is the part that matters most. auth_server()
returns immediately when a module sets no "auth users", so on an ANONYMOUS
module nothing is authenticated: without a bound there, a peer could claim the
slot, take the OK, and trickle an unterminated argument line forever. Measured:
still open after 150s before, closed at 60s after.
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.
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)
Adds six coverage tests from the code-scanner run, each closing a measured
gap in a daemon or metadata code path the suite never reached:
- daemon-include-maxconn: rsyncd.conf &include/&merge directives +
`max connections`/`lock file` (params.c include_config, connection.c
claim_connection, util1.c lock_range).
- fake-super-acl-xattr: --fake-super -A stores ACLs as user.rsync.%aacl/
%dacl xattrs (acls.c am_root<0 IVAL/SIVAL pack, xattrs.c get/set/
del_def_xattr_acl); Linux-only (the user.rsync.* namespace).
- backup-crossdev-copy: make_backup() EXDEV copy-fallback for non-regular
files (do_symlink_at/do_mknod_at/copy_file); skips without a cross-dev
tmpfs.
- daemon-http-proxy: RSYNC_PROXY HTTP CONNECT (socket.c
establish_proxy_connection + base64 Proxy-Authorization + 503 branch).
- daemon-module-options: motd file, socket options, incoming/outgoing
chmod, dont compress, list=no, comment, --sockopts.
- daemon-chroot: `use chroot = yes` incl. the /outer/./inner split and
`temp dir`; probes CAP_SYS_CHROOT and skips cleanly without it.
clientserver.c flushes gcov counters just before chroot() in rsync_module()
so the per-connection child's pre-chroot lines reach disk (the build-tree
.gcda paths are unreachable post-chroot); no-op without --enable-coverage.
CI: the require_tcp-gated tests (daemon-chroot/-http-proxy/-module-options)
plus the Linux-only fake-super-acl-xattr and the cross-dev backup-crossdev-copy
are listed in the per-platform RSYNC_EXPECT_SKIPPED sets where they skip on the
pipe-transport make-check jobs.
Co-authored-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Adds the coverage/regression tests from the code-scanner run and the
gcov plumbing they rely on:
- scanner-argv-bounds, scanner-batch-flag-mismatch,
scanner-delete-delay-overread, scanner-daemon-log-checksum:
regression tests for the argv/-v/--info/--skip-compress bounds, the
batch metadata-ndx corruption, the read_delay_line off-by-one, and
the daemon -c/%C checksum-slot leak.
- daemon-proxy-protocol, daemon-early-exec-nameconv, daemon-auth-group,
daemon-standalone-detach, misc-coverage, nonroot-restrictive-perms,
backup-acl-xattr-cache: daemon and path coverage tests.
- rsyncfns.py: CAP_MKNOD probe in devices_supported().
- gcov_flush() macro (rsync.h) + calls in the daemon fork/_exit paths
(clientserver.c, socket.c); no-op without --enable-coverage. Makefile.in
COVERAGE_EXCLUDE / gcovr / setuid .gcda refinements.
- CI: list the new TCP/root/ACL tests in the per-platform
RSYNC_EXPECT_SKIPPED sets.
Co-authored-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Route every operator-supplied directory path (--temp-dir / --partial-dir /
--backup-dir / alt-dest basis) and the transfer engine's own dest/source opens
through the secure resolver + ownership walk, so a symlink owned by another uid
can no longer redirect a read, a backup, a staging open, a rename, an unlink or a
new-destination create outside the module. Covers backup.c, generator.c (alt/
link/in-place basis), receiver.c (basis open), sender.c (remove-source / source
open / copy-links leaf), rsync.c (held-fd attr stat), clientserver.c (pid-file
parent pin), and main.c (relative-basis make-absolute, mkpath dest-arg guard).
--insecure-links / "insecure links = yes" is the local opt-out (a daemon never
honors a peer-forwarded one).
clientserver.c: close the --early-input-file FILE* on the
fstat/oversize/early-EOF error returns; it was only closed on the
success path.
getgroups.c: free the gid list before returning.
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.
Fixes: #829
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.