- 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.
- 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.
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.
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.
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.
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.
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.
- 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.
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.
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.
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.
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.
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.)
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)
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).
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().
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.
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.
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.
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.
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.
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.
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.
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.
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>
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.
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>
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>
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.
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.
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).
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
--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)
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)