Commit Graph
7554 Commits
Author SHA1 Message Date
Andrew Tridgell 6e00a3b867 daemon: bound argument lists + proxy-protocol peer/length hardening
- 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.
2026-06-15 15:24:42 +10:00
Andrew Tridgell c02380f7d3 log/batch: refuse planted symlinks at --log-file and --write/read-batch
- log: open the --log-file / daemon log file through
   safe_open_no_attacker_symlinks() so a planted symlink can't redirect the
   privileged log write;
 - batch: open the --write-batch/--read-batch files the same way (+ an
   S_ISREG check), and restore O_BINARY / an O_CLOEXEC fallback on the batch
   opens.

Tests: log-file-symlink, batch-file-symlink.
2026-06-15 15:24:42 +10:00
Andrew Tridgell ad49840f8a util1: confine the non-daemon receiver's destination chdir (cross-uid)
A non-daemon receiver named its own destination and reached it with a
plain chdir(), so a parent component an attacker raced from a real
directory to a symlink was followed -- a root-run transfer's CWD escaped
to an attacker-chosen tree before any write.  Resolve the operator-named
dest (absolute and relative) through safe_open_no_attacker_symlinks() +
fchdir: still follow the operator's/root's own symlinked dest (the
/backup -> /mnt/disk admin pattern) but refuse one owned by another uid.

Tests: symlink-race-dest, symlink-race-relative-dest, dest-symlinked-dir.
2026-06-15 15:24:42 +10:00
Andrew Tridgell 8639f647aa daemon: secure inner chroot module paths
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.
2026-06-15 15:24:42 +10:00
Andrew Tridgell e624db85f6 exclude: refuse planted symlinks at filter merge / *-from paths
parse_filter_file() opened operator- and (via per-directory merge files
like .cvsignore) sender-controlled paths via plain fopen() with no symlink
defense: an unprivileged source-tree owner could plant a dir-merge file as
a symlink to a root-readable file, leaking its content through the filter
parser, or redirect an --exclude-from/--files-from open via a planted
parent.  Open via safe_open_no_attacker_symlinks(O_RDONLY)+fdopen(), which
refuses symlinks anywhere in the path unless owned by uid 0 or our euid.

Tests: excludefrom-symlink, filter-merge-symlink.
2026-06-15 15:24:42 +10:00
Andrew Tridgell 535a562ccc syscall: resolve the daemon's served-module anchor via the pinned module_dirfd
open_anchor_dirfd() now dups the identity-pinned module_dirfd when the
anchor is the served module root, instead of re-resolving the absolute
module_dir with openat(AT_FDCWD, ...) -- which re-traverses the module's
ancestors as the dropped-privilege module uid and EACCESes when the module
sits under a non-traversable parent (a 0700 home).  Functionally identical
(same inode), but privilege-drop-safe.  syscall.o links into the t_* test
helpers without clientserver.o, so add a module_dirfd stub to t_stub.c.

Test: daemon-module-private-parent.
2026-06-15 15:24:42 +10:00
Andrew Tridgell b33571a1df daemon/authenticate: open operator-supplied paths refusing planted symlinks
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.
2026-06-15 15:24:42 +10:00
Andrew Tridgell 9e12cabd8b util1: safe_open_no_attacker_symlinks() + confine daemon module-root chdir
- safe_open_no_attacker_symlinks(): open an operator-supplied path
   refusing to traverse any symlink (parent or leaf) not owned by uid 0 or
   our euid -- a trusted-owned symlink is still followed, an untrusted one
   fails ELOOP.  Used by the daemon's operator-path opens.
 - confine the daemon module-root chdir under use-chroot=no so a planted
   symlink at the module path is not followed.
2026-06-15 15:24:42 +10:00
Andrew Tridgell 45dd14950a sender: read symlink targets through scan dirfds
The directory-scan confinement resolves the opendir and the per-entry stat
through the held scan dirfd, but readlink_stat() still read the symlink target
with a path-based do_readlink(): a raced parent symlink could redirect that read
and leak an out-of-tree symlink target for a same-name entry.

