copy_file() routed a RELATIVE source through secure_relative_open (parents
confined) but opened an ABSOLUTE source -- an operator basis such as an absolute
--copy-dest -- with bare do_open_nofollow, which refuses only a leaf symlink and
follows every parent. basis_link_stat() refuses a foreign-owned basis at stat
time, but a parent flipped to a foreign symlink between that stat and this open
redirects the basis read out of tree (an out-of-tree content read-leak into the
destination; RED on 3.4.x, GREEN here).
Resolve an absolute source's parents through owner_walk_parent (foreign-owned
parent symlink refused, operator's own dirs/uid0/euid symlinks followed).
operator_path_resolve is set only across the walk -- so module-exclude is
enforced -- and restored, leaving the caller's value for the dest side; that is
why confining the source here does not re-open the copy_xattrs dest race that
wrapping the whole copy_altdest_file would (copy-xattrs-symlink-race stays green).
(Leonid Bugaev May-2026 re-audit, KI-46.)
A peer may use MSG_IO_TIMEOUT only to ask us to adopt a SHORTER I/O timeout
(a stricter cap). A crafted server sending val <= 0 would instead zero the
client's --timeout via set_io_timeout(0), disabling it entirely and letting the
server hang the client indefinitely. Ignore a non-positive value.
(Leonid Bugaev May-2026 re-audit, KI-47; pre-existing since 2009.)
The backup-dir fix pinned chmod/chown for an operator leaf; extend the same
confinement to every other sink that re-resolves a cross-tree operator path
(an absolute --temp-dir/--partial-dir/--*-dest), each confirmed by a cross-uid
race PoC that is RED on stock 3.2.7 and GREEN here:
- set_file_attrs (rsync.c): route times (do_futimens), and -- by aliasing
held_fd to the pinned op_leaf_fd -- the xattr/ACL ops through the same
O_NOFOLLOW leaf fd, not a re-resolvable path. A raced/refused pin skips them
(op_refuse) rather than redirecting. finish_transfer now wraps the pre-rename
set_file_attrs in operator_path_resolve so an absolute --temp-dir temp file's
metadata is pinned too (in-tree temps keep their held dirfd, so op_pin is off).
- do_rename_at / do_link_at (syscall.c): an ABSOLUTE side was left at AT_FDCWD
and followed a flipped parent symlink, letting a name-disclosed --temp-dir /
predictable --partial-dir rename pull an attacker file into the destination
(content injection). Resolve an absolute (operator) side via the ownership
walk -- with module-exclude enforced -- while a relative (transfer) side stays
on secure_relative_open. --insecure-links keeps the legacy path.
- secure_basis_open (receiver.c): an alt-dest basis read
(--copy-dest/--compare-dest/--link-dest) on a non-daemon receiver used a bare
do_open; route it through the ownership walk (refuses a foreign-owned basis
symlink, still allows the "../sibling" basis of #915). Daemons keep their
existing confinement branch.
- configure.ac: probe futimens (do_futimens is gated on HAVE_FUTIMENS).
New fd wrappers: do_fchmod, do_fchown, do_futimens. Documented residuals left
as-is: copy_altdest_file's basis copy (routing it re-opens copy-xattrs-symlink-
race; basis_link_stat already refuses the foreign symlink), crtimes
(do_setattrlist_crtime/do_SetFileTime, path-based on macOS/Cygwin), and
device/socket leaf metadata (pinning a device via open has side effects).
Pins the fix in "backup: confine cross-tree operator-path metadata via a pinned
fd". A root operator runs `rsync -a -b --backup-dir=<abs> ...` while a non-root
attacker flips a backup parent component between a real dir and a foreign-owned
symlink -> outside; pre-fix, rsync's own backup-dir attribute mirroring lchowns
the planted symlink to root, laundering it into a trusted symlink the owner-walk
then follows, so the backup escapes the tree.
Root+nobody gated (cross-uid plant needs root); RED on stock 3.2.7 and under
--insecure-links, GREEN on the fix. Uses the compiled flipper for a reliable
RED oracle. cygwin runs make check non-root so the test skips there -- add it to
that workflow's RSYNC_EXPECT_SKIPPED; the root workflows (almalinux-8 container,
sudo ubuntu/macos) run it for real.
The Python path-flipper used by the symlink-race tests wins the race window
unreliably on a journaled disk fs -- on the vulnerable binary it reproduced the
escape only ~1/3 of the time, because the interpreter loop caps the swap rate.
Add compile_c_flipper()/start_c_flipper(): a small C flipper, built on demand
against the build's config.h (CC and -I taken from TOOLDIR, then SRCDIR), that
swaps two sibling names with renameat2(RENAME_EXCHANGE) where available -- one
atomic syscall, no transient missing-name window -- and a self-healing 3-rename
fallback elsewhere. Measured ~2x (plain rename) to ~7x (EXCHANGE) the swap rate
on disk, which turns a flaky RED oracle into a reliable one. It self-terminates
on parent exit plus a deadline backstop (like start_path_flipper) so a killed
test can't leak an orphan, and falls back to the Python flipper where no
compiler is available.
A symlink race on a non-daemon --backup-dir let a local attacker redirect
rsync's backup writes and chmods outside the backup tree when rsync runs as
root. make_backup() sets operator_path_resolve, but set_file_attrs() had no
held parent dir fd for an absolute backup path (held_dfd_for() returns -1), so
its chmod/chown fell through to the path-based wrappers and, for an absolute
path, to raw chmod()/lchown(). copy_valid_path() mirrors the source dir's
attrs onto each backup subdir; when an attacker flips a backup component to a
symlink in that window the raw lchown retags the planted symlink as root-owned
-- laundering it into a "trusted" (uid 0) symlink that the owner-walk then
follows, so the backup rename/chmod escapes the tree.
Pin the leaf inode of a cross-tree operator path with an O_NOFOLLOW open via
the operator owner-walk resolver and drive fchmod/fchown off that fd; a raced
symlink leaf makes the open fail and the op is refused, never redirected. Gate
on the INTENDED type (new_mode), not the attacker-controlled on-disk type. As
root any open failure is the race (a real owned leaf never fails); a non-root
operator, which cannot launder a uid-0 symlink, falls back to the legacy path
op on a benign EACCES. --insecure-links opts back out.
Adds do_fchmod()/do_fchown() fd wrappers. Residual cross-tree metadata sinks
(times, ACLs, xattrs, and --temp-dir's finish_transfer set_file_attrs) are not
covered here and are tracked for a follow-up.
Tighten man-page prose for options whose documented behaviour was
imprecise or outright wrong, matching the behaviour now pinned by the
new oracle tests and verified against the C source:
- strict modes: the real secrets-file rule is "st_mode & 06" (other
read/write) plus root-owner-when-root, so 640 is accepted and 644
rejected -- not the old "any user ID other than the daemon's".
- munge symlinks: rewrite the default in terms of chroot and the "/./"
path split (disabled only for a plain chrooted module serving the
chroot root); drop the bogus "daemon chroot" clause. Fix the helper
reference: support/munge-symlinks is a python script, not perl.
- --no-implied-dirs: spell out that an existing in-tree dest symlink is
followed.
- --files-from: ".." handling is collapse-then-reject-survivors.
- --copy-unsafe-links: describe the lexical unsafe-symlink rule instead
of the old "verbose output" phrasing.
Add behaviour tests that nail down option semantics the man pages
describe vaguely, each verified to pass against both this branch and the
3.2.7 oracle (so they document long-standing behaviour, not regressions):
daemon-strict-modes-matrix secrets-file mode rule (st_mode & 06)
daemon-chroot-munge-default munge-symlinks default vs chroot/path /./
safe-links-unsafe-def --copy-unsafe-links lexical unsafe rule
no-implied-dirs-symlink --no-implied-dirs follows in-tree dest symlink
files-from-path-clamp --files-from collapse-then-reject ".."
relative-implied-symlink --relative sends implied dirs as real dirs
keep-dirlinks-rule --keep-dirlinks opening rule
backup-dir-relative --backup-dir resolves relative to dest
no-implied-dirs-symlink relies on the -R "/./" implied-dir marker, a
protocol-30+ feature; under protocol 29 the generator rejects the
multi-component path (same as the 3.2.7 oracle), so it passes through
without testing, matching the sibling relative-implied test.
daemon-chroot-munge-default needs root to exercise the chroot regimes
and skips otherwise; add it to RSYNC_EXPECT_SKIPPED only in the
almalinux-8 and cygwin workflows, which run make check non-root. The
macos workflow runs it as root, so the test runs there for real.
The "Robustness against malicious peers" summary enumerated the classes of
peer-triggerable faults closed by the fuzzing/static-analysis pass. Add the two
classes that the later scanner batch introduced -- reads past a file-list
allocation (mostly bounded over-reads of an entry's extra slots) and
option-argument-driven length bounds (plus the suffix-list recursion sink) --
keeping the summary general (no per-finding detail; not every over-read
disclosed memory).
daemon-symlink-escape-matrix exercises, for a writable non-chroot module, every
combination of `insecure links` {no,yes} x `munge symlinks` {no,yes} x link
origin {pre-existing, uploaded} x op {read pull, write push} x five symlink
target types (rel-within, rel-outside, rel-transits [.. above the module root
then back in], abs-outside, abs-inside).
It pins the contract: the secure default follows only an in-tree (rel-within)
link and NEVER reaches an out-of-module target (read or write); the
`insecure links = yes` opt-out restores legacy following on sender AND receiver
(so an outside target escapes, matching stock 3.2.7); and an uploaded link never
escapes regardless (munge prefixes it, munge-off sanitises it). A secure-default
out-of-module access is a hard failure. require_tcp + root gated; listed in the
per-platform RSYNC_EXPECT_SKIPPED pipe make-check sets.
secure_relpath_active() (the gate that routes receiver-side filesystem ops --
get_dir_fd/dpc, do_*_at, open_tmpfile, make_path, link_stat -- through the
symlink-race-safe resolver) checked only am_daemon/am_chrooted/am_sender, not
the symlink_optout_allowed() opt-out. So `insecure links = yes` (or a non-daemon
--insecure-links) restored the legacy follow only on the SENDER enumeration
(which checks the opt-out directly), while the receiver still confined writes,
mkdirs, renames, unlinks and stats through a pre-existing in-module symlink --
i.e. the admin opt-out did not actually reproduce the pre-3.4.3 behaviour it
documents (rsyncd.conf(5) "munge symlinks"/"insecure links").
Have secure_relpath_active() return 0 when symlink_optout_allowed(), so the
opt-out uniformly disables the secure resolver on both sides. No effect on the
default (opt-out off): confinement is unchanged.
The probe() helper sends the @RSYNCD greeting after the PROXY header, then
reads the daemon's response. For a `want='drop'` leg (untrusted peer / a
daemon with no `proxy protocol hosts`) the daemon closes the connection,
which on some CI runners surfaces as EPIPE/ECONNRESET on our sendall() before
we ever read -- an uncaught BrokenPipeError that failed the test (seen on
AlmaLinux 8 and Ubuntu 22.04, a timing race; other runners closed read-side).
Wrap the send/recv in `except OSError` and leave `out` empty: for want='drop'
the absent greeting is the expected outcome; want='ok'/'denied' still fail
correctly on an absent greeting.
The four tests added with the audit fixes skip on the standard (non-ASan,
stdio-pipe) CI/fleet runs: daemon-deny-dns-failopen needs a TCP peer
(require_tcp), and the three leak reproducers need an AddressSanitizer build
(require_asan). Add them to RSYNC_EXPECT_SKIPPED so make check / the fleet
report clean instead of flagging an expected skip as a mismatch.
(cherry picked from commit 272341682b668424e1f87fd1e8f8a5878db272c2)
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)
match_hostname() did a forward-DNS lookup of a config-specified hostname
token and, on gethostbyname() failure, returned "no match" -- which
allow_access() cannot distinguish from a real non-match, so a daemon with
"hosts deny = <hostname>" silently admitted the host whenever the token
could not resolve (a resolver-less chroot, or a transient DNS failure).
No attacker DNS control required.
Thread a deny flag through access_match()/match_hostname() and, on a
forward-DNS failure, treat an unresolvable DENY-list token as a match so
the connection is refused (fail closed); allow-list tokens still fail as a
non-match. Sibling of CVE-2026-43617, which fixed only the reverse path.
Reported-by: Leonid Bugaev <leonsbox@gmail.com>
(cherry picked from commit 84e469ea03a98ef31326d3d861336d7e7d582ce1)
Python 3.14's strict UTF-8 text mode raised UnicodeDecodeError when tls
emitted a non-UTF-8 byte (a filename or symlink target with high bytes),
aborting every test that calls rsync_ls_lR via hands_setup() -- ssh-basic,
hands, delete, files-from, alt-dest, daemon-gzip-*. Decode the tls output
with errors='backslashreplace' so a stray high byte renders as \xNN in the
listing rather than crashing the run; the result stays a clean str that
write_text()/print() can consume without re-raising.
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>
The operator_path_resolve branch of do_mknod_at() called mknodat() and
returned its result directly, without the FIFO/socket fallback that the
bare-path do_mknod() and the secure-relpath branch below it both have.
mknodat() can make a FIFO only on Linux; on the BSDs/macOS/Solaris it fails
with EINVAL, so creating a special file under an operator-supplied path --
e.g. backing up a FIFO into a --backup-dir -- failed there. Retry race-safely
with mkfifoat() on the held parent dirfd, and fail a nested socket closed
(EOPNOTSUPP) exactly as the secure-relpath path does.
io.c only treats a short read as the EOF sentinel when the fd is still
open; xattrs.c never stores a -1 from find_matching_xattr() and guards
ndx < 0 in set_xattr; rsync-ssl refuses the gnutls backend without
RSYNC_SSL_CA_CERT.
Co-authored-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
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>
Move the uid/gid/acls/xattrs *_ndx assignments past check_batch_flags():
a mismatched-flag batch otherwise wrote F_XATTR(file) at offset 0 of
every file_struct, clobbering file->dirname. parse_negotiate_str() no
longer short-circuits on am_server, so each side picks its own #1 mutual
digest/checksum/compress choice rather than deferring to the peer's
order; man pages updated to match.
Co-authored-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
A '!'-prefixed delete-delay entry computed one byte short, dropping the
final character of the name.
Co-authored-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
hlink.c must confirm S_ISREG before quick_check_ok(FT_REG,...) reads
F_SUM, and start_server() must set sender_keeps_checksum when a daemon
sender runs -c with a %C log format so make_file() allocates
SUM_EXTRA_CNT. Without these, F_SUM() reads past the pool slot and (for
%C) hex-encodes adjacent heap into the transfer log.
Co-authored-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
recv_file_entry (normal and XMIT_HLINKED abbrev branches) and make_file
left F_RDEV_P / the symlink-name slot uninitialized when the matching
preserve option was off, so a later read walked into adjacent pool
memory. Empty the symlink name when !preserve_links, zero F_RDEV_P when
!preserve_devices, and mirror both in the abbreviated branch. receiver.c
saves/restores the --write-devices S_IFBLK mode flip around receive_data
so dest_mode() never sees the mutated mode.
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>
Add a per-target `xfail` field (merged with the global --xfail) so a known
platform/version-specific failure can be tolerated persistently without a
command-line flag -- the test still runs, and if it passes the entry is a no-op.
Mark crtimes xfail on mac2: older backport binaries (3.4.x/3.2.7) drive APFS
birthtime via setattrlist differently than current rsync, so the 3.5.0
testsuite's crtimes check fails there; it passes for a current binary.
variety is the heaviest test in the suite; on slow platforms (Cygwin) the
per-component O_NOFOLLOW resolver pushes it past the default 300s per-test
timeout. Give it the same 600s budget the hardlinks test already gets.
The race-safe resolver's dirstack grew its fd array with realloc() inside the
recursive ds_descend() walk and freed it via a passed pointer. clang's
unix.Malloc analyzer cannot model that ownership and reported a false "Potential
leak of ds.fds", failing the pinned-clang-18 scan-build gate.
The walk holds one open fd per path component, so its depth is already bounded
by RLIMIT_NOFILE; use a fixed inline array (DS_MAXDEPTH, mirroring DPC_MAXDEPTH)
and drop the malloc/realloc/free entirely. ds_push() fails with ENOMEM past the
cap. No behaviour change -- 1024 levels exceeds any reachable depth.
--insecure-links and the SECURITY sections in rsync(1)/rsyncd.conf(5), the
name-based exclude/filter + munge-symlinks clarification, the rsync-ssl hostname
note, and the README (incl. the thank-you to Wayne Davison, 2004-2024).
NEWS.md: the 3.5.0 security-update section. SECURITY.md: the platform-residuals
policy, symlink-race-safe path resolution, operator-path symlink defense, the
name-based daemon exclude/filter clarification, and the known residuals.
testsuite/fleettest.py builds the branch and runs the suite across a fleet of
remote VMs (BSDs/Solaris/Ubuntu/macOS/Cygwin) over multiple transports, with a
--cleanup that reaps orphaned daemons; the expect/*.expect files are the
version-mixing manifests (current rsync vs old static peers).
Gate the build on a pinned clang-18 analyzer run (deterministic checker set,
--status-bugs fails on any new report) and run the latest clang informationally.
Run the security test suite (pipe + real-TCP daemon transports, proto30/29, and a
targeted non-root pass) across Ubuntu/macOS/Cygwin/AlmaLinux, with per-platform
expected-skip baselines for the tests that legitimately skip there.
The exclude/filter is a name filter, not a symlink boundary (3.2.7-equivalent):
symlink-exclude family, daemon-exclude-namebased, the operator-path exclude /
traversal / dir-daemon cases, filter-merge and implied-trailing-backslash.
Co-authored-by: Omar Elsayed <omarelsayed161@gmail.com>
The TOCTOU / symlink-race suite for the secure resolver and operator-supplied
paths: chdir/chmod/rename/mknod/source/dest symlink races, relative make_path and
symlinked-parent cases, the operator-path matrix (--temp/partial/backup-dir,
alt-dest basis, files-from, log-file, insecure-links), and the admin-file opens
(--password-file / daemon secrets / config / log-file / early-input symlinks),
plus the daemon module-confinement and chroot inner-module cases.
Co-authored-by: Omar Elsayed <omarelsayed161@gmail.com>
The t_rename_secure / t_symlink_secure / t_acl unit harnesses (C), the rsyncfns.py
helper library (daemon fixtures, symlink matrix, tree compare, xattr/ACL drivers),
runtests.py, and the rsync_proto/xrsync/cmptree/mkvariety helper scripts that the
security tests build on.
Pin each validated path component (and a receiver-side new destination's parent)
with O_RDONLY|O_NOFOLLOW and pass /proc/self/fd/N to the exec'd rsync so the child
cannot re-resolve the path; probe the /proc/self/fd magic-symlink at runtime (not
just isdir); fail closed on a readlink anomaly; and don't abort when flock() is
unavailable (Solaris).
batch.c: single-quote every --write-batch replay-script argument, quote a "--opt="
prefix unless it is a plain option token, and refuse a newline in a filter rule
written to the replay script. rsync-ssl: bind the server certificate to the
requested hostname in stunnel mode.
authenticate.c: seed gen_challenge() from /dev/urandom, add an "auth digest" floor
to refuse weak negotiated digests, and fstat the opened --password-file fd rather
than re-stat the pathname; checksum.c carries auth_digest_rank(). socket.c: reject
control bytes in the daemon host before a proxy CONNECT and bind the stunnel server
cert to the requested hostname. clientserver/access: warn when proxy-protocol
fail-closes. loadparm + daemon-parm: only shell-quote %RSYNC_*% for shell-executed
hooks, and add the auth-digest / proxy-protocol-hosts module parameters.
Refuse malformed/hostile wire input that could crash or corrupt the receiver:
io.c (out-of-range file index, count*blength OFF_T overflow, read_args NUL room,
deferred in_multiplexed), flist.c (sub-flist after the final flist is freed,
FLAG_HLINKED on dirs / gated on preserve_hard_links, parent_ndx bound, cleared-
slot ndx in the transfer phase), hlink.c (undeclared cross-flist gnum -> error not
assert), match.c (clamp peer flength, re-check len before want_i), log.c (drop
peer-reachable asserts and F_SUM deref), exclude.c (merge-file recursion cap,
trailing-backslash heap fix), lib/pool_alloc.c (ASan redzone for pool underflow).
Apply ACLs and xattrs through a held file descriptor instead of by path, closing
the symlink-race where an attacker swaps the leaf between the transfer and the
metadata set. lib/acl.c provides fd/at POSIX-ACL primitives (the system libacl
*_at where available, else a /proc/self/fd compat that never follows on the
fallback); acls.c routes through them and stays functional (path-based) where the
OS lacks a race-safe primitive; xattrs.c routes copy_xattrs through a held fd; -VV
(usage.c) reports the runtime race-safe-ACL capability.
The daemon exclude/filter chain is a name-based visibility/tamper filter, as in
stock rsync (verified against 3.2.7): a symlink whose own name is not excluded is
followed to an excluded target, and the documented symlink defense is `munge
symlinks`, not the filter. Collapse ".." (via sanitize_path) before the daemon
dest / temp-dir / backup-dir / partial-dir / basis filter checks so a "../excluded"
path is matched by name like 3.2.7 (clean_fname's CFN_COLLAPSE_DOT_DOT_DIRS does
not collapse "a/b/c/../../../secret"), and keep a leading "/" for a "path = /"
module so an absolute filter rule still matches. The module-ROOT confinement of
operator paths is unchanged (previous commit); only the in-module name match is
restored to its 3.2.7 behaviour.
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).
The core symlink-race (TOCTOU) defense for the CVE-2026-29518 class: a portable
secure resolver that walks a path one component at a time holding an O_NOFOLLOW
dir fd per level (the dirfd-stack), plus the do_*_at() filesystem wrappers, the
held-directory fd cache, secure_relative_open[_at](), the operator-path ownership
walk (owner_walk_parent / open_no_attacker_symlinks, follow a uid0/euid symlink,
refuse a foreign one) and its module-root confinement (abspath_excluded_by_module).
util1.c routes change_dir / robust_rename / make_path / handle_partial_dir through
it; the resolver bounds its deep-path fd use against RLIMIT_NOFILE. Also drops the
obsolete android.c openat2 path and confines delete.c via the held dirfd.
configure.ac: detect fdopendir + a working dirfd() (a macro/inline on the BSDs,
so the default link probe mis-detects) and getrlimit/setrlimit for the resolver's
deep-path fd budget; add the race-safe-ACL build probes.
Makefile.in: build lib/acl.o; build the new t_rename_secure / t_symlink_secure /
t_acl unit harnesses; drop the obsolete android.o object.
mkgitver: version a git build by the dev version + commit (not the nearest tag),
and harden the version.h parse.
The description of user@host::module transfers over a remote shell only
documented the "ssh -l ssh-user" form, which led readers to conclude that
user@ never reaches the remote shell. In fact, for the simple
`--rsh=ssh user@host::module` form the user@ prefix is used both as the
ssh login user (ssh -l user) and as the rsync-user offered to the module;
the two are the same name. rsync only omits its own -l when the remote
shell command already specifies one, in which case user@ becomes the
rsync-user alone.
Spell out the default behaviour and why the explicit -l is needed to use
a different ssh login than the rsync-user.