Commit Graph
126 Commits
Author SHA1 Message Date
Andrew Tridgell 2be62f6010 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).

(cherry picked from commit 29ddd45e3d)
2026-06-13 06:04:47 +10:00
Andrew Tridgell a60e5c2d31 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.

(cherry picked from commit 25341c51e9)
2026-06-13 06:04:47 +10:00
Andrew Tridgell 853043f864 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.

(cherry picked from commit c186a81f6a)
2026-06-13 06:04:47 +10:00
Andrew Tridgell bc83f73bcc 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.

(cherry picked from commit 28e36bfd6a)
2026-06-13 06:04:47 +10:00
Andrew Tridgell ea73d877a2 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(); caught by the open-noatime test on strict-atime Linux.
2026-06-08 08:15:18 +10:00
Andrew Tridgell a9d1101fd3 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 1cff146a1280854866518efaeff44982f5c2304d)
2026-06-08 08:15:18 +10:00
Andrew Tridgell ccdc019b77 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 19:42:50 +10:00
Andrew Tridgell 57128940b1 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 19:27:21 +10:00
Andrew TridgellandClaude Opus 4.7 a277a06b10 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-08 08:20:48 +10:00
Andrew TridgellandClaude Opus 4.7 39b3074a1a 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-08 08:20:48 +10:00
Andrew TridgellandClaude Opus 4.7 24852cda3d syscall+receiver: secure receiver-side do_chmod against symlink-race TOCTOU
CVE-2026-29518's fix routed the receiver's open() through
secure_relative_open(), but every other path-based syscall the
receiver runs on sender-controllable paths is vulnerable to the
same TOCTOU primitive. This commit closes the chmod variant.

Add do_chmod_at() that opens the parent of fname under
secure_relative_open() and uses fchmodat() against the resulting
dirfd. Gate the secure path on am_daemon && !am_chrooted (the same
gate use_secure_symlinks already uses for the receiver basis-file
open), so non-daemon callers and chrooted daemons keep the original
do_chmod() fast path.

Migrate the receiver-side do_chmod() call sites in delete.c,
generator.c, rsync.c, and xattrs.c.

Adds testsuite/chmod-symlink-race.test (with t_chmod_secure helper)
as regression coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 08:20:48 +10:00
Andrew TridgellandClaude Opus 4.7 1a5ad81add syscall+clientserver: am_chrooted and use_secure_symlinks for daemon-no-chroot (CVE-2026-29518)
CVE-2026-29518: an rsync daemon configured with "use chroot = no"
is exposed to a TOCTOU race on parent path components. A local
attacker with write access to a module can replace a parent
directory component with a symlink between the receiver's check
and its open(), redirecting reads (basis-file disclosure) and
writes (file overwrite) outside the module. Under elevated daemon
privilege this allows privilege escalation. Default
"use chroot = yes" is not exposed.

Add secure_relative_open() in syscall.c. It walks the parent
components under RESOLVE_BENEATH (Linux 5.6+) /
O_RESOLVE_BENEATH (FreeBSD 13+, macOS 15+) / per-component
O_NOFOLLOW elsewhere, anchored at a trusted dirfd, so a parent-
symlink swap is rejected by the kernel. Route the receiver's
basis-file open in receiver.c through it when use_secure_symlinks
is set in clientserver.c rsync_module().

Reporters: Nullx3D (Batuhan SANCAK); Damien Neil; Michael Stapelberg.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 08:20:48 +10:00
Andrew TridgellandClaude Opus 4.7 61d987c54a syscall: also use O_RESOLVE_BENEATH on FreeBSD and MacOS
FreeBSD and MacOS have O_RESOLVE_BENEATH as an openat() flag with the same
"must not escape dirfd" semantics as Linux's RESOLVE_BENEATH. The
kernel rejects ".." escapes, absolute symlinks, and symlinks whose
target lies outside dirfd, while still following symlinks that
resolve within it -- the same trade-off that fixes issue #715 on
Linux.

