Commit Graph
144 Commits
Author SHA1 Message Date
Filipe Casal c6327e5b0e syscall: preserve ordinary mode when setgid is denied
macOS's fchmodat(..., AT_SYMLINK_NOFOLLOW) returns EPERM and applies
NOTHING when the requested setgid bit is ungrantable, so
"rsync -a --chmod=D2750,F0640" exits 23 and leaves the destination at
0755/0700 where 3.4.4 leaves 0750.  fchmod() on an already-open
descriptor does the right thing: it succeeds, drops the setgid bit it
cannot grant, and applies the ordinary bits.

That fd path existed but was fenced behind "#if defined __linux__".
Guard it on O_NOFOLLOW so every platform that can open a leaf without
following a symlink uses it, and leave the fchmodat/fchmodat2 fallbacks
under __linux__.  Measured on macOS with an ungrantable group:

  fchmodat(2750, NOFOLLOW)  EPERM, 0700 -> 0700   (dir and FIFO alike)
  fchmod(fd, 2750)          ok,    0700 -> 0750
  fchmodat(0750, NOFOLLOW)  ok,    0700 -> 0750

A FIFO observed by the lstat takes the pathname call instead, then one
retry without S_ISGID.  Opening a FIFO -- even O_NONBLOCK -- makes this
process a reader for as long as the descriptor lives, which wakes a
writer blocked in open(O_WRONLY) and can cost it a SIGPIPE or the bytes
it writes before we close; the third line above is why that is
avoidable.  This does not make the function FIFO-open-free: the type
comes from the lstat, so a leaf swapped to a FIFO after it is still
opened, and set_file_attrs() opens FIFOs elsewhere for ACL/xattr work.
Closing that needs the open constrained to the observed type, which is
tracked separately.

The retry is a pathname call and does not pin the inode, so a leaf
swapped for another object of the same name is chmod'd instead;
AT_SYMLINK_NOFOLLOW still keeps it off a symlink's target, and the held
parent fd still confines the ancestors, so the out-of-tree boundary is
unaffected.  Only S_ISGID is retried -- clearing S_ISUID too could
discard a bit that was grantable when only setgid caused the failure.
Linux keeps the fd-first order it has always had.

The raced-to-a-symlink refusal after openat() also accepts EMLINK and
EFTYPE.  ELOOP is not universal for O_NOFOLLOW on a symlink -- FreeBSD
documents EMLINK and NetBSD EFTYPE -- so those two silently skipped the
refusal and fell through to the (still symlink-safe) pathname call.

Verified on real macOS: the regression test FAILS on 93a67aa9 with rc=23
and modes 0755/0700, and PASSES here with 0750/0750.

(cherry picked from commit 5f0f8f298e)
2026-08-02 21:27:57 +10:00
Filipe Casal 0d7cd20362 syscall: preserve fake-super backups as placeholders
(cherry picked from commit 4ce54db5ef)
2026-08-02 21:27:57 +10:00
Andrew Tridgell 375f4fd152 syscall: defer a literal ".." to the walk before the leaf fast paths
secure_walk_at() has two fast paths for the final component that call
openat(ds_cur(&ds), part, ...) directly instead of going through
ds_descend().  A final component of ".." therefore never met the anchor
floor, and what happened depended entirely on the caller's flags:

    ".." with O_DIRECTORY              -> ELOOP          (refused)
    ".." with O_DIRECTORY|O_NOFOLLOW   -> fd for the directory ABOVE
                                          the anchor
    ".." without O_DIRECTORY           -> parent opened, then closed,
                                          EISDIR returned

That was harmless while every literal ".." was rejected at the front
door, but secure_relative_open_at_beneath() now admits them and
documents the held-fd stack as refusing every climb above the anchor.
The sender's own call passes O_RDONLY|O_DIRECTORY and so was never
affected -- but the guarantee the new API advertises has to hold for
whatever flags the next caller picks.

