parse_one_refuse_match() marked only the first long_options row whose
long name matched the configured spelling, then broke out for a
non-wildcard rule. --compress-threads and --zt are separate popt rows
that both write &do_compression_threads, so "refuse options =
compress-threads" disabled the canonical row and left the alias
accepted: the refused capability was still reachable under its other
name. The same shape covers zc/compress-choice and zl/compress-level.
An exact rule names a capability, not one spelling of it, so mark every
row that does the same thing. Comparing the raw table fields is not
enough for that: popt's `val` means different things per argInfo. For
POPT_ARG_VAL it IS the value stored in `arg`, while elsewhere a nonzero
`val` is an action code for the parser's switch, and POPT_ARG_NONE with
a destination stores 1 whatever `val` says. --del is
POPT_ARG_NONE/&delete_during/0 and --delete-during is
POPT_ARG_VAL/&delete_during/1: the same destination and the same
resulting value, but unequal as table entries, so "refuse options =
delete-during" was still evaded by --del and the mirror held too.
Compare what a row does instead -- the destination and the constant it
assigns, falling back to table-entry equality for rows that store a
runtime value or only dispatch an action. Enumerating all 258 rows,
this couples exactly one pair the field comparison missed, del and
delete-during, and changes nothing else. Opposite switches such as
--foo and --no-foo stay distinct because they assign different values.
Two regressions. The compress-threads one drives the raw daemon
protocol -- not to preserve the spelling, which -M--zt=N would do just as
well, but because it goes on to observe the worker pool the bypass
delivers. Its oracle is the refusal itself -- the alias connection torn
down and
"configured to refuse --zt" logged -- and deliberately not the resulting
worker count: an accepted --zt is a defeated refuse rule however few
threads it produces, and the daemon worker cap being added alongside
this holds that count to 9, so a count-based assertion passes while the
alias is still accepted. Run that test against the cap without this
parser fix and it does exactly that; the two changes were covering for
each other.
The delete one needs neither zstd nor a socket: --remote-option puts the
option in the daemon's argv verbatim, which is the reach an ordinary
user already has, so it drives a stock client both ways round against
modules refusing each spelling, with an unrefused module as the control.
The compress-threads test needs --use-tcp, so it skips in every other
column and is declared in the workflows that enforce a skip set, which a
fleet run otherwise reports as an unexpected skip on fourteen cells.
(cherry picked from commit c529163ef0)
-D and --no-D do two jobs at once: they decide whether devices and
special files are created, and they decide whether those entries carry
their rdev fields on the wire. send_file_entry() and recv_file_entry()
frame those fields with the same condition, but each end evaluates its
own preserve_devices/preserve_specials, so the two only agree because
both normally parse the same command line.
That makes --no-D unusable for a wrapper that controls one end of a
connection and wants to deny creation. Give it to the receiver alone
and the client's -D sender writes rdev the receiver never reads: the
file list desynchronises from that entry on. A FIFO or socket breaks
below protocol 31 -- a hang at 29, "File-list index 0 not in 0 - -1" at
30 -- and a device node breaks at EVERY protocol, current ones included,
because its arm of the condition has no protocol clause at all.
--drop-D separates the two jobs: it refuses the creation and leaves the
encoding alone. The entry is skipped through the existing non-regular
fall-through, so the visible result matches --no-D, and because it
touches no wire state it can be applied to one end by itself.
It has no effect on a sending rsync, which creates nothing.
(cherry picked from commit b607369f5f)
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.
Cap -v repetition so the argstr[64] global can't overflow, clamp a
negative --info/--debug level out of counts[], and cap a --skip-compress
suffix token at 32 bytes.
Co-authored-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
The daemon exclude/filter chain is a name-based visibility filter, exactly as in
stock rsync: a symlink whose own name is not excluded is still followed to an
excluded target (the documented defense for a writable module is "munge symlinks").
Drop the in-module physical-path refusal from abspath_excluded_by_module() (it now
only confines operator/peer paths to the module ROOT) and restore stock rsync's
NAME-based ".." collapse for the daemon-filter checks on the destination,
basis/partial dirs and --temp-dir/--backup-dir via sanitize_path().
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).
C99 snprintf returns the would-have-written length on truncation, so when a
daemon client sends an oversized --max-size/--min-size/--bwlimit/--block-size/
--max-alloc value (each arg can be up to BIGPATHBUFLEN-1 = 5119 bytes via
clientserver.c:read_args), len at the failure: label can far exceed
sizeof err_buf (200). The trailing
err_buf[len] = '\n';
err_buf[len+1] = '\0';
then write 0x0A,0x00 at a byte-precise attacker-chosen offset up to ~4.9KB
past err_buf into adjacent .bss. The same happens via the second snprintf
when the first lands len in [~140,189] and the (min/max: ...) suffix is
truncated.
Clamp len to sizeof err_buf - 2 before the trailing writes, covering both
paths. Reachable from an unauthenticated client against any daemon module
before chroot/privilege-drop; introduced in 66ca4fc97b (2020-07-10).
(cherry picked from commit 3b81103d7ec98145710f8845dd676ba34458169a)
Backport of the 3.5.0 option-parsing hardening:
- bound parse_size_arg() against integer overflow;
- reject a zero --max-alloc (which would disable the per-allocation
sanity cap);
- escape newlines in remote-shell argument expansion.
Tests: daemon-max-alloc-zero, max-alloc-zero-rejected,
remote-shell-newline-escaping.
Reported-by: Azizcan Dastan <azizcan.dastan5@gmail.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>
If poptGetContext returns NULL, perhaps due to OOM,
a NULL pointer is passed into poptReadDefaultConfig()
which in turns SEGVs when trying to dereference it.
This was found using https://github.com/sahlberg/malloc-fail-tester.git
$ ./test_malloc_failure.sh rsync -Pav crash crosh
Signed-off-by: Ronnie Sahlberg <ronniesahlberg@gmail.com>
- Avoid implied rules on generator and (with extra certainty) on server
- Add -R implied-directory path elements as directory includes
- Log about extra file-list checking using a new --debug=FILTER3 level
The new default is to protect args and options from unintended shell
interpretation using backslash escapes. See the new `--old-args` option
for a way to get the old-style splitting. This idiom was chosen over
making `--protect-args` enabled by default because it is more backward
compatible (e.g. it works with rrsync). Fixes#272.
- Rename unchanged_file() to quick_check_ok().
- Enhance quick_check_ok() to work with non-regular files.
- Add a get_file_type() function to the generator.
- Use the new functions in the generator code to make the logic simpler.
- Fix a bug where the `--alt-dest` functions were not checking if a
special file fully matched the non-permission mode bits before
deciding if we have found an alt-dest match.
- Enhance the `--info=skip --ignore-existing` output to include extra
info on if the existing file differs in type or passes the standard
quick-check logic.
- Add `--info=skip2` that authorizes rsync to perform a slow checksum
"quick check" when ignoring existing files. This provides the uptodate
and differs info even if we need to checksum a file to get it.
Change the logic in compat.c to construct the client_info string value
for a local copy so that the various checks of the string don't need to
make an exception for local_server.
- 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.