Add a parallel BSD path in secure_relative_open(), gated on
declared. Unlike Linux, BSD doesn't have the header/runtime split
where the symbol can exist without kernel support, so no runtime
fallback is needed: if the flag compiles in, the kernel honours it.

OpenBSD and NetBSD have no equivalent kernel primitive and continue
to use the existing per-component O_NOFOLLOW walk; issue #715
remains visible on those platforms (a userland resolver or
unveil(2)-based fence would be follow-up work).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 07:25:14 +10:00
Andrew TridgellandClaude Opus 4.7 72d1cf1c28 syscall: use openat2(RESOLVE_BENEATH) on Linux for secure_relative_open
The CVE fix in commit c35e283 made secure_relative_open() walk every
component of relpath with O_NOFOLLOW. That blocks every symlink in the
path, which is stricter than the threat model required: legitimate
directory symlinks within the destination tree (e.g. when using -K /
--copy-dirlinks) are also rejected, breaking delta transfers with
"failed verification -- update discarded".  See issue #715.

On Linux 5.6+, openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS) gives
us exactly what we want: the kernel rejects any resolution that would
escape the starting directory (via "..", absolute paths, or symlinks
pointing outside dirfd) while still following symlinks that resolve
within it. /proc magic-links are blocked too.

Use openat2 first; fall back to the existing per-component O_NOFOLLOW
walk on ENOSYS (kernel < 5.6). The lexical "../" checks at the head
of the function are kept as defense in depth. The Linux gate is
plain #ifdef __linux__: the runtime ENOSYS fallback covers the only
case that actually matters (header present + old kernel), and any
Linux build environment without linux/openat2.h will fail with a
clear "no such file" error rather than silently disabling the
protection.

Verified manually that openat2(RESOLVE_BENEATH) blocks all four
escape patterns (absolute symlink, ../ symlink, lexical .., absolute
path) while allowing direct and within-tree symlinks. The new
testsuite/symlink-dirlink-basis.test (taken from PR #864 by Samuel
Henrique) exercises the issue #715 regression and passes; full
make check passes 47/47.

Test: testsuite/symlink-dirlink-basis.test (8 scenarios)
Fixes: https://github.com/RsyncProject/rsync/issues/715

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 07:25:14 +10:00
Silent 9141bf1d83 syscall: fix a Y2038 bug by replacing Int32x32To64 with multiplication
Int32x32To64 macro internally truncates the arguments to int32,
while time_t is 64-bit on most/all modern platforms.
Therefore, usage of this macro creates a Year 2038 bug.
2026-05-07 07:22:39 +10:00
Andrew Tridgell ad7dc53795 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
2026-05-07 07:21:57 +10:00
Andrew Tridgell 16666c6e75 disallow ../ elements in relpath for secure_relative_open 2026-05-07 07:21:57 +10:00
Andrew Tridgell b31301abb7 added secure_relative_open()
this is an open that enforces no symlink following for all path
components in a relative path
2026-05-07 07:21:57 +10:00
Holger Hoffstätte 9585830e1e Fix warning about conflicting lseek/lseek64 prototypes
Clang rightfully complains about conflicting prototypes, as both lseek() variants
are redefined:

  syscall.c:394:10: warning: a function declaration without a prototype is deprecated
  in all versions of C and is treated as a zero-parameter prototype in C2x, conflicting
  with a previous declaration [-Wdeprecated-non-prototype]
        off64_t lseek64();
                ^
/usr/include/unistd.h:350:18: note: conflicting prototype is here
extern __off64_t lseek64 (int __fd, __off64_t __offset, int __whence)
                 ^
1 warning generated.

The point of the #ifdef is to build for the configured OFF_T; there is
no reason to redefine lseek/lseek64, which should have been found
via configure.