Route a literal "." or ".." through ds_descend() before the leaf fast
paths.  All three flag combinations now refuse a bare ".." with ELOOP,
and t_secure_relpath covers the matrix.

(cherry picked from commit c5bc4e3677)
2026-08-02 21:26:27 +10:00
Filipe Casal ad17cbd298 sender: allow confined parent-relative copy-links targets
(cherry picked from commit 4d8cbbecac)
2026-08-02 21:26:14 +10:00
Andrew Tridgell ee514432e9 syscall: honor operator_path_resolve in do_chmod_at/do_lchown_at
Every other mutating do_*_at() wrapper (unlink, symlink, link, mknod,
rmdir, open, mkdir, rename) resolves an operator-supplied path through
owner_walk_parent() when operator_path_resolve is set.  do_chmod_at()
and do_lchown_at() did not look at the flag at all, and both hand an
absolute name straight to the unconfined full-path do_chmod()/do_lchown().

set_file_attrs() is called with operator_path_resolve = 1 precisely so
that "a flipped temp-dir parent then can't redirect the chmod/chown"
(rsync.c).  With no held dirfd -- an absolute --temp-dir or
--partial-dir -- both fell back to these two wrappers, so that promise
did not hold.  Give them the same ownership-walk branch the others use.

S_ISLNK(mode) still takes do_chmod()'s lchmod()/setattrlist() path.

The missing branch was spotted by Omar Elsayed in review on the
partial-dir EACCES recovery PR, together with the fix approach.

Suggested-by: Omar Elsayed <omarelsayed161@gmail.com>

(cherry picked from commit 0bfcd3b0f2)
2026-08-02 21:24:12 +10:00
Andrew Tridgell 8b506aeac9 syscall: silence scan-build dead-store in do_fchmodat_nofollow fallback
When neither AT_FDCWD nor AT_SYMLINK_NOFOLLOW is available, the function body is
a no-op warning that never reads mode or dfd, so the leading 'mode &= CHMOD_BITS'
became a dead store and dfd an unused parameter -- which the pinned clang-18
scan-build gate flags (deadcode.DeadStores).  Move the mask inside the
AT_SYMLINK_NOFOLLOW guard where mode is actually used, and mark dfd/mode used in
the fallback.  No behavior change on any platform that has the symlink-safe
primitive.

(cherry picked from commit 7b16872eff)
2026-08-02 21:23:59 +10:00
Omar ElsayedandAndrew Tridgell 27a0de79b9 syscall: confine operator paths in do_symlink_at and do_rmdir_at
do_symlink_at() and do_rmdir_at() were the only operator-path syscall
wrappers still missing the operator_path_resolve branch that
do_mknod_at()/do_open_at()/do_rename_at()/do_unlink_at() already carry:
an absolute operator path (e.g. an absolute --backup-dir) took the
'*path == "/"' arm straight to the bare do_symlink()/rmdir(), whose
libc path resolution follows a parent-component symlink.  A local
attacker who owns a parent component of the backup tree could thus
redirect a backup symlink creation or a backup-dir rmdir outside the
intended tree.