Add do_readlink_atfd() (readlinkat via the held dirfd) and scan_readlink(), which
routes the read through scan_dirfd for an entry directly in the scan dir (the
same gate scan_link_stat uses), else falls back to do_readlink().  Completes the
enumeration confinement's symlink-target leg.
2026-06-15 15:24:42 +10:00
Andrew Tridgell 5aa125d472 flist/clientserver: confine the directory-scan enumeration
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.
2026-06-15 12:21:57 +10:00
Andrew Tridgell a064a92f82 sender: confine the non-daemon content open against a parent-symlink race
A local/non-daemon sender opened each file's content by path
(do_open_checklinks -> do_open_nofollow), O_NOFOLLOWing only the leaf, so
a parent component raced from a real directory to a symlink after the
file-list scan was followed -- a higher-authority sender (root nightly
backup, or a victim copying an attacker's tree) read and copied a file
from OUTSIDE the source tree.

For default symlink handling (no -L/--copy-unsafe-links/-k follow), open
the content through secure_relative_open(NULL, fname, O_RDONLY|O_NOFOLLOW)
anchored at the transfer root change_pathname() chdir'd into, so a parent
that escaped the tree is refused.  The symlink-following modes keep
following.  The daemon sender was already module_dir-anchored.

Closes symlink-race-source.
2026-06-15 12:21:57 +10:00
Andrew Tridgell 0b7ee36157 build: add t_rename_secure and t_symlink_secure check helpers
The secure-rename and secure-symlink/mknod regression tests drive these
unit helpers (which exercise do_rename_at()/do_symlink_at()/do_mknod_at()
directly), mirroring the existing t_secure_relpath/t_chmod_secure helpers.
Built by `make check-progs`.

Enables: rename-mixed-parent-symlink-race, rename-mixed-parent-escape-poc,
symlink-mknod-fakesuper-symlink-race.
2026-06-15 12:21:57 +10:00
Andrew Tridgell 65d452bb45 syscall: close the chmod leaf race with a held-fd fchmod
set_file_attrs() and the generator chmod a received entry by name relative to
the held parent dirfd.  A plain fchmodat(dfd, name, mode, 0) follows a
final-component symlink an attacker could swap in, escaping the confined tree.

do_fchmodat_nofollow() now stats the leaf NOFOLLOW and, for a regular file,
directory or FIFO, pins it with openat(O_RDONLY|O_NOFOLLOW) and fchmod()s the
held fd: leaf-safe, works on every kernel, and -- unlike the bare fchmodat2()
syscall -- goes through fchmod(), which LD_PRELOAD interceptors such as fakeroot
wrap.  A symlink leaf is refused (ELOOP).  Sockets/devices and any open failure
fall to fchmodat(AT_SYMLINK_NOFOLLOW), then the raw fchmodat2() syscall as a last
resort; if none is available we skip with a warning rather than fall back to a
leaf-following chmod.  Routes do_chmod_at() and do_chmod_atfd() through it.

(Backport of master 463aaa77; the openat2_usable() runtime probe and the
copy_file confinement-gate widening from that commit are omitted -- the former
needs infrastructure not on this branch, the latter is already present.)
2026-06-15 12:21:57 +10:00
Andrew Tridgell 9e0e40b9b9 syscall: hold an ancestor-dirfd stack as the held-directory cache
The held-directory cache kept only the single current leaf directory, so
moving to a sibling re-resolved the whole path from the anchor and re-opened
every ancestor.  The generator and receiver don't recurse -- they iterate a
path-sorted file list (iteration == DFS) -- so there was no call stack holding
the ancestors.

Keep the whole current ancestor chain open as pinned dirfds, keyed on path
components, and on each resolution reuse the longest common prefix, popping
only the divergent tail.  Each directory is then opened once while we are
inside its subtree.  get_dir_fd() (receiver/generator) and held_dir_path_fd()
(sender) share the stack; ds_descend() still resolves each component, so
in-tree dir-symlink following and confinement are unchanged.

change_dir() drops the cwd-relative stack on any real chdir -- the one
invalidation needed now that the cache is content-keyed, not pointer-keyed.
The per-chunk reset_dir_fd_cache() calls in receiver.c/generator.c are gone:
they guarded the old pointer-keyed cache's aliasing bug, which a content-keyed
stack of pinned fds cannot have, and holding the fds across chunks is strictly
more race-safe (a swapped ancestor resolves to the pinned original).

(cherry picked from commit 182bed6cc6b7c33e546132a069b2d5f4203cb91c)
2026-06-15 12:21:57 +10:00
Andrew Tridgell 29ddd45e3d syscall: broaden the symlink-race resolver to every non-chroot receiver
secure_relpath_active() now hardens path resolution for ALL non-chrooted
receivers/generators, not just the non-chrooted daemon module:

    !am_chrooted && (am_daemon || !am_sender)   (+ daemon inner-module clause)

so a root-run or cross-user local / remote-shell transfer resolves
destination paths under the held-dirfd resolver, closing the parent-
component symlink-race / confused-deputy on the receiver side.  A chroot
is its own confinement (excluded); the sender is excluded so it still
follows -L/--copy-links symlinks out of the tree.

Also confine do_rename_at()/do_link_at() each side independently: an
absolute (operator-supplied) source must not disable confinement of a
relative destination, and an absolute operator-trusted path (e.g. an
absolutized --link-dest basis) uses AT_FDCWD with the full path.

Turns green: nondaemon-symlink-race, relative-mkpath-symlink,
relative-mkpath-dir-symlink, rename-mixed-parent-transfer,
symlink-dest-backupdir.  No regression (itemize/xattrs-hlink stay green
via the absolutized alt-dest basis).
2026-06-12 17:15:21 +10:00
Andrew Tridgell 4677288a91 main/util1: keep operator-trusted alt-dest basis dirs accessible under confinement
Preparation for broadening the symlink-race resolver to non-daemon
receivers: a --link-dest/--copy-dest/--compare-dest dir is operator-
supplied and legitimately outside the destination tree (e.g.
--copy-dest=../to), so the confined resolver must not reject its "..".
 - check_alt_basis_dirs() now absolutizes a relative basis dir against the
   destination curr_dir for local (!sanitize_paths) transfers too, not just
   dry_run>1.  An absolute path makes the do_*_at() wrappers and copy_file()
   use plain (operator-trusted) resolution.
 - copy_file() routes the source open through the secure resolver only for a
   *relative* source under the broadened gate; an absolute (absolutized)
   basis source uses do_open_nofollow().
2026-06-12 17:15:21 +10:00
Andrew Tridgell c7aeb639bf rsync/generator/receiver/flist: route attr/basis/scan ops through held dirfds
Behaviour-neutral held-dirfd conversion (the substrate the broadened gate
hangs off):
 - set_file_attrs() resolves the entry's dir once (held_dfd_for) and issues
   chown/chmod/times as single-component *at() calls against that held fd;
 - the generator routes per-entry stat/mkdir/chmod through a held dirfd;
 - the receiver reads the delta basis via the held dirfd and resets the
   one-slot dir-fd cache at flist-chunk boundaries;
 - the sender scan stats entries via the opendir fd.
2026-06-12 17:03:19 +10:00
Andrew Tridgell 5ccb76b08b util1: set_times_at() + make_path parent chain via do_mkdir_at()
set_times_at() is the held-dirfd form of set_times() (utimensat tier;
returns -2 where the active tier has no dfd form so the caller falls back).
make_path() now creates its parent chain through do_mkdir_at(), which
resolves the parent via the secure resolver, closing the --relative
implied-parent symlink escape once the gate covers the receiver.
2026-06-12 17:03:19 +10:00
Andrew Tridgell 25341c51e9 syscall: held_dfd_for + secure mixed-parent rename and fake-super mknod
Backport of the held-dirfd leaf hardening:
 - held_dfd_for(): return the cached entry-directory fd when a path lives
   directly in the entry's own dir (the common held-dirfd traversal case),
   else -1 so the full-path do_*_at() wrappers are used;
 - secure the mixed-parent do_rename_at() paths;
 - secure the bare-path do_symlink_at()/do_mknod_at() in fake-super mode.

Gated like the rest of the resolver (active for the hardened receiver),
so behaviour-neutral until the gate is broadened.
2026-06-12 17:03:19 +10:00
Andrew Tridgell 41a2864353 sender: follow an in-tree symlink leaf confined for the copy-links modes
The held-dirfd resolver always applies O_NOFOLLOW to the leaf (leaf-race
safety), which refuses an in-tree symlink the operator asked to follow with
-L / --copy-unsafe-links / --copy-dirlinks.  For those modes, resolve the
leaf ourselves: read the link, refuse an absolute or escaping target (a
module escape), and re-resolve the relative target through the secure
resolver (which follows in-tree links and rejects an escape above the
anchor), looping for a symlink chain.  The final open stays O_NOFOLLOW so
the module boundary and leaf-race protection both hold.

Closes the daemon-copylinks-intree regression from the resolver rewrite.
2026-06-12 16:50:16 +10:00
Andrew Tridgell c186a81f6a syscall: replace the openat2 resolver with the held-dirfd-stack walk
Backport of the 3.5.0 resolver rewrite (drops openat2/RESOLVE_BENEATH for
a portable held-dirfd-stack walk).  secure_relative_open() now walks each
path component from the anchor, opening it O_RDONLY|O_DIRECTORY|O_NOFOLLOW
and keeping a stack of the open dirfds: descending a real subdir pushes
its fd, and an in-tree ".." in a followed symlink target pops back to the
already-pinned parent fd rather than re-resolving ".." -- so an ancestor
renamed mid-walk cannot redirect the climb and the walk can never rise
above the anchor.  In-tree symlinks are followed (unlike the per-component
O_NOFOLLOW reject), absolute targets are refused, symlink hops are bounded.

This is the substrate the per-subsystem held-dirfd routing hangs off, and
it follows in-tree symlinks while confining -- which openat2 RESOLVE_BENEATH
could not do without breaking legitimate transfers.  NOFOLLOW_HIT_SYMLINK()
(ELOOP/EMLINK/EFTYPE) moves to rsync.h.  Behaviour-neutral for the existing
daemon gate; the broadening to all non-chroot receivers is a later commit.
2026-06-12 16:50:16 +10:00
Andrew Tridgell 28e36bfd6a syscall: P0 held-dirfd scaffolding (open_dir_secure + do_*_atfd wrappers)
Backport of the 3.5.0 P0 held-dirfd scaffolding: open_dir_secure() opens
an entry's parent directory confined (through secure_relative_open) and
returns a held dirfd, and the do_*_atfd() family operates on a single
leaf name relative to that held fd, so a parent component flipped to a
symlink after resolution cannot redirect the operation.

Additive: callers are routed onto these wrappers in the following
per-subsystem commits.  The authority gate still matches the 3.4.4
daemon-only resolver; the next commit broadens it to every non-chroot
receiver.
2026-06-12 16:22:11 +10:00
Andrew Tridgell ac82ccec8f configure: detect fdopendir and a working dirfd()
The 3.5.0 directory-scan confinement opens directories with the secure
resolver and turns the held fd into a DIR* via fdopendir(), so detect
fdopendir.  dirfd() is a macro or static inline on several systems (the
BSDs), where the default AC_CHECK_FUNCS link probe gives a false
negative; probe it with a real compile+link that includes <dirent.h> and
calls it, so HAVE_DIRFD is defined and the confinement is not silently
compiled out.
2026-06-12 16:19:13 +10:00
Andrew Tridgell cd61c1cc62 xattrs: cap peer-supplied wire data
Backport of the 3.5.0 fix bounding peer-supplied xattr wire data so a
malicious peer cannot drive an oversized allocation/read.

Test: xattr-wire-cap.
2026-06-12 15:55:30 +10:00
Andrew Tridgell 6365fe750b io: reject malformed wire indices and zero-length checksum blocks
Backport of the 3.5.0 wire-input validation:
 - reject an out-of-range file index from the wire in read_ndx();
 - reject a zero-length checksum block.

Test: checksum-zero-blocklen.
2026-06-12 15:55:30 +10:00
Andrew Tridgell 0d5e8a0614 options: bound size args and escape remote-shell newlines
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>
2026-06-12 15:55:30 +10:00
Andrew Tridgell cf44c1df44 loadparm: shell-escape RSYNC_* hook expansions
Backport of the 3.5.0 daemon-hook quoting fix: only the shell-executed
hooks expand %RSYNC_*% tokens, and those expansions are shell-escaped so
a crafted module path / environment value cannot inject shell syntax.

Test: daemon-exec-rsync-env-shell-escape.
2026-06-12 15:52:39 +10:00
Andrew Tridgell 407fe4ac48 exclude+flist: confine daemon filter-merge and implied-parent delete-scope
Backport of two 3.5.0 daemon-filter fixes:
 - parse_filter_file(): strip module_dirlen off an absolute merge-file
   path before the daemon_filter_list check (parse_merge_name() prepended
   module_dir), so an anchored module-relative rule actually matches; a
   hidden merge file is treated as non-existent rather than tripping the
   fatal-vs-silent oracle (Mitchell Benjamin finding 1);
 - downgrade implied-parent directories to non-content so a malicious
   sender cannot expand --delete scope via a content-dir entry
   (Mitchell Benjamin finding 3).

Tests: daemon-filter-merge-bypass, malicious-sender-delete-scope.

Reported-by: Mitchell Benjamin <mitchell@revampstudio.com.au>
2026-06-12 15:52:39 +10:00
Andrew Tridgell 91b09b6e38 clientserver: harden daemon name-converter responses
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>
2026-06-12 15:52:39 +10:00
Andrew Tridgell 78a4073751 ci: run the v34-stable-testsuite regression suite against this build
Build rsync here and run the v34-stable-testsuite Python suite against
the built binary (helpers/config.h as tooldir, test scripts via --srcdir)
on ubuntu-latest and ubuntu-22.04, pipe + --use-tcp passes. Gives
regression coverage without importing the full master suite.
2026-06-08 11:30:42 +10:00
Andrew Tridgell 12969d43a0 t_stub: give test helpers an unlimited max_alloc
Helpers link util2.o but not options.c, so they used the stub's
max_alloc = 0, which makes every my_alloc()/my_strdup() in util2.c abort
with "exceeded --max-alloc=0". The secure_relative_open() fallback path
hits my_strdup() and aborts. Set max_alloc = (size_t)-1.
2026-06-08 11:30:42 +10:00
Andrew Tridgell 77b023b5ce syscall: honour --open-noatime in do_open_nofollow
do_open_nofollow() -- the O_NOFOLLOW read path the sender uses for local
transfers via do_open_checklinks() -- never injected O_NOATIME, so
--open-noatime was dropped and reading a source file bumped its atime.  Mirror
do_open()/do_open_at(): add `flags |= O_NOATIME' when open_noatime is set.
Caught by the open-noatime test on strict-atime Linux (O_NOATIME is #ifdef'd,
so other platforms were unaffected).
2026-06-08 08:15:27 +10:00
Andrew Tridgell 965a7a1cc1 build: add check-progs target for fleettest
Build the test-helper programs without running the suite, so an external
harness (fleettest.py) can invoke runtests.py with its own options.

(cherry picked from commit a122e5d6b6)
2026-06-08 08:15:27 +10:00
pterror 56c434d4a6 receiver: fix NULL deref on the delta discard path
receive_data() crashed a receiver that was merely DISCARDING a file's
delta stream. discard_receive_data() calls receive_data() with
fname == NULL and fd == -1, so size_r == 0 and mapbuf == NULL. A normal
block-MATCH token (against a block the basis and source share) then
reaches the !mapbuf branch added in 31fbb17d ("receiver: fix absolute
--partial-dir delta resume"), which calls full_fname(fname). full_fname()
dereferences its argument unconditionally (util1.c: `if (*fn == '/')`),
so fname == NULL faults there -> receiver SIGSEGV.

This is a normal-operation crash with a stock cooperating sender, not an
adversarial one. The generator hands the sender real block sums whenever
the basis is readable and we're in delta mode; the receiver only decides
to discard afterwards, when its output cannot be produced -- e.g. the
destination directory is not writable (mkstemp fails), the basis turns
out to be a directory, or a --partial-dir resume is skipped. A MATCH
token arriving during that discard hit the NULL deref.

The 31fbb17d branch is correct only for a REAL output transfer (fd != -1,
fname valid): there, a block match with no mapped basis is a genuine
protocol inconsistency (the generator promised a basis the receiver could
not open), and honoring it would silently omit those bytes from the
verification checksum or leave a hole, so hard-erroring -- and
full_fname(fname) -- is right. It conflated that with the discard path.

The discriminator is fd, not mapbuf: on the discard path fd == -1 always;
on the real-output inconsistency fd != -1. Scope the "no basis file"
protocol error to fd != -1 (where fname is non-NULL and full_fname is
safe) and, on the discard path (fd == -1), absorb the matched bytes
benignly (offset += len; continue) -- symmetric with the literal-token
handling just above, and restoring the pre-31fbb17d behavior. The
real-transfer inconsistency check is preserved unchanged.

(cherry picked from commit 26f13bc148)
2026-06-08 08:15:27 +10:00
Andrew Tridgell c72030fc0c build: openat2 autodetect + android probe (R1 #924/#905/#900, R10 #904)
configure now probes for <linux/openat2.h> + SYS_openat2 and defines
HAVE_OPENAT2 only when both are present; syscall.c gates the openat2 include
and the openat2(RESOLVE_BENEATH) tier on HAVE_OPENAT2, so the build no longer
fails on kernels/headers that lack the openat2 header (3.4.3 included it
unconditionally on Linux).  android.c probes openat2 usability behind a SIGSYS
handler so the Android/Termux seccomp sandbox falls back to the portable
resolver instead of killing the process.

Backport combining c73e0063, 83a24c21, the syscall.c guards from 1d5b5ab8, and
4634b0ad; the --disable-openat2/gcov coverage knobs and test changes are omitted.

Thanks to @mmayer (#924), @fda77 (#905), @darkshram (#900) and @ketas (#904) for the reports.

(cherry picked from commit 1cff146a12)
2026-06-08 08:15:27 +10:00
Zen Dodd 222a2f805f fix: update skips different file type
(cherry picked from commit def944a501)
2026-06-08 08:15:27 +10:00
Mike-Goutokuji 2d90ca6213 Always clear st out and validate nanoseconds before using it
Otherwise we get errors.
Fixes: https://github.com/RsyncProject/rsync/issues/927

(cherry picked from commit 044cfd2a5e)
2026-06-07 18:47:07 +10:00
Andrew Tridgell 44d65fbe8d main: fix --mkpath + --dry-run file-to-file copy (#880)
A single-file --mkpath copy whose destination parent does not exist
failed under --dry-run: make_path() only *reports* the directories it
would create in a dry run, so change_dir#3 then tried to chdir into a
parent that isn't there and aborted with "change_dir#3 ... failed".

When the parent is genuinely missing in a dry run, skip the chdir and
mark the destination as not-yet-present (dry_run++), exactly as the
multi-file/dir-creation path already does, so the generator doesn't
probe the missing tree.  Gating it on the missing-parent case keeps an
ordinary file-to-file dry run chdir'ing into and itemizing against an
existing destination.

Fixes: #880

Thanks to @pkzc for the report (#880).

Co-authored-by: Stiliyan Tonev (Bark) <stiliyan21@gmail.com>
(cherry picked from commit e096bbd64e)
2026-06-07 18:47:07 +10:00
Zen Dodd fc92c1b0ce fix: daemon upload delete stats
(cherry picked from commit 7259eae560)
2026-06-07 18:47:07 +10:00
Andrew Tridgell e7a81610ae token: drain the matched-block insert deflate (#951)
send_deflated_token() adds a matched block to the compressor history with
deflate(Z_INSERT_ONLY).  Our bundled zlib implements Z_INSERT_ONLY (it
produces no output and consumes the input in one call), but a build
against a system zlib lacks it and falls back to Z_SYNC_FLUSH (see the top
of the file), which emits a flush block into obuf.  For a large
incompressible matched token that block exceeds AVAIL_OUT_SIZE(CHUNK_SIZE),
so deflate returned with avail_in != 0 and the transfer aborted:

    "deflate on token returned 0 (N bytes left)"  at token.c

The insert output is never sent -- the receiver rebuilds the matching
history itself in see_deflate_token() -- so loop, resetting the output
buffer, and discard it.  Drain with the same condition as the data loop
above: until the input is consumed AND avail_out != 0.  Stopping at
avail_in == 0 alone can leave pending output in the deflate stream (a
full output buffer with bytes still buffered), which would then be emitted
by the next real deflate send and corrupt the stream.  A bundled-zlib
build still finishes in one iteration.

Thanks to @brabalan for the report (#951).

Fixes: #951
(cherry picked from commit 8504275acd)
2026-06-07 18:47:07 +10:00
Zen Dodd 31129f6cc2 fix: install generated manpages out of tree
(cherry picked from commit 2095a62808)
2026-06-07 18:47:07 +10:00
Andrew Tridgell ce4c0cfb48 daemon: un-backslash escaped option args (#829)
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)
2026-06-07 18:47:07 +10:00
Andrew Tridgell 77c34ca18e build: fall back to do_mknod() when mknodat() is unavailable (#896)
do_mknod_at() (the symlink-race-safe variant used by a non-chrooted
daemon receiver) calls mknodat()/mkfifoat(), but the at-variant was
gated only on AT_FDCWD.  Older Darwin declares AT_FDCWD without
mknodat(), so the build failed with "mknodat undeclared".

Probe mknodat()/mkfifoat() in configure and require HAVE_MKNODAT for the
at-variant; without it do_mknod_at() falls back to do_mknod(), exactly
as it already does where AT_FDCWD is missing.  Linux keeps the mknodat
path since HAVE_MKNODAT is defined there.

Thanks to @debohman for the report (#896).

Fixes: #896
(cherry picked from commit 24d75c04e4)
2026-06-07 18:47:07 +10:00
Andrew Tridgell f00262b786 alloc: revert "zero all new memory from allocations" (#959)
Commit d046525d made my_alloc() calloc every fresh allocation and made
expand_item_list() memset the freshly grown tail, to hand out predictably
zeroed memory.  But that forces the kernel to back pages callers never
touch: each per-directory file_list pre-allocates a FLIST_START-entry
(32768) pointer array -- 256KB -- and calloc now zeroes the whole array
even for an empty directory.  With incremental recursion over many
directories the resident set explodes; 80000 empty dirs went from ~336MB
to ~10.8GB.

Restore the pre-d046525d malloc/calloc split: fresh allocations use
malloc (so untouched tails stay lazy) and only explicit do_calloc
requests (new_array0) are zeroed.  Callers that need zeroed memory
already ask for it, and the full test suite passes.

Thanks to @guilherme-puida for the report (#959).

Fixes: #959
(cherry picked from commit 4bfd18d195)
2026-06-07 18:47:07 +10:00
Andrew Tridgell 8a618ca3ad generator: cap block s2length at the negotiated checksum length
sum_sizes_sqroot() capped the strong-sum length at SUM_LENGTH (16), the
legacy MD4/MD5 digest size.  Since 0902b52f the sum2 array elements are
xfer_sum_len bytes and the sender rejects a sums header whose s2length
exceeds xfer_sum_len.  When the negotiated transfer checksum is shorter
than 16 bytes -- xxh64 (8), used when the build's libxxhash lacks
xxh128/xxh3 (e.g. Ubuntu 20.04) -- the generator still emitted s2length
up to 16, so --append-verify and other full-checksum (redo) transfers
died with "Invalid checksum length 16 [sender]" (protocol incompatibility).

Cap s2length at MIN(SUM_LENGTH, xfer_sum_len): unchanged for any checksum
>= 16 bytes (md5/xxh128/sha1), corrected for short ones.  Also closes a
latent over-read of the xfer_sum_len-sized digest buffer.

(cherry picked from commit cb82d6937a)
2026-06-07 18:47:07 +10:00
Andrew Tridgell b42cae0880 syscall/receiver: honour a relative alt-basis dir on a daemon receiver (#915)
The symlink-race hardening routed the receiver's basis open through
secure_relative_open(), which rejects any '..' -- so a sibling
--link-dest=../01 on a use-chroot=no daemon was silently ignored and every file
re-transferred (#915/#928, a regression from 3.4.1).

Narrow the confinement to the sanitizing daemon (am_daemon && !am_chrooted) and
re-anchor it at the module root, the real trust boundary: secure_relative_open()
prefixes the cwd's module-relative path (from rsync's logical curr_dir[], a
guaranteed lexical prefix of module_dir) and resolves beneath module_dir, so
RESOLVE_BENEATH permits an in-module '..' climb while still rejecting one that
escapes the module.  secure_basis_open() opens with a bare do_open() in the
non-sanitizing cases.  t_stub.c gains weak curr_dir[]/curr_dir_len for the
helpers (via #pragma weak on non-GNU compilers, where rsync.h erases
__attribute__).

Two tests: link-dest-relative-basis asserts the in-module '..' is honoured;
link-dest-module-escape asserts a --link-dest=../../OUTSIDE climb that leaves
the module is refused (not hard-linked to an outside file).  See upstream
PR #930.

Thanks to @fufu65 (#915) and @JetAppsClark (#928) for the reports.

(cherry picked from commit 948edffb43)
2026-06-07 18:47:07 +10:00
Andrew Tridgell 5c8509eacf sender: open a module-root-absolute path for a path = / module (#897)
A daemon module with path=/ makes F_PATHNAME absolute, so the secure_path built
for the content open starts with '/'.  secure_relative_open() rejects an
absolute relpath with EINVAL, so a use-chroot=no daemon with path=/ could not
send any file ('failed to open ...: Invalid argument (22)') -- a regression
from 3.4.2.  Strip leading slashes to a module-relative path; resolution stays
confined beneath module_dir.

Thanks to @moonlitbugs for the report (#897).

(cherry picked from commit 9886a06610)
2026-06-07 18:47:07 +10:00
Andrew Tridgell c9992a47dc flist: accept the missing-args mode-0 entry in recv_file_entry (#910)
--delete-missing-args (missing_args==2) sends a missing --files-from arg as a
mode-0 entry (IS_MISSING_FILE), the generator's delete signal.  The mode-type
validation in recv_file_entry() rejected mode 0 as an invalid file type,
aborting the transfer with 'invalid file mode 00 ... code 2' before the
generator could act (a regression from 3.4.1).  Allow mode 0 through only when
missing_args==2 (the delete mode -- not --ignore-missing-args, which never
sends a mode-0 entry); all other modes are still rejected.

Thanks to @mgkeeley for the report (#910).

(cherry picked from commit 73fe630a70)
2026-06-07 18:47:07 +10:00
Andrew Tridgell ae16c61b03 receiver: fix absolute --partial-dir delta resume (false verification)
A delta (--no-whole-file) resume whose basis is an absolute --partial-dir
looped forever on exit code 23 ("failed verification -- update put into
partial-dir"), stranding the correct data in the partial-dir and never
populating the destination.

Cause: an absolute --partial-dir makes the basis path absolute, but the
receiver opened it with secure_relative_open(NULL, fnamecmp, ...), which by
design rejects an absolute relpath (EINVAL). The basis fd was then -1, so
receive_data() mapped no basis and (because the matched-block sum_update() is
guarded by "if (mapbuf)") computed the whole-file verification checksum over
the literal data only -> a spurious mismatch every run. (The data itself was
correct, since the in-place update leaves the matched basis bytes in place.)
Under a non-chroot daemon the in-place write went through the same call and
failed outright.

Fix: add secure_basis_open(), which treats an operator-trusted absolute basis
path as (trusted directory + confined leaf) -- the same way secure_relative_open
already trusts an absolute basedir while keeping O_NOFOLLOW on the leaf -- and
use it for both the basis read and the inplace-partial write. The strict
"reject absolute relpath" contract of secure_relative_open is left intact.

Defense-in-depth: receive_data() now treats a block-match token with no mapped
basis as a protocol inconsistency (it can only arise from a basis that the
generator opened but the receiver could not), failing cleanly instead of
silently dropping those bytes from the verify checksum or the output.

Thanks to @sylvain-ilm for the report (#724, #725).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit eee05c177a)
2026-06-07 18:47:07 +10:00
Andrew Tridgell cc84711567 util1: handle out-of-range times in timestring v3.4.1-sec-patches1 2026-05-15 11:57:01 +10:00