Signed-off-by: Holger Hoffstätte <holger@applied-asynchrony.com>
2026-05-07 07:18:39 +10:00
Wayne Davison b7b387b1f7 Add FALLTHROUGH comment. 2022-03-13 09:31:44 -07:00
Wayne Davison 142aba00d5 Silence some symlink mode-change failures. 2022-01-17 22:23:31 -08: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 296352ecb0 Tweak atime/crtime code a bit more. 2021-10-10 12:43:11 -07:00
Wayne Davison 11a9b62322 Avoid spurious warning about "code" var not being initialized. 2021-10-10 10:05:26 -07:00
Wayne Davison 452ef78517 Unify on "path" vs "fname" arg naming. 2021-10-10 09:53:35 -07:00
Wayne Davison 0d1b48893a Change do_lchmod() back to a swtich with some better ENOTSUP & ENOSYS logic. 2021-10-10 09:32:43 -07:00
Wayne Davison 78b5bc6629 Enable --atimes on macOS. 2021-10-02 15:23:30 -07:00
Wayne Davison f41cdc75a1 Check ro in set_create_time() for Cygwin too. 2021-10-02 11:39:41 -07:00
Wayne Davison 15dd2058fd Change do_chmod to always try lchmod() first (when possible). 2021-10-01 13:28:57 -07:00
Wayne Davison 291a042b3e Support --crtimes on Cygwin. 2021-07-08 18:59:26 -07:00
Wayne Davison 9dd62525f3 Work around glibc's lchmod() issue a better way. 2020-11-29 09:40:03 -08:00
Wayne Davison 5db7e4b1ee Use linkat() if available
Some OSes have a more capable linkat() function that can hard-link
syslinks, so use linkat() when it is available.
2020-07-27 16:36:55 -07:00
Wayne Davison 974f49e22a Add --crtimes option. 2020-07-22 12:12:18 -07:00
Wayne Davison c3269275a8 Don't use UNUSED() when an arg is used sometimes. 2020-07-07 13:12:51 -07:00
Wayne Davison e63ff70eae Some indentation fixes. 2020-06-13 19:15:02 -07:00
Wayne Davison 3d29fa99ec Tweak a couple var names. 2020-06-13 11:47:04 -07:00
Wayne Davison 888ce058d8 Two sparse fixes from Yuxuan Shui.
- Make "len" parameter of do_punch_hole an OFF_T.
- Clear sparse_past_write in sparse_end(), otherwise when write_sparse()
  is called for the next file, do_punch_hole() will be called with a pos
  that's not actually the current position in file, causing it to fail.
2020-05-25 14:01:52 -07:00
Wayne Davison d218be3482 Update a few more copyright years. 2020-04-25 23:34:16 -07:00
Wayne Davison 1c7785ab1e Change do_setattrlist_times() to use an stp arg. 2020-04-25 21:39:11 -07:00
Wayne Davison 87257f869c Change --set-notime to --open-noatime. 2020-04-23 14:32:26 -07:00
Wayne Davison b936741032 Added --atimes and --set-noatime options. 2020-04-23 13:24:15 -07:00
Wayne Davison 3e2e4b5a33 Tweak the copyright year. 2019-03-16 09:15:49 -07:00
Wayne Davison c2da3809f7 Fix --prealloc to keep file-size 0 when possible. 2019-01-15 08:59:35 -08:00
Wayne Davison 473108ae6e Tweak copyright date. 2018-01-14 19:55:07 -08:00
Wayne Davison b7799aaefe Add nanosecond mtime support for Mac OS X.
Slightly tweaked the patch contributed by Heikki Lindholm.
2017-08-31 08:22:14 -07:00
Wayne Davison d1a1fec134 Use S_BLKSIZE when multiplying st_blocks. 2016-10-15 11:33:07 -07:00
Wayne Davison f3873b3d88 Support --sparse combined with --preallocate or --inplace.
The new code tries to punch holes in the destination file using newer
Linux fallocate features. It also supports a --whole-file + --sparse +
--inplace copy on any filesystem by truncating the destination file.
2016-10-10 11:53:03 -07:00
Wayne Davison 453914e35b Update the copyright year. 2015-08-08 12:47:03 -07:00
Wayne Davison dfa5b49110 Bump the year to 2014. 2014-01-26 09:29:15 -08:00