Route both through owner_walk_parent() + symlinkat()/unlinkat() when
operator_path_resolve is set, mirroring the existing wrappers, so a
foreign-owned parent component is refused while the operator's own is
followed.  symlink_optout_allowed() (--insecure-links / 'insecure
links =') restores the legacy following.

Tests: operator-path-backup-symlink and operator-path-backup-rmdir
drive a live parent-component swap (native C flipper) against a local
--backup-dir push and confirm nothing escapes the backup tree (RED
before, GREEN after).

Co-authored-by: Andrew Tridgell <andrew@tridgell.net>
2026-07-20 14:20:17 +10:00
Andrew Tridgell b1b3ad45e5 syscall: drop the dead name_is_dir arg from abspath_excluded_by_module
abspath_excluded_by_module() is module-ROOT confinement only by design (the
daemon name/exclude filter is name-based visibility, not a physical-path
boundary -- munge symlinks is that defense), so its name_is_dir parameter was
always discarded ((void)name_is_dir).  Remove it, and the now-redundant
absent-leaf second call in owner_walk_parent()'s leaf check that re-tested the
SAME leafabs with name_is_dir=1 for an identical result, plus the
fstatat()/isdir/absent that only fed the ignored logic.  Behavior-preserving
(the refuse decision is unchanged at every call site); drops one incidental
lstat.

(Leonid Bugaev May-2026 re-audit, KI-49 -- dead defense-in-depth arm, no escape.)
2026-06-27 18:18:53 +10:00
Andrew Tridgell cf4cffcccb confine the remaining cross-tree operator-path syscalls
The backup-dir fix pinned chmod/chown for an operator leaf; extend the same
confinement to every other sink that re-resolves a cross-tree operator path
(an absolute --temp-dir/--partial-dir/--*-dest), each confirmed by a cross-uid
race PoC that is RED on stock 3.2.7 and GREEN here:

- set_file_attrs (rsync.c): route times (do_futimens), and -- by aliasing
  held_fd to the pinned op_leaf_fd -- the xattr/ACL ops through the same
  O_NOFOLLOW leaf fd, not a re-resolvable path.  A raced/refused pin skips them
  (op_refuse) rather than redirecting.  finish_transfer now wraps the pre-rename
  set_file_attrs in operator_path_resolve so an absolute --temp-dir temp file's
  metadata is pinned too (in-tree temps keep their held dirfd, so op_pin is off).

- do_rename_at / do_link_at (syscall.c): an ABSOLUTE side was left at AT_FDCWD
  and followed a flipped parent symlink, letting a name-disclosed --temp-dir /
  predictable --partial-dir rename pull an attacker file into the destination
  (content injection).  Resolve an absolute (operator) side via the ownership
  walk -- with module-exclude enforced -- while a relative (transfer) side stays
  on secure_relative_open.  --insecure-links keeps the legacy path.

- secure_basis_open (receiver.c): an alt-dest basis read
  (--copy-dest/--compare-dest/--link-dest) on a non-daemon receiver used a bare
  do_open; route it through the ownership walk (refuses a foreign-owned basis
  symlink, still allows the "../sibling" basis of #915).  Daemons keep their
  existing confinement branch.

- configure.ac: probe futimens (do_futimens is gated on HAVE_FUTIMENS).

New fd wrappers: do_fchmod, do_fchown, do_futimens.  Documented residuals left
as-is: copy_altdest_file's basis copy (routing it re-opens copy-xattrs-symlink-
race; basis_link_stat already refuses the foreign symlink), crtimes
(do_setattrlist_crtime/do_SetFileTime, path-based on macOS/Cygwin), and
device/socket leaf metadata (pinning a device via open has side effects).
2026-06-27 18:17:30 +10:00
Andrew Tridgell a802ffa58b backup: confine cross-tree operator-path metadata via a pinned fd
A symlink race on a non-daemon --backup-dir let a local attacker redirect
rsync's backup writes and chmods outside the backup tree when rsync runs as
root.  make_backup() sets operator_path_resolve, but set_file_attrs() had no
held parent dir fd for an absolute backup path (held_dfd_for() returns -1), so
its chmod/chown fell through to the path-based wrappers and, for an absolute
path, to raw chmod()/lchown().  copy_valid_path() mirrors the source dir's
attrs onto each backup subdir; when an attacker flips a backup component to a
symlink in that window the raw lchown retags the planted symlink as root-owned
-- laundering it into a "trusted" (uid 0) symlink that the owner-walk then
follows, so the backup rename/chmod escapes the tree.

Pin the leaf inode of a cross-tree operator path with an O_NOFOLLOW open via
the operator owner-walk resolver and drive fchmod/fchown off that fd; a raced
symlink leaf makes the open fail and the op is refused, never redirected.  Gate
on the INTENDED type (new_mode), not the attacker-controlled on-disk type.  As
root any open failure is the race (a real owned leaf never fails); a non-root
operator, which cannot launder a uid-0 symlink, falls back to the legacy path
op on a benign EACCES.  --insecure-links opts back out.

Adds do_fchmod()/do_fchown() fd wrappers.  Residual cross-tree metadata sinks
(times, ACLs, xattrs, and --temp-dir's finish_transfer set_file_attrs) are not
covered here and are tracked for a follow-up.
2026-06-27 18:17:30 +10:00
Andrew Tridgell b98c09a7b5 daemon: insecure links opt-out restores legacy symlink following on the receiver
secure_relpath_active() (the gate that routes receiver-side filesystem ops --
get_dir_fd/dpc, do_*_at, open_tmpfile, make_path, link_stat -- through the
symlink-race-safe resolver) checked only am_daemon/am_chrooted/am_sender, not
the symlink_optout_allowed() opt-out.  So `insecure links = yes` (or a non-daemon
--insecure-links) restored the legacy follow only on the SENDER enumeration
(which checks the opt-out directly), while the receiver still confined writes,
mkdirs, renames, unlinks and stats through a pre-existing in-module symlink --
i.e. the admin opt-out did not actually reproduce the pre-3.4.3 behaviour it
documents (rsyncd.conf(5) "munge symlinks"/"insecure links").

Have secure_relpath_active() return 0 when symlink_optout_allowed(), so the
opt-out uniformly disables the secure resolver on both sides.  No effect on the
default (opt-out off): confinement is unchanged.
2026-06-27 18:16:18 +10:00
Andrew Tridgell ea7fc12b9c syscall: do_mknod_at must mkfifoat for an operator-path FIFO on non-Linux
The operator_path_resolve branch of do_mknod_at() called mknodat() and
returned its result directly, without the FIFO/socket fallback that the
bare-path do_mknod() and the secure-relpath branch below it both have.
mknodat() can make a FIFO only on Linux; on the BSDs/macOS/Solaris it fails
with EINVAL, so creating a special file under an operator-supplied path --
e.g. backing up a FIFO into a --backup-dir -- failed there.  Retry race-safely
with mkfifoat() on the held parent dirfd, and fail a nested socket closed
(EOPNOTSUPP) exactly as the secure-relpath path does.
2026-06-23 11:56:51 +10:00
Andrew Tridgell 49bb61b8eb syscall/util1: race-safe path resolution via a held dirfd-stack resolver
Resolve every operator/peer-reachable filesystem path one component at a time
through a stack of O_NOFOLLOW-held directory fds, so a symlink swapped in mid-walk
cannot redirect the operation (TOCTOU).  Adds the do_*_atfd() wrappers, the held
dirfd cache (held_dfd_for) and do_mkstemp_atfd() for race-safe temp-file creation,
plus secure change_dir()/robust_rename() in util1.c.  t_stub.c gains the matching
test stubs.
2026-06-21 18:25:28 +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 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 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 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 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 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 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
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 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 TridgellandClaude Opus 4.7 90495eecd0 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 72a6634479 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 d870b43a32 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 f629772cc6 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 b32ba3ddb3 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 06:37:34 +10:00
Andrew TridgellandClaude Opus 4.7 45ce54b546 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 06:37:34 +10:00
Silent 4ce604114a 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 06:33:16 +10:00
Natanael Copa 68e9add76a Fix build on ancient glibc without openat(AT_FDCWD
Fixes: https://github.com/RsyncProject/rsync/issues/701
2025-01-16 06:43:57 +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 9f86ddc965 disallow ../ elements in relpath for secure_relative_open 2025-01-15 05:30:32 +11:00
Andrew Tridgell b4a27ca25d added secure_relative_open()
this is an open that enforces no symlink following for all path
components in a relative path
2025-01-15 05:30:32 +11:00
Holger Hoffstätte 07069880a2 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>
2024-11-19 21:28:39 -08: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