Commit Graph
32 Commits
Author SHA1 Message Date
Zayd Rajab 240bd9df96 syscall: use O_PATH for held directory traversal (#1065)
Use O_PATH on Linux for directory descriptors used only for path
traversal, fchdir(), or as *at() parents. Reopen final directory
endpoints with the caller-requested access mode and retain the existing
O_RDONLY fallback elsewhere.

Add coverage for exact sources and readable descendants beneath
search-only directories, known-file creation beneath write/search-only
destinations, and retained refusal to enumerate unreadable directories.

Fixes #1064.
2026-08-28 19:37:01 +10:00
ᴄʜʀɪsᴛᴏᴘʜᴇʀ ᴍᴇɴɢ f0177d82a8 Add IDN support (#986)
* Add IDN support

rsync can now connect to IDN (internationalized domain name) hosts, and
IDN names are recognized in a daemon's hosts allow/deny.

* idn: convert host names label by label

The IDNA mapping folds some non-ASCII characters onto ASCII ones, so
running a whole hosts allow/deny token through idn2_to_ascii_8z() could
hand back a pattern the admin never wrote: a "*" (U+FF0A FULLWIDTH
ASTERISK) entry came back as "*" and let every host in.

Convert label by label instead, keeping an ASCII label byte for byte and
using a converted label only when it comes back as a bare A-label. An
ASCII-only config now behaves as it did before there was IDN support, and
a token that cannot be converted is left alone and so matches nothing. The
client side shares the same helper, and neither side truncates a name at
its 1024-byte buffer any more. strlower() folds only ASCII now, since its
one caller is the hosts allow/deny list, which can hold UTF-8.

Adds testsuite/daemon-access-idn and extends testsuite/idn to cover
Unicode, punycode, mixed-case and invalid input on both sides.

* idn: hand libidn2 the same flags on both sides

The client path called idn2_lookup_ul() without IDN2_NFC_INPUT while the
daemon path passed it to idn2_to_ascii_8z(). Both normalize either way --
idn2_lookup_ul() ors the flag in itself, and TR46 normalizes as it maps --
but there is no reason for the two calls to read differently, so pass one set
of flags from one place. The flag asks libidn2 to normalize the label rather
than promising that it already is: it gates the u32_normalize() call, and
without it a decomposed label comes back IDN2_NOT_NFC.

Adds composed/decomposed cases to testsuite/idn, which sees the exact host
name rsync hands out, and a decomposed hosts allow token to
testsuite/daemon-access-idn.
2026-08-20 18:51:23 +10:00
Andrew Tridgell c53d107f97 util1: drop null-tests on robust_rename's from/to args
Both callers pass non-null from/to, but the 'to &&' test taught the
clang analyzer that to may be null, and it then walked a null to
through copy_file -> unlink_and_reopen -> robust_unlink into glibc's
nonnull-annotated strlcpy, failing the scan-build gate.  The args are
required non-null, so test the first byte directly.
2026-07-20 14:05:32 +10:00
Andrew Tridgell fbeb553b73 util1: fix clean_fname ".." collapse off-by-one
After the backward walk, s points at the first char of the prior component and
s[-1] is its leading '/', so the boundary test must read s[-1] (not *s) and t
must reset to s (not s+1).  The old off-by-one left CFN_COLLAPSE_DOT_DOT_DIRS
dead for all multi-component and absolute paths.  The peer-traversal guard
CFN_REFUSE_DOT_DOT_DIRS is checked first and is unaffected, so this is a
normalization-correctness fix, not a traversal hole.

Reported-by: Leonid Bugaev
2026-07-20 14:05:32 +10:00
Andrew Tridgell fd4c75116b util1: confine operator paths in the robust_rename EXDEV fallback
The cross-filesystem fallback copied to the dest and unlinked the source without
operator-path confinement, so an absolute --temp-dir/--partial-dir on another
filesystem was opened/unlinked via plain libc -- a raced parent symlink could
redirect the dest-write or source-unlink out of the module.  The source READ was
already confined; flip operator_path_resolve for an absolute (operator) path
around the copy_file dest-open and the do_unlink_at, leaving relative in-module
paths on the secure_relative_open arm.

This is the EXDEV-fallback backstop for the same absolute --partial-dir /
foreign-owned-parent-symlink escape that operator-path-partial-dir_test.py
already exercises at the handle_partial_dir() layer (which confines the staging
dir before this code runs).  A dedicated test for the EXDEV copy_file path itself
would need the main tmp->final rename to fail first and then hit EXDEV on a
foreign-owned raced parent -- a nested, timing-dependent trigger.

Reported-by: Leonid Bugaev
2026-07-20 14:05:32 +10:00
Andrew Tridgell 298efc7fe4 xattrs/backup: read source metadata through a held fd, not a path
The hardened receiver confined the destination side of an xattr/ACL copy (the
fsetxattr/acl_set_fd through a held O_NOFOLLOW fd) but still read the SOURCE side
by path: copy_xattrs() did get_xattr_names/get_xattr_data on the source path, and
make_backup() cached the backed-up file's ACL/xattr via get_acl()/get_xattr() by
path.  A local module writer could race the source/basis parent to a symlink
after the confined content/stat open and before that path-based metadata read,
so out-of-module xattrs/ACLs got copied onto an in-module destination or backup.

Thread a source fd through the read side, mirroring the existing dest-fd plumbing:
 - get_xattr_data(), get_xattr() and get_xattr_acl() gain an fd arg (get_xattr_names
   already had one) and use sys_fgetxattr/sys_flistxattr when fd >= 0; this also
   covers the --fake-super ACL-as-xattr read in get_rsync_acl().
 - copy_xattrs() gains a source_fd; copy_file() passes its held source fd (ifd)
   and keeps it open across the xattr copy (closing it on the fsync error path
   too); gen_entry_copy_xattrs() O_NOFOLLOW-opens the basis leaf under the
   confined resolver (with O_DIRECTORY for a directory basis) and passes it.
 - make_backup() pins the source leaf with a confined O_NOFOLLOW fd
   (backup_source_fd, like set_file_attrs's op_leaf_fd) and reads its ACL via
   get_acl_fdat() and its xattrs via get_xattr(fd); the in-place delta-backup in
   the generator pins fname the same way.  On a hardened receiver a raced/absent
   leaf skips the cache rather than reading through a flippable path.

Non-hardened receivers (fd < 0) keep the path-based behaviour unchanged.  The
basis COMPARE reads (the generator deciding a match) stay path-based: they never
copy out-of-module metadata onto a file, so they are not part of this sink.
2026-07-20 14:05:32 +10:00
Andrew Tridgell 7b6fbf5940 daemon: make "insecure links = yes" fully restore 3.2.7 symlink following
The opt-out is meant to restore stock-3.2.7 "follow existing symlinks in the
module" behaviour, but several daemon symlink-resolution sites called the
confined resolver unconditionally and never consulted symlink_optout_allowed(),
so a module with "insecure links = yes" still refused to follow a symlinked
directory (change_dir ELOOP) or an alt-dest basis.  Gate every such site on the
opt-out -- follow plainly like 3.2.7 when set, confine otherwise:

  - change_dir() relative daemon branch (util1.c): plain chdir() under the
    opt-out instead of secure_relative_open(), so a peer can read a path through
    a symlinked directory again.
  - basis_link_stat() (generator.c): the chrooted-relative and non-chroot
    branches honour the opt-out.  The non-chroot ABSOLUTE branch (a basis rooted
    under the module by check_alt_basis_dirs, which can reach an in-module
    symlink) is, when NOT opted out, resolved through owner_walk_parent so a
    target landing outside the module root is refused -- closing a confirmed
    --compare-dest=/symlink out-of-module read oracle.  The leaf is taken
    O_NOFOLLOW under the confined parent so --copy-links can't follow a leaf
    symlink out; --fake-super folds its %stat via the held fd.  A relative
    sibling basis (--link-dest=../01) keeps the plain path (#915/#930).
  - secure_basis_open() (receiver.c): plain do_open() under the opt-out.
  - use_secure_symlinks (clientserver.c): cleared under the opt-out, so the
    receiver's protected-regular EACCES write fallback is legacy too.
  - secure_sender_parent_fd() (sender.c): declines (errno=0) under the opt-out
    so --remove-source-files re-stats the plain path.

Default modules (insecure links = no) are unchanged and stay fully confined;
the opt-out is per-module and a client cannot enable it.
2026-07-20 14:05:32 +10:00
Andrew Tridgell 8d3b7be875 copy_file: confine an absolute operator source via the ownership walk
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.)
2026-07-20 14:05:32 +10:00
Andrew Tridgell 54965efca9 syscall/util1: race-safe path resolution via a held dirfd-stack resolver
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.
2026-07-20 14:05:30 +10:00
Andrew Tridgell 8118744f2a scan-build: drop dead assignments
Remove stores that are never read before being overwritten or going
out of scope. No behavior change except batch.c write_opt, which now
accumulates the leading-space write error into the return value
(consistent with the arg branch) instead of discarding it.

simd-checksum-x86_64.cpp, options.c, util1.c, batch.c
2026-06-16 08:55:39 +10:00
Andrew Tridgell 7db73ad9a1 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.

Fixes: #959
2026-06-05 06:35:12 +10:00
Andrew Tridgell 5972ebdaf8 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.
2026-06-04 07:41:41 +10:00
Andrew Tridgell ac692b199c util1: handle out-of-range times in timestring 2026-05-20 10:01:22 +10:00
Andrew TridgellandClaude Opus 4.7 3cc6a9e8cd util1+syscall: secure copy_file source/dest opens; bare-path defence-in-depth
Three related codex audit findings:

  Finding 3a: copy_file()'s source open in util1.c used
  do_open_nofollow(), which only rejects a final-component
  symlink. A parent-component symlink (e.g. --copy-dest=cd where
  cd -> /outside) follows freely and reads outside the module.
  Route through secure_relative_open() with O_NOFOLLOW.

  Finding 3b: generator.c's in-place backup-file create still
  used a bare do_open with O_CREAT, leaving a tiny but reachable
  parent-symlink window between the secure unlink (already
  through do_unlink_at) and the create. Add do_open_at() that
  goes through a secure parent dirfd, and route the call site
  through it.

  Finding 3c: copy_file()'s destination open in
  unlink_and_reopen() had the same bare-do_open pattern; route
  through do_open_at as well.

Adds testsuite/copy-dest-source-symlink.test and
testsuite/bare-do-open-symlink-race.test as regression coverage
for both attack shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:01:22 +10:00
Andrew TridgellandClaude Opus 4.7 30656c5e35 syscall: add symlink-race-safe do_*_at() wrappers and harden secure_relative_open
Add the rest of the path-based syscall wrappers and migrate every
receiver-side caller:
  - do_lchown_at, do_rename_at, do_mkdir_at, do_symlink_at,
    do_mknod_at, do_link_at, do_unlink_at, do_rmdir_at,
    do_utimensat_at, do_stat_at, do_lstat_at

Same shape as do_chmod_at: open each parent under
secure_relative_open(), call the *at() variant against the dirfd,
fall through to the bare path-based syscall in non-daemon /
chrooted / absolute-path / no-parent cases. macOS's
setattrlist-based set_times tier is also routed through the
utimensat_at path on daemon-no-chroot.

Hardenings to secure_relative_open() itself:
  - confine basedir resolution under the same kernel mechanism
    used for relpath (basedirs from --copy-dest / --link-dest are
    sender-controllable in daemon mode)
  - reject any '..' component (bare '..', 'foo/..', 'subdir/..')
    so the per-component O_NOFOLLOW fallback can't escape
  - return the dirfd we built up from the per-component fallback
    when the caller passed O_DIRECTORY (otherwise every do_*_at
    failed with EINVAL on platforms without RESOLVE_BENEATH)

Adds testsuite/alt-dest-symlink-race.test and
testsuite/secure-relpath-validation.test (with t_secure_relpath
helper) as regression coverage for the new hardenings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:01:22 +10:00
Andrew TridgellandClaude Opus 4.7 15d2964256 util1: secure change_dir() against symlink-race chdir-escape
The receiver's chdir(2) into a destination subdirectory followed
attacker-planted symlinks at every path component. Once CWD
escaped the module, every subsequent path-relative syscall (open,
chmod, lchown, ...) inherited the escape -- defeating
secure_relative_open's RESOLVE_BENEATH anchor against AT_FDCWD,
since the anchor itself was now outside the module.

Route change_dir's relative target through secure_relative_open()
and fchdir() to the resulting dirfd in am_daemon && !am_chrooted
mode, so the chdir step itself can no longer follow a parent-
symlink. Same treatment applied to the CD_SKIP_CHDIR /
set_path_only path so it also can't follow attacker symlinks
during path tracking.

Adds testsuite/sender-flist-symlink-leak.test covering the
sender-side flist resolution variant of the same primitive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 10:01:22 +10:00
Michal Ruprich 8e11f0c169 Using a correct time in log file 2026-04-22 13:02:10 +10:00
Andrew TridgellandClaude Opus 4.6 d046525de3 zero all new memory from allocations
Change my_alloc() to use calloc instead of malloc so all fresh
allocations return zeroed memory. Also zero the expanded portion
in expand_item_list() after realloc, since it knows both old and
new sizes. This gives more predictable behaviour in case of bugs
where uninitialised or stale memory is accidentally accessed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 11:44:10 +10:00
Andrew Tridgell 9e0898460d util: fixed issue in clean_fname()
fixes buffer underflow (not exploitable) in clean_fname
2025-12-30 17:49:35 +11:00
Andrew Tridgell 0590b09d9a fixed symlink race condition in sender
when we open a file that we don't expect to be a symlink use
O_NOFOLLOW to prevent a race condition where an attacker could change
a file between being a normal file and a symlink
2025-01-15 05:30:32 +11:00
Andrew Tridgell 407c71c7ce make --safe-links stricter
when --safe-links is used also reject links where a '../' component is
included in the destination as other than the leading part of the
filename
2025-01-15 05:30:32 +11:00
Kenneth Finnegan 8fe8cfd60a Use string length diff heuristic to skip Levenshtein Algo (#369)
When using the --fuzzy option to try and find close matches locally,
the edit distance algorithm used is O(N^2), which can get painful on
CPU constrained systems when working in folders with tens of thousands
of files in it.

The lower bound on the calculated Levenshtein distance is the difference
of the two strings being compared, so if that difference is larger than
the current best match, the calculation of the exact edit distance between
the two strings can be skipped.

Testing on the OpenSUSE package repo has shown a 50% reduction in the CPU time
required to plan the rsync transaction.
2022-09-15 10:12:02 -07:00
Wayne Davison 84ad83525b Remove unneeded var. 2022-08-19 08:56:04 -07:00
Wayne Davison a48c20c97c Combine some alt-dest tests. 2022-03-26 10:01:12 -07:00
Sam Mikes ef76d6cfa5 Extract unlink_and_reopen from copy_file (#294)
* add tests to exercise copy_file

* Extract new function unlink_and_reopen from copy_file

The argument `ofd` to copy_file is always set to -1 unless
`open_tmpfile()` is called at generator.c:909

This change
 * removes assignment to a function argument
 * renames argument `ofd` to `tmpfilefd` in line with existing uses
 * extracts a new function `unlink_and_reopen` which is static to util1.c
 * rewrites header comments for copy_file
2022-03-26 09:14:10 -07:00
Wayne Davison c3b553a93f Preparing for release of 3.2.4pre2 2022-01-15 17:21:01 -08:00
Wayne Davison 3e44bbd313 Preparing for release of 3.2.4pre1 2022-01-02 15:13:19 -08:00
Wayne Davison b774dbc1c0 Improve --omit-dir-times & --omit-link-times
The code now better handles skipping time setting on dirs and/or links
when --atimes and/or --crtimes is specified without --times.
2021-10-10 13:39:09 -07:00
Wayne Davison 82f023d7e3 Add --fsync option (promoted from patches). 2021-09-27 10:30:00 -07:00
Wayne Davison 3cc7f0ba43 Tweak a comment. 2021-09-26 17:23:33 -07:00
Wayne Davison c529782a8d Fix compiling without ftruncate. 2021-06-27 09:45:41 -07:00
Wayne Davison d3085f7add Rename util.c to util1.c
Fixes an issue where the Makefile's glob of *.c could sort util.c &
util2.c in an order that depends on the current collation setting.
2021-02-25 09:14:33 -08:00